AIエージェントはクラッシュせずに、止まらないまま予算を焼き切ることがあります。

この記事では、freeCodeCampが公開した実践ガイドと参照リポジトリ「production-safe-agent-loop」をもとに、本番運用でエージェントループを安全に組むための設計を解説します。出口条件の定義からターン・トークン制限、SQLite監査ログ、人間による確認まで、Pythonで実装できる5つの部品を順に整理します。

この記事でわかること

  • AIエージェントが「静かに失敗」する典型パターン
  • ループ開始前に「完了条件」を固定するSpecWriterの役割
  • ターン数・トークン数の上限を強制するサーキットブレーカー
  • 追記専用SQLite監査ログの設計とPII対策
  • 下流システムへ渡す前の人間レビュー(5要素フレーム)

https://www.freecodecamp.org/news/how-to-build-a-production-safe-agent-loop-from-exit-conditions-to-audit-trails/

エージェントは「止まらない」ことで失敗する

2025年7月、Claude Codeの再帰ループが5時間で1万6千〜5万ドルを消費した事例が報じられています。エラーで落ちたのではなく、エージェントが指示どおり動き続けた結果です。同年11月には、LangChainの4エージェント構成が11日間稼働し、4万7千ドルの請求につながった例もあります。いずれもテストでは正常に見え、ダッシュボード上も活動が続いていたため、請求書が届くまで気づかれませんでした(参考)。

従来のプログラムは未定義状態でクラッシュします。LLMベースのエージェントは曖昧さに対して「役に立とう」と再試行し、検証エージェントや修正エージェントを連鎖させます。「正しい」とは何かを誰も定義していなければ、ループは見た目は順調のままコストだけが膨らみます。

コスト面の差も大きいです。チャットボット1回あたり約0.04ドルに対し、複数エージェントのオーケストレーションは約1.20ドルと、単純比で30倍になります。Gartnerはパイロットのチャットボットと本番エージェントのトークン消費差を5〜30倍とし、複雑なタスクでは70倍に達するベンチマークも報告されています。FinOps Foundationの2026年調査では、企業の73%がAIコストが当初見積もりを超えたと回答しています。Gartnerは2027年までにエージェント系プロジェクトの40%が経済的理由で中止されると予測しており、多くはより良いモデルではなく出口条件の欠如が原因です。

問題の本質は、次の3行に集約されます。

while True:
    result = agent.run(task)
    # done when...?

この「?」に答えを書かないまま本番へ出すと、課金イベントが走り続けます。

5つの部品でループを囲む

開発者Daniel Nwaneri氏のガイドでは、次の5モジュールを独立して組み合わせる設計を推奨しています。

モジュール 役割
spec_writer.py ループ開始前に3つの質問へ答えさせる
circuit_breaker.py ターン数・トークン数の上限を強制
ledger.py 追記専用SQLite監査ログ
agent_loop.py 上記を束ねてLLMを呼ぶ唯一のループ
review_surface.py 人間レビュー用の5要素フレームを組み立てる

いずれも単体でインポート・テスト可能です。Python 3.10以上が必要で、デフォルトのLLMクライアントはAnthropic SDK形状を想定していますが、約20行のアダプタでOpenAIやGemini、Ollamaにも差し替えられます。

Phase 1: 完了条件をループ前に固定する

最も高コストなミスは、モデル選定やリトライ設定の前に「完了の定義」を一文で答えられないことです。SpecWriter.run()を呼ぶと、次の3問に答えるまで戻りません。

  1. 何をするか(what_it_does)
  2. 何をしないか(what_it_does_not)
  3. 完了はどう見えるか(done_looks_like)

3問目が核心です。「サイトを監査する」は条件になりません。「対象URLをクロールし、titleとmeta descriptionを抽出、欠落や長すぎる項目を列挙して停止する」のように、検証可能な一文が必要です。回答はSQLiteに保存され、session_id付きの凍結済みSpecResultとして返ります。以降の監査ログとループ結果はこのIDで一貫して追跡します。

