10月1日、Machine Learning Masteryが「AI Agent Observability: Logging, Tracing, and Debugging Explained」と題した記事を公開した。カスタマーサポートを担当するAIエージェントが、丁寧で読みやすい、しかし完全に誤った回答でチケットをクローズする。refund-lookupツールを2回呼び出し、2回目の結果を採用して自信満々に応答した。クラッシュなし。エラーなし。ダッシュボードは緑のまま。問題が発覚するのは、2日後に困惑した顧客から返信が届いてからだ。
「エラーなし、でも答えは完全に間違い」——従来の監視では見えない失敗
記事はこの実例から始まる。AIエージェントは「それっぽい正解」を返しながら完全に失敗できる。従来のHTTPサービスであればエラー200か例外を投げるかのどちらかだが、エージェントはそのどちらでもなく誤答する。アップタイム監視や例外ハンドラーで設計された既存ツールは、この種の失敗に対してほぼ盲目だ。
AI Agent Observability(オブザーバビリティ)とは、エージェントが実行するモデル呼び出し・ツール実行・推論ステップをすべて構造化データとして記録し、問題発生時に「何が起きたか」を正確に再現できるようにする実践だ。従来の3本柱(ログ・メトリクス・トレース)を借用しつつ、エージェント特有の失敗モードに対応する独自の規律として位置づけられている。
なぜエージェントは従来の監視モデルを壊すのか
記事は「エージェントは予測不能」という曖昧な説明を避け、具体的なメカニズムを列挙する。
- 同じ入力が同じ動作を保証しない。temperatureの設定、RAGの検索結果、利用可能なツールの違いによって、同一プロンプトが異なるツール呼び出し列を生む。
- コストとレイテンシの相関先が変わる。リクエスト数ではなくトークン消費量に相関する。1リクエストが通常の10倍のトークンを消費していても、RPS(秒間リクエスト数)ベースの監視では検知できない。
- 1ユーザーリクエストが複数の障害点を持つ。モデル呼び出し、ツール呼び出し、検索クエリがそれぞれ独立した失敗点となり、集計エラーメトリクスでは区別できない。
- プロンプトが個人情報を含む。素朴なログ実装でプロンプト全文をバックエンドに流すと、デバッグ価値より先にコンプライアンス問題が発生する。
| シグナル | 従来のアプリ | LLM / AIエージェント |
|---|---|---|
| レイテンシの要因 | CPU、I/O、ネットワーク | トークン数、モデルサイズ、コンテキスト長 |
| コスト単位 | リクエスト数 | 消費トークン数 |
| 失敗モード | 例外、タイムアウト | ハルシネーション、コンテキスト溢れ、ツールエラー |
| デバッグ成果物 | スタックトレース | プロンプト・補完・その間の推論チェーン |
ログ:すべてのログ行にtrace IDを紐付ける
記事が最初に強調するのは、ログの構造化と実行単位(run)との紐付けだ。「tool call failed」だけのログは、深夜2時に3ユーザーが同時に3つの異なるrunを走らせていたとき、ほぼ役に立たない。
OpenTelemetryを使えば、trace.get_current_span()で現在アクティブなスパンを取得でき、trace IDを関数の引数として手動で引き回す必要がない。これが実際のコードベースに後付けしやすい理由だ。
import logging
from opentelemetry import trace
logger = logging.getLogger("agent")
tracer = trace.get_tracer("agent-service")
def call_tool(tool_name: str, arguments: dict):
span = trace.get_current_span()
trace_id = format(span.get_span_context().trace_id, "032x")
logger.info("tool_call_started", extra={
"trace_id": trace_id,
"tool_name": tool_name,
"arguments": arguments,
})
try:
result = execute_tool(tool_name, arguments)
logger.info("tool_call_succeeded", extra={
"trace_id": trace_id,
"tool_name": tool_name,
"result_length": len(str(result)), # 全文ではなく長さだけ
})
return result
except Exception as e:
logger.error("tool_call_failed", extra={
"trace_id": trace_id,
"tool_name": tool_name,
"error": str(e),
})
raise
ツール引数と結果の長さだけをログに残す設計は意図的なものだ。ツール出力はサイズが大きくなりがちで、機密データを含むこともある。長さや切り詰めたプレビューで問題の検知には十分であり、全文ログは不要なプライバシーリスクを生む。開始イベントと終了イベントの両方を記録するのは、後でそのツール呼び出しの所要時間を正確に計測するためだ。
トレース:「なぜそのツールを呼んだか」を親子関係で表現する
ログが時系列の点を記録するのに対し、トレースはその点を形に繋ぐ。1回のエージェントrunを最初のリクエストから最終回答まで記録し、各ステップをトリガーした親子関係のツリーとして表現する。
記事ではOpenTelemetry GenAI semantic conventionsに準拠した実装を紹介している。このspecはgen_ai.*スパン型と属性を標準化しており、以下の操作タイプが定義されている。
create_agent:エージェントの定義時invoke_agent:1回のエージェントruninvoke_workflow:複数エージェント間のオーケストレーションexecute_tool:個別のツール呼び出しchat:モデル推論呼び出し本体
共通属性としてはgen_ai.request.model、gen_ai.usage.input_tokens、gen_ai.usage.output_tokensなどがある。異なるチームやフレームワークが生成したトレースが同じ構造を持つことで、ツールやダッシュボードの互換性が生まれる。
Pythonのwith tracer.start_as_current_span(…)ブロックのネスト構造が、そのままOpenTelemetryの親子スパン関係にマッピングされる。手動でスパンの親子を配線する必要はなく、コードのネスト構造が自動的に意味を持つ。
メトリクスとデバッグワークフロー
記事はトークンコストの追跡から実践的なデバッグシナリオまでをカバーしている。メトリクスの中核となるのがgen_ai.client.token.usageカウンターだ。モデル呼び出しのたびに入力・出力トークン数をこのカウンターに記録し、Prometheusへエクスポートすることで、「特定のrunが他の10倍のトークンを消費していた」といった異常をグラフ上で即座に検出できるようになる。RPSベースの監視では素通りしてしまうコスト爆発を、トークン単位の粒度で捕捉できる点が実用上の大きなメリットだ。
デバッグシナリオとしては、たとえば「ツール呼び出しが重複している」疑いがある場合、トレースのウォーターフォール(滝グラフ)を開いて同一ツールのexecute_toolスパンが複数並んでいないかを確認するアプローチが紹介されている。冒頭のrefund-lookupの二重呼び出しのような失敗も、トレースビューがあれば「いつ・何回・どの順序でツールが呼ばれたか」を数秒で特定できる。
エージェント開発の現場では、LangChainやLangSmith、あるいはArize AIなどのツールがオブザーバビリティの文脈で語られることが増えているが、この記事は特定フレームワークへの依存なしにOpenTelemetry標準だけで完結する実装を示している点が実用的だ。既存のどのオブザーバビリティスタックにも接続できる汎用性が、このアプローチの強みといえる。
詳細はAI Agent Observability: Logging, Tracing, and Debugging Explainedを参照していただきたい。