文章通过网关请求中的三个标识头组织执行轨迹和会话,复现十类代理性能、费用及故障问题。案例中,并行检查将中位耗时从3.7秒降至1.8秒,调整提示词时间戳位置使缓存命中率从0%升至73%。
LLM Agent 的可观测性,首先要把模型调用按两种方式组织起来:trace 表示 Agent 的一次运行,以时间线展示;session 表示一次完整对话,汇总对话的总成本。只需在每次调用中添加三个请求头,LLM gateway(位于你的代码与模型提供商之间、记录每次请求的代理)就能根据请求日志构建这两种视图,无需 tracing SDK。我们在一个小型客服 Agent 和 Claude Code 上使用这套配置,复现了十种常见的 Agent 问题:三种让 Agent 变慢,四种让它变贵,还有三种会导致运行出错。
三个请求头(X-Trace-Id、X-Span-Id、X-Session-Id),就能把 Synthorai gateway 的请求日志组织成 trace 和 session,无需 tracing SDK。
把三个相互独立的 sub-agent 检查一起发出后,在成本不变的情况下,耗时中位数从 3.7 秒降到了 1.8 秒。
在 GPT-6 Luna 上,把时间戳从 prompt 第一行移走后,从空缓存开始测得的缓存命中率从 0% 提升到了 73%。
Claude Code 的一次运行花费 $0.1728,其中三个 Agent 各自的首次调用占了总成本的 72%。
本文是一套七篇指南的总览。下面每个章节都会概述其中一篇指南,并列出测得的数据;对应指南中提供了代码和截图。所有运行都发生在 2026 年 10 月 10 日和 11 日。延迟数据取五次运行的中位数,其余数据均来自单次运行。代码位于 github.com/synthorai-io/use-cases。
trace 是 Agent 为处理一条用户消息或一项任务所做的全部工作,span 是 trace 中的一个步骤,session 则是由多个 trace 组成的完整对话。在我们的客服 Agent 中,一段包含两轮交互的客户对话对应一个 session,其中有两个 trace,每个 trace 包含五到六次模型调用。有些调用来自 sub-agent:这是主 Agent 为完成任务中的一部分而启动的另一个 Agent,它有自己的 prompt,通常还会使用更便宜的模型。
这些 ID 都是你自行指定的字符串,每个最多包含 128 个可打印 ASCII 字符。标签是一个扁平的 JSON 对象,值均为字符串;每个请求最多带 10 个标签,每个值最多 128 字节。gateway 会在响应中返回这些 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"}',
},
)
如果你的 Agent 已经通过其他工具发送 trace ID,可能什么都不用加。对于请求中缺失的这三个 ID,gateway 会分别从 OpenTelemetry、LiteLLM、Langfuse、Open WebUI、Claude Code 或 OpenAI Codex CLI 的对应请求头中读取。OpenTelemetry 是 tracing 的开放标准,它的 traceparent 请求头会携带 trace ID。《用 3 个 HTTP 请求头追踪 LLM Agent》逐步构建了这个演示 Agent:先在 GPT-6 Luna 上进行一次规划调用,再运行两个本地工具,然后在 DeepSeek V4.1 Flash 上并行执行三项检查,最后再调用模型生成答案。它的一次演示运行共发起 21 次模型调用,花费 $0.0017。
这个系列中的每个问题,都可以通过控制台中的某个页面,或从控制台外读取日志的某种方式来回答。控制台把每个 trace 展示为瀑布图:每次模型调用对应一条横条,共享同一条时间轴。因此,并行执行的调用会重叠,需要相互等待的调用则会形成阶梯。
每个 trace 页面还有一个 Compare 按钮,可以把两个 trace 放在同一页中,按名称配对各次调用,并将两张瀑布图放在同一条时间轴上。这个系列中的每项修复,都用这种方式对比了修复前后的运行。
先看 trace 顶部的耗时拆分。它会把总耗时,也就是从第一个请求发出到最后一个响应返回、用户实际等待的时间,拆成两部分:至少有一个模型请求尚未结束的 gateway 耗时,以及只有你自己的代码在运行的应用耗时。在演示 trace 中,8.5 秒总耗时被拆分为 7.0 秒 gateway 耗时和 1.6 秒应用耗时,因此应该优先检查模型调用。延迟指南复现了三种原因,每种都运行了五次:
在代码执行缓慢的案例中,两次运行的模型调用耗时基本相同,变化的是应用耗时:指南展示的那组对比中,your app 这一行从 4.6 秒降到了 1.3 秒。换模型无法缩短这部分时间,因此修复要从你自己的 profiler 或 OpenTelemetry span 入手。第三个案例使用 GPT-6 Luna。reasoning_effort 是控制模型回答前推理程度的请求参数;将它设为 none 后,五次回答的输出 token 数从 1,150 降到了 190。《为什么我的 LLM Agent 很慢?》提供了这三个案例的代码和并排对比的 trace。
先在 Traces 列表中按成本排序,再打开最贵的 session,逐轮查看缓存命中率和输入列。缓存命中率是指从提供商的 prompt 缓存中读取的输入 token 占比。对于曾经见过的 prompt 前缀,缓存只按正常输入价格的一部分计费。成本指南复现了四种原因。由于演示规模很小,金额都不到一美分,因此真正具有参考价值的是比例:
第一行使用 GPT-6 Luna,从空缓存开始运行。指南还介绍了另一种导致缓存失效的情况:发送每条用户消息时都会追加一条指令,保存消息时却没有保存这条指令,导致每一轮重建出的 prompt 都与上一轮实际发送的内容不同。按实际发送的内容保存消息后,这段对话的命中率从 0% 提升到了 76%。在历史消息案例中,输入 token 数变成了三倍,输入成本却只增加了 21%,因为其中 71% 的 token 都从缓存读取。最后一行中,使用 GPT-6.1 Sol 执行的三项 sub-agent 检查占了 session 成本的 81%;将相同检查改用 DeepSeek V4.1 Flash 后,成本只有原来的十分之一。《为什么我的 LLM Agent 很贵?》逐一讲解了这些案例。
先通过标签找到这名用户的运行记录,再查看出问题那次调用所在的 trace,而那次调用返回的状态码往往还是 200。如果每个请求都带有 _user 标签,在 Traces 列表的搜索框中输入 metadata._user = u_1002,就能找到这名用户的 session 和 trace。调试指南随后复现了三种故障:
一次 404 及其重试。 调用一个不存在的模型 ID,在 95 毫秒后失败,花费 $0。由于重试沿用了 trace ID,并使用了新的 span 名称 policy-check.retry,因此失败调用和重试会在瀑布图中相邻显示。
状态码为 200,回复却被截断。 设置 max_tokens=40,并保持推理开启时,连续三次调用 DeepSeek V4.1 Flash 都返回了状态码 200,输出 token 数恰好为 40,但回复为空,因为推理耗尽了 token 上限。trace 中会看到同一个 span 出现三次,输出数量都等于上限。将上限提高到 2,000 并关闭推理后,一次调用就用 49 个 token 给出了答案。只提高上限也能解决问题,但那次调用用了 1,876 个推理 token,耗时 22 秒。
第二轮返回 400。 保存的历史记录中丢失了一次工具调用,导致 GPT-6 Luna 在第二轮用 0.5 秒就拒绝了请求,没有收费,而第一轮原本运行正常。
第二种故障说明了为什么每次调用都应该记录 finish_reason。这个响应字段说明模型为什么停止生成。值为 length 表示回复触及了 max_tokens 上限,即使状态码为 200,也应该算作失败。《调试 LLM Agent》列出了每个 Agent 都应该记录的五个字段,本文接近结尾处也会再次列出。
无需修改 Claude Code,就可以追踪它,因为它已经在每个请求中发送了 session ID 和 Agent ID。只需两个环境变量,就能让它通过 gateway 发送请求;第三个变量则用于添加各个 sub-agent 的类型:
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 带着两个 sub-agent 修复了一个失败的测试,共发起 11 个请求,花费 $0.1728。session 页面按 Agent 拆分了成本:两个 sub-agent 占总成本的 39%,缓存写入占 77%,三个 Agent 各自的首次调用合计占 72%。半分钟后,在缓存已经预热的情况下重复相同运行,成本为 $0.0812。《按 sub-agent 查看 Claude Code 成本》还介绍了如何按开发者和代码仓库拆分团队开销。
模型可以通过 MCP 读取 trace。MCP 即 Model Context Protocol,是向模型提供工具的标准方式;gateway 会通过 MCP server 提供自己的请求日志。具体采用哪种方式读取日志,取决于是谁在发起查询:
在聊天场景中,Claude Fable 5.1 通过 OAuth 连接,也就是用浏览器登录代替粘贴 API key,并且只被授予请求日志的读取权限。它因此获得了四个只读工具,以及一个 7 天后过期的 key。这些工具返回成本、token 和状态信息,不包含 prompt 或回复正文。在程序场景中,GPT-6 Luna 用一次工具调用,就从 26 个 session 中找出了存在缓存、模型选错和重试循环问题的 session,花费 $0.0015;Claude Sonnet 5.5 则在查询价格后,用三次工具调用找到了相同的 session,花费 $0.0631。《向 Claude 查询你的 LLM 日志》和《为你的 Agent 提供日志工具》分别介绍了这两种配置。
五个字段就能覆盖这个系列中的所有问题:
此外,每次调用都应设置客户端超时和 max_tokens,而且都要留出充足余量。前面那些回复为空的 200 响应,就是因为只按可见回复的长度设置了上限;推理模型的一次长调用可能需要几分钟。
基于请求日志的 tracing 只能覆盖模型调用,看不到你自己代码内部的执行过程。gateway 会根据请求之间的空档推断代码运行了多久,前面的慢代码案例就展示了这一点。要为工具、检索和队列添加 span,需要使用 OpenTelemetry SDK。两种视图可以共享一个 trace ID,因为当 X-Trace-Id 缺失时,gateway 会读取 traceparent。这些 trace 也不包含 prompt 和响应正文,而且成本是在调用结束后报告的。如果要实时控制预算,就需要在自己的 Agent 循环中,在每个响应到达时根据其用量计算成本。
LLM Agent 可观测性,就是能够看清 Agent 每次运行做了什么:调用了哪些模型、调用顺序如何、每次耗时多久、各自花了多少钱,以及哪些调用失败了。落实到实践中,就是把调用组织成 trace(一次运行)和 session(一段对话),再加上用户和功能标签。
如果需要进一步采取行动,你可以考虑屏蔽此人和/或举报滥用行为。