SummerWind

Web, Photography, Space Development

LLM を使った開発と宣言的なソースコードの管理

2年ほど前に gptask という名前のエージェントを開発してから、LLM とユーザーが協調して働くにはどうしたらよいのか、その場合の最適なインターフェースとは何か、といったあたりに興味を持って色々と作ったりして遊んでいる。LLM のモデルの進化もあって、今まで手が出しにくかった技術を気軽に試せるようになってきていて、エンジニアリングが誰でも楽しめる時代が本当にやってきたんだなと感じている。

LLM を使った開発

自分でコードを書く時は Visual Studio Code と LLM のコーディングアシスタントとして Cody を組み合わせて使っている。Cody は LLM を開発支援に活用する一連の流れの中でわりと早い段階にリリースされていて、今でも気に入っているプロダクトだ。以前は Vim でコードを書いていたが、Cody がうまく使えるという理由で今は Visual Studio Code でコードを書くようにもなった。

LLM にコードを書いてもらう時、あるいは Vibe Coding 的なことをする時には Amp Code を使っている。これも Cody と同じ Sourcegraph の開発チームが作っていて、最近話題の Claude Code と似たようなものなんだけど、Amp Code の方がとてもシンプルにできていて、うまい具合にコードも書いてくれる。こっちはプロダクトとしてまだ始まったばかりなので、今後の発展にも期待している。

宣言的なソースコードの管理

LLM と一緒にコードを書いていると、次のような問題が起こってくる。

  • 自分があまり書かないようなスタイルのコードなど、違和感のあるコードが出力される
  • 出力されたコード内の自作ライブラリの使い方が意図したものになっていない
  • LLM が出力したコードが増えてくると、その内容をちゃんと把握してメンテナンスしていくのが大変

だいたいこういった問題は頭の中にある暗黙的なコンテキストが適切に LLM に共有されていないから起きることが多かったりするわけだけど、もうちょっとうまい具合に LLM との共通認識を確立できないものかなぁと感じることも多い。

そんなわけで最近は Kubernetes の Reconciliation Loop のように、まず仕様書にインターフェースを記述して、そこからコード生成を繰り返す方法を試していて、ここで簡単に紹介したい。この方法だと SaaS が提供する (中身はよく分からないが機能する) API を呼び出す感覚で LLM が生成したコードを使えるので、個人的には違和感が生まれにくい。

この方法の実体はシンプルなもので、以下のようなフローになる。

  1. Markdown ファイルに go doc コマンドが出力するような内容の仕様書を定義する
  2. 仕様書 (と既存のコード) を LLM に与えてその仕様を満たすコードを生成してもらう
  3. コードにしか表現されないような詳細な仕様を仕様書にフィードバックする
  4. 必要に応じて仕様書やコードを修正しつつ、ステップ1から3を繰り返す

最初のステップでは Markdown ファイルに仕様書を定義する。仕様書にはファイル名やインターフェースなんかを定義する。以下の例では Chrome DevTools Protocol を実装するパッケージの仕様書を定義している (車輪の再発明もまた楽しい)。もちろんこの仕様書を書く時に LLM に手伝ってもらうこともできる。

# Package Name

cdp

# Description

Chrome DevTools Protool (CDP) のクライアント機能を実装するパッケージです。
...(省略)...

# Files

ファイル名 | 概要
--- | ---
cdp.go | CDP クライアントのコア機能、接続管理、メッセージ送受信を実装する
dom.go | DOM Domain の機能を実装する
...(省略)...

## cdp.go

### type Client

```go
type Client struct {
    // ...(省略)...
}
```

CDP クライアントを抽象化する構造体です。この構造体のメソッドを通じて CDP クライアントを操作します。

### func Connect(context.Context, string) (*Client, error)

```go
func Connect(ctx context.Context, u string) (*Client, error)
```

Chrome の Debugger URL (`http://...` または `ws://...`) を指定して、新しい CDP クライアントのインスタンスを返す関数です。

仕様書ができたら LLM に与えてコードを生成する。コードの生成は blueprint という名前の自作コマンドを用意してあり、次のように実行している。仕様書の長さなどにもよるが、自分は Long Context の機能が活かせる Gemini 2.5 Pro をモデルとして選択して使っている。

$ blueprint implement spec.md

しばらくすると仕様書に従って実装されたコードが生成されるので、仕様書のインターフェース通りになっているか実際に使ってみて確認をする。挙動の問題や仕様の考慮漏れなどがあれば仕様書を修正してコードを再生成したり、手で直接コードを修正したりしている。

$ ls -1
spec.md
cdp.go
cdp_test.go
dom.go
dom_test.go
...

コードには LLM が良きにはからって実装してくれたバリデーションなどの細かい仕様が含まれることがあり、これを仕様書にフィードバックしたくなることがあるので、そのためのコマンドも用意している。このコマンドを実行すると、生成されたコードの内容をもとに仕様書の内容を修正してもらえる。

$ blueprint revise .

あとは仕様書の修正と生成を繰り返してコードを仕上げていく。なんかうまく動かないぞ、となった部分はだいたい仕様書にも明確な記載がなかったりするので、仕様書を仕上げていくことがよりよいコードの出力につながっていく。

こういった「仕様をまず宣言して、そこからコードを生成して使う」という開発フローは個人的にはわりとしっくりきているのだけど、1回のコード生成に数分かかる場合があり、待ち時間が長いのが不満としては残る。とはいえ並列に開発するとか、差分更新ができるようにするとか、うまいやり方は色々ありそうではある。

LLM が生成したコードをもっと受け入れていく

正直なところ、LLM が生成したコードをそのまま受け入れて使っていくことにまだ一定の抵抗感はある。とはいえ、先に書いたように SaaS の API なんかは中身を知らずに使っているし、OSS のライブラリなんかも「便利な機能があるから」という理由で中身を知らずに使っているわけで、(もちろん全ての外部コードはセキュリティやリスクなども考慮した上で使うという大前提はあるが) LLM が生成した「動く」コードはもっと受け入れていくべきなのかもしれない。いずれは LLM が生成したコードの方が信頼できる、みたいな時代もやってくるのかもね。

Moto Ishizawa

Moto Ishizawa
ソフトウェアエンジニア。ロケットの打上げを見学するために、たびたびフロリダや種子島にでかけるなど、宇宙開発分野のファンでもある。