第12章 デバッグと監視(Debugging and Monitoring)
12.1 概要
Section titled “12.1 概要”第11章の評価が「良くなったか」を測るものだとすれば、本章の可観測性(observability)は「いま何が起きているか」を見るものである。両者は補完関係にある。評価は改善の方向を決め、可観測性は問題の存在と原因を教える。
エージェントのデバッグが難しい理由は、これまでの章で見てきた性質から導かれる。
| 性質 | デバッグへの影響 | 参照 |
|---|---|---|
| 出力が確率的 | 同じ入力でも再現しない | 第2章2.1節 |
| 経路が動的 | 実行のたびにスタックが変わる | 第4章4.3.1節 |
| ステップが多段 | どこで狂ったか特定しにくい | 第5章 |
| 誤りが累積する | 表面化した時点で原因は遠い過去にある | 第5章5.4.4節 |
| コンテキストが巨大 | 「何を見て判断したか」の記録が膨大 | 第8章 |
とりわけ誤りの累積が厄介である。ステップ18で明らかにおかしな出力が出たとき、原因はステップ3で読み違えたファイルにある、ということが起こる。エラーが出た場所と原因の場所が離れているため、その場のログを見ても分からない。
したがって、エージェントの可観測性では1回の実行を丸ごと再構成できることが要件になる。これがトレーシングである。
12.2 構造化ログとトレーシング
Section titled “12.2 構造化ログとトレーシング”12.2.1 ログでは足りない
Section titled “12.2.1 ログでは足りない”従来のアプリケーションログ(行単位のテキスト)は、エージェントには不向きである。1回の実行が数十のLLM呼び出しとツール実行から成り、それらが階層構造を持つためである。
必要なのはトレースである。1回の実行を1つの木構造として記録する。
スパン(span) が個々の処理単位、トレース(trace) がそれらをまとめた1回の実行である。共通の trace_id で紐づけられ、親子関係を持つ。
この構造があると、次のことが可能になる。
- ステップ18の異常から、ステップ3の入力まで遡れる
- どのステップが遅いのか、どこにコストがかかっているのかが分かる
- 「モデルはそのとき何を見ていたか」を正確に再現できる
12.2.2 何を記録するか
Section titled “12.2.2 何を記録するか”各スパンに記録すべき情報を整理する。
LLM呼び出しのスパン
| 記録するもの | 用途 |
|---|---|
| モデル名(要求したものと実際に応答したもの) | バージョン変更の影響追跡(第2章2.6節) |
| 入力トークン数・出力トークン数 | コスト計算(第2章2.4節) |
| キャッシュ読み書きのトークン数 | キャッシュが効いているかの確認 |
停止理由(stop_reason) |
max_tokens での打ち切り検出(第2章2.3.4節) |
| レイテンシ、最初のトークンまでの時間 | 体感品質 |
| 温度・effort などのパラメータ | 設定と結果の相関 |
| 入出力の内容 | 要注意。12.3節で扱う |
ツール実行のスパン
| 記録するもの | 用途 |
|---|---|
| ツール名 | ツール別のエラー率(第7章7.7.2節) |
| 引数 | 引数の誤りの分析 |
| 成否と、失敗ならエラー種別 | 障害の切り分け |
| 実行時間 | 遅いツールの特定 |
| 出力サイズ | コンテキスト膨張の原因追跡(第8章8.3.1節) |
| 切り詰めの有無 | 情報が失われていないかの確認 |
エージェント全体のスパン
| 記録するもの | 用途 |
|---|---|
| タスクの識別子、ユーザー/テナント識別子 | 絞り込み。テナント分離の検証にも使う(第8章8.4.2節) |
| 総ステップ数 | 停滞・暴走の検出 |
| 総コスト | 予算管理(第5章5.4.1節) |
| 終了理由 | 正常終了か、上限到達か、エラーか |
| プロンプトのバージョン | どのプロンプトでの結果か(第6章6.7.2節) |
最後の項目を強調しておきたい。 プロンプトをバージョン管理していても、トレースにそのバージョンが記録されていなければ、「先週の失敗はどのプロンプトのものか」が分からない。プロンプトのハッシュやGitのコミットIDをスパンに載せておくと、評価(第11章)との突き合わせが容易になる。
12.2.3 最小限の自前実装
Section titled “12.2.3 最小限の自前実装”ツールを導入する前に、何が必要かを理解しておく。
import timeimport uuidimport jsonimport loggingfrom contextlib import contextmanagerfrom dataclasses import dataclass, fieldfrom typing import Any
logger = logging.getLogger("agent.trace")
@dataclassclass Span: name: str trace_id: str span_id: str parent_id: str | None attributes: dict[str, Any] = field(default_factory=dict) start: float = field(default_factory=time.monotonic) end: float | None = None error: str | None = None
def emit(self) -> None: """構造化ログとして1行で出力する。JSONにするのが要点。""" logger.info(json.dumps({ "name": self.name, "trace_id": self.trace_id, "span_id": self.span_id, "parent_id": self.parent_id, "duration_ms": round((self.end - self.start) * 1000, 1), "error": self.error, **self.attributes, }, ensure_ascii=False))
class Tracer: def __init__(self) -> None: self._stack: list[Span] = [] self.trace_id: str = uuid.uuid4().hex
@contextmanager def span(self, name: str, **attributes): span = Span( name=name, trace_id=self.trace_id, span_id=uuid.uuid4().hex[:16], parent_id=self._stack[-1].span_id if self._stack else None, attributes=attributes, ) self._stack.append(span) try: yield span except BaseException as e: span.error = f"{type(e).__name__}: {e}" raise finally: span.end = time.monotonic() self._stack.pop() span.emit()import { randomUUID } from 'node:crypto'
// OpenTelemetry の SDK は使わず、必要なものだけを自前で書く。// 属性名は OpenTelemetry の生成AI向けの規約(gen_ai.*)に合わせておくと、// あとから本物のSDKに載せ替えるときに記録側を変えずに済む// Python の logging に相当する標準のロガーは Node にないため、// 1行のJSONを出すだけの最小のロガーを用意するconst logger = { info: (line: string): void => { console.log(line) },}
class Span { name: string traceId: string spanId: string parentId: string | null attributes: Record<string, unknown> start: number end: number | null error: string | null
constructor( name: string, traceId: string, spanId: string, parentId: string | null, attributes: Record<string, unknown> = {} ) { this.name = name this.traceId = traceId this.spanId = spanId this.parentId = parentId this.attributes = attributes // Python の time.monotonic() は秒、performance.now() はミリ秒を返す this.start = performance.now() this.end = null this.error = null }
/** 構造化ログとして1行で出力する。JSONにするのが要点。 */ emit(): void { logger.info( JSON.stringify({ name: this.name, trace_id: this.traceId, span_id: this.spanId, parent_id: this.parentId, // 単位がミリ秒なので 1000 倍は不要。小数第1位に丸める duration_ms: Math.round(((this.end ?? this.start) - this.start) * 10) / 10, error: this.error, ...this.attributes, }) ) }}
class Tracer { readonly #stack: Span[] = [] traceId: string
constructor() { // uuid4().hex に相当。ハイフンを除いた32桁 this.traceId = randomUUID().replaceAll('-', '') }
/** Python の contextmanager に相当。JavaScript には with 文がないため、 * 中で行う処理をコールバックで受け取り、try/finally で閉じる。 * **attributes に相当するものもないため、属性はオブジェクトで渡す。 */ async span<T>( name: string, attributes: Record<string, unknown>, body: (span: Span) => T | Promise<T> ): Promise<T> { const span = new Span( name, this.traceId, randomUUID().replaceAll('-', '').slice(0, 16), this.#stack.at(-1)?.spanId ?? null, attributes ) this.#stack.push(span) try { return await body(span) } catch (e) { span.error = e instanceof Error ? `${e.name}: ${e.message}` : String(e) throw e } finally { span.end = performance.now() this.#stack.pop() span.emit() } }}使う側は次のようになる。
tracer = Tracer()
with tracer.span("invoke_agent", task_id=task_id, user_id=user_id) as root: for iteration in range(1, MAX_ITERATIONS + 1): with tracer.span("chat", model=model, iteration=iteration) as s: response = client.messages.create(...) s.attributes.update({ "gen_ai.request.model": model, "gen_ai.response.model": response.model, "gen_ai.usage.input_tokens": response.usage.input_tokens, "gen_ai.usage.output_tokens": response.usage.output_tokens, "gen_ai.response.finish_reasons": [response.stop_reason], })
for tu in tool_uses: with tracer.span("execute_tool", **{"gen_ai.tool.name": tu.name}) as s: outcome = registry.execute(tu.name, tu.input) s.attributes.update({ "tool.is_error": outcome.is_error, "tool.output_chars": len(outcome.content), })
root.attributes.update({"steps": iteration, "cost_usd": budget.spent_usd})const tracer = new Tracer()
await tracer.span( 'invoke_agent', { task_id: taskId, user_id: userId }, async (root) => { // Python の for 変数はループを抜けても残るが、TypeScript の let は // ブロックスコープなので、あとで使う変数は外で宣言する let iteration = 0 for (iteration = 1; iteration <= MAX_ITERATIONS; iteration++) { // with 文と違い、中で作った値はコールバックの戻り値として受け取る const response = await tracer.span( 'chat', { model, iteration }, async (s) => { const response = await client.messages.create(params) Object.assign(s.attributes, { 'gen_ai.request.model': model, 'gen_ai.response.model': response.model, 'gen_ai.usage.input_tokens': response.usage.input_tokens, 'gen_ai.usage.output_tokens': response.usage.output_tokens, 'gen_ai.response.finish_reasons': [response.stop_reason], }) return response } )
const toolUses = response.content.filter((b) => b.type === 'tool_use') for (const tu of toolUses) { await tracer.span('execute_tool', { 'gen_ai.tool.name': tu.name }, async (s) => { const outcome = await registry.execute( tu.name, tu.input as Record<string, unknown> ) Object.assign(s.attributes, { 'tool.is_error': outcome.isError, 'tool.output_chars': outcome.content.length, }) }) } }
Object.assign(root.attributes, { steps: iteration, cost_usd: budget.spentUsd, }) })重要なのは、ログを1行1JSONの構造化形式で出すことである。テキストで f"ツール {name} を実行しました" と書いても、後から集計できない。「ツール別のエラー率」を出すには、フィールドとして持っている必要がある。
12.3 記録してはならないもの
Section titled “12.3 記録してはならないもの”トレースには、そのままでは記録してはならない情報が含まれる。エージェントは業務データを扱うため、この配慮は必須である。
12.3.1 リスク
Section titled “12.3.1 リスク”- 個人情報(PII): 顧客の氏名、住所、連絡先が、プロンプトやツール結果に含まれる
- 認証情報: APIキー、トークン。第7章7.6.4節で「認証情報はツールの引数にしない」と述べたが、環境によっては混入しうる
- 機微な業務データ: 未公開の財務情報、契約内容
- 量そのもの: エージェントのコンテキストは巨大である。全入出力を保存すると、ストレージコストが無視できない
12.3.2 内容記録の3段階
Section titled “12.3.2 内容記録の3段階”OpenTelemetry の GenAI 規約は、プロンプトや応答の内容をどう記録するかについて、3つのモードを想定している。
| モード | 内容 | 適する場面 |
|---|---|---|
| オフ(既定) | 内容を一切記録しない | 機微性が高い、または量が多い場合 |
| スパン属性に記録 | gen_ai.input.messages / gen_ai.output.messages として構造化JSONで持つ |
開発・検証環境 |
| 外部ストレージに記録 | 内容はS3等に保存し、スパンには参照だけを載せる | 本番環境で量が多い、または機微な場合 |
外部ストレージ方式の位置づけに注意。 これは規約上 MAY(任意)のフックにとどまり、参照をどう表現するかの標準属性はまだ定義されていない。規約側にも「共通の記録方法を文書化する」というTODOが残っている。設計方針としては妥当だが、「規約が推奨する標準的なやり方」ではなく、実務上の工夫として採るものである。属性名は自分で決めることになるため、12.4.4節の「1か所にまとめる」方針が効いてくる。
既定がオフである点に注意してほしい。 「トレースを入れたのに内容が見えない」という状況は、多くの場合これが理由である。逆に言えば、内容を記録するのは明示的な選択であり、その時点で扱いを設計する責任が生じる。
12.3.3 マスキング
Section titled “12.3.3 マスキング”内容を記録する場合、機微な情報は保存前に落とす。
import re
PATTERNS = [ (re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b"), "[EMAIL]"), (re.compile(r"\b\d{3}-\d{4}-\d{4}\b"), "[PHONE]"), (re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"), "[API_KEY]"), (re.compile(r"\bBearer\s+[A-Za-z0-9._~+/-]+=*"), "Bearer [REDACTED]"),]
def redact(text: str) -> str: for pattern, replacement in PATTERNS: text = pattern.sub(replacement, text) return text// Python の re.sub は全件を置換する。JavaScript の replace で同じ動きにするには// g フラグが要る。また JavaScript の \w は ASCII のみで、Python の \w のように// 日本語を含まない(ここでは対象がいずれも ASCII なので影響はない)const PATTERNS: [RegExp, string][] = [ [/\b[\w.+-]+@[\w-]+\.[\w.-]+\b/g, '[EMAIL]'], [/\b\d{3}-\d{4}-\d{4}\b/g, '[PHONE]'], [/\bsk-[A-Za-z0-9]{20,}\b/g, '[API_KEY]'], [/\bBearer\s+[A-Za-z0-9._~+/-]+=*/g, 'Bearer [REDACTED]'],]
function redact(text: string): string { for (const [pattern, replacement] of PATTERNS) { text = text.replace(pattern, replacement) } return text}正規表現によるマスキングは不完全である。 人名や住所は正規表現では捕捉しきれない。より確実にするには、専用のPII検出モデルを使う(第13章13.4節)。しかし完璧を目指して何もしないより、明らかなものだけでも落とすほうがよい。
保持期間も設計する。トレースを無期限に保持すると、コンプライアンス上の負債になる(第8章8.7.1節で記憶について述べたのと同じ理屈である)。開発環境は短く、本番の集計値は長く、内容そのものは短く、といった分離が有効である。
ただし、短くすればよいとは限らない。 第13章13.8.3節で述べるとおり、エージェントの判断について後から説明を求められる場合がある。規制業種では、これが法的な要件になることもある。プライバシーの観点は保持期間の上限を、説明責任の観点は下限を決める。両者は逆方向に働くため、デバッグ目的の保持と監査目的の保持を別要件として分けて設計するのが実務的である。監査目的なら、内容全文ではなく「どのツールをどの引数で呼び、何を根拠に判断したか」の要点だけを長期保持する、といった分離が考えられる。
12.4 OpenTelemetry の GenAI セマンティック規約
Section titled “12.4 OpenTelemetry の GenAI セマンティック規約”12.4.1 なぜ標準が要るか
Section titled “12.4.1 なぜ標準が要るか”各社の可観測性ツールが独自の形式でデータを持つと、乗り換えができない。OpenTelemetry(OTel) は分散トレーシングの標準であり、その上に GenAI セマンティック規約 — LLMやエージェント向けの属性名の取り決め — が定められつつある。
規約に沿って計装しておけば、バックエンドのツールを差し替えても計装コードを書き直さずに済む。第10章10.5.2節で述べた「移行可能性を残す」という考え方が、可観測性にも適用される。
12.4.2 スパンの階層
Section titled “12.4.2 スパンの階層”規約は、エージェントのトレースを複数の層として捉える。
| 層 | 操作名 | 内容 | スパン種別 |
|---|---|---|---|
| モデル呼び出し | chat / text_completion / embeddings / generate_content |
LLMへの直接の呼び出し | CLIENT |
| エージェント | create_agent / invoke_agent |
エージェントの生成・実行 | ローカルなら INTERNAL |
| ワークフロー | invoke_workflow |
事前定義されたワークフローの実行 | — |
| ツール | execute_tool |
ツールの実行 | INTERNAL |
| MCP | (JSON-RPC呼び出し) | MCPサーバーとのやりとり(第9章) | クライアント側 CLIENT / サーバー側 SERVER |
典型的なトレースは次の構造になる。
invoke_agent (INTERNAL)├── chat (CLIENT) ← モデルがツール呼び出しを決定├── execute_tool (INTERNAL) ← ツール実行│ └── tools/call (CLIENT) ← MCP経由なら入れ子になる│ └── [MCPサーバー側] (SERVER)├── chat (CLIENT)└── chat (CLIENT) ← 最終応答この階層があることで、エージェントの推論過程がトレース上で追えるようになる。ブラックボックスとして1つのスパンにまとめてしまうと、内部で何が起きたか分からない。
12.4.3 主な属性名
Section titled “12.4.3 主な属性名”# 汎用gen_ai.provider.name プロバイダ識別子(openai / anthropic / aws.bedrock など)gen_ai.operation.name 操作の種別(chat / execute_tool / invoke_agent など)gen_ai.request.model 要求したモデルgen_ai.response.model 実際に応答したモデル(バージョン込み)gen_ai.response.finish_reasons 終了理由の配列(["stop"] / ["tool_calls"] など)
# トークンgen_ai.usage.input_tokens 入力トークン数gen_ai.usage.output_tokens 出力トークン数gen_ai.usage.cache_read.input_tokens プロバイダ管理キャッシュの読み出しgen_ai.usage.cache_creation.input_tokens 同・書き込みgen_ai.usage.reasoning.output_tokens 思考トークン(第3章3.3節)
# ツールgen_ai.tool.name 実行したツール名
# 内容(記録する場合のみ)gen_ai.input.messages 入力メッセージgen_ai.output.messages 出力メッセージ
# MCP(第9章)mcp.method.name 呼び出したメソッド(tools/call など)mcp.protocol.version プロトコルバージョンgen_ai.request.model と gen_ai.response.model を分けて記録する設計は重要である。エイリアスを指定した場合、実際にどのスナップショットが応答したかが記録される(第2章2.6節)。
キャッシュ・思考のトークン属性はいずれも規約の標準属性である。プロバイダ固有の拡張ではない。なお規約上、これらの値は gen_ai.usage.input_tokens / output_tokens にも含めて報告することとされている。二重計上しないよう、コスト計算の際は定義を確認すること。
推奨される主なメトリクスは2つである。
gen_ai.client.operation.duration— 操作ごとのレイテンシ(秒)gen_ai.client.token.usage— トークン消費量
トークンについては、請求対象のトークン数を報告すること、そして値が取れない場合は推定せず省略することが推奨されている。推定値が混ざると、コスト分析が信用できなくなる。
12.4.4 成熟度に注意
Section titled “12.4.4 成熟度に注意”2026年5月時点で、GenAI規約とMCP規約は「Development(開発中)」ステータスにある。 安定化の時期は公表されていない。属性名は今後変わる可能性がある。
したがって実務では、次の構えが妥当である。
- 規約に沿って計装する(将来の移行が楽になる)
- ただし属性名を直接コードに散らさず、1か所にまとめる。変更があったときの修正箇所を減らす
- 独自の属性を足すことは問題ない。規約は独自属性を禁じていない
12.5 何を監視するか
Section titled “12.5 何を監視するか”12.5.1 4つの観点
Section titled “12.5.1 4つの観点”本番のエージェントで監視すべきものを整理する。
① 健全性(動いているか)
| 指標 | 異常の兆候 |
|---|---|
| エラー率 | 上昇 → API障害、ツールの不調 |
| レイテンシ(P50 / P95) | P95の悪化は一部ユーザーの体験を大きく損なう |
| スループット | 急減 → 上流の障害 |
| レート制限の発生率 | 上昇 → 並行度の設計見直し(第10章10.2.4節) |
② コスト(第2章)
| 指標 | 異常の兆候 |
|---|---|
| 1タスクあたりコスト | 上昇 → コンテキスト膨張、ステップ増加 |
| キャッシュヒット率 | 低下 → プロンプトの前方が変わっている(第2章2.2.2節) |
| 総支出のペース | 予算超過の予兆 |
③ 挙動(第5章の失敗モード)
| 指標 | 異常の兆候 |
|---|---|
| 平均ステップ数 | 上昇 → 停滞、または課題の難化 |
| 上限到達率 | 上昇 → 暴走、または設計の不備 |
| 停滞検知の発火率 | 上昇 → ツール設計かプロンプトの問題 |
max_tokens による打ち切り率 |
上昇 → 出力上限の設定が不適切 |
| ツール別エラー率 | 特定ツールの上昇 → そのツールの説明・スキーマ(第7章) |
④ 品質(第11章)
| 指標 | 異常の兆候 |
|---|---|
| タスク完遂率 | 低下 → 何かが壊れた |
| 人間へのエスカレーション率 | 上昇 → 自律性の低下 |
| 否定的フィードバック率 | 上昇 → 品質低下 |
| 参照なしLLM判定のスコア | 低下 → ただし11.7.3節のバイアスに注意 |
12.5.2 アラート設計
Section titled “12.5.2 アラート設計”すべてにアラートを設定すると、通知が鳴りすぎて誰も見なくなる。人が対応すべきものだけを鳴らす。
| 優先度 | 例 | 対応 |
|---|---|---|
| 緊急 | エラー率が急上昇、コストが時間あたり閾値を突破 | 即時対応 |
| 警告 | 完遂率が前週比で有意に低下、P95レイテンシの悪化 | 当日中に調査 |
| 情報 | ステップ数の緩やかな増加、特定ツールのエラー率上昇 | 定期レビューで確認 |
コストのアラートは特に重要である。 第1章1.5節で「利用上限を設定してあるか」を確認項目に挙げたが、本番でも同じである。エージェントのループが暴走すると、コストは急速に増える。時間あたりの支出に閾値を設けておく。
12.5.3 ドリフトの検出
Section titled “12.5.3 ドリフトの検出”急な障害より発見が難しいのが、じわじわした劣化である。
- モデルが更新された(エイリアスを使っている場合)
- ユーザーの使い方が変わった
- 参照している文書が更新された
- 外部APIの応答形式が微妙に変わった
これらは急には壊れないが、少しずつ品質を下げる。対策は、時系列で指標を追い、週次で比較することである。単一時点のスナップショットでは見えない。
第11章11.8節で述べた人間による定期レビューは、ドリフト検出の重要な手段でもある。指標に現れない劣化を捉えられるのは人間だけである。
12.6 デバッグの実践
Section titled “12.6 デバッグの実践”12.6.1 手順
Section titled “12.6.1 手順”エージェントの不具合を調べる標準的な手順を示す。
**「最終出力から遡る」**のが要点である。エラーが出た場所ではなく、方向がずれ始めた場所を探す。第5章5.4.4節の誤りの累積を思い出してほしい。
12.6.2 コンテキストを再現する
Section titled “12.6.2 コンテキストを再現する”エージェントのデバッグで最も効くのは、「モデルはそのとき何を見ていたか」を正確に再現することである。
トレースに入出力の内容が記録されていれば、そのステップのメッセージ列をそのまま取り出して、手元で再実行できる。プロンプトを変えて試すこともできる。
そのためには、記録する項目を増やす必要がある。 12.2.3節の Tracer の例はトークン数やモデル名しか記録しておらず、これだけでは再現できない。再現デバッグを行うなら、12.3.2節で「既定でオフ」と述べた内容記録を有効にしたうえで、システムプロンプトとツール定義も記録する。いずれにも12.3節の機微情報の扱いが適用される。
def replay_step(trace_id: str, step: int, **overrides): """特定ステップのコンテキストを取り出して再実行する。
前提: system / tools / gen_ai.input.messages が記録されていること。 12.2.3節の最小実装のままでは記録されていないので、計装を拡張する必要がある。 """ span = load_span(trace_id, operation="chat", iteration=step) return client.messages.create(**{ "model": span["gen_ai.request.model"], "system": span["system"], "tools": span["tools"], "messages": span["gen_ai.input.messages"], "max_tokens": 4096, **overrides, # 例: temperature や effort を変えて挙動を比べる })/** 特定ステップのコンテキストを取り出して再実行する。 * * 前提: system / tools / gen_ai.input.messages が記録されていること。 * 12.2.3節の最小実装のままでは記録されていないので、計装を拡張する必要がある。 */async function replayStep( traceId: string, step: number, overrides: Partial<Anthropic.MessageCreateParamsNonStreaming> = {}): Promise<Anthropic.Message> { const span = loadSpan(traceId, { operation: 'chat', iteration: step }) return client.messages.create({ model: span['gen_ai.request.model'], system: span['system'], tools: span['tools'], messages: span['gen_ai.input.messages'], max_tokens: 4096, ...overrides, // 例: temperature や effort を変えて挙動を比べる })}これができるかどうかが、デバッグの速さを大きく変える。「再現できない不具合」は直せない。
12.6.3 モデルに分析させる
Section titled “12.6.3 モデルに分析させる”第7章7.7.3節で述べた手法は、デバッグにも使える。失敗したトレースをまとめてモデルに読ませ、共通のパターンを探させる。
以下は、失敗した20件のエージェント実行トレースです。共通する失敗のパターンを特定してください。特に次の観点で見てください。
1. 同じツールで繰り返し誤った引数が渡されていないか2. ツールの説明が誤解を招いている形跡はないか3. 特定のステップで方向がずれる傾向はないか4. 本来1回で済むはずの操作を複数回行っていないか人間が20件のトレースを読むのは骨が折れるが、モデルは俯瞰して傾向を拾える。エラーとして表面化しない非効率を見つけるのに特に有効である。
12.7 可観測性ツールの現況(2026年7月)
Section titled “12.7 可観測性ツールの現況(2026年7月)”| ツール | 位置づけ | 特徴 |
|---|---|---|
| LangSmith | トレーシング + 評価 | データセット管理と実験比較(第11章)。LangChain/LangGraph との統合が厚いが、LangChain非依存でも使える(OpenAI、Anthropic、CrewAI、Pydantic AI などに対応) |
| Langfuse | トレーシング + 評価 | オープンソース。自前ホスティング可。フレームワーク非依存 |
| OpenLLMetry | OTel計装ライブラリ | OpenTelemetry の規約に沿った計装を提供。任意のOTelバックエンドへ送れる |
| Arize Phoenix | トレーシング + 評価 | オープンソース。埋め込みやRAGの分析に強い |
| Helicone | プロキシ型 | LLM APIの前段に置くだけで計測が始まり、導入は容易。キャッシュやレート制限も担う。⚠️ 2026年3月に Mintlify に買収され、メンテナンスモードに入った(下記) |
Helicone の現況(2026年7月時点): 2026年3月に Mintlify による買収が発表され、サービスは「当面のあいだメンテナンスモードで稼働を継続する」とされている。セキュリティ更新・新モデル対応・バグ修正・性能改善は継続されるが、新機能の開発は行われない。既存の利用は当面問題ないが、新規採用にあたっては将来の移行を織り込んで判断すべきである。
第10章10.5.2節で「依存先の寿命はリスクである」と述べたが、これは可観測性ツールにも当てはまる。本書の執筆中だけでも、Assistants API のサンセット、AutoGen のメンテナンスモード移行、そして Helicone の買収が起きている。
12.7.1 選択の観点
Section titled “12.7.1 選択の観点”① データの所在。 プロンプトとツール結果には業務データが含まれる。SaaSに送れない場合、自前ホスティング可能なもの(Langfuse、Phoenix)か、内容を送らない設定が要る。
② 導入の容易さ。 プロキシ型(Helicone)はコードをほぼ変えずに導入できる。ただしプロキシを経由するため、レイテンシと障害点が1つ増える。
③ ロックインの度合い。 OTel規約に沿ったもの(OpenLLMetry)は、バックエンドを差し替えやすい。専用SDKで計装するものは、乗り換え時に計装コードの書き直しが発生しうる。また上の Helicone の例が示すとおり、ツール自体の存続もリスク要因である。計装の抽象化(12.4.4節)は、この両方への備えになる。
④ 評価との統合。 第11章で述べたとおり、本番トレースから評価データセットを作る流れが重要である。トレーシングと評価が統合されているツール(LangSmith、Langfuse)は、この回路が作りやすい。
12.7.2 ツールを導入する前に
Section titled “12.7.2 ツールを導入する前に”第10章・第11章と同じ指針が適用される。まず12.2.3節のような最小限の構造化ログを入れ、何が必要かを把握してから選ぶ。
ツールを入れても、何を記録すべきかを理解していなければ役に立たない。ツールは記録の手段を提供するが、「プロンプトのバージョンを記録すべき」「ツール出力のサイズを記録すべき」といった判断は自分でするしかない。
12.8 まとめ
Section titled “12.8 まとめ”- エージェントのデバッグが難しいのは、エラーが出た場所と原因の場所が離れているからである(誤りの累積)
- 行単位のログでは足りない。1回の実行を木構造で再構成できるトレースが要件になる
- ログは1行1JSONの構造化形式で出す。テキストでは後から集計できない
- プロンプトのバージョンをトレースに記録する。 これがないと「どのプロンプトでの失敗か」が分からない
- トレースには機微な情報が入る。内容の記録は既定でオフであり、記録するのは明示的な選択である
- 本番で内容を残すなら、外部ストレージに置いてスパンには参照だけを載せる方式が有力。ただしこれは規約上 MAY であり、参照の標準的な表現はまだ定まっていない
- マスキングは正規表現では不完全だが、完璧を目指して何もしないより明らかなものだけでも落とす
- OpenTelemetry の GenAI セマンティック規約に沿って計装すると、バックエンドの乗り換えが楽になる
- スパンは階層で持つ。
invoke_agent→chat/execute_tool→ MCPの入れ子、という構造で推論過程が追える - 規約はまだ Development ステータスである。 属性名は変わりうるので、コードの1か所にまとめておく
- トークンは請求対象の値を記録し、取れない場合は推定せず省略する
- 監視は4観点。健全性・コスト・挙動・品質
- コストのアラートは必須である。 ループの暴走は急速にコストを増やす
- アラートは人が対応すべきものだけを鳴らす。鳴りすぎると誰も見なくなる
- ドリフトは急に壊れないぶん発見が難しい。時系列で追い、人間の定期レビューと組み合わせる
- デバッグは最終出力から遡り、方向がずれ始めた場所を探す
- 「モデルはそのとき何を見ていたか」を再現できることが、デバッグの速さを決める
- 失敗トレースをまとめてモデルに分析させると、表面化しない非効率が見つかる
- ツール選択の観点は、データの所在・導入容易性・ロックイン・評価との統合
問1 あるエージェントで「たまに全く見当違いの回答をする」という報告があった。トレースには最終出力とツール名しか記録されていない。
(a) この状態で診断できないことを3つ挙げよ (b) 12.2.2節を参考に、追加すべき記録項目を5つ挙げ、それぞれ何の診断に使えるかを述べよ (c) 12.6.1節の手順のうち、記録を追加しても実行できないステップはあるか
問2 次の監視データが観測された。それぞれ何が起きている可能性が高いか、12.5.1節を参考に述べ、確認すべきトレースの条件(どう絞り込むか)を示せ。
(a) 1タスクあたりコストが2週間で1.6倍になった。完遂率とステップ数は横ばい (b) P50レイテンシは変わらないが、P95が3倍になった (c) キャッシュヒット率が90%から12%に急落した (d) 完遂率は横ばいだが、人間へのエスカレーション率が2倍になった
問3 12.3節を踏まえ、次の3つの環境それぞれについて、トレースの内容記録の方針(オフ/スパン属性/外部ストレージ)と保持期間を設計し、理由を述べよ。
(a) 開発環境。少人数のチームが機能を作っている (b) 本番環境。医療機関向けのサービスで、問い合わせ内容に患者情報が含まれる (c) 本番環境。社内の開発者向けのコード検索アシスタント
問4
12.2.3節の Tracer 実装について答えよ。
(a) この実装は非同期処理(第10章10.2.4節の並列ツール実行)で正しく動くか。問題があるなら指摘し、対処を述べよ
(b) emit() はスパン終了時に呼ばれる。長時間かかるスパンの途中経過を見たい場合、どう変更すべきか
(c) trace_id が Tracer インスタンスに固定されている。複数タスクを同時に処理する場合、どう変更すべきか
問5 本章と第11章の関係について答えよ。
(a) 「評価」と「監視」で測る指標には重複がある。同じ指標でも、評価と監視で意味が異なるのはなぜか (b) 12.6.1節の手順の最後は「評価セットに追加」で終わっている。この回路がなぜ重要か、第11章11.4.2節を踏まえて説明せよ (c) 本番トレースから評価データセットを作る際、12.3節の制約はどう影響するか
問6 OpenTelemetry の GenAI 規約が「Development ステータス」であることを踏まえ、いま計装を実装するとしたらどのような設計にすべきか。12.4.4節の指針を具体的なコード構造として示せ。
参考文献・出典
Section titled “参考文献・出典”| 出典 | 内容 | 参照日 |
|---|---|---|
| OpenTelemetry GenAI Semantic Conventions の解説 | スパンの階層(chat / invoke_agent / execute_tool / MCP)、gen_ai.* 属性名、内容記録の3モード、Development ステータス |
2026-07-29 |
| OpenTelemetry — GenAI Semantic Conventions | 規約が専用リポジトリへ移管されたこと | 2026-07-29 |
| Anthropic — Building Effective Agents | エージェントの誤り累積とデバッグの難しさ | 2026-07-29 |
| Anthropic — Writing Tools for Agents | トレースをモデルに分析させる手法 | 2026-07-29 |
| SigNoz — LLM Observability Tools 2026 | 可観測性ツールの比較 | 2026-07-29 |
| roadmap.sh — AI Agents Roadmap | 章構成の基準、ツールの分類 | 2026-07-29 |
次章予告: 最終章となる第13章ではセキュリティと倫理を扱う。プロンプトインジェクション、ツールのサンドボックス化と権限管理、データプライバシーとPII、バイアスとガードレール、そしてレッドチームテスト — 本書が各章で断片的に触れてきた安全性の話題を、ひとつの体系としてまとめる。
Built with Astro ・ Deployed on Cloudflare Pages
© 2026 watakumi — made with 💜 & ☕ ・watakumi.page