Phase 2: サーキットブレーカーで上限を強制する

出口条件の定義は規律、サーキットブレーカーは執行です。デフォルトはturn_limit=5token_limit=15000です。どちらかの上限超過でCircuitBreakerErrorが即座に発生し、戻り値ではなく例外にすることで無視を防ぎます。

境界は厳格です。turn_count == turn_limitは許容し、turn_limit + 1で発動します。猶予や警告はありません。人間のチェックポイントを強制するためです。

実装で最も重要なルールは、.check()を毎回のLLM呼び出しの前に置くことです。呼び出し後にチェックすると、上限超過が分かった時点でトークンは既に消費済みです。

# 誤り — 事後チェック
result = client.messages.create(...)
breaker.check(turn_count, accumulated_tokens)

# 正しい — 事前チェック
breaker.check(turn_count, accumulated_tokens)
result = client.messages.create(...)

Phase 3: 追記専用の監査ログを残す

サーキットブレーカーは予算を守り、レジャーは何が起きたかを証明します。多くのチームのログはデバッグ用ですが、ここでのレジャーはガバナンス用です。各行はループが境界内に収まったか、いつ逸脱したかの証拠になります。

ledgerテーブルは1ターン1行、更新・削除なしの追記専用です。編集可能なログは監査証跡ではなくメモ帳に等しくなります。入力本文は保存せず、SHA-256のinput_hashのみ記録します。同一入力の検出と、個人情報が監査証跡に入らないことの両方を満たします。タイムスタンプはdatetime.now(timezone.utc).isoformat()でタイムゾーン付きに統一しています。

Phase 4: 境界を守るエージェントループ

AgentLoopはLLMを呼ぶ唯一のコンポーネントです。1ターンの流れは次のとおりです。

  1. circuit_breaker.check(turn, accumulated_tokens)
  2. client.messages.create(...)
  3. ledger.write(...)
  4. stop_reason == "end_turn"なら終了、そうでなければ継続

サーキットブレーカーが途中で発動した場合、ループは例外を捕捉し、違反行をレジャーに書き込んでLoopResult(success=False, breach_reason=...)を返します。システムプロンプトには3つの仕様フィールドをすべて渡し、特に「やらないこと」でスコープ外の作業を抑えます。

Phase 5: 人間レビューで下流へ渡す

サーキットブレーカーとレジャーは、約束どおりに動いたかまでは教えてくれません。ReviewSurfaceはセッションを読み、次の5要素フレームを毎回同じ順序で組み立てます。

  1. 元の約束(SpecWriterの3回答)
  2. 受け入れ基準(done_looks_like
  3. 差分(初回入力ハッシュ、最終状態、ターン数、トークン、違反フラグ)
  4. 証拠(全レジャー行)
  5. 未解決の前提(違反行やpass_fail=False行から導出)

レビュアーは.attest()で確認を記録します。frame_hashはフレーム内容のSHA-256で、レビュアーが要約ではなく実際のフレームを見た証跡になります。承認はプロセスが回ったことの確認、証明(attestation)は出力が約束と一致したかの確認です。規制対象の下流へ渡す前に、この差を分けておく必要があります。

実運用での使い方

リポジトリにはSEO監査、コードレビュー、サポートトリアージ、文書処理の4つの実行例があります。SEO監査のように定期実行が自然なタスクでは、24時間常時稼働ではなくcronで起動し、結果を人間の前に置く構成が正直な設計です。

テストスイートは80件、5コアモジュールで100%カバレッジです。FakeClientによりネットワークやAPIキーなしでループ全体を検証できます。

実装を始める前に

AIエージェントの安全性は、賢いモデル選びだけでは足りません。完了の定義、実行時の上限、改ざんしにくい記録、人間による最終確認——この4層をループの外側に置くことで、「静かな失敗」を本番投入前に止められます。while Trueに出口を書かないループは自律性ではなく、請求待ちの状態です。まず一文で「完了の姿」を固定し、その境界をコードで強制する——エージェント実装の出発点はここにあります。