新帳號 免費註冊,送 10 次呼叫,最高 $1,免綁卡。

用 3 個 HTTP 標頭追蹤 LLM 代理,不必安裝追蹤 SDK

在代理程式的模型呼叫中加入三個請求標頭,即可取得每次執行的瀑布圖,檢視平行呼叫、自己的程式碼所花費的時間、失敗的呼叫及費用。透過同一次執行的時間軸,區分模型呼叫與自有程式碼的耗時,查看哪些呼叫同時進行、哪些呼叫失敗,並掌握該次執行的成本。

我們的推薦

即時價格
模型 結論 價格
GPT-6 Luna 處理示範中的函式呼叫迴圈。在本頁顯示的執行中,每回合的兩次呼叫(規劃與回答)合計費用為 $0.0002。 示範中的主要代理程式 低至 $0.1/百萬
DeepSeek V4.1 Flash 在關閉推理的情況下,平行執行三項單句檢查。在本頁顯示的工作階段中,其六次呼叫的費用為 $0.0004,而第七次子代理程式呼叫失敗,費用為 $0。 示範中的子代理程式 低至 $0.3/百萬
目錄
  1. 要傳送哪些資料才能產生追蹤?
  2. 範例代理做了什麼?
  3. 如何解讀瀑布圖?
  4. 工作階段比追蹤多提供了什麼?
  5. 如何依功能或使用者拆分費用?
  6. 呼叫失敗時會發生什麼事?
  7. 如何在程式碼中取得每次呼叫的費用?
  8. 如果代理已經傳送追蹤標頭呢?
  9. 有了追蹤之後,可以做什麼?
  10. 範例沒有涵蓋哪些部分?
  11. 常見問題
  12. 追蹤 LLM 代理需要 OpenTelemetry 嗎?
  13. 追蹤與工作階段有什麼差別?
  14. 追蹤標頭會改變路由、計費或模型看到的內容嗎?
  15. 追蹤會保留多久?
  16. 失敗呼叫會收費嗎?
  17. 延伸閱讀

如果代理為了回答一則訊息要呼叫模型五次,光看逐筆請求記錄很難除錯:你看不出哪些呼叫屬於同一次執行、哪些同時進行,也不知道等待時間有多少花在自己的程式碼上。本文在一個小型客服代理中加入三個請求標頭,再到 Synthorai 主控台查看追蹤(以時間軸呈現一次執行)與工作階段(一段對話及其費用)。不必安裝追蹤 SDK,也不必執行 collector(接收追蹤資料的獨立服務)。原始碼位於 github.com/synthorai-io/use-cases 的 agent-tracing/ 目錄。需要 Python 3.10 以上版本,以及從主控台取得的 API 金鑰。

TL;DR

  • 每次模型呼叫都帶上三個請求標頭(X-Trace-Id、X-Span-Id、X-Session-Id),Synthorai 主控台就能把請求記錄整理成追蹤與工作階段,不必使用追蹤 SDK。
  • 範例追蹤的實際耗時為 8.5 s,其中約 7.0 s 用於模型呼叫,1.6 s 用於呼叫之外的代理程式碼與網路傳輸。
  • 一次回傳 404 的失敗呼叫仍保留在追蹤中,費用為 $0。
  • 範例共執行 21 次模型呼叫,費用為 $0.0017。
git clone https://github.com/synthorai-io/use-cases
cd use-cases
cp .env.example .env    # put your API key in it
cd agent-tracing
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python run.py --fail

我們在 2026 年 10 月 11 日執行時,不到一分鐘就完成 21 次模型呼叫,費用為 $0.0017。程式會印出兩個工作階段 ID 與四個追蹤 ID。在主控台開啟追蹤,就能看到相同的 ID。主控台使用瀏覽器的時區顯示時間,因此本頁截圖中的 19:24 對應 UTC 11:24。

