AIにリポジトリを渡して「これを直して」と頼むと、ファイルは変更されます。でも、それで作業が終わったことにはなりません。テストを実行していないかもしれないし、テストは通っていても要件を満たしていないかもしれません。途中で失敗した時に、どこまで進んだのかも分からなくなります。
このあたりを毎回その場しのぎで扱うのが面倒になって、小さなコーディングハーネスを作り始めました。
まずはモデルを賢くしない
最初から実際のモデルにつなぐと、失敗した理由がモデルなのか、ツール呼び出しなのか、検証処理なのか分からなくなります。
そこで、固定した応答を返す Fake Model で一通りの流れを通すことにしました。
契約を読む
↓
隔離した worktree を作る
↓
探索してパッチを当てる
↓
許可された検証を実行する
↓
失敗したら原因を渡して修復する
↓
レビュー用のレポートを残す
足し算のような小さいフィクスチャでも、パッチが適用され、検証が走り、結果がレポートに残れば、ハーネスの縦切りとしては確認できます。
この順番にしておくと、後から実際のモデルやプロバイダーを差し替えても、最低限の完了条件は変わらない、というのが狙いです。
元のリポジトリを触らない
モデルに作業をさせる時、元の作業ツリーを直接渡すのは少し怖いです。
今回は Git の worktree を使い、1回の実行ごとに専用の作業場所を作るようにしました。探索、編集、テストはその中だけで行います。元のリポジトリには、最後に人が確認してから変更を戻します。
パッチを受け取るところでも、いきなり適用せず git apply --check を先に通します。パスの ..、シンボリックリンク経由の脱出、秘密ファイルへのアクセスも止めます。
ここはモデルの善意に任せない方がよさそうでした。モデルが悪いというより、失敗した時の影響範囲を小さくしておきたい、という話です。
検証が通るまで成功にしない
最初の実装では、モデルの最終メッセージを見て成功扱いにしそうになりました。「直しました」で完了になっちゃうやつです。
それだと話にならないので、実行状態を次のように分けました。
- 探索中
- 編集中
- 検証中
- 失敗の診断中
- 修復中
- レビュー待ち
- 完了
検証を通過していない実行は、どれだけ自信ありげな文章を返しても完了になりません。失敗した場合は、検証結果を次の修復ループに渡します。ただし無限に直し続けると別の問題になるので、試行回数とトークン、実行時間に上限を置きました。
途中の状態はチェックポイントに保存します。プロセスが落ちても、実行中だった場所から再開できるようにしました。再開時に同じ編集を最初からやり直さないで済むだけでも、ログを追う負担がかなり減ります。
プロバイダーを混ぜない
実際のモデルを使う段階では、サブスクリプション経由のバックエンドと OpenAI 互換のプロバイダーを、名前付きプロファイルとして切り替えられるようにしました。
認証情報そのものは設定ファイルやトレースに保存しません。子プロセスへ渡す環境変数も必要なものだけにし、APIキーや Bearer トークンらしい文字列はログへ書く前に伏せます。
モデルを切り替えること自体よりも、認証・課金の経路と、リポジトリを変更する経路を分けて考えられるのが良かったです。
CLIから先に使う
ブラウザ画面を作る前に、対話型 CLI と非対話の実行コマンドを用意しました。
契約を確認
実行
検証結果を表示
失敗したら再開または修復
レポートを読む
実行ごとのレポートやトレースを .ri/runs に置く形にして、何を考えていたかではなく、何を実行して何が確認できたかを追えるようにしました。
パッケージ化した tarball を一時ディレクトリへインストールするテストも入れました。開発ディレクトリでは動くのに、配布物にファイルが足りない、というありがちな問題を先に見つけたかった、というやつです。
おわり
AIコーディングの難しさは、パッチを書く部分だけではなさそうでした。どの範囲を触らせるか、何を検証したら完了なのか、失敗した時にどこから再開するのかを先に決めておかないと、モデルの出力を眺めるだけになってしまいます。
まだ実際の大きなリポジトリで万能に動くものではないですが、「モデルが成功と言った」ではなく「隔離された場所で必須の検証を通った」を完了条件にできたのは良さそうです。
今なら、コード修正だけでなく、ドキュメント生成や依存更新のような定型作業にも同じ枠を使えそうだと思っています。