LLM 代理可觀測性:追蹤與工作階段,重現 10 種問題
目錄
要建立 LLM 代理的可觀測性,先將模型呼叫分成兩個層級:追蹤(trace)以時間軸呈現代理的一次執行;工作階段(session)則涵蓋一段完整對話及其總費用。每次呼叫只要帶上 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 執行花費 $0.1728,其中 3 個代理各自的第一次呼叫就占了 72%。
本文是 7 篇系列指南的總覽。以下各節摘要介紹其中一篇指南,列出實測數據,並附上連結,完整程式碼與畫面截圖都在各篇指南中。所有測試都在 2026 年 10 月 10 日與 11 日執行。延遲數據是 5 次執行的中位數,其餘數據皆取自單次執行。程式碼位於 github.com/synthorai-io/use-cases。
LLM 代理的追蹤、跨度與工作階段是什麼?
追蹤涵蓋代理處理一則使用者訊息或一項任務的所有動作;跨度(span)是追蹤中的一個步驟;工作階段則是由多筆追蹤組成的完整對話。在我們的客服代理中,一段有 2 輪互動的客戶對話,就是包含 2 筆追蹤的 1 個工作階段,每筆追蹤有 5 或 6 次模型呼叫。部分呼叫來自子代理,也就是主代理為了處理部分任務而啟動的另一個代理。子代理有自己的提示詞,而且通常使用較便宜的模型。
| 名稱 | 涵蓋範圍 | 標頭 | 哪些呼叫使用相同值 |
|---|---|---|---|
| 追蹤 | 代理的一次執行 | X-Trace-Id | 該次執行的每次呼叫 |
| 跨度 | 執行中的一次模型呼叫 | X-Span-Id | 僅該次呼叫 |
| 工作階段 | 一段對話 | X-Session-Id | 該段對話的每次呼叫 |
| 代理 | 發出呼叫的子代理 | X-Agent-Id | 該子代理的每次呼叫 |
| 標籤 | 使用者、功能、版本 | X-Synthorai-Metadata | 任何需要分組或篩選的呼叫 |
識別碼是自行指定的字串,每個最多 128 個可列印 ASCII 字元。標籤是只含字串值的單層 JSON 物件,每筆請求最多 10 個標籤,每個值最多 128 位元組。閘道會在回應中帶回識別碼,但不會將這些標頭轉送給模型供應商。使用 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"}',
},
)
如果代理已經透過其他工具傳送追蹤識別碼,可能不必再加任何設定。這 3 個識別碼若有缺漏,閘道會分別從 OpenTelemetry、LiteLLM、Langfuse、Open WebUI、Claude Code 或 OpenAI Codex CLI 的對應標頭讀取。OpenTelemetry 是追蹤的開放標準,其 traceparent 標頭包含追蹤識別碼。用 3 個 HTTP 標頭追蹤 LLM 代理 會逐步建立示範代理:先用 GPT-6 Luna 規劃,接著執行 2 個本機工具、用 DeepSeek V4.1 Flash 平行執行 3 項檢查,最後再呼叫模型產生回答。示範執行共發出 21 次模型呼叫,費用為 $0.0017。
各種問題該看哪個檢視?
系列中的每個問題,都能在主控台的特定位置,或透過主控台以外的紀錄查詢方式找到答案。主控台以瀑布圖呈現每筆追蹤:每次模型呼叫各占一條橫條,共用同一條時間軸。平行執行的呼叫會重疊,必須等待前一個完成的呼叫則會排成階梯。
| 問題 | 查看位置 | 指南 |
|---|---|---|
| 這次執行為什麼花了 8 秒? | 追蹤摘要:比較閘道時間與應用程式時間 | 代理延遲 |
| 這段對話哪一輪最貴? | 工作階段頁面,每列一筆追蹤 | 代理費用 |
| 快取命中率為什麼是 0%? | 工作階段頁面的快取命中欄位 | 代理費用 |
| 這位使用者昨天的請求發生了什麼事? | 在追蹤清單中用標籤搜尋 | 代理除錯 |
| Claude Code 的哪個子代理花了這些錢? | 工作階段頁面的代理表格 | 依子代理分析 Claude Code 費用 |
| 能在聊天視窗裡問這些問題嗎? | Claude 中的 MCP 連接器 | 用 Claude 查詢 LLM 紀錄 |
| 自己的程式能調查紀錄嗎? | MCP 工具或 REST API | 讓代理使用紀錄查詢工具 |
每個追蹤頁面也都有比較按鈕,可在同一頁顯示兩筆追蹤,依名稱配對各次呼叫,並讓兩張瀑布圖共用時間軸。系列中的每項修正,都用這個方式比對修正前後的執行結果。
LLM 代理為什麼很慢?
先看追蹤頂端的時間拆分。它會將總經過時間,也就是使用者從第一筆請求到最後一筆回應的等待時間,分成閘道時間與應用程式時間。只要至少有一筆模型請求尚未完成,就計入閘道時間;只有自己的程式碼在執行時,則計入應用程式時間。示範追蹤的總經過時間為 8.5 s,其中閘道占 7.0 s,應用程式占 1.6 s,因此應該先檢查模型呼叫。延遲指南重現了 3 種原因,每種都執行 5 次:
| 原因 | 追蹤中的現象 | 修正方式 | 修正前中位數 | 修正後中位數 |
|---|---|---|---|---|
| 彼此獨立的呼叫逐一送出 | 橫條排成階梯 | 用 asyncio.gather 一起送出,這是 Python 同時等待多次呼叫的方式 | 3.7 s | 1.8 s |
| 呼叫之間的程式碼執行太慢 | your app 那一列很長 | 同時執行 4 次查詢 | 11.0 s | 9.2 s |
| 一行就能回答的問題也啟用推理 | 輸出 token 數遠多於實際回答 | reasoning_effort="none" | 5 個回答共 25.6 s | 10.2 s |
在程式碼過慢的案例中,兩次執行的模型呼叫耗時相近,改變的是應用程式時間:指南中並列的兩次執行,your app 那一列從 4.6 s 降至 1.3 s。換模型無法縮短這段時間,必須先用自己的效能分析工具或 OpenTelemetry 跨度找原因。第三個案例使用 GPT-6 Luna。reasoning_effort 是控制模型回答前推理程度的請求參數,設為 none 後,5 個回答的輸出 token 從 1,150 降至 190。LLM 代理為什麼很慢? 提供這 3 個案例的程式碼與並排追蹤畫面。
LLM 代理為什麼這麼貴?
先將追蹤清單依費用排序,再開啟最貴的工作階段,逐輪查看快取命中與輸入欄位。快取命中率是從供應商提示詞快取讀取的輸入 token 比例;供應商對先前看過的提示詞前綴,只收取原輸入價格的一部分。費用指南重現了 4 種原因。示範規模很小,金額都不到 1 美分,實際可套用的是改善比例:
| 原因 | 出現位置 | 修正前 | 修正後 |
|---|---|---|---|
| 提示詞第一行放了時間戳記 | 快取命中率維持 0% | 0% 命中快取 | 73% 命中快取,輸入費用降低 66% |
| 歷史紀錄每次重送所有工具結果 | 每輪輸入持續增加 | 24,546 個輸入 token | 8,053 個輸入 token |
| 重試迴圈 | 同一筆追蹤有過多呼叫,狀態碼全是 200 | 10 次呼叫 | 2 次呼叫 |
| 簡單步驟用了昂貴模型 | 工作階段頁面的代理表格 | $0.0012 | $0.0003 |
第一列使用 GPT-6 Luna,從空快取開始測試。指南也介紹了另一種無法命中快取的情況:送出使用者訊息時附加一段指令,儲存訊息時卻沒有保留,導致每輪重建的提示詞都與上一輪實際送出的不同。將訊息依實際送出的內容儲存後,該段對話的命中率從 0% 升至 76%。在歷史紀錄的案例中,輸入 token 增至 3 倍,輸入費用卻只增加 21%,因為其中 71% 是從快取讀取。最後一列中,GPT-6.1 Sol 執行的 3 項子代理檢查占工作階段費用的 81%;改用 DeepSeek V4.1 Flash 執行相同檢查,費用只要十分之一。LLM 代理為什麼這麼貴? 逐一說明這些案例。
收到使用者回報後,如何為 LLM 代理除錯?
先用標籤找出使用者的執行紀錄,再從追蹤中找出出問題的那次呼叫。出錯的呼叫往往也會回傳狀態碼 200。只要每筆請求都帶有 _user 標籤,在追蹤清單的搜尋欄輸入 metadata._user = u_1002,就能找出該使用者的工作階段與追蹤。除錯指南接著重現了 3 種失敗情況:
- 回傳 404 後重試。呼叫不存在的模型識別碼,95 ms 後失敗,費用為 $0。重試沿用相同追蹤識別碼,並使用新的跨度名稱(
policy-check.retry),因此失敗呼叫與重試會在瀑布圖中相鄰顯示。 - 狀態碼是 200,回答卻被截斷。設定
max_tokens=40並保留推理功能時,連續 3 次呼叫 DeepSeek V4.1 Flash 都回傳狀態碼 200,輸出恰好是 40 個 token,回答內容卻是空的,因為推理耗盡了額度。追蹤中會看到相同跨度出現 3 次,輸出數量都等於上限。將上限提高至 2,000 並關閉推理後,只需 1 次呼叫,就能用 49 個 token 回答。只提高上限也有效,但那次呼叫用了 1,876 個推理 token,耗時 22 s。 - 第二輪回傳 400。儲存的歷史紀錄遺失了一筆工具呼叫,導致 GPT-6 Luna 在第二輪於 0.5 s 內拒絕請求,未收費;第一輪則正常完成。
第二種失敗說明了為什麼每次呼叫都該記錄 finish_reason,這個回應欄位會指出模型停止的原因。值為 length 代表回答碰到了 max_tokens 上限,即使狀態碼是 200,也應視為失敗。為 LLM 代理除錯 列出每個代理都該記錄的 5 個欄位,本文接近結尾處也會再列一次。
不是自己寫的程式代理,也能追蹤嗎?
不必修改 Claude Code 就能追蹤,因為它原本就會在每筆請求附上工作階段識別碼與代理識別碼。設定 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%。半分鐘後,在快取已暖機的情況下重跑相同任務,費用為 $0.0812。依子代理分析 Claude Code 費用 也示範如何依開發者與儲存庫拆分團隊費用。
能讓模型幫你讀追蹤紀錄嗎?
可以透過 MCP 讓模型讀取,因為閘道會以 MCP 伺服器提供請求紀錄。MCP 是 Model Context Protocol,也是提供工具給模型使用的標準方式。該用哪種方式讀取紀錄,取決於誰在查詢:
| 查詢者 | 使用方式 | 模型 token | 實測結果 |
|---|---|---|---|
| 有問題要問的人 | 在 Claude 中加入閘道連接器 | 包含在聊天方案中 | 3 個問題共呼叫工具 14 次,所有數字都與帳務紀錄一致 |
| 執行固定查詢的程式 | REST API | 不需要 | 查出費用最高的 3 個工作階段,花費 $0 |
| 自行決定調查方向的代理 | 程式中的 MCP 工具 | 需支付費用 | 一次費用調查花費 $0.0015 |
在聊天案例中,Claude Fable 5.1 透過 OAuth 連線,以瀏覽器登入取代貼上 API 金鑰,且只取得請求紀錄的讀取權限。它因此取得 4 個唯讀工具,以及一把 7 天後到期的金鑰。工具只回傳費用、token 與狀態,不包含提示詞或回答文字。在程式案例中,GPT-6 Luna 只呼叫工具 1 次,就從 26 個工作階段中找出快取、模型選擇錯誤與重試迴圈的案例,費用為 $0.0015;Claude Sonnet 5.5 則先查詢價格,再經過 3 次工具呼叫找出相同工作階段,費用為 $0.0631。用 Claude 查詢 LLM 紀錄 與 讓代理使用紀錄查詢工具 分別介紹這兩種設定。
每個代理都該記錄什麼?
5 個欄位就能涵蓋系列中的所有問題:
- 每次執行的追蹤識別碼,以及每段對話的工作階段識別碼,透過標頭送出,也寫入自己的紀錄。
- 每次呼叫的跨度名稱,標明步驟與嘗試次數,例如
plan-2或policy-check.retry。 - 使用者、功能與版本標籤,讓使用者回報或帳單能對應到相關執行紀錄。
- 每次呼叫的
x-request-id回應標頭,方便日後查詢該次呼叫的實際計費金額。 - 每次呼叫的
finish_reason與輸出 token 數,並將length視為失敗。
每次呼叫也要設定用戶端逾時與 max_tokens,兩者都要留足餘裕。前面那些回傳 200 卻沒有內容的案例,就是因為只按可見回答的長度設定上限;推理模型的一次長呼叫也可能花上數分鐘。
閘道端追蹤有哪些看不到的部分?
從請求紀錄建立的追蹤只能涵蓋模型呼叫,看不到自己程式碼內部的執行細節。如同程式碼過慢的案例,閘道是從請求之間的空檔推算程式碼執行時間。工具、檢索與佇列的跨度仍需要 OpenTelemetry SDK;兩邊可以共用追蹤識別碼,因為沒有 X-Trace-Id 時,閘道會讀取 traceparent。這些追蹤也不包含提示詞與回應本文,而且只能在呼叫完成後回報費用。若要即時控管預算,就必須在自己的代理迴圈中,每收到一筆回應便依其 usage 計算費用。
常見問題
什麼是 LLM 代理可觀測性?
LLM 代理可觀測性,是指能看清楚代理每次執行做了什麼:呼叫哪些模型、執行順序、各次耗時與費用,以及哪些呼叫失敗。實作上,就是將呼叫整理成追蹤(一次執行)與工作階段(一段對話),再加上使用者與功能標籤。
延伸閱讀
- 用 3 個 HTTP 標頭追蹤 LLM 代理:逐步介紹示範代理與標頭設定。
- 請求標頭參考文件:本文使用的所有標頭,以及可從其他工具讀取的別名。
- 主控台中的追蹤與工作階段:清單、瀑布圖與工作階段頁面。
- LLM token 用量解析:各供應商紀錄中的 token 數量分別代表什麼。
- 提示詞快取完整指南:提示詞前綴為何命中或未命中快取。