主控台中的一筆追蹤:實際耗時 8.5 s,其中閘道耗時 7.0 s、應用程式耗時 1.6 s;6 次模型呼叫中有 1 次失敗。下方瀑布圖依序顯示規劃呼叫、本機工具執行的空檔、三個重疊的子代理呼叫、標示為 404 的 95 ms 記號,以及回答呼叫。右側面板顯示失敗呼叫:95 ms、費用 $0、未收費,以及錯誤訊息

要傳送哪些資料才能產生追蹤?

每次模型呼叫都傳送三個標頭,值是由你指定的字串。閘道原本就會記錄每筆請求;這些標頭只是告訴它哪些請求屬於同一組。

標頭哪些呼叫使用相同的值產生的結果
X-Trace-Id同一次執行的所有呼叫(從頭到尾處理一則使用者訊息)將呼叫畫在同一張瀑布圖上
X-Span-Id僅用於一次呼叫該呼叫在瀑布圖中的列名稱
X-Session-Id同一段對話的所有呼叫將各次執行歸為一組,並統計整段對話的總量

span 是追蹤中的一個步驟。本文每個 span 都對應一次模型呼叫。每個 ID 最多可使用 128 個可列印的 ASCII 字元。閘道會在回應中傳回這些 ID,不會轉送給模型供應商。

另有三個選用標頭。X-Agent-Id 指出哪個子代理發出呼叫,X-Parent-Agent-Id 指出哪個代理啟動了該子代理,X-Synthorai-Metadata 則攜帶功能或終端使用者等標籤。完整說明見請求標頭參考。範例會傳送 X-Agent-Id 與標籤,但不傳送 X-Parent-Agent-Id:唯一的子代理由主代理啟動,沒有需要描述的巢狀關係。

範例用一個函式建立每次呼叫的標頭,這個函式就是整個追蹤層:

@dataclass
class Run:
    """One agent run, which the gateway shows as one trace."""
    session_id: str
    labels: dict = field(default_factory=dict)
    trace_id: str = field(default_factory=lambda: new_id("trace"))

    def headers(self, span: str, agent: str | None = None, parent: str | None = None) -> dict:
        h = {
            "X-Session-Id": self.session_id,
            "X-Trace-Id": self.trace_id,
            "X-Span-Id": span,
        }
        if agent:
            # Only sub-agents need an id. Calls without one belong to the main agent.
            h["X-Agent-Id"] = agent
        if parent:
            h["X-Parent-Agent-Id"] = parent
        if self.labels:
            h["X-Synthorai-Metadata"] = json.dumps(self.labels, separators=(",", ":"))
        return h

代理中的每次模型呼叫都經過同一個包裝函式,由它附加標頭。使用 OpenAI Python SDK 時,個別請求的標頭放在 extra_headers:

raw = await client.chat.completions.with_raw_response.create(
    model=model, messages=messages,
    extra_headers=run.headers(span, agent, parent), **kwargs)
resp = raw.parse()
request_id = raw.headers.get("x-request-id")   # the gateway's id for this call

使用 with_raw_response 只是為了讀取回應標頭。如果不需要請求 ID,直接用 create(..., extra_headers=...) 即可。

範例代理做了什麼?

它分四個階段、五次模型呼叫來回答客戶訊息,刻意安排這個流程,讓追蹤圖有內容可看。主代理使用 GPT-6 Luna,子代理使用 DeepSeek V4.1 Flash,兩者都透過環境變數設定。子代理呼叫設定 reasoning_effort="none",因為只要用一句話完成的檢查,不需要模型先進行推理。如果改用不接受這個值的子代理模型,請修改或清除 ANALYST_REASONING。

Span執行者執行內容
plan主代理讀取訊息並要求使用工具(function calling:模型傳回函式名稱與引數,供你的程式碼執行)
無你的程式碼在本機平行執行兩個工具:訂單查詢(1.2 s)與政策搜尋(0.6 s)
sentiment, policy-check, risk子代理 analyst同時送出三項檢查,每項以一句話作答
answer主代理根據工具結果與三項檢查撰寫回覆

