本文へスキップ
hdknr blog
戻る

停止条件は測れても収束しない — AIエージェントのループ設計と「不動点」

停止条件を書いたのに、ループが収束しない

「エージェントにプロンプトを打つのではなく、ループを設計する」という言い方は、2026年半ばのAnthropic公式ガイドを境に一気に広まった。4つの型(Turn / Goal / Time / Proactive)も、停止条件を書けという原則も、いまや一通り流通している。型の整理そのものは以前にClaude Codeチーム公式ガイド「ループ設計」を読み解くで扱ったので、ここでは繰り返さない。

本記事が扱うのはその次の問題だ。停止条件を書いたのに、ループが収束しない。

具体例がある。GMOペパボのあたに氏は、Claude Codeで6人の専門レビュアーを並列に走らせた。停止条件は「指摘がゼロになるまで」。記事のタイトルがそのまま結論になっている——「指摘ゼロになるまで回すレビューループ」を組んだが、ゼロにならなかった。停止条件は明確だった。件数はちゃんと数えられた。それでもゼロには到達しなかった。

なぜか。停止条件が「測れる」ことと、ループが「収束する」ことは、別の話だからだ。

3つの壊れ方 — 見取り図

ループが収束しない3つの壊れ方。①停止条件を判定できず回り続ける、②作る役の自己採点で条件未達のまま止まる、③直すと別の指摘が出て振動する。対策はそれぞれ反復上限、採点役の分離、不動点のある量の選択

収束しないループは、見た目の症状こそ違うが、原因で分けると3つに落ちる。

壊れ方症状原因
① 止まらない反復上限に達するまで回り続ける停止条件を機械が判定できない
② 嘘で止まる条件未達なのに「完了」と報告される作る役が自分で採点している
③ 振動する直すたびに新しい指摘が出て件数が減らない停止条件に選んだ量に不動点がない

③の「不動点」は本記事の中心概念で、後半で定義する。さしあたり「もう1周かけても状態が変わらない点」と読んでおけばいい。

①と②は停止条件の書き方の問題で、対処法はよく知られている。厄介なのは③で、これは停止条件に選んだ量そのものの性質に起因する。書き方をいくら磨いても直らない。順に見ていく。

① 止まらない — 判定器がYes/Noを返せない

一番素朴な失敗がこれだ。「いい感じになったら終わり」「たぶん大丈夫そうなら」といった停止条件は、判定する側が何を根拠に差し戻せばいいか分からない。結果、永遠に回るか、根拠なく止まる。

Anthropicの公式ガイドがLighthouseスコアを例に使うのは、これが数字で測れるからだ。

/goal get the homepage Lighthouse score to 90 or above, stop after 5 tries.

90 or above は機械的にYes/Noを返せる。stop after 5 tries は、それでも到達しなかったときの脱出口だ。

Ralph Wiggum(Anthropic公式プラグイン ralph-wiggum)はこれをさらに極端にしていて、完了判定を合言葉の完全一致で行う。stop-hook.sh はワイルドカード照合ではなく文字列リテラルの比較で、<promise> タグの中身が --completion-promise に渡した文字列と一字一句同じかだけを見る。

/ralph-loop "ToDo の REST API を作る。要件: CRUD 4 操作・入力バリデーション・テスト。
             すべて満たしたら <promise>COMPLETE</promise> と出力する。" \
            --completion-promise "COMPLETE" \
            --max-iterations 50

判定を文字列一致まで落とすと、判定器の曖昧さはゼロになる。代償として、合言葉を出す判断そのものがモデルに委ねられる——これが次の②だ。

なお合言葉と反復上限のどちらも設定しないと、Ralphは文字どおり無限に走る。セットアップスクリプトも起動時に「このループは手動では止められない、--max-iterations か --completion-promise を設定しない限り無限に走る」と警告を出す。反復上限とコスト上限は、収束設計の一部というより、収束に失敗したときの火災報知器だと思ったほうがいい。

② 嘘で止まる — 作る役に自分を採点させない

長く回していると、エージェントが「もう十分だろう」と判断して、条件を満たしていないのに合言葉を出してしまう。Ralphの公式コマンド定義には、これを明確に禁じる文言が入っている。

完了の合言葉は、その記述が完全かつ疑いなく真であるときにのみ出力してよい。詰まったと思っても、抜けるために嘘をついてはならない。

