結論:本番AIエージェントのバグは「モデルの問題」ではなく「計装の欠如」が原因の8割を占める。無限ループ・ツール誤呼び出し・ハルシネーションの3大障害は、トレース→ガードレール→eval の3層を整備するだけで診断・修正できる。
要点3つ
- 無限ループの根本原因は「エージェント自身が終了条件を判断できない」こと。
max_iterationsと繰り返しツール呼び出し検出器を外部コードで実装する - ツール誤呼び出しはLLMのtool selectionバグではなくツール定義の曖昧さが主因。description を「いつ使うか」「いつ使わないか」の2文で書き直すだけで改善する
- ハルシネーション診断の最速手順は「モデル呼び出し直前のプロンプトをそのまま再現してデバッグ環境で叩く」こと。トレースなしで発生箇所を特定しようとしても無駄
対象読者:AIエージェントを本番に出したが、想定外の挙動・コスト爆発・謎のループ終了で困っているエンジニア・テックリード。
AIエージェントが開発環境では完璧に動くのに、本番に出した途端に壊れる——この体験をした開発者は2026年現在、急増している。マルチエージェント構成が一般化し、ツール呼び出し・外部API連携・長時間タスクが当たり前になった今、バグの性質が根本的に変わった。従来のWebアプリのように「エラーログを見れば原因がわかる」という前提が通用しなくなっている。
本記事では、AIエージェントの本番障害を「無限ループ」「ツール誤呼び出し」「ハルシネーション」の3カテゴリに分類し、各障害の診断手順と修正コードをセットで解説する。観測ツールはLangSmith・OpenTelemetry・Langfuseを前提に、フレームワーク非依存で使える診断パターンを紹介する。本番デバッグに必要なCI/CD回帰検知の全体像はAIエージェントの継続的評価とCI/CD回帰検知ガイドもあわせて参照してほしい。
本番AIエージェントの3大障害カテゴリ
2026年に主要エージェント開発者コミュニティで報告される障害の内訳を整理すると、大まかに以下の3カテゴリに収束する。
| 障害カテゴリ | 典型症状 | 発生頻度 | 診断難度 |
|---|---|---|---|
| 無限ループ | API費用が爆発、レスポンスが返らない、同じツールを繰り返し呼ぶ | 高 | 低(トレースで一目瞭然) |
| ツール誤呼び出し | 間違ったAPIを実行する、引数が欠損・型ズレする、不要なツールを呼ぶ | 高 | 中 |
| ハルシネーション | 存在しないデータを返す、前のステップの結果を無視する、文脈が飛ぶ | 中 | 高 |
障害1:無限ループの診断と修正
なぜ無限ループが起きるか
LLMベースのReActエージェントは、自分が「完了した」かどうかをモデルが判定する設計になっている。しかし、ツール呼び出しが失敗した場合や期待する結果が返らなかった場合、LLMは「もう一度試せばいい」と判断してループを繰り返す。外部から強制終了する仕組みがなければ、APIクォータが尽きるまで実行し続ける。
具体的な発生パターンは主に3つある。
- ツール結果が空または曖昧:検索ツールが0件を返し続けるが、LLMはクエリを変えながら再挑戦する
- max_iterationsの未設定:LangChain / LangGraphのデフォルトはフレームワーク依存で、意図せず無制限になっているケースがある
- エラー隠蔽:ツールがexceptionをキャッチして空文字やNoneを返し、LLMが「まだ試せる」と誤解する
診断手順
LangSmithのトレース画面で「同一ツールの連続呼び出し」を確認する。OpenTelemetryを使っている場合は以下のクエリでスパンを集計できる。
# OpenTelemetry スパン集計でループ検出
from collections import Counter
def detect_loop(spans: list[dict]) -> bool:
"""同一ツールを3回以上連続呼び出しているかチェック"""
tool_calls = [s["name"] for s in spans if s.get("kind") == "tool"]
counts = Counter(tool_calls)
return any(v >= 3 for v in counts.values())
修正:3層のガードレール実装
ループを防ぐには「ハードイテレーション上限」「繰り返しツール検出」「完了条件の明示」の3層を外部コードで実装する。モデルに委ねてはいけない。
from langchain.agents import AgentExecutor
executor = AgentExecutor(
agent=agent,
tools=tools,
max_iterations=15, # ハードイテレーション上限
max_execution_time=60.0, # 秒単位のタイムアウト
early_stopping_method="generate", # 上限に達したら強制完了
handle_parsing_errors=True,
)
# 繰り返しツール呼び出し検出器(カスタム実装例)
from collections import defaultdict
class LoopDetector:
def __init__(self, max_repeat: int = 3):
self.call_history: defaultdict[str, list] = defaultdict(list)
self.max_repeat = max_repeat
def check(self, tool_name: str, tool_input: dict) -> bool:
"""同一ツール+入力が max_repeat 回以上ならTrueを返す"""
key = f"{tool_name}:{str(sorted(tool_input.items()))}"
self.call_history[tool_name].append(key)
recent = self.call_history[tool_name][-self.max_repeat:]
return len(set(recent)) == 1 and len(recent) == self.max_repeat
ツールが空結果を返した場合も「空でした」と明示的にLLMへ伝える設計にする。エラーを隠蔽するとLLMが誤判断の連鎖に入る。
def search_tool(query: str) -> str:
results = external_search(query)
if not results:
# Noneや空文字ではなく、明示的なメッセージを返す
return f"検索結果なし(クエリ: {query})。別のアプローチを検討してください。"
return "n".join(results[:5])
障害2:ツール誤呼び出しの診断と修正
ツール誤呼び出しの主な原因
ツール誤呼び出しはモデルのfunctionバグのように見えて、ほとんどのケースでツール定義の問題が原因だ。特に多いのは「description が”何ができるか”だけを書いていて、”いつ使うか”が曖昧なケース」だ。
| 原因 | 症状 | 修正方針 |
|---|---|---|
| description が曖昧 | 類似ツールを混同する | “いつ使うか”と”いつ使わないか”を明記 |
| 引数の型定義がない | 文字列のはずが数値で渡される | Pydanticスキーマで型を厳密定義 |
| ツールが多すぎる | 無関係なツールを選ぶ | タスクごとにツールセットを絞り込む |
| エラーが伝わらない | 不正引数でも成功を返す | バリデーションエラーをLLMに戻す |
診断:ツール選択トレースの読み方
Latitude社の本番エージェントデバッグガイドによると、ツール誤呼び出しの診断には「トレース収集→失敗クラスタリング→根本原因分析→evalデータ生成」の4ステップが有効とされている。LangSmithのトレースでは各ToolCallに「選択理由(model’s reasoning)」が記録される。OpenTelemetryを使っている場合はスパンの gen_ai.tool.name 属性と gen_ai.tool.call.id を照合することで誤選択の頻度を集計できる。
# Langfuse で誤ツール呼び出しを集計するスクリプト
from langfuse import Langfuse
client = Langfuse()
# 過去7日間のトレースからtool_nameごとの失敗件数を取得
traces = client.get_traces(limit=500, from_timestamp="2026-06-11")
tool_errors = {}
for trace in traces.data:
for obs in trace.observations or []:
if obs.type == "TOOL" and obs.status_message:
name = obs.name
tool_errors[name] = tool_errors.get(name, 0) + 1
# 失敗件数の多い順に表示
for name, count in sorted(tool_errors.items(), key=lambda x: -x[1]):
print(f"{name}: {count} errors")
修正:ツール定義の書き直し
ツールdescriptionに「使うべきケース」と「使ってはいけないケース」の2文を追加するだけで、ツール選択精度が大幅に上がることが多い。
from langchain.tools import tool
from pydantic import BaseModel, Field
class SearchInput(BaseModel):
query: str = Field(description="検索クエリ(日本語可)")
max_results: int = Field(default=5, ge=1, le=20, description="取得件数(1-20)")
@tool(args_schema=SearchInput)
def web_search(query: str, max_results: int = 5) -> str:
"""
Webから最新情報を検索する。
使うべきとき: ユーザーが最新ニュース・製品仕様・価格など、
トレーニングデータに含まれない可能性がある情報を求めているとき。
使ってはいけないとき: 一般的な知識(歴史・定義・計算)の質問。
その場合は直接回答せよ。
"""
results = _do_search(query, max_results)
return "n".join(results) if results else f"結果なし: {query}"
ツールセットが10個以上ある場合、タスク種別ごとに使用するツールセットを絞り込む「tool routing」を実装する。タスク分類をLLMで行い、該当するツールサブセットだけを次のエージェントに渡す構成は、誤呼び出し率を30-50%削減する効果が報告されている。
障害3:ハルシネーションの診断と修正
エージェント固有のハルシネーション特性
単発LLM呼び出しのハルシネーションとエージェントのハルシネーションは性質が異なる。エージェントでは「前のステップの結果を正しく引き継がなかった」「コンテキストウィンドウから押し出された重要情報を参照した」「ツール結果を無視して既存知識で回答した」の3パターンが多い。
診断の鉄則:モデル呼び出し直前のプロンプトを再現する
ハルシネーションの発生箇所を特定する最速手順は、「問題が起きたステップの直前プロンプト(messages配列の全内容)をそのままローカルで再現して叩く」ことだ。LangSmithならトレース画面の「Inputs」タブに完全なmessages配列が表示される。
# LangSmith から問題スパンのプロンプトを取得して再現
from langsmith import Client
ls_client = Client()
# 問題のあったランIDを指定
run = ls_client.read_run("your-run-id-here")
# LLM呼び出しのインプット(messages配列)を取り出す
for child in ls_client.list_run_children(run.id):
if child.run_type == "llm":
messages = child.inputs.get("messages", [])
print(f"Step: {child.name}")
for m in messages:
print(f" [{m['role']}]: {m['content'][:200]}...")
break
この手順で得たプロンプトを、同モデル・同パラメータで複数回実行し、ハルシネーションが安定再現するかを確認する。再現しない場合は確率的ノイズであり、temperature調整や出力検証で対処する。安定再現する場合はプロンプト設計に問題がある。
コンテキスト圧縮によるハルシネーション抑制
長時間エージェントではコンテキストウィンドウの圧迫がハルシネーションを誘発する。LangSmithの公式ドキュメントでは、トレース画面でコンテキスト圧迫の兆候を確認しながら圧縮戦略を設計することが推奨されている。各ステップの会話履歴を要約・圧縮して次ステップに渡す「コンテキスト圧縮」を実装することで、参照精度が維持される。
from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-haiku-4-5")
def compress_history(messages: list[BaseMessage], max_tokens: int = 2000) -> list[BaseMessage]:
"""会話履歴が長すぎる場合に要約して圧縮する"""
# メッセージをテキスト化してトークン数を概算(1トークン≒4文字)
total_chars = sum(len(str(m.content)) for m in messages)
if total_chars < max_tokens * 4:
return messages # 圧縮不要
# 最初のシステムメッセージと最新N件だけ残し、中間を要約
system = [m for m in messages if isinstance(m, SystemMessage)]
recent = messages[-6:] # 直近3往復は必ず保持
middle = messages[len(system):-6]
if not middle:
return messages
# 中間部分を要約
summary_prompt = "以下の会話を3行以内で要約してください:n" +
"n".join(f"{m.__class__.__name__}: {m.content}" for m in middle)
summary = llm.invoke([HumanMessage(content=summary_prompt)])
summary_msg = SystemMessage(content=f"[会話履歴要約]: {summary.content}")
return system + [summary_msg] + recent
トレース計装:デバッグを可能にする最低限の設定
上記の診断手順はすべて「適切にトレースが記録されている」前提で機能する。まだ計装していないエージェントがあれば、以下の最小構成から始める。LangSmithとOpenTelemetryの2択で、どちらもenv変数1行で有効になる。各ツールの詳細な設定方法はLangSmith完全ガイドとLangfuse実装ガイドも参照してほしい。
# LangSmith(LangChain / LangGraph ユーザー向け)
export LANGCHAIN_TRACING_V2="true"
export LANGCHAIN_API_KEY="your-langsmith-api-key"
export LANGCHAIN_PROJECT="production-agent-debug"
# Langfuse(フレームワーク非依存・OSS・セルフホスト可)
export LANGFUSE_PUBLIC_KEY="pk-..."
export LANGFUSE_SECRET_KEY="sk-..."
export LANGFUSE_HOST="https://cloud.langfuse.com"
# OpenTelemetry(既存インフラに統合したい場合)
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://your-collector:4318/v1/traces"))
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("ai-agent")
# エージェント実行をスパンで囲む
with tracer.start_as_current_span("agent_run") as span:
span.set_attribute("gen_ai.system", "anthropic")
span.set_attribute("gen_ai.operation.name", "agent")
result = agent.run(user_input)
span.set_attribute("gen_ai.response.status", "success")
Langfuse公式ドキュメントによると、Langfuseはセルフホスト(MIT License)が可能なため、コスト面での選択肢になる。1か月あたり5万ユニット(トレース・観測・スコアの合算。1リクエストで複数ユニット消費する点に注意)までクラウド無料枠で使え(30日保持)、それ以上はセルフホストが現実的だ。LangSmithはLangChainエコシステムとの親和性が高く、AgentExecutorのトレースが自動で詳細に記録される強みがある。両ツールの詳細な機能比較はZenMLのLangfuse vs LangSmith比較記事も参考になる。
デバッグサイクルの自動化:本番失敗からevalを生成する
本番で発生した障害をそのままevalデータセットに変換し、次のデプロイ前にCI/CDで自動検証するサイクルを組むことで、同じバグの再発を防げる。
import json
from datetime import datetime, timedelta
from langsmith import Client
ls_client = Client()
def collect_failures_as_eval(days: int = 7, error_keyword: str = "") -> list[dict]:
"""
過去N日間の失敗ランをeval用データセット形式で取り出す
"""
since = datetime.utcnow() - timedelta(days=days)
runs = ls_client.list_runs(
project_name="production-agent-debug",
start_time=since,
error=True, # エラーがあったランのみ
)
eval_cases = []
for run in runs:
if error_keyword and error_keyword not in (run.error or ""):
continue
eval_cases.append({
"input": run.inputs,
"expected_output": None, # 人間がレビューして埋める
"error": run.error,
"trace_url": f"https://smith.langchain.com/runs/{run.id}",
"created_at": run.start_time.isoformat() if run.start_time else "",
})
return eval_cases
# 失敗ケースをJSONで保存→チームでレビュー→evalに昇格
failures = collect_failures_as_eval(days=7, error_keyword="loop")
with open("eval_dataset_loops.json", "w", encoding="utf-8") as f:
json.dump(failures, f, ensure_ascii=False, indent=2)
print(f"{len(failures)} 件の失敗ケースをevalデータセット候補として保存")
本番デバッグの優先順位チェックリスト
新しい本番障害が発生したとき、以下の順序でチェックすることで診断時間を短縮できる。
- トレースでループ有無を確認:同一ツールの3回以上の繰り返しはループ確定
- API費用の異常スパイクを確認:短時間で費用が数倍になった場合はループかツール過剰呼び出し
- 最後のエラーメッセージを読む:タイムアウト→max_iterations到達 or ネットワーク問題
- 問題ステップの直前プロンプトを再現:ハルシネーション疑いはここから始める
- ツール定義のdescriptionを見直す:誤ツール呼び出しの7割はここで解決
- 失敗ケースをevalデータに追加:再発防止のためCI/CDに組み込む
よくある質問
Q: LangSmithとLangfuseはどちらを使えばいいですか?
LangChain / LangGraphを使っているならLangSmithが初期設定ゼロで詳細なトレースを取得できるため推奨。フレームワーク非依存のエージェントや、コストを抑えたいチームにはセルフホスト可能なLangfuseが向いている。両者はOpenTelemetry形式でデータを相互にエクスポートできるため、後から移行も容易だ。
Q: max_iterationsはいくつに設定すべきですか?
タスクの複雑度によるが、一般的なシングルタスクエージェントで10-15、調査・分析系の複数ステップを含むエージェントで20-30が目安だ。まず本番トレースの平均イテレーション数を計測し、p95値の1.5倍を初期上限として設定し、運用しながら調整する。
Q: ハルシネーションが完全にランダムで再現しません。どうすれば?
temperature=0 で再現テストを行う。それでも再現しない場合、出力をGroundTruthと自動比較するeval(LLM-as-a-judge)を本番に組み込み、ハルシネーション率をメトリクスとして継続監視する方向に切り替える。個別バグの再現より「ハルシネーション率を閾値以下に保つ」目標管理が現実的だ。
Q: マルチエージェント構成でどのエージェントが問題を起こしているか特定できません
OpenTelemetryの trace_id と parent_span_id を活用して、エージェント間の呼び出しツリーを再構築する。LangSmithはマルチエージェントのトレースを親子関係で自動可視化するため、どのサブエージェントで失敗が起きたかが一目でわかる。各エージェントに固有の tracer_name を設定しておくことで、フィルタリングが容易になる。
この記事を読んで「自社のエージェント運用を整備したい」と感じた方へ
UravationではAIエージェント導入・本番運用の研修・コンサルを提供しています。デバッグ設計からCI/CD統合まで、実務に即した支援をしています。