工具是一般的 Python 函式,內含 sleep,用來模擬資料庫與搜尋索引。加上 --fail 後,其中一次 policy-check 呼叫會先要求不存在的模型,收到 404,再以 policy-check.retry 作為 span 名稱重試。

run.py 會執行兩段對話,每段各有兩輪。每段對話使用一個工作階段 ID,每一輪則產生新的追蹤 ID:

session_id, history = new_id("support"), []
for text in turns:
    run = Run(session_id, labels={"feature": "support-agent", "release": "v2", "_user": user})
    reply = await handle(run, history, text)

如何解讀瀑布圖?

每一列代表一次模型呼叫,或自己的程式碼執行期間留下的空檔。時間軸從該次執行的第一個呼叫抵達閘道時開始。上方截圖是第一段對話的第二輪,由上往下讀:

  • plan 耗時 2.3 s。長條表示閘道處理該請求的時間,從收到請求到完成回應為止。
  • your app 耗時 1.4 s。這段期間沒有處理中的請求,因此時間花在閘道之外:你的程式碼,以及程式碼與閘道之間的網路傳輸。此處是兩個本機工具,較慢的那個會暫停 1.2 s。在實際代理中,這一列能讓你看到耗時的資料庫查詢或向量搜尋。
  • sentiment、risk 與 policy-check.retry 互相重疊。代理透過 asyncio.gather 送出三項檢查,重疊的長條表示平行呼叫。名稱前的箭頭表示呼叫來自子代理。
  • policy-check 是標有 404 的 95 ms 斜線記號。失敗呼叫仍會留在追蹤中,並顯示程式碼收到的狀態碼。
  • answer 耗時 3.0 s,是最慢的呼叫,瀑布圖上方的摘要也會指出這一點。
  • Output 欄顯示每次呼叫的輸出 token 數。呼叫的時間主要花在輸出,因此這一欄通常能解釋旁邊長條的長度:answer 輸出 101 個 token,三項檢查各輸出 14 到 57 個。較大的輸出若大多用於推理,主控台會在數量下方加上底線,此處的 101 就是如此。

摘要將 8.5 s 的實際耗時拆成閘道內的 7.0 s,以及應用程式的 1.6 s。這個拆分能回答大多數延遲調查的第一個問題:該查模型呼叫,還是自己的程式碼?這次執行有 82% 的等待時間花在模型上,主代理的 plan 與 answer 兩次呼叫就占了 5.3 s。因此,替主代理換成更快的模型,比加快三項檢查更有幫助。

閘道看不到程式碼內部。它根據請求之間的空檔推算 your app 列,所以空檔只告訴你程式碼執行了多久,不會告訴你它做了什麼。如果自己的函式也需要 span,就要使用開放追蹤標準 OpenTelemetry 的 SDK。兩者可以共用 ID:未傳送 X-Trace-Id 時,閘道會讀取攜帶追蹤 ID 的標準標頭 traceparent。

工作階段比追蹤多提供了什麼?

工作階段將同一段對話的各次執行歸為一組,並計算總量。你可以從追蹤清單開啟工作階段,也可以在任一追蹤中使用「開啟工作階段」。

主控台中的工作階段:11 次呼叫,費用 $0.0008,其中一次回傳 404;顯示 _user、feature 與 release 標籤;代理表格列出使用 gpt-6-luna 的主對話,以及使用 deepseek-v4.1-flash 的 analyst 子代理;下方列出工作階段的兩筆追蹤,以及各自的耗時、輸入、快取命中率與費用