ただ、プロンプトで禁じるのは最後の防波堤であって、構造的な対策ではない。構造側の答えが、Anthropicの公式ガイドが繰り返すgeneratorとevaluatorの分離だ。作業するモデル(generator)とは別に、停止条件だけを見る採点役(evaluator)を立て、generatorが「終わった」と言うたびに検算させる。ここまで「判定器」と呼んできたものが、この採点役にあたる。

分ける理由ははっきりしている。自分の成果を自分で採点させると、バグを生んだ盲点をそのまま引きずって甘い自己評価が通る。新しいコンテキストで立ち上げた採点役は、生成時の推論に引きずられない。

ペパボの事例では、6人のレビュアーが出した指摘をもう一段別のClaude Codeエージェントに渡し、コードベースの文脈に照らして妥当性を判定させている。妥当性が低いと判定された指摘は削除するか info レベルに降格する。

ただし同記事は、その効き目についてかなり抑制的に書いている。検証用エージェントが誤って妥当な指摘を却下するリスクは常にある。AI同士の判断が一致したからといって正しいとも限らない。「フィルタとしてある程度機能する」くらいの期待値で使っている、としている。分離は自己採点バイアスを断つが、正しさを保証はしない。

③ 振動する — 停止条件に選んだ量に不動点がない

①も②も潰した。停止条件は数えられる。採点役も分離した。それでもペパボの「指摘ゼロ」は達成されなかった。

同記事はその機構を明示している。AIが修正した箇所に対して別のAIが新しい指摘を出し、それを直すとまた別の指摘が出る、という無限ループが起きうる。

これは判定が甘いのでも、プロンプトが下手なのでもない。「指摘ゼロ」という状態が、もう1周回すと維持されないのだ。

不動点という判別軸

ループの1周を「いまの状態を受け取って次の状態を返す関数」と見ると、収束とは不動点(fixed point、もう1周かけても状態が変わらない点)に到達することだ。停止条件を選ぶときの本当の問いは「測れるか」ではなく、「その条件を満たした状態にもう1周かけて、条件は満たされたままか」になる。

最後の1行が、ペパボの記事のタイトルの正体だ。

なぜレビュー指摘には不動点がないのか

理由は2つある。

判定器が非決定的だから。 テストランナーは同じコードに同じ結果を返す。LLMレビュアーは返さない。指摘ゼロの状態にもう一度レビューをかければ、前回は出なかった観点が出る。ペパボの実測では、指摘の内訳は有用が約4割、ノイズが約3割、的外れが約2割、意外な発見が約1割だった。ノイズと的外れでちょうど半分を占める。件数ゼロを狙うとは、非決定的に湧き出るノイズがたまたま1件も出ない周を待つということだ。

判定器の出力が、次の修正の入力になっているから。 指摘 → 修正 → その修正が新しい指摘の対象になる、というフィードバックが閉じている。修正が状態を動かし、動いた状態が新しい判定を呼ぶ。これは制御系で言えば、ゲインが高すぎて発振している状態に近い。

つまり収束は、次の3つを全部満たしたときにだけ期待できる。

  1. 判定器は決定的か(同じ入力に同じ答えを返すか)
  2. 判定器の出力が、次の修正の入力になっていないか
  3. 条件を満たした状態にもう1周かけても、条件は満たされたままか

LLMを判定器に据えた時点で1が崩れる。レビューのように「直した箇所がそのまま次の観測対象になる」タスクでは2も崩れる。

非決定的な判定器しか使えないときの逃げ道

成否を機械的に測れないタスク——レビュー品質、文章品質、翻訳——では、判定器をLLMにするしかない。そのときの選択肢は3つある。

(a)決定的な代理指標に翻訳する。 「良いドキュメントになったら」を、用語集との突き合わせ、見出し数の充足、リンク切れゼロ、といった測れる量に置き換える。停止条件が本来測りたいものとズレる代わりに、不動点が手に入る。

(b)量を変えて不動点を作る。 「指摘ゼロ」ではなく「差分が変化しなくなった」を停止条件にする。同じ入力に対して修正が生まれなくなった状態は、判定器が非決定的でも不動点になりうる。

# 前周から差分が出なくなったら完了の合言葉を出す、という素朴な収束判定
# HEAD@{1} は reflog 依存なので、各周で必ず1コミットし、
# ループ外で HEAD を動かさないことが前提になる
git diff --quiet HEAD@{1} HEAD && echo "<promise>STABLE</promise>"

