只用 3 个 HTTP 请求头追踪 LLM 智能体,无需追踪 SDK
为智能体发出的模型调用添加三个请求头,即可查看每次运行的瀑布图,了解并行调用、自有代码执行耗时、失败调用和费用。瀑布图将模型请求与自身代码的耗时放在同一次运行中呈现,帮助区分哪些调用并行发生、时间花在何处、哪些请求失败,并查看该次运行的调用成本。
我们的推荐
实时价格| 模型 | 结论 | 价格 |
|---|---|---|
| GPT-6 Luna 处理演示中的函数调用循环。在本页展示的运行中,每轮的两次调用(规划和回答)合计花费 $0.0002。 | 演示中的主智能体 | 低至 $0.1/百万 |
| DeepSeek V4.1 Flash 在关闭推理的情况下,并行执行三项单句检查。在本页展示的会话中,它的六次调用花费 $0.0004,第七次子智能体调用失败,花费 $0。 | 演示中的子智能体 | 低至 $0.3/百万 |
目录
一个智能体为了回复一条消息要调用模型五次,仅靠平铺的请求日志很难排查问题:哪些调用属于同一次运行,哪些并行执行,等待时间有多少花在自己的代码里,都看不出来。本文给一个小型客服智能体加上三个请求头,然后在 Synthorai 控制台中查看追踪记录(用时间线展示一次运行)和会话(一次对话及其费用)。不需要安装追踪 SDK,也不需要运行 collector(单独接收追踪数据的服务)。源码位于 github.com/synthorai-io/use-cases 的 agent-tracing/ 目录。运行需要 Python 3.10 或更高版本,以及从控制台获取的 API key。
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。

生成追踪记录需要发送什么?
每次模型调用发送三个请求头,值由你自行指定,都是字符串。网关本来就会记录每个请求,这些请求头只是告诉它哪些请求属于同一组。
| 请求头 | 哪些调用使用相同的值 | 得到什么 |
|---|---|---|
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 | 主智能体 | 根据工具结果和三个检查结果生成回复 |
工具只是带有 sleep 的普通 Python 函数,用来模拟数据库和搜索索引。启用 --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是一个耗时 95 ms、标有404的斜线填充标记。失败调用会留在追踪记录中,并显示你的代码收到的状态码。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 次调用,1,890 个输入 token,750 个输出 token,费用 $0.0008,1 个错误。页面分为三部分:
- 本会话中的智能体 按智能体拆分调用和费用。主智能体调用 GPT-6 Luna 4 次,费用 $0.0004;
analyst子智能体调用 7 次,费用 $0.0004,其中 6 次使用 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 和费用:

上面的卡片覆盖一个小时,期间示例运行了两次: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 请求头,再将它传给每次查询。用同一个客户端查询二十次调用,只需复用少量连接。如果每次查询都新建客户端,就会同时打开二十条连接;我们运行时,其中一两条连接会断开。示例根据自身计时和这些记录,为每次调用打印一行。下面是截图中会话的第一轮:
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
这里的时间在客户端测量,包含网络耗时,所以比控制台中的条形更长;控制台只测量请求在网关内部的耗时。在第二轮,也就是上面的瀑布图中,plan 的客户端耗时为 2,499 ms,网关耗时为 2.3 s;answer 分别为 3,161 ms 和 3.0 s。并行发送的调用差距可能更大:失败调用在客户端耗时 694 ms,在网关中耗时 95 ms。
如果想在自己的日志中记录每次调用的实际计费费用,可以复制这个函数。它不适合用来中止费用过高的运行,因为每条记录都要在调用结束后稍等才可用,函数可能为此等待数秒。实时预算控制应在运行过程中根据每个响应的 usage 自行计算费用,之后再用这些记录核对。
智能体已经发送追踪请求头怎么办?
这种情况下可能无需添加任何内容。如果请求没有携带三个 ID 中的某一个,网关会读取常见工具已发送的对应请求头:
| 使用的工具 | 发送的请求头 | 读取为 |
|---|---|---|
| OpenTelemetry HTTP 自动埋点 | traceparent | 追踪 ID,其中的父 ID 作为 span |
| LiteLLM SDK | X-Litellm-Trace-Id、X-Litellm-Session-Id | 追踪 ID、会话 ID |
| Langfuse SDK | baggage: langfuse_trace_id=…、langfuse_session_id=… | 追踪 ID、会话 ID |
| Open WebUI | X-OpenWebUI-Chat-Id | 会话 ID |
| Claude Code | X-Claude-Code-Session-Id | 会话 ID |
| OpenAI Codex CLI | Session-Id | 会话 ID |
两者同时存在时,你设置的 X-Trace-Id、X-Span-Id 和 X-Session-Id 优先。完整列表见请求头参考。如果智能体框架不发送这些请求头,就需要通过它提供的自定义请求头配置来添加。
有了追踪记录之后做什么?
最常见的用途是与另一条追踪记录比较。每个追踪页面都有比较按钮:选择第二条追踪记录,控制台会在同一页展示两者。每项指标显示 A、B 和变化量,同名调用逐一配对,两张瀑布图放在同一条时间轴上。这样就能检查 prompt、模型或自身代码的改动对运行产生了什么影响。
下面三篇后续指南使用同一个智能体和相同的请求头,按这种方式复现问题、修复问题并比较结果:
- LLM 智能体为什么慢? 串行调用、自身代码中的慢工具,以及一句话回答中的推理耗时。
- LLM 智能体为什么贵? 破坏缓存的时间戳、不断增长的上下文、重试循环,以及选错模型。
- 排查 LLM 智能体问题 从用户投诉定位到出错的调用。
另外三篇从其他角度查看同一份日志:
- 按子智能体统计 Claude Code 费用 通过相同的会话和智能体视图,分析并非由你编写的编程智能体。
- 让 Claude 分析你的 LLM 日志 通过只读 MCP 连接,在聊天窗口中回答有关这些会话的问题。
- 给智能体接入日志工具 在自己的程序中使用相同工具,以及模型 token 的费用。
示例没有覆盖什么?
示例覆盖的是模型调用。生产环境通常还会补上四项:
- 自有代码的 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,并注明未扣费。
相关阅读
- 构建 LLM 聊天机器人:单智能体场景,包含上下文管理、记忆和 prompt 缓存。
- 请求头参考:本文涉及的全部请求头,以及从其他工具读取的别名。
- 控制台中的追踪与会话:列表、瀑布图和会话页面。
- 解析 LLM token 用量:各提供商记录中的 token 数分别代表什么。