AIが直したコードを別のAIに反証させるパイプライン
同じモデルに自分のコードをレビューさせると、自分の出した結論をまた承認する。ほぼすべてのコードをAIと一緒に書く1人開発者が、一つのAIの仕事を別のAIに「反論してみろ」とぶつけるクロスチェックのループを常設パイプラインに固めた話。セルフレビューが見逃したバグを3ラウンド連続で捕まえた実例とプロンプト設計。
要点まとめ
自分の仕事を自分で審査するAIは甘い。別のモデルに「この修正は間違っていると仮定して反論しろ」と命じると、セルフレビューが見逃した欠陥が出てくる。ログアウトバグ一つから3ラウンド連続でより深い欠陥が見つかった事例、反証フレーミングや証拠形式といったプロンプト設計、そしてこのループをいつ回すかの基準。
目次
AIエージェントにコードレビューをさせたことがある人なら知っている。他人のコードはかなりよく見つける。ところが自分がたった今書いたコードをレビューさせると、不思議なほど甘くなる。「実装は意図に沿っています」という答えが返ってきて、実際に動かすとバグはそのまま残っている。
当然の話だ。同じモデル、同じコンテキストの中では、コードを書くときに犯した思い込みを、レビューのときも同じように犯す。自分の出した結論を自分でもう一度承認する構図なので、レビューというより復唱に近い。
だから私は審査を外に出した。Claudeが直したコードをCodexに渡して、注文は一つだけ。「この修正は間違っていると仮定して、反論してみろ。」
3ラウンド連続でより深いバグが出てきた
このループの価値を最初に実感したのはログアウトのバグだった。症状は単純だ。ログアウトボタンを押しても、たまにログアウトされない。Claudeが原因を見つけて直し、コードを読む限りはもっともらしかった。私もあやうくマージするところだった。
Codexに反証させたら、1ラウンド目でこういう答えが来た。修正そのものは正しいが、認証ページのリダイレクトがそのコードパスに到達する前にフローを横取りするので、修正が実行される機会がない。直したコードはデッドコードだったのだ。
リダイレクトまで直して、もう一度反証させた。2ラウンド目でまた出た。今度は二重実行を防ぐために立てておいた処理中フラグが問題だった。特定のタイミングでは、このガードのせいでログアウト要求そのものが何もしないno-opになる。3ラウンド目でようやく「反論の材料が見つからなかった」という答えが出て、そこで初めてマージした。
3ラウンドとも、セルフレビューは通っていたコードだ。そして3ラウンドとも、反証者は「コードが良さそうに見えるか」ではなく「この主張が崩れるシナリオはあるか」を探したから捕まえられた。
その後もパターンは繰り返された。サーバー修正にセルフレビューを2回回してもなお残っていたDB制約の抜け値を一つ、クロスチェックが捕まえてマイグレーションを追加したことがある。リトライロジックの修正では、特定のタイムアウト応答を恒久的な失敗と誤分類する境界ケースを、反証者がブロック判定で突き返した。どれも「もっともらしくて通りかけた」ものばかりだった。
レビューではなく反証をさせる
プロンプト設計でいちばん大事なのはフレーミングだ。「レビューして」と頼むと、モデルは称賛半分、些細な指摘半分の無難な答えを返す。「この修正は間違っていると仮定して反論しろ。反論に失敗したときだけ通せ」と言うと態度が変わる。通過がデフォルトではなく、反論失敗の結果になるからだ。
これに三つを重ねる。
- 証拠の形式を強制する。すべての指摘には、ファイルと行番号、そしてその欠陥が実際に発火する再現シナリオを付けなければならない。「この部分が危なそうです」は受け付けない。シナリオが作れないなら、その疑いは指摘ではなく棄却だ。実際、反証者が自分の疑いを自分で反証して引っ込めるケースがかなり多く、これが誤検知を濾し取る中核の仕掛けになっている。
- 判定を等級で受け取る。通過 / 軽微な修正のうえ通過 / ブロック、の3段階で十分だ。等級があれば、「指摘はあるがマージしてよい」状態と「マージしてはいけない」状態が混ざらない。
- 独立したコンテキストで回す。反証者にはdiffと関連コードだけを渡す。作業過程の会話や「なぜこう直したのか」という物語は渡さない。物語を共有した瞬間、反証者も同じ思い込みに染まる。
運用上のコツも一つ。反証者は読み取り専用で回す。コードを直す権限を与えると、指摘する代わりに自分で直そうとし、そうなると今度はその修正を誰が検証するのかという問題がまた生まれる。役割は審査に固定し、修正は元の作業者に差し戻す。
いつ回すのか
すべての変更にこのループを回すわけではない。タイポ修正に反証者を呼ぶのは無駄だ。必ず回すのは、元に戻しにくい変更だ。DBマイグレーション、データ削除、決済や認証の経路がここに入る。「直した」という主張が懸かった変更も対象だ。バグ修正には本来、修正前にバグが再現し、修正後に消えたという対が必要で、反証者はこの対の隙間をよく見つける。残る一つは、私がコードを全部は読まないだろう変更だ。AIが作った大きなdiffを人間がすべて読むのは、現実には破綻する。読まないのなら、せめて別のモデルに敵対的に読ませる。
コストの話を避けては通れないが、反証1ラウンドはモデル呼び出し数回ぶんの値段だ。プロダクションに出たバグ一つを人間が追跡するコストと比べれば、悩む余地はなかった。上のログアウトバグがデプロイまで出ていたら、私は「たまにログアウトできない」という報告だけを手がかりに、リダイレクトとフラグガードの二層を自力で掘り下げるはめになっていたはずだ。
別のモデルでなければならない理由
同じモデルの新しいセッションでも、ある程度の効果はある。コンテキストが分離されるだけで、物語に染まる問題は消えるからだ。ただ、数か月回してみた結論として、モデルそのものが違うときに捕まるものの方が明らかに多い。
モデルごとに注意の向く場所が違う。一方が状態管理のタイミング問題に敏感なら、もう一方は契約違反と境界値に敏感、という具合だ。同じモデルの2セッションは同じ死角を共有するが、別のモデルは死角がずれる。クロスチェックの値打ちは、まさにそのずれから生まれる。
この構図には見覚えがあるはずだ。人間のチームがコードレビューを作者以外に任せる理由と同じだ。AIと働くからといってその原則が消えるわけではなく、むしろ安く常時適用できるようになっただけだ。作者ではない審査者、物語に染まっていない目。そして、反論に失敗して初めて下りる承認。人間の組織ではコストが高くてたまにしかできなかったことを、いまはマージの前に常に立たせておける。
関連記事
- 💻 開発
AIエージェントを複数、同じリポジトリに放り込んだら起きたこと
エージェントのセッションを並列に立ち上げれば生産性が倍になると思っていた。実際にはブランチの乗っ取り、他人のブランチに載ったコミット、reflogで拾い上げた孤児コミット、同じ機能の二重実装まで、一週間のうちに実際の事故が立て続けに起きた。一つのチェックアウトを共有する並列エージェントが生む事故の類型学と、それを経て立てたルール。
- 💻 開発
AIが「もう終わりました」と言ったとき、それを鵜呑みにしない仕組み
AIコーディングエージェントは、仕事を終えていなくても終わったと言う。テストを走らせずに通ったと言い、再現もせずに直したと言う。ほぼすべてのコードをAIと一緒に書く1人開発者が、完了を言葉ではなく証拠でしか認めないようにした検証ゲートの話。
- 💻 開発
エージェントの「完了」を信じないことにした - verify.shゲート、prove-it
AIコーディングエージェントはテストを回していないのに「テスト通過」と報告する。嘘ではなく構造の問題だ。報告を検査に変えるオープンソースのゲートprove-itを作りながら学んだこと - そして自作の5行フックが静かに失敗する4つの形。