你的 Agent 链路中无人分配的延迟预算
你的 Agent 有一个延迟 SLO。有人把它写在了文档里:“响应时间低于 8 秒,p95”。但没人决定这 8 秒该如何分配。检索调用没有细分预算,规划步骤没有细分预算,模型因为感到不确定而决定调用的第三个工具也没有细分预算。预算在边界处只是一个数字,而在内部则完全不存在。因此,当一个五跳链路冲破 8 秒时,值班工程师盯着追踪链路(trace),却无法回答那个唯一重要的问题:到底是哪一跳超时了?
这就是拥有“延迟预算”的服务与拥有“延迟愿望”的服务之间的区别。预算是分配给每个组件并强制执行的;而愿望是在出口处测量并祈祷达标的。大多数 Agent 系统发布时都带着愿望,因为跳数结构是动态的——模型决定调用多少次工具——而且为无法控制的事情制定预算感觉是不可能的。事实并非如此。正因为你无法控制它,你才更需要为它制定预算。
这之所以对 Agent 的打击比对之前的微服务一代更大,是因为 Agent 的跳数比我们习惯的 RPC 调用具有更高的变数和数量。模型一次往返的 p95/p50 比例可以达到 4-6 倍——受 LLM 限制的系统展现出你技术 栈中最宽的长尾,因为一次额外的反思轮次或多一次工具调用,就可能在毫无预警的情况下使实际耗时翻倍。将五个这样的步骤堆叠在一个链路上,长尾效应不是简单的加法,而是乘法。这就是 8 秒这个数字所掩盖的部分,也是我们要开始讨论的地方。
长尾是乘积式的,而非叠加式的
工程师习惯于用加法来思考延迟,因为中位数表现出的规律正是如此。如果检索在 p50 时耗时 200ms,生成在 p50 时耗时 2s,那么该链路在 p50 时大约为 2.2s。简洁、直观,但在长尾端完全错误。
在长尾端,你关心的是至少有一跳变慢的概率。这是一个复合概率,而且恶化速度极快。这里典型的结果是 Dean 和 Barroso 的“规模下的长尾”(tail at scale):如果一个请求并发分发到 100 台服务器,每台服务器有 1% 的概率响应缓慢,那么至少有一台缓慢的概率约为 63%。“百分之一的罕见事件”在近三分之二的请求中都会发生。单节点的 p99 变成了整体的 p50。
Agent 不会向 100 台服务器分发请求,但它们不需要那么多。考虑一个简单的五跳链路——规划、检索、工具调用、工具调用、合成——其中每一跳独立达到其缓慢阈值的概率为 5%。所有五跳都保持快速的概率是 0.95^5 ≈ 0.77。也就是说,你 23% 的请求至少会遇到一次长尾事件。你精心测量的 2.2s p50 所描述的请求,在大多数情况下,实际上并不会按照你想象的方式发生。
这就是为什么“我们的平均值还不错”是一个陷阱。平均值是 由快速路径主导的。而用户体验是由长尾主导的,且长尾随着你增加的每一跳而增长。你给 Agent 增加的每一个新工具、每一次重试、每一个自纠正循环,都是一次针对 SLO 的伯努利试验。你让 Agent 功能越强大,它的长尾就越糟糕——除非你为此制定预算。
每一跳都在消耗一笔无人记录的预算
这里有一个可以解决此问题的思维模型。你的端到端 SLO 就是一个银行账户。每一跳都是一次取款。现在,没人记录这些取款,所以账户透支了,而你直到最后才知道。
解决方法与 SRE 团队十年来在 RPC 服务中使用的方法相同:将 SLO 分解为每跳预算。8 秒的 p95 可以分解为:300ms 用于输入护栏和路由,1.5s 用于检索,4s 用于主要生成,1.5s 用于所有工具调用的执行,以及 700ms 用于序列化、排队和网络的缓冲。数字是可以协商的,但数字的存在是不容商榷的。
一旦你写下这些数字,两件事会发生改变。首先,你可以强制执行它们。拥有预算的每一跳都可以设置等于该预算的超时时间——而超时是唯一能将无界长尾转换为有界长尾的方法。如果没有每跳预算,你唯一的超时就是全局超时,这意味着一次缓慢的检索会消耗掉生成所需的预算,生成步骤会因为检索的过错而被终止。
其次,你可以进行违规归因。当链路超过 8 秒时,你不再问“为什么这么慢”,而是问“哪一跳超过了它的预算”,答案就是一个简单的减法。这就是整场游戏的精髓: 将无法归因的整体拆解为一组可归因的细目。
最棘手的情况是动态跳数——模型决定进行三次工具调用而不是一次。你为这种可变成本制定预算的方式与你为任何可变成本制定预算的方式相同:设定上限。给这个类别一个预算(所有工具执行总共 1.5s),而不是为每个单独的调用制定预算。如果 Agent 想要在 1.5s 的范围内进行五次工具调用,它可以这样做,但这个范围不会仅仅因为模型变得话多而扩大。变数被限制在固定的边界内,而不是泄露给用户。
如果无法在 Span 维度可见,你就无法制定预算
如果你的追踪(Trace)只是一个显示 “agent: 9.4s” 的单一 Span,那么逐跳(Per-hop)预算就毫无意义。你需要 Span 级的归因 —— 每一个跳转对应一个 Span,通过嵌套反映调用结构,并标注耗时及具体内容。
好消息是,这已不再是一个需要定制化解决的问题。OpenTelemetry GenAI 语义规范(semantic conventions)现在已经标准化了这种形态:每一次 LLM 调用、每一个工具调用以及每一个检索步骤都成为一个子 Span,通过 gen_ai.* 属性携带模型、供应商、操作名称和 Token 计数。工具执行拥有专门的 execute_tool Span —— 而这些 Span 正是延迟离群值(latency outliers)浮现的地方,因为工具延迟是整个技术栈中你了解最少且最难控制的部分。
具体来说,Span 级归因能为你带来:
- 关键路径分析。在任何存在并行处理的链条中,总延迟取决于最长路径,而非各项总和。Span 嵌套能向你展示哪些跳转真正处于关键路径上,而哪些则隐藏在更慢的兄弟节点阴影下。优化非关键路径上的跳转毫无意义;追踪会告诉你谁是谁。
- 逐跳尾部追踪。你需要针对每种跳转类型建立延迟直方图,而不仅仅是端到端延迟。端到端的 P99 只能告诉你存在长尾问题。而逐 Span 的 P99 则能告诉你长尾发生在检索阶段而非生成阶段 —— 这决定了你该进行缓存优化还是更换模型。
- 首个 Token 生成时间 (TTFT) vs. 总耗时。对于流式响应,影响感知延迟的跳转是首个 Token 生成时间,它存在于生成 Span 内部。如果追踪仅记录总生成时间,就会掩盖这样一个事实:模型在 800ms 时就开始流式输出,用户在 Span 关闭前很久就已经感到满意了。
如果你的检测工具仅在边界记录一个数值,那么每一次诊断都只是猜测。如果它记录了每一跳的 Span 及其耗时,诊断就变成了算术。这正是插桩(instrumentation)投资的全部回报。
既然看见了长尾,就去约束它
归因告诉了你时间花在了哪里。接下来你要做的就是约束长尾,这些技术完全借鉴自微服务方案 —— 只是现在你有了逐跳预算,它们的应用变得更加清晰。
每一跳都设置超时(Timeboxes)。每一跳的超时时间等于其预算。当某一跳超过预算时,不要等待 —— 立即采取行动。这是杠杆率最高 的改变,因为它是将无边界长尾转化为有边界长尾的唯一机制。一个没有计时的跳转是没有 P100 的;它的 P100 是“历史上发生过的最慢情况”。
在支持的跳转上使用对冲请求(Hedged requests)。对于幂等的、读密集型的跳转(如检索),在短暂延迟(例如在 P95 处)后发起第二个请求,并采用最先返回的结果。Dean 和 Barroso 已经证明,这能以极小的总负载增加为代价,大幅压缩尾部延迟,因为两次尝试同时陷入长尾的概率是两个极小数值的乘积。这不适用于非幂等的工具调用 —— 你不能对冲一笔付款 —— 但对于检索和重排序(re-ranking)来说,这几乎是免费的优化。
非关键跳转过期时提供部分结果。如果一个信息增强跳转超出了预算,宁愿在没有增强信息的情况下进行渲染,也不要让用户等待。将你的预算分配权重向关键用户旅程上的跳转倾斜,允许边缘跳转“故障开启”(fail open)。对于几乎所有交互式 Agent 来说,一个降级但快速的回答胜过一个完整但迟到的回答。
在结构允许的情况下进行推测执行和并行执行。如果两个工具调用互不依赖,模型就不应该串行运行它们。Agent 的许多长尾延迟是由于“自残式”的排序造成的 —— 那些本可以重叠的跳转,却因为编排层一次只运行一个而变成了串行。通过 Span 观察到的关键路径视图会明确告诉你哪些串行链是并行化的候选对象。
从记录数字开始
Agent 延迟感觉难以归因的原因,并不是因为 Agent 神秘莫测,而是因为没有人预先 做那些无聊的分配工作。SLO 只是边界上的一个数字,内部跳转既没有预算也没有计时,长尾延迟只能在黑暗中不断累加。
上述所有操作都源于一个动作:记录下逐跳预算。一旦每一跳都有了数字,你就可以对其计时、进行归因分析并加以约束。乘法效应导致的尾部延迟不再是谜团,而变成了你可以指出的分项账单。一个导致 P99 爆炸的五跳链条不再是“Agent 很慢”,而是变成“在 8% 的请求中,检索耗时 2.1s,超出了 1.5s 的预算,这里需要使用对冲请求”。
先做那件平庸但正确的事。打开你最糟糕的一个追踪,画出 Span,并在每个 Span 旁边写下预算。你几乎肯定会发现,那个被所有人指责的跳转其实没问题,而那个没人关注的跳转才是吞噬你尾部延迟的元凶。这个发现正是核心意义所在 —— 而它就潜伏在你已经拥有的追踪数据中。
- https://www.getmaxim.ai/articles/monitoring-latency-and-cost-in-llm-operations-essential-metrics-for-success/
- https://greptime.com/blogs/2026-05-09-opentelemetry-genai-semantic-conventions
- https://uptrace.dev/blog/opentelemetry-ai-systems
- https://www.datadoghq.com/blog/llm-otel-semantic-convention/
- https://sysctl.id/reduce-long-tail-latency-microservices/
- https://medium.com/@ThinkingLoop/slo-first-development-10-latency-budgets-you-can-keep-6bcdb19e9c95
- https://www.radview.com/blog/p99-latency-why-matters-how-measure-load-testing/
- https://traceintime.com/posts/p50-p95-p99-average-latency/
- https://systemssaturday.substack.com/p/systems-saturday-1-why-latency-lies
