なぜ多くのエージェントが本番で失敗するのか — ステートマシン、明示的なツール契約、リトライ、深い可観測性を使って信頼できるエージェントを設計する方法。

エージェントシステムは、LLMが単にプロンプトに答えるだけでなく、次に何をするかを決めるアプリケーションです:どのツールを呼ぶか、どのデータを取得するか、どのステップを実行するか、そしていつ「完了」とするか。これらはモデル、複数のツール(API、DB、サービス)、計画/実行ループ、そしてそれらを繋ぐインフラの組み合わせです。
デモでは魔法のように見えます:エージェントが計画を立て、いくつかのツールを呼び、完璧な結果を返します。ハッピーパスは短く、レイテンシは低く、同時に何も失敗しません。
実際の負荷下では、同じエージェントがデモでは見なかった負荷に晒されます:
結果として、再現が難しいフレーク、沈黙するデータ破損、時折ハングや無限ループするユーザーフローが起きます。
フレークするエージェントは「驚き」を損なうだけではありません。:
この記事は「より良いプロンプト」ではなく、エンジニアリングパターンに焦点を当てます。ステートマシン、明示的なツール契約、リトライと障害処理戦略、メモリと同時実行制御、そして負荷下でエージェントを予測可能にする可観測性パターンを見ていきます。
多くのエージェントは単一のハッピーパスのデモでは問題ありません。しかし、トラフィック、ツール、エッジケースが同時に来ると壊れます。
素朴なオーケストレーションはモデルが1〜2回の呼び出しで「正しく振る舞う」ことを期待します。実使用では次のようなパターンが繰り返し現れます:
明示的な状態と終了条件がないと、これらの振る舞いは避けられません。
LLMのサンプリング、レイテンシのばらつき、ツールのタイミングは隠れた非決定性を生みます。同じ入力が異なる分岐を辿り、異なるツールを呼び、ツール結果を異なる形で解釈することがあります。
スケールすると、ツールの問題が支配的になります:
これらはすべてスパリュースなループ、再試行、誤った最終回答につながります。
10 RPSでは滅多に壊れないものが、1,000 RPSでは常に壊れます。同時実行は次を露呈します:
プロダクトチームはしばしば決定論的ワークフロー、明確なSLA、監査性を期待します。エージェントを無制約に放置すると、確率的でベストエフォートな振る舞いになり、信頼性が最も重要なときに予測不能になります。
アーキテクチャがこのミスマッチを無視して、エージェントを従来サービスのように扱うと、システムは壊れやすくなります。
本番対応のエージェントは「賢いプロンプト」ではなく、規律あるシステム設計です。役立つ考え方は、エージェントを時折LLMを呼ぶ“小さく予測可能な機械”として扱うことです。ミステリアスなLLM塊がシステムを操るのではありません。
重要なのは次の4つの性質です:
これらはプロンプトだけでは得られません。構造から得られます。
多くのチームが最初に採るパターンは「doneでない間、モデルを呼び、考えさせ、ツールを呼ぶ、繰り返す」です。プロトタイプは簡単ですが運用は難しい。
より安全なパターンは、エージェントを明示的なワークフローとして表現すること:
COLLECTING_INPUT, PLANNING, EXECUTING_STEP, WAITING_ON_HUMAN, DONE)。\n- 状態間で許可される遷移を定義する。\n- LLMは主に局所的決定:次の状態、ツールの選択、パラメータの埋めを行う。これによりエージェントは各ステップが検査可能、テスト可能、リプレイ可能なステートマシンになります。自由形式ループは柔軟に見えますが、明示的ワークフローがインシデントのデバッグ性と挙動の監査性をもたらします。
すべてをやろうとする巨大なエージェントは魅力的ですが、計画、検索、ビジネスロジック、UIオーケストレーションなど無関係な責務が密結合します。
代わりに、小さく責務が明確なエージェント/スキルを組み合わせます:
各スキルは独自のステートマシン、ツール、安全ルールを持てます。組成ロジックは高レベルのワークフローになり、一つのエージェントの巨大なプロンプトが肥大化するのを防ぎます。
このモジュール化により各エージェントは推論しやすくなり、ある機能だけを進化させても他が不安定になりにくくなります。
便利なメンタルモデルはエージェントを三層に分けることです:
この分離によりビジネスロジックをプロンプトに隠す落とし穴を避けられます。LLMは明確で決定論的な殻の中の意思決定コンポーネントになるのです。
最も信頼できるエージェントは派手なデモではありません—ホワイトボード上で挙動を説明できるものです。
具体的には:
小さく、合成可能で構造化されたエージェントを志向することで、スコープが増えても複雑さで倒れることがなくなります。
多くのエージェントは「考える/行動する/観察する」のループとして始まります。デモには良いですがすぐに不透明で脆くなります。より良いアプローチは、エージェントを明示的なステートマシンとして扱うこと:有限の状態集合とイベントで駆動される遷移です。
モデルに次の行動を暗黙に決めさせる代わりに、小さな状態図を定義します:
これらの状態間の遷移は、UserRequestReceived, ToolCallSucceeded, ToolValidationFailed, TimeoutExceeded, HumanOverrideなどの型付きイベントで引き起こされます。各イベントと現在の状態が次の状態とアクションを決定します。
これによりリトライやタイムアウトが簡単になります:CALL_TOOLには指数バックオフで3回までリトライ、PLANは再試行しない、のように各状態にポリシーを付与できます。
現在の状態と最小限のコンテキストを外部ストア(DB、キュー、ワークフローエンジン)に永続化します。エージェントは純粋関数になります:
next_state, actions = transition(current_state, event, context)
これにより:
ステートマシンがあれば、エージェントの挙動は全て明示的になります:どの状態にいて、どのイベントが起き、どの遷移が発火し、どの副作用が発生したか。これはデバッグを高速化し、インシデント調査を簡素化し、コンプライアンス向けの自然な監査トレイルを作ります。ログと状態履歴から、特定のリスクのあるアクションがどの状態からのみ取られているかを証明できます。
ツールが“散文の中のAPI”ではなく、明示的な保証を持つインターフェースに見えるとエージェントはずっと予測可能に振る舞います。
各ツールは次をカバーする契約を持つべきです:
InvalidInput, NotFound, RateLimited, TransientFailureといった型付きエラーの意味。\n- SLA:レイテンシ期待値、可用性目標、レート制限。この契約を構造化されたドキュメントとしてモデルに提示し、どのエラーが再試行可能か、どれがユーザー介入が必要か、どれがワークフローを止めるべきかをプランナーが理解できるようにします。
ツールのI/Oは他の本番APIと同様に扱います:
これによりプロンプトを簡素化でき、スキーマ駆動の制約で幻覚的な引数や意味のないツールシーケンスを減らせます。
ツールは進化します。エージェントが壊れないように:
v1, v1.1, v2)エージェントをバージョンにピンする。\n- フィールドは段階的に非推奨にする。古いフィールドはしばらく読めるように保つ。\n- フィールド追加は後方互換的に行い、意味を黙って変えない。これにより成熟度の異なるツールとエージェントの混成が安全になります。
契約は部分的失敗を念頭に設計します:
エージェントはこれを受けて機能を低下させながら続行するか、ユーザーに確認を求めるか、代替ツールに切り替えるかを選べます。
ツール契約はセキュリティ制限をエンコードする自然な場所です:
confirm: true)を要求する。\n- ユーザースコープとシステムスコープの操作を区別する。これらはサーバー側のチェックと組み合わせ、モデルだけに「正しく振る舞う」ことを頼らないでください。
ツールに明確でバリデートされ、バージョン管理された契約があると、プロンプトは短くなり、オーケストレーションロジックは単純になり、デバッグが格段に容易になります。複雑さを壊れやすい自然言語指示から決定論的なスキーマとポリシーに移すことで、幻覚的なツール呼び出しや予期せぬ副作用が減ります。
信頼できるエージェントは「すべてはいつか失敗する」と仮定します:モデル、ツール、ネットワーク、調整レイヤーでさえ。目標は失敗を避けることではなく、それを安価かつ安全にすることです。
冪等性とは「同じ要求を繰り返しても外部から見た効果は1回と同じ」であること。これは、部分失敗や曖昧な応答の際にツール呼び出しを頻繁に再発行するLLMエージェントにとって重要です。
ツールを冪等にする方法:
request_idを付け、ツールがそのIDを見たら同じ結果を返す。\n- Upsert設計:自動採番IDではなく自然キーや合成キーで“作るか更新するか”の意味を持たせる。\n- チェックサムとバージョニング:コンテンツハッシュやバージョン番号を付与して重複や古い書き込みを検出する。一時的な失敗(タイムアウト、レート制限、5xx)には構造化されたリトライを使う:指数バックオフ、スロットリング回避のジッター、厳格な最大試行回数。各試行を相関IDでログに残してエージェント挙動を追跡できるようにします。
恒久的な失敗(4xx、バリデーションエラー、ビジネスルール違反)は再試行しないでください。構造化エラーをエージェントポリシーへ返し、再計画、ユーザーへの問い合わせ、別ツール選択などを行わせます。
エージェント層とツール層の両方にサーキットブレーカーを実装します:繰り返し失敗したツールへの呼び出しを一時的に遮断し、即時失敗にしてフォールバックや劣化モードに切り替える。
エージェントループからの無差別な再試行を避けてください。冪等なツールと明確な失敗クラスがなければ、副作用、レイテンシ、コストが増幅するだけです。
信頼できるエージェントは「状態とは何か」「それがどこにあるか」を明確にします。
エージェントはリクエストを扱うサービスのように扱います:
これらを混同するとバグに繋がります。例えば一時的なツール結果をメモリに入れると、将来の会話で古いコンテキストを使ってしまいます。
主な選択肢は次の通り:
良いルール:LLMは明示的状態オブジェクトに対するステートレス関数である。状態をモデルの外に永続化し、そこからプロンプトを再生成する。
会話ログやトレース、プロンプトをそのままメモリ代わりに使うのは一般的な失敗パターンです。
問題点:
代わりに構造化メモリスキーマを定義します:user_profile, project, task_historyなど。ログは状態から派生させるものとし、その逆にしないでください。
複数のツールやエージェントが同じエンティティ(CRMレコードやチケット)を更新するときは次が必要:
重要な操作では会話ログとは別に決定ログを記録する:何を変えたか、なぜ、どの入力に基づくか。
クラッシュ、デプロイ、レート制限を乗り切るためにワークフローは再開可能であるべきです:
これによりタイムトラベルデバッグが可能になり、悪い決定に至った正確な状態を検査・再生できます。
メモリは資産であると同時に負債でもあります。本番エージェントでは:
メモリは設計・バージョン管理・ガバナンスされたプロダクトの一部として扱い、無秩序なテキストの蓄積にしないでください。
エージェントはホワイトボード上では順次に見えますが、実負荷では分散システムのように振る舞います。多くの同時ユーザー、ツール、バックグラウンドジョブがあるとレース、重複作業、順序の問題を扱う必要があります。
一般的な故障モード:
これらは冪等なツール契約、明示的ワークフロー状態、データ層での楽観的/悲観的ロックで緩和します。
同期のリクエスト–レスポンスは単純だが脆い:依存先が全て稼働して高速である必要がある。エージェントが多くのツールや並列サブタスクに扇状展開するなら、長時間実行や副作用をキュー化します。
キューベースのオーケストレーションの利点:
エージェントは通常三種類の制限に直面します:
明示的なレート制限層を用意し、ユーザー/テナント/グローバル単位で制御します。トークンバケットやリーキーバケットを使い、RATE_LIMIT_SOFTやRATE_LIMIT_HARDのような明確なエラーを返してエージェントが優雅にバックオフできるようにします。
バックプレッシャはシステムが負荷時に自らを守る方法です。戦略として:
キュー深度、ワーカー利用率、モデル/ツールのエラー率とレイテンシパーセンタイルを監視します。キュー増大と429/503/遅延の上昇はエージェントが環境を超えている初期信号です。
任意のタスクについて「何をしたか?」と「なぜそうしたか?」を素早く答えられなければ、信頼できるエージェントは作れません。エージェント可観測性はその答えを安価かつ正確にすることです。
各タスクに対し、次を通すトレースを設計します:
トレース内に重要決定(ルーティング、プラン修正、ガードレール発動)の構造化ログと、ボリュームと健全性のためのメトリクスを付与します。
有用なトレースには通常次が含まれます:
プロンプト、ツール入力、出力は構造化形式でログに残しますが、マスキング層を通してください:
本番はデフォルトでマスク済みにし、低環境でのみ未加工を許可する機能フラグを用いるとよいです。
最低限追跡すべきは:
インシデント時、良いトレースとメトリクスがあれば「エージェントがフレークだ」ではなく「P95タスクがToolSelectionで2回のリトライ後に失敗、原因はbilling_serviceの新しいスキーマ」といった具体的な声明が出せます。
エージェントのテストは、彼らが呼ぶツールと、それらを繋ぐフローの両方をテストすることです。分散システムのテストとして扱い、単なるプロンプト調整に留めないでください。
まずツール境界の単体テストから始めます:
これらのテストはLLMに依存せず、合成入力でツールを直接呼んで期待される出力やエラー契約を検証します。
統合テストはエージェントワークフロー(LLM + ツール + オーケストレーション)をエンドツーエンドで検証します。
シナリオベースのテストを設計します:
これらのテストは状態遷移とツール呼び出しを検証します。LLMのすべてのトークンを検証するのではなく、どのツールがどの引数で呼ばれたか、どの順序か、最終状態が何かをチェックします。
テストを再現可能に保つため、LLMレスポンスとツール出力をフィクスチャ化します:
典型パターン:
with mocked_llm(fixtures_dir="fixtures/llm"), mocked_tools():
result = run_agent_scenario(input_case)
assert result.state == "COMPLETED"
(コードブロックはそのまま保持しています)
プロンプトやスキーマの変更は回帰ランを必須にします:
スキーマ進化(フィールド追加や型の厳格化)は専用の回帰ケースでカバーして、古い契約を前提にしているエージェントやツールを壊さないようにする。
新しいモデルやポリシー、ルーティング戦略を本番に直接投入してはいけません。
代わりに:
オフラインゲートを通過した後にのみ、新バリアントを段階的に本番へロールアウトします(機能フラグ経由が望ましい)。
エージェントログには機密データが含まれます。テストはこれを尊重しなければなりません:
これらをCIパイプラインに組み込み、匿名化チェックなしにテストアーティファクトが生成・保存されないようにします。
エージェントの運用は静的なモデルのデプロイではなく、分散システムの運用に近いです。ロールアウト制御、信頼性目標、厳格な変更管理が必要です。
新しいエージェントや振る舞いは段階的に導入します:
これらはすべて機能フラグと設定駆動のポリシーで裏付ける:ルーティングルール、使用ツール、温度、セーフティ設定はコードではなく設定で切り替えられ、即時に元に戻せるようにします。
システム健全性とユーザ価値を反映したSLOを定義します:
これらをアラートに結び付け、他の本番サービスと同様に所有者、ランブック、標準的な緩和手順(フラグのロールバック、トラフィックドレイン、安全モード)を用意します。
ログ、トレース、会話の記録を使ってプロンプトやツール、ポリシーを洗練します。各変更はバージョン化されたアーティファクトとしてレビュー、承認、ロールバック可能に扱います。
プロンプトやツールの変更を黙って行うのは避けてください。変更管理がなければ回帰と特定の編集の関連付けができず、インシデント対応は推測合戦になります。
本番対応のエージェントシステムは関心の分離が明確だと恩恵があります。目標は「意思決定は賢く、インフラでは愚かに保つ」ことです。
1. ゲートウェイ / APIエッジ
クライアント(アプリ、サービス、UI)の単一入口。次を担います:
2. オーケストレータ
オーケストレータは「脳幹」であって脳ではありません。次を調整します:
LLMはオーケストレータの背後にあり、プランナーや言語理解を要する特定ツールから呼ばれます。
3. ツール&ストレージ層
ビジネスロジックは既存のマイクロサービス、キュー、データシステムに残します。ツールは薄いラッパーとして次を扱います:
オーケストレータは厳格な契約経由でツールを呼び出し、ストレージが真の情報源であり続けます。
ゲートウェイで認可とクォータを強制し、オーケストレータで安全性、データアクセス、ポリシーを強制します。すべての呼び出し(LLMとツール)は構造化テレメトリを発行し、次にフィードします:
シンプルなアーキテクチャ(ゲートウェイ→単一オーケストレータ→ツール)は運用しやすいです。プランナー、ポリシーエンジン、モデルゲートウェイを分離すると柔軟性は増しますが、調整とレイテンシ、運用の複雑度も増します。
明示的なステートマシン、明確なツール契約、規律あるリトライ、深い可観測性というコア要素が揃えば、現実負荷下で予測可能に振る舞うエージェントが作れます。最後はこれらをチーム内で繰り返し実践することです。
各エージェントをステートフルなワークフローとして考えます:
これらが揃うと、システムはエッジケースで優雅に劣化し、崩壊しなくなります。
プロトタイプを本番ユーザーに出す前に確認してください:
どれかが欠けていれば、まだプロトタイプ段階です。
持続可能なセットアップは通常次の分担になります:
これによりプロダクトは速く動け、プラットフォームは信頼性・セキュリティ・コスト管理を強制できます。
基盤が安定したら次を検討できます:
ここでの進展は段階的に行い、機能フラグの裏でオフライン評価と強力なガードレールを用いて導入してください。
テーマは一貫しています:失敗を設計し、巧妙さより明快さを優先し、観測と巻き戻しが容易な範囲で反復すること。これらの制約を守れば、エージェントシステムは怖いプロトタイプではなく組織が依存できるインフラになります。
エージェントシステムは、LLMが単一のプロンプトに答えるだけでなく、次に何をするかを決定するアプリケーションです:どのツールを呼び出すか、どのデータを取得するか、ワークフローのどのステップを実行するか、そしていつ終了するかを判断します。
単なるチャット補完と違い、エージェントシステムは次を組み合わせます:
本番では、LLMは全システムではなく、明確で決定論的な殻の中にある一つの意思決定コンポーネントになります。
デモは通常、ひとつのハッピーパスで動きます:単一ユーザー、理想的なツール動作、タイムアウト無し、スキーマ差分無し、短い会話。実運用ではエージェントは以下に直面します:
ワークフローや契約、障害処理が明確でなければ、これらはループ、停止、部分的な作業、沈黙するエラーを引き起こし、デモ環境では表面化しません。
LLMを自由なループの中ではなく、明確な構造の中で動かすことです:
こうすれば、挙動を段階的に説明・テスト・デバッグでき、不透明な“エージェントの思考”ループを追いかける必要がなくなります。
エージェントをwhile not done: call LLMのようなループではなく、名前付きの状態と型付きイベントを持つワークフローとして設計します。
典型的な状態例:
ツールをプロンプトの中の散文ではなく、プロダクションAPIのように設計します。各ツールが備えるべき要素:
外部呼び出しはいつか失敗すると想定し、それを安価で安全にする設計をします。
主要なパターン:
request_idやビジネスキーを受け取り、同じIDなら同一の結果を返す。\n- ターゲットを絞ったリトライ:タイムアウトや5xx、レート制限などの一時的エラーのみを指数バックオフ+ジッターでリトライし、最大試行回数を厳格にする。\n- サーキットブレーカー:繰り返し失敗するツールへの呼び出しを一時停止してフォールバックに切り替える。\n- 構造化された失敗面:エージェントがリトライ、再計画、ユーザーへの問い合わせのいずれをするか判断できるように明示的なエラー型を返す。これにより信頼性を保ちつつ、無限ループや重複副作用、コスト暴走を防げます。
短期状態と長期メモリを分離し、LLM自体はステートレスとして扱います。
多くのユーザやバックグラウンドジョブがあると、ワークフローは分散システムとして振る舞います。
対策:
429/503やキュー深度の上昇を監視して機能劣化やトラフィック遮断を行う。\n- 冪等性とロック:重複作業や競合を避けるため、冪等なツール契約と楽観的/悲観的ロックを組み合わせる。キュー深度、ワーカー利用率、レイテンシパーセンタイル、エラー率を監視して過負荷を早期に検知します。
タスクについて「何をしたか」と「なぜそうしたか」を素早く答えられるようにします。
必要な可観測性:
これがあれば「エージェントが不安定だ」と感じる段階から、どの状態・ツール・変更が原因かを特定できるようになります。
エージェントは静的なプロンプトではなく進化するサービスとして扱い、他の本番サービスと同じ運用規律を適用します。
良い運用プラクティス:
こうして継続的に改善しつつ、障害を抑え、診断可能で巻き戻し可能な変更を行えます。
PLAN – リクエストを解釈しステップに分解するCALL_TOOL – 特定のツール呼び出し(またはバッチ)を実行するVERIFY – 出力を簡単な不変条件や二次チェックで検証するRECOVER – リトライ、フォールバック、エスカレーションでエラーを処理するDONE / FAILED – 終端状態イベント(例:ToolCallSucceeded, TimeoutExceeded)と現在の状態が次の状態を決めます。これによりリトライやタイムアウトの方針が明示化され、プロンプトや寄せ集めのコードに散らばることがなくなります。
InvalidInput, NotFound, RateLimited, TransientFailureのような型付きエラー呼び出す前に入力を検証し、呼び出し後にも出力を検証します。ツール契約にバージョンを付け、エージェントは特定バージョンにピン留めすることでスキーマの変化で壊れないようにします。