この記事は Synthorai ブログの記事 LLM エージェントの可観測性:トレースとセッションで 10 の問題を再現 の転載です。原文(多言語対応)はリンク先をご覧ください。
LLM エージェントの可観測性は、モデル呼び出しを 2 つの単位でまとめるところから始まります。トレースはエージェントの 1 回の実行をタイムラインで表したもの、セッションは合計コストを含む 1 つの会話です。各呼び出しに 3 つのリクエストヘッダーを付ければ、LLM ゲートウェイがリクエストログから両方を構成します。トレーシング用の SDK は不要です。ゲートウェイはアプリケーションコードとモデルプロバイダーの間に置くプロキシで、すべてのリクエストを記録します。この構成を小規模なサポートエージェントと Claude Code に適用し、よくある 10 の問題を再現しました。遅延の原因が 3 つ、コスト増の原因が 4 つ、動作不良の原因が 3 つです。
TL;DR
- 3 つのリクエストヘッダー(
X-Trace-Id、X-Span-Id、X-Session-Id)を付けるだけで、Synthorai ゲートウェイのリクエストログからトレースとセッションを構成できます。トレーシング用の SDK は不要です。 - 独立した 3 つのサブエージェントのチェックを同時に送ると、コストを変えずに所要時間の中央値を 3.7 s から 1.8 s に短縮できました。
- GPT-6 Luna でプロンプトの先頭行からタイムスタンプを移すと、空のキャッシュから開始してもキャッシュヒット率が 0% から 73% に上がりました。
- Claude Code の 1 回の実行コストは $0.1728 で、そのうち 72% を 3 つのエージェントそれぞれの初回呼び出しが占めていました。
この記事は、全 7 本のガイドシリーズの概要です。各節では、対応するガイドの検証結果を数値とともにまとめています。コードとスクリーンショットはリンク先で確認できます。実行日はすべて 2026 年 10 月 10 日と 11 日です。レイテンシーは 5 回の実行の中央値で、それ以外の数値はすべて 1 回の実行結果です。コードは github.com/synthorai-io/use-cases にあります。
LLM エージェントのトレース、スパン、セッションとは
トレースは、1 つのユーザーメッセージやタスクを処理するためにエージェントが行う一連の動作です。スパンはその中の 1 ステップ、セッションは複数のトレースからなる会話全体を指します。今回のサポートエージェントでは、2 ターンの顧客との会話が 1 セッションになり、その中に 2 つのトレースが入ります。各トレースには 5 回または 6 回のモデル呼び出しがあります。一部はサブエージェントによる呼び出しです。サブエージェントは、メインエージェントがタスクの一部を任せるために起動する別のエージェントで、専用のプロンプトを持ち、多くの場合はより安価なモデルを使います。
| 用語 | 対象 | ヘッダー | 同じ値を送る範囲 |
|---|---|---|---|
| トレース | エージェントの 1 回の実行 | X-Trace-Id |
その実行の全呼び出し |
| スパン | 実行内の 1 回のモデル呼び出し | X-Span-Id |
その呼び出しのみ |
| セッション | 1 つの会話 | X-Session-Id |
その会話の全呼び出し |
| エージェント | 呼び出し元のサブエージェント | X-Agent-Id |
そのサブエージェントの全呼び出し |
| ラベル | ユーザー、機能、リリース | X-Synthorai-Metadata |
グループ化や絞り込みの対象にしたい呼び出し |
ID は任意の文字列で、それぞれ最大 128 文字の印字可能な ASCII 文字を使えます。ラベルは文字列のキーと値を持つフラットな JSON オブジェクトです。リクエストごとに最大 10 個、各値は最大 128 バイトです。ゲートウェイはレスポンスに ID を返しますが、これらのヘッダーをモデルプロバイダーには転送しません。OpenAI Python SDK では、リクエストごとのヘッダーを extra_headers に指定します。
resp = client.chat.completions.create(
model="gpt-6-luna",
messages=messages,
extra_headers={
"X-Session-Id": session_id, # one per conversation
"X-Trace-Id": trace_id, # one per user message
"X-Span-Id": "plan", # one per call
"X-Synthorai-Metadata": '{"feature":"support-agent","_user":"u_1002"}',
},
)
別のツール経由ですでにトレース ID を送っているエージェントなら、何も追加せずに使える場合があります。3 種類の ID のうちリクエストに含まれていないものは、OpenTelemetry、LiteLLM、Langfuse、Open WebUI、Claude Code、OpenAI Codex CLI の対応するヘッダーから読み取ります。OpenTelemetry はトレーシングのオープン標準で、traceparent ヘッダーにトレース ID を格納します。3 つの HTTP ヘッダーで LLM エージェントをトレースするでは、デモ用エージェントを順に構築しています。GPT-6 Luna による計画の呼び出し、2 つのローカルツール、DeepSeek V4.1 Flash による 3 つの並列チェック、そして回答の呼び出しという構成です。デモでは 21 回のモデル呼び出しで $0.0017 かかりました。
調べたいことに応じて、どの画面を見るか
このシリーズで扱う疑問は、それぞれコンソール内の特定の画面、またはコンソール外からログを読む方法で調べられます。コンソールでは、各トレースをウォーターフォールで表示します。共通の時間軸上にモデル呼び出しごとのバーが並ぶため、並列実行された呼び出しは重なり、前の呼び出しを待って実行されたものは階段状に見えます。
| 疑問 | 確認する場所 | ガイド |
|---|---|---|
| この実行に 8 秒かかったのはなぜか | トレースの概要にある、ゲートウェイ内の時間とアプリ内の時間 | エージェントのレイテンシー |
| この会話で最もコストがかかったターンはどれか | トレースごとに 1 行で表示するセッションページ | エージェントのコスト |
| キャッシュヒット率が 0% なのはなぜか | セッションページのキャッシュヒット列 | エージェントのコスト |
| 昨日のこのユーザーのリクエストで何が起きたか | トレース一覧でのラベル検索 | エージェントのデバッグ |
| Claude Code のどのサブエージェントにコストがかかったか | セッションページのエージェント別テーブル | Claude Code のサブエージェント別コスト |
| チャット画面でこれらを質問できるか | Claude の MCP コネクター | LLM ログについて Claude に質問する |
| 自作プログラムでログを調査できるか | MCP ツールまたは REST API | エージェントにログ調査ツールを渡す |
各トレースページには比較ボタンもあります。2 つのトレースを同じページに表示し、呼び出しを名前で対応付け、両方のウォーターフォールを同じ時間軸に並べます。このシリーズの修正はすべて、この方法で修正前後の実行を比較して確認しました。
LLM エージェントが遅いのはなぜか
まず、トレース上部にある時間の内訳を確認します。最初のリクエストから最後のレスポンスまでの経過時間、つまりユーザーが待った時間を、ゲートウェイ内の時間とアプリ内の時間に分けています。ゲートウェイ内の時間は、少なくとも 1 つのモデルリクエストが処理中だった時間です。アプリ内の時間は、アプリケーションコードだけが動いていた時間です。デモのトレースでは、経過時間 8.5 s のうちゲートウェイ内が 7.0 s、アプリ内が 1.6 s だったため、モデル呼び出しを調べるべきだと分かります。レイテンシーのガイドでは 3 つの原因を再現し、それぞれ 5 回ずつ実行しました。
| 原因 | トレース上の特徴 | 修正 | 修正前の中央値 | 修正後の中央値 |
|---|---|---|---|---|
| 独立した呼び出しを順番に送信 | バーが階段状に並ぶ | Python で複数の呼び出しを同時に待つ asyncio.gather でまとめて送る |
3.7 s | 1.8 s |
| 呼び出し間のコードが遅い |
your app の行が長い |
4 つの検索処理を同時に実行する | 11.0 s | 9.2 s |
| 1 行の回答にも推論を使う | 出力数が回答の長さに比べて大きすぎる | reasoning_effort="none" |
5 回答で 25.6 s | 10.2 s |
コードが遅いケースでは、修正前後でモデル呼び出しの所要時間はほぼ同じでした。変わったのはアプリ内の時間です。ガイドに載せた比較では、your app の行が 4.6 s から 1.3 s に短くなりました。この部分はモデルを変えても短縮できません。アプリ側のプロファイラーや OpenTelemetry のスパンから調べます。3 つ目のケースには GPT-6 Luna を使いました。reasoning_effort は回答前にどれだけ推論するかを指定するリクエストパラメーターです。none にすると、5 回答分の出力トークンが 1,150 から 190 に減りました。LLM エージェントが遅いのはなぜかに、3 ケースすべてのコードと比較用トレースを載せています。
LLM エージェントのコストが高いのはなぜか
まずトレース一覧をコスト順に並べ、最も高額なセッションを開きます。そこでキャッシュヒット列と入力列をターンごとに確認します。キャッシュヒット率は、入力トークンのうちプロバイダーのプロンプトキャッシュから読み取られた割合です。プロバイダーが以前に処理したプロンプトの接頭部分には、通常の入力より低い料金が適用されます。コストのガイドでは 4 つの原因を再現しました。小規模なデモなので金額は 1 セント未満ですが、他の環境でも参考になるのは比率です。
| 原因 | 確認できる箇所 | 修正前 | 修正後 |
|---|---|---|---|
| プロンプトの先頭行にタイムスタンプがある | キャッシュヒット率が 0% のまま | キャッシュ利用率 0% | キャッシュ利用率 73%、入力コスト 66% 減 |
| 履歴に全ツールの結果を含めて再送している | ターンごとに入力が増える | 入力トークン 24,546 | 入力トークン 8,053 |
| リトライループ | 1 つのトレースに呼び出しが多すぎるが、ステータスはすべて 200 | 10 回の呼び出し | 2 回の呼び出し |
| 安価に処理できるステップに高価なモデルを使っている | セッションページのエージェント別テーブル | $0.0012 | $0.0003 |
最初の行は、空のキャッシュから GPT-6 Luna で実行した結果です。ガイドでは、キャッシュが効かなくなるもう 1 つのパターンも扱っています。ユーザーメッセージの送信時に指示を追加するのに、保存時にはその指示を含めないケースです。これでは毎ターン、前のターンで実際には送っていないプロンプトを組み立て直すことになります。送信したとおりにメッセージを保存すると、その会話のヒット率は 0% から 76% に上がりました。履歴のケースでは、入力トークンが 3 倍でも入力コストの増加は 21% にとどまりました。71% がキャッシュから読み取られていたためです。最後の行では、GPT-6.1 Sol を使う 3 つのサブエージェントのチェックが、セッションコストの 81% を占めていました。同じチェックを DeepSeek V4.1 Flash で実行すると、コストは 10 分の 1 になりました。LLM エージェントのコストが高いのはなぜかで、各ケースを詳しく説明しています。
ユーザーからの不具合報告をもとに、どうデバッグするか
ラベルでユーザーの実行を探し、トレースを読んで問題のある呼び出しを特定します。問題の呼び出しでも、ステータスは 200 であることがよくあります。各リクエストに _user ラベルを付けておけば、トレース一覧の検索欄に metadata._user = u_1002 と入力して、そのユーザーのセッションとトレースを取得できます。デバッグのガイドでは、続いて 3 つの不具合を再現しました。
-
404 とそのリトライ。存在しないモデル ID への呼び出しは 95 ms で失敗し、コストは $0 でした。リトライ時にトレース ID を引き継ぎ、スパン名を新しくした(
policy-check.retry)ため、失敗した呼び出しとリトライがウォーターフォール上で隣り合って表示されます。 -
ステータス 200 なのに回答が途切れる。
max_tokens=40で推論を有効にしたまま DeepSeek V4.1 Flash を 3 回続けて呼ぶと、いずれもステータス 200、出力トークン数はちょうど 40、回答は空になりました。推論だけで上限を使い切ったためです。トレースには同じスパンが 3 回現れ、出力数が上限と一致します。上限を 2,000 に上げて推論を無効にすると、1 回の呼び出しで 49 トークンの回答が返りました。上限を上げるだけでも回答は返りましたが、推論に 1,876 トークンを使い、22 s かかりました。 - 2 ターン目に 400 が返る。保存した履歴からツール呼び出しが欠落していたため、1 ターン目は動いていた GPT-6 Luna が 2 ターン目を拒否しました。0.5 s で終了し、料金はかかりませんでした。
2 つ目の不具合を検知するには、すべての呼び出しで finish_reason を記録する必要があります。これはモデルが生成を止めた理由を示すレスポンスフィールドです。値が length なら max_tokens の上限に達したことを意味するため、ステータスが 200 でも失敗として扱うべきです。LLM エージェントをデバッグするでは、すべてのエージェントが記録すべき 5 つの項目を挙げています。この記事の後半にも再掲します。
自分で実装していないコーディングエージェントもトレースできるか
Claude Code は各リクエストにセッション ID とエージェント ID を送るため、本体を変更せずにトレースできます。2 つの環境変数で接続先をゲートウェイに切り替え、3 つ目で各サブエージェントの種類を追加できます。
export ANTHROPIC_BASE_URL="https://synthorai.io" # no /v1: Claude Code adds /v1/messages itself
export ANTHROPIC_API_KEY="<your gateway key>"
export CLAUDE_CODE_GATEWAY_HINT_HEADERS=1 # optional: names sub-agent types such as Explore
Claude Sonnet 5.5 を使う Claude Code で、2 つのサブエージェントとともに失敗するテストを 1 つ修正したところ、11 リクエストで $0.1728 かかりました。セッションページでエージェント別に見ると、2 つのサブエージェントが全体の 39% を占めていました。また、キャッシュ書き込みが全体の 77%、3 つのエージェントそれぞれの初回呼び出しが全体の 72% でした。30 秒後にキャッシュが温まった状態で同じ処理を再実行すると、コストは $0.0812 でした。Claude Code のサブエージェント別コストでは、チームの支出を開発者別、リポジトリ別に分ける方法も扱っています。
モデルにトレースを読ませられるか
ゲートウェイはリクエストログを MCP サーバーとして公開しているため、モデルから MCP 経由で読めます。MCP(Model Context Protocol)は、モデルにツールを渡すための標準です。ログの読み方は、誰が調査するかで変わります。
| 調査する主体 | 使うもの | モデルのトークン料金 | 検証結果 |
|---|---|---|---|
| 質問したい人 | ゲートウェイをコネクターとして追加した Claude | チャットプランに含まれる | 3 つの質問に 14 回のツール呼び出しで回答し、すべての数値が課金記録と一致 |
| 固定クエリを実行するプログラム | REST API | なし | コスト上位 3 セッションを $0 で取得 |
| 調べる対象を自分で決める自作エージェント | プログラム内の MCP ツール | 別途支払い | 1 回のコスト調査で $0.0015 |
チャットのケースでは、Claude Fable 5.1 を OAuth で接続し、リクエストログへの読み取り権限だけを与えました。OAuth では API キーを貼り付ける代わりに、ブラウザーでログインします。これにより、4 つの読み取り専用ツールと、有効期間が 7 日のキーが付与されました。ツールが返すのはコスト、トークン数、ステータスで、プロンプトや回答の本文は含みません。プログラムのケースでは、GPT-6 Luna が 26 セッションの中からキャッシュ、モデル選択、リトライループに問題があるセッションを、1 回のツール呼び出しで特定しました。コストは $0.0015 でした。Claude Sonnet 5.5 も価格を調べたうえで、3 回のツール呼び出しにより同じセッションを特定し、$0.0631 かかりました。LLM ログについて Claude に質問するとエージェントにログ調査ツールを渡すで、それぞれの設定を説明しています。
すべてのエージェントが記録すべき項目
次の 5 項目で、このシリーズのすべての問題を追えます。
- 実行ごとのトレース ID と会話ごとのセッション ID をヘッダーで送り、アプリ側のログにも記録します。
-
処理ステップと試行を識別できる呼び出しごとのスパン名 を付けます。例は
plan-2やpolicy-check.retryです。 - ユーザー、機能、リリースのラベル を付け、不具合報告や請求額に対応する実行を絞り込めるようにします。
-
各呼び出しのレスポンスヘッダー
x-request-idを記録し、後からその呼び出しの請求コストを調べられるようにします。 -
各呼び出しの
finish_reasonと出力トークン数 を記録し、lengthは失敗として扱います。
また、すべての呼び出しでクライアントのタイムアウトと max_tokens を設定し、どちらにも余裕を持たせます。表示される回答の長さだけを基準に上限を決めると、先ほどの空の 200 レスポンスにつながります。推論モデルでは、長い 1 回の呼び出しに数分かかることもあります。
ゲートウェイ側のトレーシングでは分からないこと
リクエストログから追跡できるのはモデル呼び出しだけで、アプリケーションコードの内部は見えません。コードが遅いケースで示したように、ゲートウェイはリクエスト間の空白からコードの実行時間を推定します。ツール、検索、キューのスパンを取得するには OpenTelemetry SDK が必要です。X-Trace-Id がなければゲートウェイは traceparent を読むため、両方のビューでトレース ID を共有できます。また、トレースにはプロンプトとレスポンスの本文は含まれず、コストも事後の報告です。予算をリアルタイムに管理するには、エージェントのループ内でレスポンスを受け取るたびに、その usage からコストを計算する必要があります。
よくある質問
LLM エージェントの可観測性とは
LLM エージェントの可観測性とは、実行ごとに何が行われたかを把握できることです。どのモデルをどの順序で呼び出し、それぞれにどれだけ時間とコストがかかり、どれが失敗したかを確認します。実際には、呼び出しをトレース(1 回の実行)とセッション(1 つの会話)にまとめ、ユーザーや機能のラベルを付けて管理します。
関連記事
- 3 つの HTTP ヘッダーで LLM エージェントをトレースする:デモ用エージェントとヘッダーの設定を順に解説します。
- リクエストヘッダーリファレンス:この記事に登場する全ヘッダーと、他のツールから読み取る別名ヘッダーの一覧です。
- コンソールのトレースとセッション:一覧、ウォーターフォール、セッションページの説明です。
- LLM のトークン使用量を読み解く:各レコードのトークン数が何を意味するかを、プロバイダーごとに説明します。
- プロンプトキャッシュ完全ガイド:プロンプトの接頭部分がキャッシュにヒットする条件、しない条件を説明します。