(c)収束を諦めて、上限と残件リストにする。 ペパボが実際に選んだのがこれだ。5ラウンドで強制終了し、収束しなかった場合は残りの指摘をリストにして人間に報告する。

(c) を「妥協」と読むと本質を外す。収束しない量を停止条件に選んだ以上、ループの成果物は「ゼロになった」ではなく「N周回した結果、残ったのはこれ」になる。これは設計上の正しい出力であって、失敗ではない。

収束しないと分かっているループに、何を持たせるか

ここまでの整理を踏まえると、③のタイプのループに必要なのは収束条件ではなく、止め際の設計だということになる。ペパボが入れた安全弁は、そのまま実務のチェックリストとして使える。

注目したいのは、これらが全部「どこで人間に返すか」の線引きだという点だ。同記事も、6人のレビュアーの並列実行そのものより、この安全弁の設計と「どの指摘を人間に返すか」の判断基準のほうに一番時間を使った、と書いている。

ループ設計の難所は、回す仕掛けではなく、回すのをやめて人間に戻す境界のほうにある。

周をまたいで収束させる — 失敗の記録とCLAUDE.md

(a)(b)(c) はどれも1周の中で完結する選択肢だった。もう一つ、スケールを変えるという手がある。実質は (b) の「量を変えて不動点を作る」を、セッションの外側でやることだ。収束させる対象を、1周の中の指摘件数から、周をまたいだ同じ失敗の再発回数に移す。

合同会社playparkの記事は、この視点をはっきり打ち出している。LLMエージェントは推論時の自己修正(self-correction)は持つが、セッションをまたいだ記憶は持たない。前回の失敗を「覚えて」はいないし、重みも変わらない。だから避けられた失敗も翌日には再発する——これも「再発回数」という量に不動点がない状態だ。

対策として同記事が提示するのは、失敗を外部ストレージに記録し、パターンを抽出し、設定ファイルを書き換えるサイクルだ。実装例では各スキルが実行ログを構造化JSONで ~/.claude/journal/ に書き出し、8つの軸(同じエラーの2回以上の再発、SKILL.mdの記述漏れ、など)でパターンを検出する。優先順位はスコアの掛け算で決める。

pattern_score = frequency * impact * preventability

frequency は発生回数、impact は重大度、preventability は対処可能性(記述なし=3、部分的=2、対処済み=1)。スコア9以上は即座に修正提案、4未満は放置。掛け算にすることで、頻度が低くても重大で防げるものが強制的に浮上する。

ここで収束の対象が入れ替わっていることに注意したい。同じエラーの再発回数は、対策を設定ファイルに書けば実際に減る。減ったまま戻らない。つまりこの量には不動点がある。1周の中では収束しなかったループが、周をまたいだスケールでは収束しうる、ということだ。

同記事の実測でも、上位パターンは npm ci 漏れ(env系、5回、スコア45)、Biomeのformatエラー(lint系、8回、スコア8)などで、修正済みSKILL.mdを反映した3週目以降に上位2件がほぼ消え、ヘルススコアが64→82に上昇したと報告されている。

そして、これは専用ツールがなくても始められる。同記事が「最小構成」として挙げているのは、CLAUDE.mdへの3行だ。

## エラー記録ルール

- エラーが発生したら `errors.md` に「日付 / カテゴリ(lint|test|env|config) / 内容 / 対処」を追記すること
- 同じカテゴリのエラーが3回記録されていたら、このCLAUDE.mdに再発防止策を追記すること
- セッション終了時に errors.md を確認し、未対処のパターンがあれば報告すること

記録 → 検出 → 改善。掛け算のスコアリングも8軸の分析も、この3行の延長線上にある。

まとめ

「ループを設計する」の実体は、停止条件を書くことではなく、収束する停止条件を選ぶことだった。

壊れ方の分類

判別軸

打ち手

ループは賢さを生む魔法ではなく、人間が「何をもって完了とするか」に込めた判断を増幅する装置だ。増幅器に不動点のない量を入れれば、増幅されるのは振動になる。回す前に、その量にもう1周かけたらどうなるかを一度考えておきたい。

参考リンク



前の記事
ホワイトボックスとブラックボックスの違いを実測した — 分岐カバレッジ 100% でミューテーションスコアは 0% と 100%
次の記事
多段 SSH は ProxyJump で — Match host と ControlMaster で踏み台設定を短くする