レガシー案件を抜け出すためにGoで何か作ってみる。
というわけで、仕事ではJavaでレガシーシステムをぽちぽち保守してたりしますが、
さすがに今の時代AI駆動もできない案件ばかりだとつまらなくなってきたので、
モダン案件につきたいという願いをこめた個人開発の作業メモ。
やりたいこと
Go + LLM + GitHub ActionsでREADMEを自動更新するBot作成
- プルリクエスト作成
- GitHub ActionsからBOTがキックされる
- プルリクのdiffを読んでLLMで分析、READMEとCHANGELOGに書くべき内容を返してもらう
- READMEとCHANGELOG更新
- GitHub Actionsでpush
これを自動化する。
この記事でやること
- Go で LLM クライアントを叩く最低限の実装例
- Markdown(RULES.md)から特定セクションだけを抜き出す方法
- カスタムルールを環境変数で差し替えつつ、デフォルトは go:embed で同梱する構成
環境
| 項目 | 内容 |
|---|---|
| OS | Windows11 pro |
| エディタ | Visual Studio Code |
| 言語 | Go 1.24.0 |
| ライブラリ | go-github v79.0.0 go-openai v1.41.2 oauth2 |
VSCodeで書くときにCopilotは切ってます。
やったこと
RULES.mdの読み込み
実際に実装に入ったときに「なんもなしだったら別に既存のエージェントでいいな、、」となったので、
カスタムルールの読み込みを導入。
とりあえずmdファイル読んで、ファイルの内容をLLMに投げるところまで実装
RULES.mdはバイナリに同梱してembedで読み込めるようにしつつ、
環境変数 + ファイル配置でカスタムルールファイルを格納できるようにしてみた。
// rule_reader.go
package rules
import (
_ "embed"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
//go:embed rules/RULES.md
var RulesMd string
func LoadRules(section string) (string, error) {
var sectionStr string
if path := os.Getenv("RULES_PATH"); path != "" {
abs, err := filepath.Abs(path)
if err != nil {
sectionStr = readSection(RulesMd, section)
return sectionStr, fmt.Errorf("your RULES_PATH is wrong %s, fallback to default rules", path)
}
file, err := os.ReadFile(abs)
if err != nil {
sectionStr = readSection(RulesMd, section)
return sectionStr, errors.New("failed to load RULES_PATH file, fallback to default rules")
}
sectionStr = readSection(string(file), section)
return sectionStr, nil
}
sectionStr = readSection(RulesMd, section)
return sectionStr, nil
}
func readSection(rules string, section string) string {
start := strings.Index(strings.ToLower(rules), fmt.Sprintf("## %s", section))
if start == -1 {
return "" // not found
}
// スタート位置から先だけ抽出
rest := rules[start:] // restを行split
lines := strings.Split(rest, "\n")
var out []string
for _, l := range lines[1:] { // 1行目は "## readme"
if strings.HasPrefix(l, "## ") {
break
}
out = append(out, l)
}
result := strings.Join(out, "\n")
return result
}
何も考えずにカスタムルールをmdにしてしまったので、
今回はとりあえず「Markdown からセクションをインデックスで切り出す」という雑実装。
将来的にはルールのファイルをjsonとかyamlにしてstructで受けたいきもち。
ちなみに今のRULES.mdはこんな感じ
RULES.md の例
# README Bot Rules
## readme
- このセクションは README の更新方針
- 変更内容の概要を書いてもらう
## report
"## report"以降は将来的に追加したい機能のやつ
LLM呼び出しの最小実装
以下はopenai-goでOpenAI APIを叩く最小実装
外出ししたプロンプト に diff と rule を埋め込んで LLM に JSON を返してもらう
func CallLLM(diff, rule string) (response.Response, error) {
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
return response.Response{}, errors.New("missing OPENAI_API_KEY environment variable")
}
client := openai.NewClient(apiKey)
resp, err := client.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: openai.GPT4oMini,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: fmt.Sprintf(Template, diff, rule),
},
},
},
)
if err != nil {
return response.Response{}, fmt.Errorf("api call is failed %w", err)
}
var resJson response.Response
if err := json.Unmarshal([]byte(resp.Choices[0].Message.Content), &resJson); err != nil {
return response.Response{}, fmt.Errorf("failed to parse LLM JSON: %w", err)
}
return resJson, nil
}
たぶんChoicesがあるかチェック入れたりするべきなんだろうけどとりあえず無視
あと将来的には環境変数なんかで使用するモデルの切り替えとかも実装したい。
まだ修正は入るだろうけどとりあえずはこんな感じ。
rule_readerで読み込んだルールをプロンプトのテンプレートに埋め込んでLLMに投げる。
次にやること
次はGitHubでプルリク差分を取得してファイルに書き込むところまで実装して記事にまとめる。