本記事の内容は私(R2-san)の体験(実装コード)・判断・指示に基づきますが、文章作成は生成 AI が行い、私の監修・確認を経て公開しています。
前回、失敗したら、何も変えない で、AI モデルが出した変更案がファイルへ当たるまでの話を書きました。当てる直前に控えを取り、失敗したら何も変えず、あとから丸ごと戻せるようにする、という話です。
あの記事では飛ばした工程があります。当てたあとに走る検証と、探索の途中で走るコマンドです。この2つを決めているのは、AI モデルでも、R2 本体(Runtime)でもありません。
第2回で、意味を考えるのは AI モデル、事実と状態を保証するのは Runtime、そして許可を与えるのは三者目 = ユーザー、と書きました。今回はその三者目の中身です。
先に結論を書きます。
R2 は、決まったルールを確実に適用するだけで、ルールそのものを決めません。
そして、この記事の山場は、その原則を守っていたつもりで**「検証に通りました」と嘘をついていた**話です。
何をもって検証とするかを、R2 は決めない
変更を当てたあと、R2 は検証を走らせます。テストや lint です。何を走らせるかは、ユーザーが事前に登録したコマンドだけ。登録が無ければ検証は行われず、R2 が「このリポジトリならたぶんこれだろう」と気を利かせることもしません。
そして、検証が行われなかったときの表示を、途中で直しました。コマンドが0件の検証は、内部では「0件の成功」となり、成功として扱われます。それをそのまま「検証に通った」と表示していました。嘘ではないけれど、読む側には嘘です。今は「検証はスキップされた(検証コマンドが登録されていない)」と明示します。
0件の成功を、実行済みの検証の成功として表示しない。
登録が無い場合に、AI モデル側が検証コマンドを提案することもできます。ただしそれは、承認より前には実行されません。提案の一部として、承認の対象に含まれます。
つまり「見て承認したもの」の中に、実行されるコマンドまで含まれているということです。
育つ許可リスト
もう1つ、探索の途中で AI モデルがコマンドを自分で考案する経路があります。「今失敗したテストだけをもう一度走らせたい」「変更予定のファイルだけに lint を掛けたい」といった仮説検証です。
実行するかどうかは、ユーザーが持っている許可リストとの照合だけで決まります。照合のやり方は、意図的にごく単純なままにしてあります。
実行ファイル名の完全一致 + 引数の先頭部分の完全一致
glob なし。正規表現なし。シェル経由の実行なし。
どのルールにも当てはまらないコマンドは、実行せずにユーザーへ聞きます。そして、その場で許可したものをルールとして保存できます。使い込むほど聞かれる回数が減っていく、育つ許可リストです。実行されたかどうかに関わらず、考案されたコマンドは全部そのままの文字列で記録されます。これが監査の背骨です。なお、拒否ルールだけは AI モデルへ知らせません。「これは拒否されます」は、回避のヒントになるからです。
正直に書いておくと、この経路の安全の根拠は「そのコマンドが安全かどうか」ではありません。コマンドが何をするかを機械的に見分けることは諦めていて、根拠はユーザーの事前承認とその場の承認です。契約にも、そう書いてあります。
「検証に通りました」が嘘になっていた
さて、山場です。
検証コマンドとして、こう登録できてしまっていました。
登録された検証コマンド cd リポジトリの場所 && テストを実行
実際に走ったもの ディレクトリ移動だけ(&& から後ろは黙って捨てられた)
終了コード 0
出力 空
R2 の表示 Validation: passed
シェルで日常的に打つ形です。ところが R2 は、コマンドをシェル経由では実行しません(そのほうが安全だからです)。結果、この1本は「ディレクトリ移動」というコマンドに余計な引数がくっついたものとして扱われ、&& から後ろは黙って捨てられました。ディレクトリ移動は成功し、終了コードは 0。
そして R2 は、堂々と「検証に通りました」と表示していました。テストは1件も走っていないのに。
第2回で「AI モデルの主張と、R2 が確認した事実を混ぜない」と書きましたが、これはもっと手前の問題でした。
確認したつもりの事実が、事実ではなかった。
直し方として、実行方式をシェル経由へ変える案は取りませんでした。安全な実行方式のほうを変えたくなかったからです。実行結果を後から怪しむ案(終了コードが 0 で出力も空なら、たぶん何も走っていない)も捨てました。もともと何も出力しないコマンドまで疑うことになり、しかも疑う根拠が推測だからです。
代わりに、受け付ける時点で弾くことにしました。Task を作るより前に、登録されたコマンドを検査して、「書いたとおりには実行されない形」を拒否します。該当したら、Task の記録すら作りません。エラーには、該当したコマンドをそのまま出して、次にどう書けばよいかを添えます。
同じ検査は、AI モデルが考案するコマンドにも、許可リストとの照合より前に掛けています。自動許可のルールに一致していようが、ユーザーがその場で承認しようが、この形は実行しません。
そのうえで、実行そのものにも枠を掛けました。実行時間には既定の上限があり、無制限には設定できません。出力は上限を持ち、超えたら末尾を優先して残します(失敗の要約は末尾に出るからです)。そして、資格情報の値は切り詰めより先に消します。
危険かどうかを、AI に決めさせない
検証の手前、適用するかどうかを決める段階にも、同じ形の線があります。
AI モデルは提案と一緒に「この変更は低リスクです」と言えます。R2 はそれを、危険度を下げる方向には一切使いません。使うのは、危険度を上げる方向と、「分からない」を書き添える方向だけです。
そして、「分からない」は決して「低リスク」ではありません。判定できなかったことを、安全だったことにしない。
ちなみに、この判定を緩めたことが一度あります。ファイルの削除や改名は、当初それだけで高リスクへ昇格していましたが、今は昇格しません。緩められたのは、削除と改名では「対象ファイルの内容が想定どおりであること」を今も無条件に確かめていて、そちらが代わりに効いているからです。
失敗したという事実も、残す
検証が失敗すると、R2 は AI モデルに修正案を求めます。その修正案づくり自体が失敗することもあります。以前は、そのとき何も残っていませんでした。Task を開き直しても「修正を試みたのか、まだ試していないのか」が読めなかった。
素朴な直し方は「Task の失敗理由を、修正試行の失敗で上書きする」です。これは却下しました。Task がそもそもなぜ失敗したのかという情報が消えるからです。それは、ユーザーが対処すべき対象であり、次の修正案の入力でもあります。
代わりに、専用の記録を足すことにしました。試行1回につき、必ず1件。そして、
- Task の失敗理由は書き換えない
- 状態も変えない
- 修正の残り回数も消費しない
最後の1つが、ささやかに気に入っています。修正案づくりが失敗したというこちら側の都合で、ユーザーが使える修正の回数を削らない。
この判断を残した記録には、却下の理由がこう書いてあります ――「対象利用者はログファイルを読まない」。だから、製品の中で読める形にする必要がありました。
実装して分かった難所 ― 待たないほうが親切
最後に、この領域で一番ひやりとしたところを1つ。
同じリポジトリに対して Task を並行して走らせられるようにすると、こういうことが起こり得ます。
Task A が、あるファイルの「元の内容」を控えて、書き換える
その最中に Task B が、同じファイルの「元の内容」を控える
↑ これは A が書いた後の内容
→ B の控える「元の内容」は、もう元ではない
→ どちらの控えからも、本当の元へ戻れない
戻せるという保証そのものが壊れます。 前回書いたとおり、R2 は「最後は git がある」を前提にしていません。だからこれは致命的です。
対処は、書き込みの経路にだけ、作業ツリーごとに、書き込みを一度に1つしか通さない鍵を掛けることでした。読み取りの経路には掛けません。そして、その鍵が取れなかったら待たずに「今は実行できない」として失敗させ、その事実を記録に残します。
待たないのは意図的です。待つと、待っている間に何が起きているのか、押した本人に分からなくなります。失敗して、もう一度押してもらうほうが分かりやすい。
まとめ
AI coding agent を作っていると、判断を AI モデルへ寄せるか、Runtime へ寄せるかの二択で考えたくなります。でも実際には、どちらでもない決定がありました。
作ってみて分かったことを3つ。
1. R2 はルールを適用するだけで、ルールを決めない
何をもって検証とするか、どのコマンドを実行してよいか。これはユーザーが決めます。「安全そうだから通す」という判断を、R2 はしません。
2. 「確認したつもり」がいちばん危ない
AI モデルの主張を疑うのは、比較的やさしい。難しいのは、自分が確認したはずの事実を疑うことでした。0件の成功を「検証に通った」と表示していたのも、何も走っていないのに「通りました」と言っていたのも、同じ根です。
3. 禁止を増やすのではなく、育つようにする
許可リストは、使い込むほど聞かれる回数が減ります。その場の承認をルールとして残せるからです。安全を「聞く回数」で買い続けると、いずれ中身を読まないまま承認されます。
現在、この考え方を R2 Fugu Agent Runtime という個人開発の AI coding agent runtime で検証しています。
次回は、**実際のリポジトリに向き合ったときに出てくる問題(複数リポジトリの重ね登録と、多言語の索引)**について書く予定です。
あとがき
この R2(AI エージェント)を作り始めた当初は、開発に使っていた AI エージェントへの不満もあって、それを解消しようという意気込みがありました。ところが今では、開発エージェント(および AI モデル)で不満の多くが解消されて、R2 への開発意欲は減退中です(先行開発のつもりが、いつの間にか後追い開発に変わりました)。特にこれから作り込もうとしていたセッション間のメッセージ機能は、AI モデルのレベルから実装が進んでいて、AI エージェント(R2)のレベルだけでは見劣りがしてしまいます。
記事の内容も、AI モデルの振る舞いではなく(それは AI モデルで変わります)、考え方やアプローチを読み取っていただければと思います。
関連記事
このシリーズ(Qiita・製品設計):
- AI coding agentは、なぜ毎回リポジトリを調べ直すのか ― Repository理解を使い捨てにしない設計
- AIに全部判断させるのをやめた ― AI ModelとCoding Agent Runtimeの責務をどう分けたか
- AIも人間も設計判断を忘れる ― Repository Decision Memoryを作った理由
- 失敗したら、何も変えない ― AIの変更提案を、リポジトリに当てるまで
この製品を AI に作らせる開発の進め方については、Zenn に別のシリーズを書いています:
- 設計も実装もレビューもAIがやる個人開発で、精度をどう出しているか — 全体像
- 作業指示書は、最初に書かせない ― 設計AIとのやり取りを全部見せる — 設計 AI 編
- 実装AIには、指示書のパスを渡すだけ ― コピペで始めた運用が仕組みになるまで — 実装 AI 編
- レビューAIには、報告を信じさせない ― 突合レビューをどうさせているか — レビュー AI 編