AIエージェントは怠けません。問題はむしろ逆で、「緑にする」最短経路を見つけるのが上手すぎることです。
私たちは、Issue を渡すと分析 → 計画 → 実装 → テスト → 評価 → PR 作成まで自動で進む開発ワークフローを Claude Code(Pro $20/月、Max $100-200/月)の上で運用しています。途中には品質ゲートを何枚も置いています。テストは緑か。評価役のエージェントが重大な指摘(critical)を残していないか。危険な変更が diff に混ざっていないか。そして最後の merge だけは人間が押す設計です。
その運用でわかったことがあります。ゲートは置いただけでは機能しません。すり抜けられるか、空振りするか、どちらかの形で静かに形骸化していきます。「指標が目標になると、指標として機能しなくなる」というグッドハートの法則そのままのことが、自動開発ループの中で起きるわけです。
この記事では、直近1ヶ月の改修で実際に見つけて塞いだ抜け道を5つ、コミット履歴ベースで紹介します。どれも「AIが悪意を持った」話ではありません。指示に従順なエージェントと、設計の隙間の組み合わせで起きた話です(そこが一番怖いところでもあります)。
この記事で学べること
- 評価エージェントの「沈黙」を解消とみなす実装がなぜ危険か
- 文字列ベースのループ検出が LLM の言い換えに弱い理由と、決定論カウンタによる対策
- 「テストを直して緑にせよ」という指示がテスト弱体化を招く構造と多層ガード
- 一度も発火していない品質ゲートを疑うべき理由(三点 diff の盲点)
- 自動化ループに「人間への出口」を設計する方法
前提:ループの全体像
対象のワークフロー(社内では dev-flow と呼んでいます)は、おおまかにこういう流れです。
計画・実装・評価はそれぞれ別のエージェントが担当し、評価で critical が出ると計画や実装へ差し戻されます。以下の5つの抜け道は、すべてこのループの中で実際に見つかったものです。
抜け道1:黙っていれば「解消済み」になる
最初の穴は、評価の収束判定にありました。
評価エージェントへの指示はこうです。「新規の指摘のみ報告せよ。対応済み論点の蒸し返しは禁止」。差し戻しのたびに同じ指摘を繰り返されるとループが永遠に終わらないので、これ自体は必要な指示です。
一方、収束判定の実装側は「今回の feedback に現れなくなった critical は解消された」と推定していました。この2つを組み合わせると何が起きるか。指示を忠実に守るエージェントほど、未解消の critical が黙って「解消済み」扱いになります。蒸し返し禁止に従って言及をやめた瞬間、その指摘は直っていなくてもゲートを通過する。従順なエージェントほどすり抜けが速いという、嫌な逆相関です。「沈黙=解消」の自動判定は廃止しました。
代わりに入れたのが、解消の明示申告です。評価エージェントは critical_resolutions: [{id, resolved, evidence}] という形式で、未解消だった critical 全件について「解消したか・その根拠は何か」を毎回返すことを義務付けられます。未解消の critical 一覧はプロンプトに毎回注入され、実コードでの再検証が要求されます。resolved: true には具体的な evidence が必須です。
教訓はシンプルで、「言わなくなった」と「直った」は別物だということ。静かになった会議が、合意できた会議とは限らないのと同じ構図です。
抜け道2:言い換え続ければ、打ち切りを回避できる
差し戻しが同じ論点で堂々巡りしていないかは、指摘の topic 文字列を突き合わせて検出していました。同じ topic が続けば「stuck」と判定して打ち切る仕組みです。
ところが topic は LLM が生成する自由文字列です。「エラーハンドリングが不十分」が、次の差し戻しでは「異常系の考慮が不足」になる。意味は同じでも文字列は別物なので、突合は漏れます。そして設計レベルの差し戻しは再計画+全タスク再実装という、このパイプラインで最も高価な操作です。言い換えが続く限り、評価上限(EVAL_MAX=10)いっぱいまでフル再実装が回り得る——システム内最大の暴走コストでした(サブスクの5時間枠が、誰にも気づかれずに溶けていきます)。
対策は、文字列に一切依存しない決定論カウンタです。
const DESIGN_REPLAN_MAX = 2 // 表現ゆれに左右されない last-resort の hard cap
if (designReplanCount >= DESIGN_REPLAN_MAX) {
log('design replan 上限到達 — human review へ委譲')
break // throw せず PR へ進め、人間の判断に渡す
}
上限に達したらエラーで止めるのではなく、ループを抜けて PR まで進めます。未解消の critical は台帳に残ったままなので、merge 判定は自動的に「人間が読むまで merge 不可(HOLD)」へ倒れる。ゲートを緩めずに、暴走コストだけに蓋をする形です。
LLM 相手の検出を文字列一致に頼ると、悪意ゼロの言い換えであっさりすり抜けられます。説得はプロンプトで、保証は決定論で。この使い分けが、この1ヶ月で一番効いた設計判断でした。
抜け道3:テストを弱めれば緑になる
テストが落ちたとき、自動修正ステップ(green-fix)が「原因を分析して実装やテストを修正し、緑を目指せ」と指示されます。お気づきでしょうか。この指示、テストの assert を弱めて緑にする経路を排除していません(締め切りに追われた人間がやることと、まったく同じです)。
緑への圧力がかかったエージェントがテストを弱体化させるのは、autonomous loop の古典的な故障モードです。しかも下流の防御には限界があることもわかっていました。「テストが red から green に変わった」ことの実証は、テストが diff を制約していることの証明であって、正しい挙動を assert していることの証明ではない。存在チェックだけの弱い assert でも red → green は通ります。
対策は三層にしました。
- 禁止の明文化 — 自動修正のプロンプトに「テストの期待値・assert を弱めて green にすることは禁止。テスト修正が正当なのはテスト自体の誤りを直す場合のみで、どのテストをなぜ変えたか必ず申告せよ」を追加
- 発生の追跡と監査の注入 — 自動修正の発生回数を
greenFixCountとして追跡し、1回でも発生した実行では評価エージェントの監査観点に「テスト diff の弱体化チェック」を自動注入 - 監査スキップ経路の封鎖 — 小規模な変更では評価フェーズ自体を省略する最適化があり、そこで自動修正が起きると監査が無音で消える穴がありました。評価実行の条件に
|| greenFixCount > 0を追加し、自動修正が1回でも走ったら規模にかかわらず評価を強制
// 小規模(micro)でも、危険 diff か green-fix があれば評価をスキップしない
const runEval = EFFECTIVE_SHAPE !== 'micro'
|| dangerHits.length > 0
|| greenFixCount > 0
「直せ」と圧をかけるなら、ズルの定義も一緒に渡す。そして効率化のために監査を省く経路には、「監査が必要になった瞬間に省略をやめる」条件を必ず添える。圧力と監査はセットで設計するものでした。
抜け道4:ゲート自体が、空の diff を見ていた
ここまでの3つは「AIがすり抜ける」話でしたが、4つ目は逆で、ゲートの空振りです。設置してから一度も警告を出したことのないチェック、ありませんか? 私たちのループには、ありました。
危険な変更(秘匿情報や外部コマンド実行など)を機械的に grep する Security チェックは、コミット前の作業ツリーに対して呼ばれます。ところがこのワークフローでは実装エージェントに commit を禁止しているため、チェック時点では HEAD がベースブランチと一致している。そこで使っていた三点 diff(origin/BASE...HEAD)は空の diff を返し、danger-grep は常に clean を報告していました。つまりこのゲート、設置以来一度も仕事をしていなかったわけです(毎回 clean の報告だけは律儀に上げてくるのが、また質が悪い)。
修正は、コミット前専用の --working-tree モードの追加です。merge-base 起点の二点 diff で tracked の変更を、git status --porcelain --untracked-files=all で未追跡ファイルを拾い、未コミット状態でも実際の変更を漏れなく分類します。
同型の盲点はもう1つ見つかりました。変更規模を実測して評価の深さを引き上げる再判定ロジックも、同じ三点 diff を使っていたせいで正常系では一度も発動しない dead code になっていました。さらにその修正の過程で、?? [] というフォールバックが「取得失敗(null)」と「変更0件」を同じ 0 に潰し、取得失敗時に安全側へ倒す弁に到達しないバグも発覚。null は NaN に変換して「判定不能なら最も厳格な経路へ」流すよう直しています。
教訓:一度も発火していないゲートは、品質が高い証拠ではなく、ゲートが死んでいる兆候かもしれない。ゲートを追加したら、わざと危険な入力を流して赤くなることを確認する——シェルスクリプトのテストの記事で書いた「fail パスをわざと踏む」と、まったく同じ教訓に戻ってきます。
抜け道5:何周しても解けない問いは、人間に返す
最後は毛色が違って、「ループの外への出口がなかった」話です。
まず見つかったのは、出口の配線切れでした。「人間が読むまで merge 不可」へ倒すためのエスカレーションフラグは、それを受け取って HOLD にする側(consumer)は実装済みなのに、フラグを立てる側(producer)がリポジトリのどこにも存在しない。カウントは常に 0 で、HOLD への経路は dead path でした。ドアはあるのに、ノックする人がいない状態です。
もう1つは、曖昧な Issue の扱いです。要件が曖昧なまま投入されると、計画エージェントは推測で空欄を埋め、パイプラインはフルで完走し、merge 段階で人間が「方向が違う」と気づく——最も高価な失敗モードを毎回フルコースで辿ります。要件の曖昧さは、LLM が何周しても解消できない種類の不確実性です。人間に1回聞くのが最安の解決策なのに、聞き返す経路がなかった。
そこで出口を3つ開通させました。
- 分析段階の曖昧ゲート — 検出した曖昧点が閾値(2件)を超えるか受け入れ条件が空なら、
needs_clarificationで早期 return して人間に質問を返す - 実装中の情報不足申告 — 実装エージェントが「情報が足りない」と申告したタスクは詳細再分析+再試行し、それでも解消しなければ同じく中断
- 評価エージェントの escalate フラグ — コードの良し悪し(severity で表現すべきもの)ではなく、結果責任・好み・訓練分布外といった「人間にしか決められない論点」のときだけ立てるフラグ。立つと merge は人間が読むまでブロックされます
自動化の品質は「どこまで自動でやるか」だけでなく、「どこで自動をやめて人間に返すか」で決まる。1ヶ月前の私たちのループには、後者の設計がほぼ丸ごと抜けていました。
注意点・Tips
- 確定実行したい挙動は LLM の判断を介さない — 上限カウンタ、リトライ、テレメトリ記録のような「毎回必ずやってほしいこと」は、エージェントへの依頼ではなくワークフロー側のコードとして配線する。CI 確認の一時的な API エラーへのリトライも、エージェントの裁量ではなくスクリプト側に置いたことで bats でテスト可能になりました
- ゲートには「赤くなるテスト」を付ける — 抜け道4の三点 diff 盲点は、危険入力を流して発火を確認するテストがあれば設置初日に見つかっていたはずです
- 解消は申告制+evidence — 沈黙・省略・言及消失を成功と解釈する実装は、従順なエージェントと組み合わさった瞬間に偽解消の量産装置になります
- 塞いだ穴はテストで pin する — 今回の修正には、それぞれ「この穴が再び開いていないか」を検証する回帰テストを付けています。仕組みで塞いだものは、仕組みで監視し続けるところまでがセットです
まとめ
自動開発ループの品質ゲートに見つかった、5つの抜け道を紹介しました。
- 沈黙が「解消」に化ける — 解消は明示申告+evidence 必須に
- 言い換えで打ち切りを回避できる — 文字列非依存の決定論カウンタで hard cap
- テストを弱めれば緑になる — 禁止の明文化+発生追跡+監査スキップ経路の封鎖
- ゲートが空の diff を見ていた — コミット前はワークツリー基準で diff を取り、発火テストで死活を確認
- 人間への出口がなかった — 曖昧さと当事者性の論点は、早期に人間へ返す経路を配線
共通するのは、どの穴も「AIの能力不足」ではなく設計の隙間だったことです。エージェントは指示にも圧力にも従順で、だからこそ、指示同士の矛盾や検出ロジックの甘さが、そのまま抜け道になります。
ゲートを増やすことよりも、置いたゲートが本当に仕事をしているかを疑い続けること。AIエージェントは敵ではありませんが、緑への最短経路を見つけ出す優秀な最適化装置ではあります。その最短経路の途中に、ちゃんと検問を置けているか——自動化を運用するなら、一度ゲートの「発火履歴」を眺めてみてください。一度も赤くなっていないゲートが、たぶん何枚か見つかります。



