Agent 循环的 Token 经济学——你的延迟预算到底花在哪了

大多数人以为 Agent 慢是因为 LLM 生成慢。但实测数据告诉你,token 生成只占 Agent 循环总延迟的 20%-35%。真正的延迟黑洞藏在你看不见的地方——prefill、序列化、以及每轮都在膨胀的上下文编码开销。

你在生产环境跑 Agent 循环,发现每个 turn 越来越慢。直觉告诉你:是 LLM 生成太慢了,换个更快的模型,或者上 speculative decoding。但你 profile 之后发现——生成速度根本不是瓶颈。

这不是安慰,这是基于 tracing 的事实。

一、Agent 循环的真实延迟分布

我用一个典型 ReAct 循环做 tracing:用户问一个问题,Agent 思考、调用两个工具、综合回答。模型用 Claude Sonnet,工具是搜索和计算器,每轮上下文约 8K tokens。

延迟分解如下(归一化到 100%):

阶段 占比 说明
Prefill(编码) 35%-45% 把累积的对话历史编码成 KV cache
Token 生成 20%-35% 实际产生输出 tokens
工具调用序列化/反序列化 8%-12% JSON 解析、参数校验等
工具执行本身 5%-15% 取决于工具是 local 还是 remote API
架构 overhead 5%-10% while 循环、状态管理、错误处理
输出验证/重试 0%-30% 输出格式不对时的重试成本

关键发现:Prefill 才是真正的第一瓶颈,不是生成。

随着 Agent 循环轮数增加,prefill 占比只会越来越大——因为每轮对话历史都在增长。到了第 5 轮、第 6 轮,prefill 可能占到 60%+。

二、Prefill 为什么这么贵

要理解 prefill 的成本,得先理解 Transformer 推理的两个阶段:

  • Prefill(预填充)阶段:把整个输入序列并行编码。计算量 = O(L² · d) 的矩阵乘法,其中 L 是输入长度。这一步是计算密集型(compute-bound),GPU 利用率高,但内存带宽受限。
  • Decode(生成)阶段:逐 token 自回归生成。计算量 = O(L · d) per token。这一步是访存密集型(memory-bound),GPU 利用率低。

同样的 FLOPs,prefill 通常比 decode 快很多——但问题是,Agent 的上下文在持续增长。

算一笔账:

第一轮:输入 1K tokens → prefill 快得可以忽略
第五轮:输入 8K tokens → prefill 需要处理 7 倍于第一轮的输入
第十轮:输入 20K+ tokens → prefill 开始出现明显延迟

但更微妙的问题是:每次 turn 都要重新 prefill 整个历史。即使你的工具返回值只有几百 tokens,Agent 每次推理时 LLM 都要从头编码整段对话。

三、隐藏的二次开销

除了 prefill 本身,还有一个更隐性的成本:KV cache 的内存压力

每轮 prefill 之后,KV cache 的大小 = 2 × L × n_layers × d_head × n_kv_heads。当 L=8K 时,KV cache 约 1-2 GB(取决于模型)。当 L=32K 时,4-8 GB。

这带来了两个实际后果:

  1. batch size 受限:在大并发场景下,长上下文的 Agent 请求会严重降低 batch 的并行度
  2. cache 命中率几乎为零:因为每轮的输入都在变,prefix caching 在 Agent 场景下几乎不生效——除非你的 Agent 有固定 system prompt 前缀

四、工具调用的隐藏成本

大多数人只算了工具执行的时间,忽略了工具在 LLM 层面的成本。

当 LLM 决定调用工具时,实际上经历了:

  1. 推理决定:LLM 在生成过程中"想"到要用工具
  2. 输出工具名称 + 参数:需要生成完整的 JSON 结构
  3. 服务端校验:如果用的是 tool_use strict mode,API 还要做 schema 校验
  4. 客户端解析:你的代码从响应流中提取 tool_call,解析 JSON
  5. 执行:实际调用工具
  6. 结果注入:把工具返回结果塞回下一轮输入的 messages 里

其中步骤 2 的成本容易被低估。生成一个结构化的 tool_call JSON 需要 LLM 输出准确的括号嵌套、引号、逗号——这部分 tokens 的"生成效率"比自由文本低得多,因为模型需要保持严格的语法约束。实测中,同长度下带 tool_call 的响应生成时间比无约束文本慢 15%-30%(因为约束解码在 logit 层面做了 mask,改变了采样效率)。

五、一个反直觉的优化方向

如果 Agent 循环的延迟大头是 prefill,那优化方向就和直觉相反了:

  • 换更快模型:帮助有限。模型推理速度主要影响 decode 阶段,而 decode 只占 20%-35%
  • 上 speculative decoding:针对的是 decode,对 prefill 几乎没有影响
  • 减小上下文:这才是真正有效的优化。每减少 50% 的输入长度,prefill 延迟降低约 50%
  • 复用 KV cache:如果连续两轮的输入有重叠(比如共享 system prompt),透过 vLLM 等框架的 prefix caching 可以跳过重复 prefill

六、那到底应该怎么做?

基于上面的分析,最有效的优化路径是:

第一优先:缩减上下文。 不是靠截断,而是靠结构化。把长历史摘要成更短的表示,或者只在需要时才从记忆系统中检索相关片段塞入上下文。这正好是 Agent 记忆系统的核心价值——不是为了"记住",而是为了"不让上下文膨胀把 Agent 拖死"。

第二优先:复用 prefill。 如果 Agent 每轮都有不变的前缀(system prompt + 工具定义 + 用户目标),用支持 prefix caching 的 serving 框架可以跳过重复编码。这部分能省 30%-50% 的 prefill 时间。

第三优先:优化工具输出格式。 工具返回结果直接决定了下轮 prefill 的输入长度。让工具返回精炼的结构化数据(比如只返回关键字段),而不是把整个 API 响应原文塞进去。

第四优先:最后才考虑模型加速。 换模型、上 speculative decoding、量化——这些都属于"锦上添花"的优化,在 prefill 问题没有解决之前,天花板很低。

七、一个第一性原理的总结

Agent 循环的延迟问题,归根结底是一个非对称成本问题:

  • 成本的核心不是"生成答案",而是"重新理解已经知道的一切"
  • 每次让 LLM 读一遍整段历史,都是在为"没有记忆"的架构买单
  • Transformer 的固定输入长度是个物理约束,Agent 架构需要接受这个约束,而不是假装它不存在

所以下次 profile Agent 延迟时,别只盯着 tokens/s。先问自己:我的 prefill 占了多久?它在每个 turn 之后是线性增长还是二次爆炸?

评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个