上方工作階段是一段兩輪的客戶對話:11 次呼叫、1,890 個輸入 token、750 個輸出 token、費用 $0.0008,以及一次錯誤。頁面分為三個部分:

  • 此工作階段中的代理 依代理拆分呼叫次數與費用。主代理使用 GPT-6 Luna 呼叫 4 次,費用 $0.0004;analyst 子代理呼叫 7 次,費用 $0.0004,其中六次使用 DeepSeek V4.1 Flash,另一次是失敗呼叫。因為子代理呼叫帶有 X-Agent-Id: analyst,才會出現這張表。未帶此標頭的呼叫會計入主對話。
  • 此工作階段中的追蹤 列出每一輪的錯誤、耗時、輸入 token、快取命中率與費用。不必捲動記錄,就能找出長對話中較慢、失敗或費用較高的那一輪。
  • 快取命中率 是從供應商 prompt cache 讀取的輸入 token 比例。此處為 0%,因為 prompt 只有幾百個 token,而供應商只會快取超過最低長度的前綴(多數供應商為 1,024 個 token,詳見 prompt 快取)。

當 prompt 長到足以使用快取時,工作階段 ID 還有另一個重要用途。同一段對話的每次呼叫都傳送相同的 X-Session-Id,閘道會用它維持該對話的 prompt cache 命中一致性,讓第二輪讀取第一輪寫入的快取。在 /v1/chat/completions 與 /v1/responses 上,如果框架不方便設定標頭,也可以把值放在請求主體的 session_id。

如何依功能或使用者拆分費用?

透過 X-Synthorai-Metadata 傳送值皆為字串的扁平 JSON 物件,再依它分組或篩選。範例在每次呼叫加上功能、版本與終端使用者標籤:

labels = {"feature": "support-agent", "release": "v2", "_user": "u_1002"}

這些標籤會以小標籤顯示在追蹤與工作階段上。若要看總量,開啟分析頁面,選擇分類明細分頁,找到「依中繼資料統計費用」卡片。選取一個鍵後,卡片會列出該鍵的所有值,以及各自的請求數、錯誤數、輸入 token 與費用:

依中繼資料統計費用卡片,選取的鍵為 _user:u_1001 有 22 筆請求、2 次錯誤,費用 $0.0018;u_1002 有 20 筆請求、0 次錯誤,費用 $0.0017

上方卡片涵蓋範例執行兩次的一小時:u_1001 的執行包含兩次失敗呼叫,共 22 筆請求、$0.0018;u_1002 則有 20 筆請求、$0.0017。執行一次後,你會看到 11 與 10。選取 feature 可查看各功能的費用,選取 release 則能查看修改 prompt 對費用的影響。卡片也可以並排比較同一個鍵的兩個值。同一分頁還有「依代理統計費用」卡片,可跨工作階段加總各 X-Agent-Id 的費用。

要找出某個數字背後的執行,標籤也能用來搜尋。在追蹤清單的搜尋框輸入 metadata._user = u_1002 並按 Enter,就只會保留該使用者的工作階段與追蹤。記錄頁面的篩選框也接受同樣的運算式,用來篩選個別請求。

標籤規則很簡單:

  • 每筆請求最多 10 個標籤。鍵長度為 1 到 32 個字元,可使用 a-z、0-9、_、. 與 -。值是最多 128 位元組的字串。
  • _user 用來標示該請求所服務的終端使用者。其他底線開頭的鍵為保留名稱。
  • 違反規則的標籤會被捨棄,但請求仍會執行。回應標頭 X-Synthorai-Metadata-Dropped 會列出被捨棄的鍵,設定時請記錄這個標頭。我們傳送名為 Bad?Key 的鍵來測試,回傳標頭中確實包含 Bad?Key。
  • 不要把 prompt 文字、個人資料或機密放進標籤。_user 請使用內部使用者 ID,不要使用電子郵件地址。

呼叫失敗時會發生什麼事?

它會以附有狀態碼的斜線記號保留在追蹤中,而且不收費。範例中失敗的 policy-check 要求了一個不存在的模型 ID。在瀑布圖中選取它,會開啟上方截圖右側的面板,顯示程式碼收到的錯誤:The model "model-that-does-not-exist" does not exist or is not available to this workspace,以及 95 ms 延遲、$0 費用與未收費註記。

範例為重試指定獨立的 span 名稱:

try:
    await call(run, span, model=BROKEN_MODEL, messages=messages, agent="analyst")
except APIStatusError:
    span = f"{span}.retry"      # the retry is its own span, so both show in the waterfall
resp = await call(run, span, model=ANALYST_MODEL, messages=messages, agent="analyst", max_tokens=400,
                  **analyst_options())   # reasoning_effort="none" unless ANALYST_REASONING says otherwise

保留追蹤 ID、改變 span 名稱,就能把失敗與重試並排放在同一張圖中。如果重試邏輯位於重複使用相同標頭的函式庫,兩次嘗試仍然會顯示,只是會成為名稱相同的兩列。

錯誤也會彙總。追蹤摘要會顯示「6 次模型呼叫,1 次失敗」,工作階段會依狀態碼列出錯誤(1 × 404 Not found),追蹤清單則有可排序的錯誤欄,方便找出需要處理的執行。

如何在程式碼中取得每次呼叫的費用?

使用回應傳回的請求 ID,向閘道查詢該請求的計費記錄。GET /v1/generation?id=<request id> 會傳回一筆請求的費用、token 數、延遲與狀態。

async def cost_of(http: httpx.AsyncClient, request_id: str) -> float | None:
    """The billing record of one request. It is written a moment after the call returns."""
    if not request_id:
        return None                                  # a call that got no response has no record
    for _ in range(6):
        try:
            r = await http.get(f"{BASE_URL}/generation", params={"id": request_id})
            if r.status_code == 200:
                return r.json()["data"]["total_cost"]
        except (httpx.HTTPError, KeyError, TypeError, ValueError):
            pass                                     # a dropped connection or a half-written record: try again
        await asyncio.sleep(2)
    return None

記錄會在回應完成後不久寫入,因此函式會重試,直到收到 200。呼叫端建立一次 httpx.AsyncClient,設定 Authorization 標頭,再將它傳入每次查詢。透過同一個 client 查詢二十次呼叫,只需重複使用少數連線。如果每次查詢都建立新的 client,就會同時開啟二十條連線;我們執行時,其中一兩條曾中斷。範例會根據自行量測的時間與這些記錄,為每次呼叫印出一行。以下是截圖中工作階段的第一輪:

trace trace-5258a3328d7d  session support-d8ac3cdd4411
  plan                 main     gpt-6-luna                 200   1878 ms    153 in   22 out  $0.000026
  sentiment            analyst  deepseek-v4.1-flash        200   1431 ms     91 in   28 out  $0.000061
  risk                 analyst  deepseek-v4.1-flash        200   1877 ms     96 in   33 out  $0.000068
  policy-check         analyst  deepseek-v4.1-flash        200   1885 ms     96 in   39 out  $0.000076
  answer               main     gpt-6-luna                 200   4142 ms    350 in  339 out  $0.000205

這裡的時間由 client 量測,包含網路傳輸,因此比主控台的長條更長;主控台量測的是請求在閘道內的時間。在上方瀑布圖所示的第二輪,plan 在 client 端耗時 2,499 ms,在閘道內為 2.3 s;answer 則分別為 3,161 ms 與 3.0 s。平行送出的呼叫可能差距更大:失敗呼叫在 client 端耗時 694 ms,在閘道內只有 95 ms。

如果想在自己的記錄中寫入每次呼叫實際計費的金額,可以複製這個函式。但它不適合用來中止花費過高的執行,因為每筆記錄都要等呼叫完成後才產生,函式也可能等待數秒。若要即時控管預算,請在執行過程中自行依每個回應的 usage 計算費用,之後再用這些記錄核對計算結果。

如果代理已經傳送追蹤標頭呢?

