本番環境でAIエージェントの暴走を防ぐ7つのパターン
AIエージェントが開発環境で完璧に動く。テスト全通過、デモシナリオもばっちり、スプリントレビューでステークホルダーも感心する。そしてデプロイ。48時間以内に、再帰ループにハマってAPI費用$400を溶かし、顧客に隣人の個人情報をメールで送りつけ、本番DBのインデックスを落とすSQLを自信満々に生成する。
これ、仮定の話じゃないんです。2026年の業界全体で実際に起きているパターン。「デモで動く」と「プロダクションで安全」の間のギャップは、ほとんどのチームが想像するよりはるかに大きい。しかも障害パターンが従来のソフトウェアと根本的に違う。REST APIは聞かれた質問を勝手に変えて回答しない。DBドライバがテーブル名をハルシネーションすることもない。でもAIエージェントは両方やる。しかも確信を持って。
このガイドでは、本番AIエージェントの信頼性を守る7つの実戦パターンを紹介します。理論じゃなくて、実際のインシデントポストモーテム、本番障害、大規模エージェント運用で手痛い目に遭って得た教訓をまとめたものです。
パターン1:サーキットブレーカー
従来のソフトウェアは、ダウンストリームが落ちたときのカスケード障害を防ぐためにサーキットブレーカーを使う。AIエージェントにも必要なんですが、ちょっと違う。HTTP 500を防ぐだけじゃなくて、モデルがゴミを返し始める状況まで食い止めなきゃいけないわけです。
なぜAIエージェントにサーキットブレーカーが必要か
失敗するツールを呼ぶAIエージェントはクラッシュしない。リトライする。また、リトライ。「インテリジェント」なので毎回少し違うアプローチを試みるかもしれない。全部失敗し、全部トークンを消費する。サーキットブレーカーがないと、1つの壊れたツールが日次APIバジェット全体を数分で燃やしかねないんです。
実装
class AgentCircuitBreaker { private failures: Map<string, { count: number; lastFailure: number }> = new Map(); private readonly threshold = 5; private readonly resetTimeout = 60000; async callTool(toolName: string, fn: () => Promise<any>): Promise<any> { const state = this.failures.get(toolName) || { count: 0, lastFailure: 0 }; if (state.count >= this.threshold) { const elapsed = Date.now() - state.lastFailure; if (elapsed < this.resetTimeout) { throw new CircuitOpenError( `ツール"${toolName}"は一時的に無効化中。` + `${Math.ceil((this.resetTimeout - elapsed) / 1000)}秒後にリトライ可能。` ); } state.count = this.threshold - 1; } try { const result = await fn(); this.failures.set(toolName, { count: 0, lastFailure: 0 }); return result; } catch (error) { state.count++; state.lastFailure = Date.now(); this.failures.set(toolName, state); throw error; } } }
肝心なポイント
サーキットが開いたら、エラーをエージェントにコンテキストとしてフィードバックする。単に例外を投げるだけじゃなく、ツールが使えないことをモデルに伝えて代替案を提示させます:
if (error instanceof CircuitOpenError) { return { role: 'tool', content: `${toolName}サービスは一時的に利用不可です(サーキットブレーカー開放)。` + `この機能が一時的にダウンしていることをユーザーに伝えるか、` + `このツールを使わない代替アプローチを試してください。` }; }
これでハード障害がグレースフルな対応に変わる。エージェントがユーザーに謝って、回避策を提案するか、そのステップを丸ごとスキップできるわけです。黙ってループし続けてトークンを燃やすよりはるかにマシ。
パターン2:リトライ分類(盲目的リトライ禁止)
単純なリトライ、つまり「失敗したら全く同じことをもう一度」はAIエージェントではむしろ逆効果。モデルが不正なAPI呼び出しを生成したなら、同じプロンプトでリトライしても同じ不正な呼び出しが出るだけ。同じ失敗に倍額払ってるだけなんですよね。
リトライ分類パターン
盲目的リトライの代わりに、まずエラーを分類して適切な復旧戦略にルーティングする:
class RetryClassifier: def classify(self, error: Exception, tool_name: str) -> RetryStrategy: if isinstance(error, RateLimitError): return RetryStrategy.BACKOFF if isinstance(error, ValidationError): return RetryStrategy.REPAIR if isinstance(error, AuthenticationError): return RetryStrategy.FAIL_FAST if isinstance(error, TimeoutError): return RetryStrategy.BACKOFF if isinstance(error, ToolNotFoundError): return RetryStrategy.FALLBACK return RetryStrategy.FAIL_FAST async def execute_with_retry(agent, action, max_retries=3): classifier = RetryClassifier() for attempt in range(max_retries): try: return await agent.execute(action) except Exception as e: strategy = classifier.classify(e, action.tool_name) if strategy == RetryStrategy.FAIL_FAST: raise if strategy == RetryStrategy.BACKOFF: wait = (2 ** attempt) + random.uniform(0, 1) await asyncio.sleep(wait) continue if strategy == RetryStrategy.REPAIR: action = await agent.repair_action(action, error=str(e)) continue if strategy == RetryStrategy.FALLBACK: action = agent.get_fallback_action(action) continue raise MaxRetriesExceeded(f"{max_retries}回試行後に失敗")
修復(REPAIR)戦略の詳細
REPAIR戦略が面白いところ。同じプロンプトをリトライする代わりに、エラーメッセージを追加コンテキストとしてモデルにフィードバックする:
async def repair_action(self, failed_action, error: str): repair_prompt = f"""前回のツール呼び出しが以下のエラーで失敗しました: ツール: {failed_action.tool_name} 入力: {json.dumps(failed_action.input)} エラー: {error} エラーを分析して修正済みのツール呼び出しを生成してください。 失敗を引き起こした入力をそのまま繰り返さないでください。""" corrected = await self.llm.generate(repair_prompt) return corrected
このパターンは最初の修復試行でかなりの割合のバリデーションエラーを解決します。日付フォーマットの誤り、必須フィールドの欠落、範囲外の値。具体的なエラーメッセージを見せればモデルが自力で修正できる構造的エラーです。実務ではスキーマレベルの障害に対して50%を大きく上回る修復成功率が報告されています。
パターン3:バジェットガバナー
AIエージェントで一番ヤバい障害はクラッシュじゃない。コストの暴走なんです。推論ループにハマったエージェントは、誰も気づかないうちに数百ドルのAPI費用を燃やせる。バジェットガバナーはこれを強制的に止めるハードリミット。
3層のバジェット制御
interface BudgetConfig { maxTokensPerRequest: number; maxTokensPerSession: number; maxToolCallsPerSession: number; maxCostPerSession: number; maxDurationSeconds: number; } class BudgetGovernor { private usage = { tokens: 0, toolCalls: 0, cost: 0, startTime: Date.now() }; check(config: BudgetConfig): void { if (this.usage.tokens > config.maxTokensPerSession) throw new BudgetExceededError('トークンバジェット超過'); if (this.usage.toolCalls > config.maxToolCallsPerSession) throw new BudgetExceededError('ツール呼び出し上限 — 無限ループの可能性'); if (this.usage.cost > config.maxCostPerSession) throw new BudgetExceededError(`コスト上限到達: $${this.usage.cost.toFixed(2)}`); const elapsed = (Date.now() - this.usage.startTime) / 1000; if (elapsed > config.maxDurationSeconds) throw new BudgetExceededError(`セッションタイムアウト: ${elapsed.toFixed(0)}秒`); } recordUsage(tokens: number, cost: number, isToolCall: boolean): void { this.usage.tokens += tokens; this.usage.cost += cost; if (isToolCall) this.usage.toolCalls++; } }
適切なリミット値の設定
| バジェットタイプ | 開発 | ステージング | プロダクション |
|---|---|---|---|
| セッションあたりトークン | 50,000 | 30,000 | 20,000 |
| セッションあたりツール呼び出し | 50 | 25 | 15 |
| セッションあたりコスト | $5.00 | $2.00 | $0.50 |
| タイムアウト | 5分 | 3分 | 2分 |
プロダクションでは厳しめに始めて、実際の利用データを見て緩めていく。リミットを上げるのは簡単だけど、$2,000のサプライズ請求書を説明するのは大変です。
「スタック検知」パターン
def detect_stuck_agent(tool_call_history: list[str], window: int = 5) -> bool: """進捗なく同じツールを繰り返し呼んでいるかを検知""" if len(tool_call_history) < window: return False recent = tool_call_history[-window:] most_common = max(set(recent), key=recent.count) return recent.count(most_common) / len(recent) >= 0.8
スタックが検知されたら、メタプロンプトを注入する:
同じアクションを反復しているのに進捗がありません。
一度止まってアプローチを見直してください。
まったく異なる戦略を試すか、この特定のタスクを
完了できないことをユーザーに伝えてください。
パターン4:出力ガードレール
モデルはいずれ、出力してはいけないものを出力します。顧客向けレスポンスにPII。WebhookペイロードにSQL文。フィッシングサイトに繋がるハルシネーションURL。出力ガードレールは、エージェントの出力がユーザーや外部システムに届く前の最後の防衛線です。
ガードレールパイプライン
class GuardrailPipeline { private guardrails: Guardrail[] = []; async validate(output: string, context: AgentContext): Promise<string> { for (const guardrail of this.guardrails) { const result = guardrail.check(output, context); if (result.action === 'BLOCK') throw new GuardrailViolation(guardrail.name, result.reason); if (result.action === 'REDACT') output = result.redactedOutput; if (result.action === 'FLAG') await this.alertOncall(guardrail.name, output, result.reason); } return output; } }
プロダクション必須ガードレール
1. PII検出
const piiGuardrail: Guardrail = { name: 'pii-detector', check(output: string): GuardrailResult { const patterns = { ssn: /\b\d{3}-\d{2}-\d{4}\b/, email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, phone: /\b(\+\d{1,3}[-.]?)?\(?\d{3}\)?[-.]?\d{3}[-.]?\d{4}\b/, creditCard: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/, }; for (const [type, pattern] of Object.entries(patterns)) { if (pattern.test(output)) { return { action: 'REDACT', reason: `出力で${type}を検出`, redactedOutput: output.replace(pattern, `[REDACTED_${type.toUpperCase()}]`) }; } } return { action: 'PASS' }; } };
2. コードインジェクション防止
ユーザー向けレスポンスでDROP TABLE、DELETE FROM、<script>、eval()、rm -rfなどの危険パターンをブロック。レスポンスタイプがuser-facingの場合のみ発動させる。内部ログや開発者向け出力まで遮断してしまうと、デバッグ自体が不可能になる。
3. グラウンデッドネスアンカー
出力で参照されているURLがソースコンテキストに存在するかを検証する。存在しないURLが見つかった場合はFLAGアクションで即座にオンコールチームに通知。完全にブロックするのではなく、フラグを立てるのがポイント。正当な外部参照まで遮断すると使い物にならなくなる。
パターン5:キルスイッチ
すべての本番AIエージェントには緊急停止メカニズムが必要。「数分かけて段階的に終了」じゃなくて、全インスタンスの全エージェント活動を即座に停止するハードストップ。
なぜ必要か
キルスイッチは通常のエラーハンドリング用じゃない。こういう場面で使うもの:エージェントが顧客に不適切なコンテンツを送り始めた、プロンプトインジェクション攻撃がリアルタイムで悪用されている、エージェントが本番データを勝手に変更している、コストが暴走してバジェットガバナーが設定ミスで機能していない、など。
実装:フィーチャーフラグ + リモートコンフィグ
最もシンプルで信頼性の高いキルスイッチはフィーチャーフラグです:
class AgentKillSwitch { // すべてのエージェントアクション前にチェック async checkBeforeAction(agentId: string): Promise<void> { // リモートコンフィグチェック(5秒TTLでキャッシュ) const config = await this.getRemoteConfig(); if (config.globalKillSwitch) { throw new AgentHaltedError('グローバルキルスイッチで全エージェント停止'); } if (config.disabledAgents.includes(agentId)) { throw new AgentHaltedError(`エージェント${agentId}がキルスイッチで停止`); } // リアルタイム悪用シグナルのチェック if (await this.abuseDetector.isCompromised(agentId)) { await this.activateKillSwitch(agentId, '自動: 悪用検知'); throw new AgentHaltedError('エージェント停止: 悪用パターン検知'); } } async activateKillSwitch(agentId: string, reason: string): Promise<void> { await this.remoteConfig.set(`agents.${agentId}.killed`, true); await this.alerting.sendPagerDutyAlert({ severity: 'critical', summary: `エージェント${agentId}キルスイッチ作動: ${reason}`, }); await this.auditLog.record('KILL_SWITCH_ACTIVATED', { agentId, reason }); } }
絶対ルール
キルスイッチのチェックはすべてのLLM呼び出しとすべてのツール実行の前に行う必要がある。セッション開始時だけじゃなく、実行の途中でも。
while (hasMoreSteps) { await killSwitch.checkBeforeAction(this.agentId); // ← 毎イテレーション const response = await llm.chat(messages); await killSwitch.checkBeforeAction(this.agentId); // ← LLM後、ツール前 if (response.toolCalls) { for (const call of response.toolCalls) { await killSwitch.checkBeforeAction(this.agentId); // ← 各ツール前 await executeTool(call); } } }
パターン6:オブザーバビリティとトレーシング
見えないものは直せない。AIエージェントは本当にブラックボックスで、同じ入力から全然違う推論チェーンが走り、違うツール呼び出しシーケンスを踏み、違う出力が出てくる。従来のモニタリング(レスポンスタイム、エラーレート)だけじゃ、エージェントがなぜ壊れたかほぼわからない。
トレース対象
すべてのエージェント実行は構造化されたトレースを残すべきです:traceId、sessionId、ステップの完全なチェーン(各LLM呼び出し、ツール呼び出し、ガードレールチェック)、集約メトリクス(トークン、コスト、所要時間、ツール呼び出し数、リトライ数、ガードレールトリガー有無)、そして最終outcome。
必要な3つのダッシュボード
1. リアルタイム運用ダッシュボード
| メトリクス | わかること |
|---|---|
| アクティブセッション | 今何個のエージェントが動いているか |
| エラーレート(5分ウィンドウ) | 何かが壊れたかどうか |
| P95レイテンシ | ユーザー体験の劣化 |
| 1分あたりコスト | バジェット消費速度 |
| サーキットブレーカー状態 | どのツールが障害中か |
2. 品質ダッシュボード(日次)
| メトリクス | わかること |
|---|---|
| タスク完了率 | エージェントが実際に問題を解決しているか |
| ガードレールトリガー率 | モデルがどのくらいの頻度で問題を起こすか |
| ツール別リトライ率 | どの統合が不安定か |
| タスクあたり平均ステップ数 | プロンプト最適化の必要性 |
| ユーザー満足度(取得可能な場合) | 最終的に唯一重要なメトリクス |
3. インシデント調査ビュー
問題が発生したとき、正確なシーケンスを再生できる必要がある:すべてのメッセージ、すべてのLLMレスポンス、すべてのツール呼び出しの入出力、すべてのガードレールチェック。トレースは最低30日間保管する。インシデント発生時、このトレースがデジタルフォレンジックの証拠になる。
実践ティップ:レスポンスだけじゃなくプロンプトもログに残す
ほとんどのチームがLLMレスポンスはログに残すけど、送った完全なプロンプトは残さない。これだとデバッグが不可能。すべてのLLM呼び出しで完全なプロンプト(システムメッセージ+会話履歴+ツール定義)をログに残してください。冗長なのはわかる。ストレージコストもかかる。でも問題が起きたときにデバッグ時間を何時間も節約してくれます。
パターン7:Human-in-the-Loopの承認ゲート
完全自律は目標であって、出発点じゃない。一番信頼できる本番エージェントは階層的な権限を使ってる。低リスクな操作は自律的に、高リスクな操作は人の承認が必要という構造。
リスクティアの定義
enum RiskTier { LOW = 'low', // 自律: データ読み取り、検索、テキスト生成 MEDIUM = 'medium', // 通知: メール送信、レコード更新 HIGH = 'high', // 承認: データ削除、金融取引、外部APIへの書込み CRITICAL = 'critical', // 多重承認: スキーマ変更、アクセス制御、バルク操作 } const toolRiskMap: Record<string, RiskTier> = { 'search_documents': RiskTier.LOW, 'generate_summary': RiskTier.LOW, 'send_email': RiskTier.MEDIUM, 'update_customer_record': RiskTier.MEDIUM, 'delete_records': RiskTier.HIGH, 'execute_sql': RiskTier.HIGH, 'modify_billing': RiskTier.CRITICAL, 'update_permissions': RiskTier.CRITICAL, };
承認フロー
async function executeWithApproval( agent: Agent, toolCall: ToolCall, context: AgentContext ): Promise<ToolResult> { const risk = toolRiskMap[toolCall.name] || RiskTier.HIGH; switch (risk) { case RiskTier.LOW: return await executeTool(toolCall); case RiskTier.MEDIUM: const result = await executeTool(toolCall); await notifyTeam(toolCall, result, context); return result; case RiskTier.HIGH: const approval = await requestApproval({ toolCall, context, timeout: 300_000 }); if (approval.approved) return await executeTool(toolCall); else return { role: 'tool', content: `アクション拒否: ${approval.reason}` }; case RiskTier.CRITICAL: const approvals = await requestMultiApproval({ toolCall, context, requiredApprovals: 2, timeout: 600_000 }); if (approvals.every(a => a.approved)) return await executeTool(toolCall); else return { role: 'tool', content: '追加の承認が必要です。' }; } }
現実的な話
Human-in-the-Loopはレイテンシを生みます。シニアエンジニアが承認リクエストをレビューするのに2-5分。その間エージェントは停止、ユーザーは待機、リソースは開いたまま。
緩和策:
- 頻出パターンの事前承認。 同じツール呼び出しが似たパラメータで20回承認されたら、以降は自動承認
- 承認のバッチ化。 関連する高リスク操作を1つのレビューにまとめる
- 非同期ワークフロー。 急ぎでないタスクはキューに入れて、承認・完了時にユーザーに通知
- 段階的信頼。 最初は全部HITLで始めて、信頼度が上がったら特定ツールのリスクティアを下げる
統合:信頼性スタックの全体像
この7つのパターンは防御の層を形成します。単一パターンだけでは不十分。信頼性は組み合わせから生まれる:
┌─────────────────────────────────────────┐
│ Human-in-the-Loop │ ← 高リスク操作のゲート
├─────────────────────────────────────────┤
│ 出力ガードレール │ ← PII、インジェクション、ハルシネーション
├─────────────────────────────────────────┤
│ バジェットガバナー │ ← コスト、トークン、時間
├─────────────────────────────────────────┤
│ キルスイッチ │ ← 緊急停止
├─────────────────────────────────────────┤
│ サーキットブレーカー │ ← ツール障害の隔離
├─────────────────────────────────────────┤
│ リトライ分類 │ ← インテリジェントなエラー回復
├─────────────────────────────────────────┤
│ オブザーバビリティ │ ← すべての意思決定のフルトレース
└─────────────────────────────────────────┘
実装の順序
7つを一度に全部出そうとしないこと。リスク対コスト比の順でこう進める:
- バジェットガバナー(Day 1)— 財務ダメージを即座に防止
- キルスイッチ(Day 1)— 使わなくても非常ブレーキは必要
- オブザーバビリティ(Week 1)— 測定できないものは改善できない
- 出力ガードレール(Week 1-2)— 悪いコンテンツがユーザーに届くのを阻止
- サーキットブレーカー(Week 2)— ツール障害の隔離
- リトライ分類(Week 2-3)— 成功率の向上
- Human-in-the-Loop(Week 3-4)— 高リスク操作への信頼を追加
2026年の現実
AIエージェントのエコシステムは急速に成熟しています。LangGraph、CrewAI、OpenAI/GoogleのAgents SDKはビルトインの信頼性プリミティブを増やしている。でもそれだけじゃ足りない。フレームワークのデフォルトは寛容で、デモを簡単にするために設計されていて、本番システムを安全に保つためじゃないんです。
エージェントはいずれ予想外のことをします。「もし」じゃなくて「いつ」の問題。そのとき信頼性スタックがユーザー、データベース、課金システムに届く前にキャッチできるかどうかです。
最高のAIエージェントは最も賢いやつじゃない。最もきれいに失敗できるやつです。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう