TL;DR
- 無限ループはステップカウンタ+状態ハッシュで検知する
- ハルシネーションは実行前バリデーション+ツール戻り値の構造検証で抑止する
- コンテキスト汚染はスライディングウィンドウ+要約プロキシで制御する
- 診断順序は「ループ→幻覚→汚染」の優先度で疑う
はじめに
こんにちは、みねです。
AIエージェントを本番環境に投入してから「エージェントが同じコマンドを30回繰り返した」「存在しないAPIをツール呼び出しし続ける」「途中から全く関係ない処理を始めた」といった報告が増えています。
これらはすべてAIエージェント固有の障害パターンです。従来のソフトウェアデバッグと根本的に違うのは次の3点です。
- 非決定性: 同じ入力でも異なる出力が生じる
- 長実行: 障害点が複数ステップ後に出現する
- ツール副作用: 外部APIやファイルシステムへの実際の操作が発生する
この記事ではAIエージェントの三層成熟度モデルでL1〜L2段階のエージェントに頻出する3大障害を、診断フローチャートと実装コードで体系的に解説します。
AIエージェントのデバッグが難しい3つの理由
非決定性:同じ入力で異なる出力
従来のバグは再現性がありますが、LLMの出力はtemperatureや内部状態により変動します。同じプロンプトで同じ障害を再現しようとしても、再現しないことがあります。これが原因特定を困難にします。
長実行:障害点が複数箇所に分散
エージェントは「plan → execute → verify → replan」サイクルを繰り返します。障害はサイクルの最初に起きていても、症状が出るのは数サイクル後です。ログがなければ、どの時点で問題が発生したか追えません。
ツール呼び出し:外部副作用のデバッグ
エージェントがAPIを100回呼んだとき、そのうち1回の失敗レスポンスが以降の判断を狂わせます。ツール呼び出しの入出力を全件ログしていないと、原因の特定は不可能です。
デバッグフローチャート(診断ツリー)
エージェントで問題が発生したら、以下の順序で診断してください。
エージェントが期待どおりに動かない
│
├─ 同じアクションを繰り返しているか?
│ YES → [障害1: 無限ループ] → ステップカウンタを確認
│ NO ↓
│
├─ 存在しないリソースを参照しているか?
│ YES → [障害2: ハルシネーション] → ツール戻り値を確認
│ NO ↓
│
├─ 指示と無関係な行動をとっているか?
│ YES → [障害3: コンテキスト汚染] → メッセージ履歴の長さを確認
│ NO ↓
│
└─ 外部サービスの障害・レート制限を確認
このフローチャートに従えば、3大障害の8割以上を最初の確認で特定できます(経験則)。
障害1 — 無限ループの検知と対処
よくある発生パターン
Step 1: ファイルを読み込む → エラー
Step 2: ファイルを読み込む → エラー
Step 3: ファイルを読み込む → エラー
... (繰り返し)
エラーに対してエラーハンドリングの指示がない場合、エージェントは同じアクションをリトライし続けます。ツール呼び出しが失敗したときの次の行動がプロンプトに定義されていないのが根本原因です。
検知方法
ステップカウンタ: 最も単純な方法です。
class AgentLoop:
def __init__(self, max_steps: int = 50):
self.step_count = 0
self.max_steps = max_steps
def step(self, action):
self.step_count += 1
if self.step_count > self.max_steps:
raise LoopLimitExceeded(f"最大ステップ数 {self.max_steps} を超えました")
return self._execute(action)
状態ハッシュ比較: 同一アクションの繰り返しを検知します。
import hashlib
from collections import deque
class LoopDetector:
def __init__(self, window: int = 5):
self.history = deque(maxlen=window)
def check(self, action: dict) -> bool:
"""Trueならループ検知"""
h = hashlib.md5(str(action).encode()).hexdigest()
if h in self.history:
return True
self.history.append(h)
return False
対処法
- プロンプトにexit条件を明記する: 「3回試みて失敗したら、理由を説明して停止すること」
- ループブレーカーを実装する: 上記のステップカウンタ・状態ハッシュを組み合わせる
- 指数バックオフを導入する: リトライ間隔を増やし、同一パターンを避ける
# プロンプトへのexit条件追加例
SYSTEM_PROMPT = """
あなたはファイル操作エージェントです。
## 停止条件(必ず守ること)
- 同じ操作が3回失敗した場合: 失敗理由を報告して停止する
- ステップ数が50を超えた場合: 現在の進捗を報告して停止する
- ツールエラーが連続5回発生した場合: エラー内容を報告して停止する
"""
障害2 — ハルシネーションの検知と対処
よくある発生パターン
LLMは「それらしい」API名や関数名を生成します。実際には存在しないものです。
# エージェントが生成したコード(ハルシネーション例)
result = client.files.upload_and_process(
file_path="data.csv",
processing_mode="intelligent" # このメソッドは存在しない
)
Anthropic公式のTool Use ドキュメントでも、ツール定義の正確な記述が幻覚防止の基本と示されています。
検知方法
実行前バリデーション: ツール呼び出しのパラメータをスキーマで検証します。
from pydantic import BaseModel, ValidationError
class FileReadInput(BaseModel):
file_path: str
encoding: str = "utf-8"
def validate_tool_call(tool_name: str, params: dict) -> bool:
schemas = {
"read_file": FileReadInput,
}
if tool_name not in schemas:
return False # 未知のツール名 → 幻覚の可能性
try:
schemas[tool_name](**params)
return True
except ValidationError:
return False
ツール戻り値の構造チェック: 成功・失敗を明示的に判定します。
def execute_tool(tool_name: str, params: dict) -> dict:
result = run_tool(tool_name, params)
# 戻り値の構造を検証
if "error" in result:
# エラー内容をLLMに正確に伝え、捏造を防ぐ
return {
"success": False,
"error_code": result["error"]["code"],
"message": result["error"]["message"],
"suggestion": "ツール名・パラメータを確認してください"
}
return {"success": True, "data": result}
対処法
- ツール定義を詳細に記述する: 使えるメソッドを列挙し、存在しないものを生成させない
- 幻覚率を測定する: 全ツール呼び出しのうち、バリデーション失敗率を計測する
LLMガードレール設計で説明している出力フィルタリングをツール呼び出し層に適用することで、幻覚によるアクション実行を構造的に防止できます。
# 幻覚率モニタリング
class HallucinationMetrics:
def __init__(self):
self.total_calls = 0
self.validation_failures = 0
@property
def hallucination_rate(self) -> float:
if self.total_calls == 0:
return 0.0
return self.validation_failures / self.total_calls
def record(self, is_valid: bool):
self.total_calls += 1
if not is_valid:
self.validation_failures += 1
障害3 — コンテキスト汚染の検知と対処
よくある発生パターン
メッセージ1: 「Aファイルを編集してください」
メッセージ50: 「Aの編集結果を確認...」
...エラーログが大量に蓄積...
メッセージ120: 「(エラーログを見て)まずエラーを解消します」← 元の指示から逸脱
コンテキストウィンドウが長くなると、前半の指示よりも直近のエラーログや中間出力が判断に影響します。これが「コンテキスト汚染」です。
検知方法
メッセージ長とトークン使用量の監視:
import tiktoken
class ContextMonitor:
def __init__(self, model: str = "gpt-4o", threshold: float = 0.8):
self.enc = tiktoken.encoding_for_model(model)
self.max_tokens = 128_000 # gpt-4o のコンテキスト長
self.threshold = threshold
def get_usage_ratio(self, messages: list) -> float:
total = sum(
len(self.enc.encode(m["content"]))
for m in messages
)
return total / self.max_tokens
def is_contaminated(self, messages: list) -> bool:
return self.get_usage_ratio(messages) > self.threshold
対処法
スライディングウィンドウ: 古いメッセージを切り捨てます。
def sliding_window(messages: list, keep_last: int = 20) -> list:
# システムプロンプト + 最新N件のユーザー/アシスタントメッセージを保持
system = [m for m in messages if m["role"] == "system"]
others = [m for m in messages if m["role"] != "system"]
return system + others[-keep_last:]
要約プロキシ: 古い会話を要約して圧縮します。
async def summarize_old_context(messages: list, llm) -> list:
"""コンテキストが長くなったら前半を要約する"""
if len(messages) < 40:
return messages
old_messages = messages[1:-20] # システムプロンプトと最新20件は除く
summary_prompt = f"""
以下の会話履歴を3-5行で要約してください。
完了したタスクと判明した情報のみを含めてください。
会話履歴:
{format_messages(old_messages)}
"""
summary = await llm.complete(summary_prompt)
return [
messages[0], # システムプロンプト
{"role": "assistant", "content": f"[これまでの要約] {summary}"},
*messages[-20:] # 最新20件
]
マルチエージェント開発パターンでは、コンテキスト管理をエージェント間で分散することで汚染リスクを下げるアーキテクチャも紹介しています。
3障害を横断するデバッグの実践
観測可能性の設計(OTel + span)
OpenTelemetry公式ドキュメントに基づき、エージェントの各ステップをspanとして計測します。
from opentelemetry import trace
tracer = trace.get_tracer("agent")
def agent_step(action: dict):
with tracer.start_as_current_span("agent.step") as span:
span.set_attribute("action.type", action["type"])
span.set_attribute("action.tool", action.get("tool", ""))
result = execute_action(action)
span.set_attribute("result.success", result["success"])
if not result["success"]:
span.set_attribute("result.error", result.get("error", ""))
return result
Langfuse公式ドキュメントやLangSmithは、LLMエージェント専用のトレーシングツールです。ループ・幻覚・汚染の3障害をすべて可視化できます(社内データ: Langfuse導入後のMTTR 40%短縮)。
リプレイ可能なログ設計
障害を再現するには、LLMへの入出力をすべて記録する必要があります。
import json
from datetime import datetime
class ReplayableLogger:
def __init__(self, session_id: str):
self.session_id = session_id
self.events = []
def log_llm_call(self, messages: list, response: str):
self.events.append({
"type": "llm_call",
"timestamp": datetime.utcnow().isoformat(),
"messages": messages,
"response": response,
})
def log_tool_call(self, tool: str, params: dict, result: dict):
self.events.append({
"type": "tool_call",
"timestamp": datetime.utcnow().isoformat(),
"tool": tool,
"params": params,
"result": result,
})
def save(self, path: str):
with open(path, "w") as f:
json.dump({"session_id": self.session_id, "events": self.events}, f, indent=2)
このログがあれば、障害発生時に全ステップを再現できます。
本番環境でのデバッグ手順
AI駆動開発の計画策定でも触れたように、本番エージェントのデバッグは本番ログを活用した事後分析が基本です。
- ステップ1: 障害セッションのセッションIDを取得する
- ステップ2: リプレイログからLLM入出力をすべて確認する
- ステップ3: 診断フローチャートに沿って障害種別を特定する
- ステップ4: 対処法を実装し、同一入力でステージング環境でリプレイする
- ステップ5: 修正後の幻覚率・ループ発生率をメトリクスで確認する
FAQ
AIエージェントのループ検知はどうすればいい?
ステップカウンタ(max_steps)と状態ハッシュ比較の2つを組み合わせるのが効果的です。ステップカウンタだけでは「違うアクションを大量に実行する」パターンを検知できないため、最近N件のアクションハッシュを保持して重複を検知します。
ハルシネーションを0にすることはできる?
現状のLLMでは完全な除去は困難です(経験則)。目標は「ハルシネーションが発生しても実行をブロックする」仕組みの整備です。Pydanticによる実行前スキーマ検証と、ツール戻り値の構造チェックを組み合わせると、幻覚率を大幅に下げられます。
コンテキスト汚染はどのタイミングで起きる?
経験則として、128Kトークンのモデルでは70-80%(約90K〜100Kトークン)を超えた時点から症状が出始めます。エラーログや長い中間出力が蓄積したときに加速します。tiktoken等でトークン使用量を継続監視し、80%を超えたらスライディングウィンドウか要約プロキシを起動するのが標準的な対処です。
デバッグに使えるツールは?
LangSmith・Langfuseが代表的なLLMエージェント専用トレーシングツールです。どちらも無料プランで開始できます。汎用的なObservabilityにはOpenTelemetry + JaegerやDatadogが使えます。ログベースのデバッグには上記のリプレイ可能なLogger実装が最もシンプルです。
本番エージェントのデバッグで注意することは?
本番環境でエージェントをデバッグモードで再実行するのは危険です(副作用の再発生)。必ずログからのリプレイ分析を先行し、修正はステージング環境で検証してから本番に反映してください。また、デバッグ中にコンテキストにデバッグ情報を大量追加するとコンテキスト汚染が悪化するため、デバッグ情報は別スレッドで管理することを推奨します。
まとめ
AIエージェントのデバッグは、従来のソフトウェアデバッグとは根本的に異なります。3大障害(無限ループ・ハルシネーション・コンテキスト汚染)を体系的に理解し、診断フローチャートに沿って順序よく確認することで、原因特定の時間を大幅に短縮できます。
本記事で紹介したコード実装(LoopDetector・HallucinationMetrics・ContextMonitor・ReplayableLogger)はすべて組み合わせて使えます。まずリプレイ可能なログを入れることから始めることを推奨します。ログがなければ障害分析は始まりません。
AI駆動開発の計画策定も参考に、デバッグ基盤ごと設計に組み込んでください。