這種情況可能不必再加任何東西。對於請求未攜帶的三種 ID,閘道會分別讀取常見工具已經傳送的對應標頭:

使用的工具傳送的標頭解讀為
OpenTelemetry HTTP instrumentationtraceparent追蹤,並以其父 ID 作為 span
LiteLLM SDKX-Litellm-Trace-Id, X-Litellm-Session-Id追蹤、工作階段
Langfuse SDKbaggage: langfuse_trace_id=…, langfuse_session_id=…追蹤、工作階段
Open WebUIX-OpenWebUI-Chat-Id工作階段
Claude CodeX-Claude-Code-Session-Id工作階段
OpenAI Codex CLISession-Id工作階段

兩種標頭同時存在時,以你設定的 X-Trace-Id、X-Span-Id 與 X-Session-Id 為優先。完整清單見請求標頭參考。如果代理框架不會傳送任何這類標頭,就要透過它提供的自訂標頭設定加上去。

有了追蹤之後,可以做什麼?

最常見的用途是與另一筆追蹤比較。每個追蹤頁面都有比較按鈕:選取第二筆追蹤後,主控台會在同一頁顯示兩者,每項指標列出 A、B 與差異,同名呼叫互相配對,兩張瀑布圖則共用一條時間軸。這樣就能確認修改 prompt、模型或自己的程式碼,對執行造成了什麼影響。

以下三篇延伸指南使用相同的代理與標頭,並以這種方式重現問題、修正問題,再比較結果:

另外三篇從不同角度查看同一份記錄:

範例沒有涵蓋哪些部分?

範例涵蓋模型呼叫。正式環境通常還會補上以下四個面向:

  • 自己的程式碼 span。閘道會顯示模型呼叫之間的空檔,但不知道空檔裡執行了什麼。使用 OpenTelemetry 對工具、檢索與佇列加入追蹤,並傳遞 traceparent,就能用同一個追蹤 ID 串起兩種檢視。
  • Prompt 與回應主體。本文使用每次呼叫的時間、token 與費用記錄,不涵蓋檢查實際送給模型的內容。
  • 預算。cost_of 提供事後的實際計費金額。要在達到上限時停止執行,就得在每個回應抵達時,依其 usage 計算費用;這需要在代理迴圈中加入幾行程式碼,範例未包含這部分。
  • 取樣。每次呼叫都會記錄,因為追蹤是由閘道原本就保留的請求記錄建立的。沒有需要取樣的資料,代理端除了幾個標頭以外,也沒有額外負擔。

常見問題

追蹤 LLM 代理需要 OpenTelemetry 嗎?

不需要。每次模型呼叫帶上三個請求標頭(X-Trace-Id、X-Span-Id、X-Session-Id),就能產生模型呼叫的瀑布圖,呼叫之間的時間則顯示為應用程式耗時。如果自己的函式也需要 span,OpenTelemetry 才派得上用場。

追蹤與工作階段有什麼差別?

追蹤代表代理的一次執行,也就是處理一則使用者訊息或一項任務所做的所有事情。工作階段則是一段對話,由多次執行組成。範例中,每段客戶對話都是一個工作階段,包含兩筆追蹤,每筆追蹤有五或六次模型呼叫。

追蹤標頭會改變路由、計費或模型看到的內容嗎?

X-Trace-Id 與 X-Span-Id 不會改變任何行為。X-Session-Id 會維持同一段對話的 prompt cache 命中一致性。這些標頭都不會轉送給模型供應商,計費則以閘道自己的請求 ID 為依據。

追蹤會保留多久?

追蹤與工作階段由請求記錄建立,同樣涵蓋 90 天。追蹤清單預設顯示最近 7 天。

失敗呼叫會收費嗎?

範例中因未知模型 ID 而回傳 404 的失敗呼叫,記錄的費用為 $0,並註明未收費。

延伸閱讀

← 應用情境