前回、AI coding agentは、なぜ毎回リポジトリを調べ直すのか で、
AI coding agentがRepositoryを毎回調べ直す問題について書きました。
その最後のほうで、Model と Runtime の役割分担に少しだけ触れました。
AI Model
↓
意味を考える
Runtime
↓
事実・状態・制御を保証する
方針としては、これで合っていると今も思っています。
ただ、この一行を実際にコードにしてみると、まったく足りませんでした。
「Modelに任せすぎない」という方向だけを見ていたのですが、実装して分かったのは、厄介なのはむしろ逆方向だったということです。
今回は、その話を書きます。
最初は「賢いなら任せればいい」と思っていた
正直に書くと、私は最初、モデルが賢いなら多くを任せればよいと考えていました。
Repositoryを読むのも、どこを読むか決めるのも、変更するかどうかも、全部Modelに判断させればいい。
Runtimeは、その手足を提供するだけでいい。
この考えが変わったのは、後述する1つの不具合に出会ったときです。
先に、そこへ至る前提から書きます。
Runtimeが意味を推測し始める
Model に任せすぎないのと同じくらい、Runtime が意味判断を始めてしまうことに注意が必要でした。
例えばAI Modelが、
foo.py を確認したい
と要求したものの、実際には foo.py が存在しなかったとします。
このときRuntimeが、
たぶん foo_service.py のことだろう
と勝手に補完すると、Runtimeまで意味判断を始めたことになります。
こうなると、あとから記録を見たときに、
その判断を下したのがModelなのかRuntimeなのかが分からなくなります。
Runtimeは便利な「第二のAI」ではなく、Modelが安全に仕事をするための実行基盤にしたい。
そう考えて、R2では補完をしないことにしました。
「補完しない」を実装すると「黙る」になる
では、存在しないpathを要求されたRuntimeは、何を返すのか。
R2の現在の答えは、
何も返さない
です。
存在しないpath、Allowed Pathsの外、空のディレクトリ、ヒットしないパターン。
これらはすべて、同じように何も生まれずに終わります。
Modelは次のラウンドで、自分が要求したファイルがContextに増えていないことに気づき、別の探索をします。
この設計には、はっきりした利点があります。
不正な要求の扱いが全部同じになることです。
「存在しない場合はこう、スコープ外の場合はこう、権限がない場合はこう」と分岐を増やすと、その分岐自体がRuntime側の判断になっていきます。
一様に何も返さないなら、Runtimeは何も解釈していません。
一方で、代償もあります。
Modelには理由が伝わりません。
「そのファイルは無い」のか「読んではいけない場所だった」のかを、Modelは区別できない。
ここは今も、きれいに解けたとは思っていません。
一様さを取ると説明が減り、説明を取るとRuntimeが意味を持ち始める。
現状のR2は前者を選んでいる、というだけです。
沈黙が、誰かのせいになっていた
そして、この「黙る」設計が、私の考えを変えた不具合につながります。
R2にはRepository Indexがあります。
前回書いた「何がどこにあるか」の索引です。
ここで、次のことが起きました。
1. ファイルが削除される
2. しかし索引には、まだそのpathが残っている
3. Modelが索引を見て、そのpathを要求する
4. Runtimeは検証を通す(スコープ内だし、索引にもある)
5. 実際に読もうとして、ファイルが無いので黙ってスキップする
6. 結果、Contextが空のまま変更提案の生成に進む
7. 当然、まともな提案は出ない
8. 記録には「Provider応答が不正」と残る
最後の行が問題でした。
PROVIDER_RESPONSE_INVALID。
つまり記録の上では、
モデルの応答品質の問題
として残っていたのです。
しかし実際に起きていたのは、
Coreが、モデルに渡せる中身を1つも持っていなかった
ということでした。
Modelは、空の材料から何かを作れと言われて、作れなかっただけです。
ここで気づきました。
責務分離は、うまくやると効率が上がるという話ではありません。
間違えると、Runtimeの落ち度がModelの落ち度として記録されます。
そして記録が間違っていると、次に何を直すべきかも間違えます。
このとき私は「Providerの応答品質が悪いのでは」と疑っていました。プロンプトを直そうとしていたのです。
原因はプロンプトではなく、Runtimeが空の材料を渡していたことでした。
現在のR2は、変更提案の生成を呼ぶ前に、Core自身が「読めたものが1つも無い」と判定して、そう記録します。
Providerのせいにはしません。
AIの主張と、確認済みの事実は別
この経験から、R2で最も重視するようになった規約がこれです。
AI Modelが、
この変更で問題ありません
と言っても、実際にテストしていなければ、
Validation passed
とは言えません。
同じように、
ファイルを更新しました
というModelの主張と、
実際にファイルが変更された
というシステム上の事実も別です。
R2では、この2つを同じフィールドに入れないようにしています。
Taskの記録は、こうなっています。
Task Record
├─ Providerが宣言したもの (出所: Provider)
└─ Coreが計算・検証したもの (出所: Core)
この2つは、片方がもう片方を上書きしません。
平坦化して1つの「結果」にもしません。
両方をそのまま保持して、両方を表示します。
だから、
Provider:
この変更で問題ありません
Core:
pytest exit code = 1
Result:
Validation Failed
という記録がそのまま残ります。
Providerが何と言ったかは消さない。
ただし、結果は成功にしない。
「主張と事実を分ける」は、統計にも効く
これは大きな判断だけの話ではありませんでした。
R2は、探索が実際に何をしたかを数えています。読んだファイル数、検索回数、path要求の回数などです。
ここで、Modelが3つのpathを要求し、その全部がスコープ外だったとします。
path要求の回数は、いくつと数えるべきか。
R2は 0 と数えます。
Modelが「要求した」ことは事実ですが、実際には1つもpath読み取りが起きていないからです。
宣言があったからといって1と数えると、探索の測定値が「Modelが何を言ったか」で膨らみ、「実際に何が起きたか」を表さなくなります。
同じことが、探索の終了判定にもあります。
Modelは「もう十分な情報が集まりました」と宣言できます。
しかし探索が終わる理由は、それだけではありません。
SUFFICIENT … Providerが十分だと宣言した
NO_PROGRESS … そのラウンドで何も得られなかった
(ラウンド上限) … Core側の打ち切り
この3つは、別の語彙として記録されます。
「Modelが十分だと言った」のか「Coreが打ち切った」のかは、後から区別できます。
Modelの「十分です」は、終了条件のうちの1つでしかありません。
ただし、握った事実はModelに返す
ここで1つ、失敗した設計の話を書いておきます。
責務を分けると、つい遮断したくなります。
R2では以前、Validationが失敗して修正を試みるとき、Modelに渡していた情報が、
declared validation did not pass
という固定の一文だけでした。
Runtime側は、どのコマンドがどのexit codeで落ちて、どんな出力を吐いたかを全部握っています。
にもかかわらず、Modelには「通らなかった」としか伝えていなかった。
これでは、Modelは推測で修正するしかありません。
当然、同じところで失敗します。
今は、失敗したステップごとに、
コマンド
exit code
出力(範囲を制限し、secretを除去したもの)
を渡しています。
責務分離は、情報を渡さないことではありませんでした。
判断を渡さない
事実は渡す
この2つは、まったく別のことです。
実は、三者目がいる
ここまでModelとRuntimeの2つで書いてきましたが、実装を進めると、どちらでもない決定が出てきました。
例えばR2では、Modelが「このコマンドを実行したい」と自分で考案できます。
失敗したテストを1件だけ再実行する、変更予定のファイルだけlintする、といった用途です。
では、そのコマンドは実行されるのか。
決めるのはRuntimeではありません。
Modelでもありません。
ユーザーです。
ユーザーが事前に定めた許可ルールに合致するか、合致しないなら、その場で承認するかどうか。
Runtimeは「安全そうだから通す」という判断をしません。
同じ構造が、他にもあります。
どこを読んでよいか (Allowed Paths) → ユーザーが決める
何をもって検証とするか (Validation Plan) → ユーザーが決める
そのコマンドを実行してよいか → ユーザーが決める
Runtimeがやるのは、決まったルールを確実に適用することであって、ルールそのものを決めることではありません。
なので、今のR2の分け方は、正確にはこうです。
意味を考える → AI Model
事実と状態を保証する → Runtime
許可を与える → User
「AIとRuntime」の二分法で設計していたときは、この三者目を暗黙にRuntime側へ寄せていました。
そうすると、Runtimeが少しずつ「良かれと思って」判断を持ち始めます。
補完をしないと決めたのと、同じ理由で、ここも分けています。
まとめ
AI coding agentを作ると、つい何でもAI Modelへ任せたくなります。
しかしRepository Taskには、性質の違う仕事が混ざっています。
私は現在、こう分けています。
意味判断 → AI Model
事実・状態・制御 → Runtime
許可 → User
そのうえで、実装して分かったことが3つあります。
1. Runtimeを賢くしてはいけない
補完も推測もしないと決めると、Runtimeは黙ることになります。不親切ですが、誰が判断したかは曖昧になりません。
2. 責務を間違えると、記録が嘘をつく
Runtimeが空の材料を渡したことが、Modelの応答品質の問題として記録されていました。記録が間違うと、直す場所も間違えます。
3. 判断は渡さないが、事実は渡す
分離を遮断と取り違えると、Modelは推測で修正を試みることになります。
AIの主張と、確認済みの事実。
この2つを最後まで混ぜずに持ち続けることが、継続してRepositoryを扱えるAgentの条件なのではないかと考えています。
現在、この考え方を R2 Fugu Agent Runtime という個人開発のAI coding agent runtimeで検証しています。
ところで、この記事に出てきた「変更提案の前に読めるものが無いと判定する」という修正には、いつ・なぜそう決めたのかという記録が残っています。
R2には、設計判断そのものを保存する仕組みがあります。
次回は、
「AIも人間も設計判断を忘れる ― Repository Decision Memoryを作った理由」
について書く予定です。
関連記事
このシリーズ(Qiita):
- AI coding agentは、なぜ毎回リポジトリを調べ直すのか ― Repository理解を使い捨てにしない設計 — 前回。Repository Index と Repository Knowledge の分離、根拠と鮮度を持った理解の再利用
この製品を「設計も実装もレビューも AI に任せて」作っている開発プロセスについては、Zenn に別のシリーズを書いています:
- 設計も実装もレビューもAIがやる個人開発で、精度をどう出しているか — 全体像
- 作業指示書は、最初に書かせない ― 設計AIとのやり取りを全部見せる — 設計 AI 編