跳到主要内容

30 秒都去哪了:APM 无法察觉的 Agent 步骤内部延迟归因

· 阅读需 13 分钟
Tian Pan
Software Engineer

仪表盘显示 p95 的 agent.run = 28s。用户反馈该功能感觉已经挂了。值班工程师打开 Trace(追踪),看到一个没有任何值得调查的子节点的“肥大”长条,然后开始盲猜。当有人重建出足够的心理模型,搞清楚瓶颈到底是模型、检索器,还是某个没人添加 Span 的工具调用时,故障已经变成了积压的任务单,而用户早已放弃了。

这就是 2026 年 Agent 运营核心的失败模式:传统的 APM 将 Agent 步骤视为一个黑盒,而“Agent 延迟”并不是一个单一指标——它是七个指标的总和,这些指标根据 Agent 在该轮次中的决策,以不同的方式分解实际用时 (Wall-clock time)。如果一个团队不暴露这七个数字,他们交付的功能虽然大家都能感觉到慢,但谁也无法修复。

隐藏在一个 Span 里的七个阶段

当面向用户的仪表盘报告 agent.step = 12s 时,实际用时几乎总是由以下阶段按顺序叠加而成的:

  1. 输入组装 (Input assembly) —— 构建消息列表、应用系统提示词、填充会话状态、获取先前的工具输出。在热启动时成本较低,但当会话滚动更新且缓存未命中强制每一轮都读取数据库时,成本高得惊人。
  2. 检索 (Retrieval) —— 对查询进行向量化、访问向量数据库、以及可选的重排序。生产环境中的向量数据库查询通常在每次 50–300ms 之间,而重排序根据模型和候选数量的不同,可能会再增加 100–500ms。
  3. 预填充 (Prefill) —— 模型处理整个输入上下文以生成第一个输出 Token。预填充受算力限制,并随输入长度增长;提示词长度增加一倍,首字延迟 (TTFT) 大致也会翻倍。
  4. 解码 (Decode) —— 模型逐个生成 Token,每一步都受 KV 缓存的内存带宽限制。这是推理工作量和输出长度线性消耗预算的地方,也是与其他流量共享 GPU 批处理导致竞争的地方,而应用团队对此一无所知。
  5. 工具分发 (Tool dispatch) —— 框架 (Harness) 解析模型的工具调用、验证参数并路由到正确的处理程序。通常很小,但如果框架通过队列进行往返调度,则很容易变得臃肿。
  6. 工具执行 (Tool execution) —— 实际的外部工作。可能是 50ms 的缓存读取,也可能是 9 秒的 LLM 作为工具的嵌套调用。这里的方差是“为什么 p99 比 p50 差得多”的最大来源。
  7. 重新预填充 (Re-prefill) —— 模型将工具结果重新处理回上下文中,以生成下一个响应 Token。在心理模型中经常被遗忘,因为它视觉上看起来像“模型再次思考”,但它是对不断增长的上下文进行的全新预填充过程,上下文会随着每一轮工具调用而增长。

一个普通的 Trace 会将步骤 1–7 压缩成一个单一的 agent.step Span,或者更糟,只拆分出两个(llm.calltool.call),而将其余五个归入父 Span 的时间区间内。在 2024–2025 年间通过规范制定的 OpenTelemetry GenAI 语义约定定义了诸如 chatexecute_toolinvoke_agent 之类的操作名称——但采用这些约定才是解锁分解视图的关键,仅发出父 Span 的团队得到的依然是和以前一样模糊的视图。

可见的 12 秒与不可见的 4 秒

在 Agent 产品上最常见的优化预算错配是:LLM 调用在 Trace 中显示为最大的可见块,因此团队花费两个冲刺的时间来削减提示词 Token、更换更快的模型并调整流式传输缓冲区。经过这一切,p95 从 28 秒降到了 24 秒。用户仍然在抱怨。

那没人去省下的 4 秒钟分散在其他阶段:

  • 600ms 的输入组装,因为每一轮都会重新获取用户的偏好文档,而不是在会话中缓存它。
  • 1.2s 的检索设置,因为嵌入模型运行在冷容器上,首个请求后才预热,且仅在会话活跃时保持预热。
  • 800ms 的工具分发,因为框架通过一个为批量吞吐量而非交互延迟调优的 Redis 流来排队工具调用。
  • 第二轮中 1.4s 的重新预填充,因为 Agent 将 4k Token 的工具结果放入上下文中,模型必须在生成下一个 Token 之前处理它。

这四项成本中没有一项会作为你可以注意到的单个 Span 出现在 Trace 中。它们表现为 Span 之间的微小间隙、归于通用“中间件”Span 的工作,或者父 Span 拥有但子 Span 未能解释的时间。如果你只看到已埋点的部分,你就会去优化已埋点的部分,而将那 4 秒钟白白流失。

诊断修复是程序化的:父 Span 开始与第一个子 Span 开始之间的每个间隙、任何两个相邻子 Span 之间的每个间隙,以及最后一个子 Span 与父 Span 结束之间的每个间隙,都必须是一个命名的阶段或一个明确的“未计算 (unaccounted)”Span。如果一个请求声称耗时 12 秒,而各 Span 之和仅为 9 秒,那么缺失的 3 秒就是 Bug 所在。

关键路径并非所有 span 的总和

这是团队在终于拆分阶段后犯的第二个错误:他们看到一个包含 9 秒工具调用和 8 秒并行的 LLM 调用的 trace,然后问“我应该先优化哪一个?” —— 而答案是 8 秒的 LLM 调用构成了全部的墙钟耗时(wall-clock cost),而 9 秒的工具调用位于一个不会延长请求时间的并行分支上。

关键路径追踪(Critical path tracing)是 Google 在其分布式延迟研究中正式提出的概念,自 2022 年以来被广泛的观测社区所采用。它对每个 span 提出一个问题:如果我能让这个 span 瞬间完成,整个请求会变快吗?如果是,该 span 就在关键路径上。如果不是 —— 因为并行运行的其他任务耗时更长 —— 该 span 就存在松弛量(slack),优化它就是浪费精力。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates