← logs/

面向 AGENT 负载的推理系统:为什么 agent 循环 != 对话式在线服务

LLM 在线服务的默认心智模型仍然是对话:用户发出一条提示词,服务器流式返回回复,连接关闭。对话式在线服务系统针对这种形态做了优化。token 吞吐量、首 token 时间、每输出 token 时间,都在大量并发对话的稳态下测量。

agent 的一次运行完全不是这样。一个任务产生几十次生成:一次工具调用、一段 JSON、一段简短分析,然后是对整条轨迹的一次长规划扫描。每一次生成都与之前的生成共享前缀。有些步骤很小。有些带着累积十万 token 的上下文。能把对话应用服务得很好的系统,服务 agent 可能很差,因为优化目标不对。

这篇文章是一张推理系统的领域地图,透过 agent 工作负载的视角来读。我精读了三篇塑造我对这个问题看法的论文,再从更广的在线服务文献中补充三篇,最后落到我会构建的设计上,映射到我负责的产品 PiPlan.ai 的服务层。

从请求层面看 agent 循环长什么样

agent 循环从来不是一次生成;它是一串生成。形状如下:

turn 1:  system prompt + task         -> model -> plan (tool calls)
turn 2:  prefix + tool results        -> model -> next action
turn 3:  prefix + more tool results   -> model -> next action
...
turn N:  full trajectory              -> model -> final answer

三个性质把它与对话式在线服务区分开。

  1. 前缀复用是主导成本驱动。系统提示词、任务和不断累积的轨迹被每一轮重新读取。一次二十步的运行,把同一个不断增长的前缀读二十遍。如果 KV cache 不在轮间复用,每一步都要为整个历史支付预填充成本,运行成本随步数平方级增长。

  2. 工作以突发方式到达,而不是流式。工具执行让生成暂停几秒或几分钟,然后一个大块落地,必须尽快处理。服务器在空闲与饱和之间交替,这种节奏没有任何稳态吞吐量数字能够刻画。最后的规划轮带着比循环中间任何步骤所产生的都长得多的上下文到达,这使它成为一个性质上不同的请求。

  3. 尾部很长,且延迟受限。大多数步骤很短:一次函数调用、一个分类器输出、一行摘要。用户感受到的是每步延迟;聚合吞吐量对他们不可见。每步 100ms 的四十步循环,几秒内完成;同样的循环每步两秒,就让人觉得坏了,而吞吐量可能全程看起来都很好。

对话式在线服务优化稳态。agent 在线服务必须优化序列:跨轮的缓存命中率、短步骤的 p95、整次运行的完成时间。

DistServe:把预填充与解码分开

DistServe(Zhong 等人,2024)主张预填充和解码不应共享一块 GPU。预填充计算密集、突发性强;解码内存密集、平稳。共置时它们相互干扰:一次长预填充拖慢批中每一个解码器,而首 token 时间与每输出 token 时间又有各自独立的延迟要求,系统最终要么牺牲一个,要么过度配置来同时保住两者。

DistServe 把两个阶段分配给不同的 GPU,并分别对每个阶段联合优化资源分配与并行度。评测显示,与最先进的系统相比,在超过 90% 的请求满足延迟约束的前提下,最多多服务 7.4x 的请求,或把 SLO 收紧 12.6x。

这对 agent 为什么重要:一次 agent 运行同时产生阶段谱的两端。工具调用轮解码密集、延迟敏感。规划轮预填充密集,上下文在整个运行期间不断增长。在共置服务器中,规划器与执行器互相打架,运行越长,冲突越狠。分离让你把每一侧调成合适规模:执行步骤用许多小 GPU,规划轮用专用预填充容量。

代价是通信。KV cache 必须在预填充与解码 worker 之间穿越网络,DistServe 在放置阶段时把集群带宽放在心上。这是诚实的告诫:只有当互连能跟得上 cache 流量时,分离才划算,而且上下文越长,传输越大。

Mooncake:把 KV cache 当作调度器的通货

Mooncake(Qin 等人,2024)是 Moonshot AI 的 Kimi 背后的服务平台,它把分离推得更远。预填充与解码集群被分开,在此之上,KV cache 被当作一等存储层级:GPU 集群中未充分利用的 CPU、DRAM 和 SSD 容纳本来会被丢弃的 cache。

核心是一个以 KVCache 为中心的调度器,在有效吞吐量与延迟 SLO 之间取平衡,外加一个基于预测的过载提前拒绝策略。论文报告,在某些模拟的长上下文场景中,在保持 SLO 的同时吞吐量最多提升 525%;在真实 Kimi 工作负载上,处理的请求增加 75%。

有两个想法直接迁移到 agent 在线服务。第一,资产是 cache,不是 GPU。在多轮循环中,共享前缀比任何单次生成都值钱,所以系统应该围绕保持它温热来组织,包括在工具调用运行、GPU 本来会空闲时,把它放到更便宜的存储层级上。Mooncake 的分层是对的直觉:cache 在 GPU 之外有家,调度器知道在哪里。

第二,准入控制是正当的。Mooncake 提前拒绝请求,而不是让所有人都降级。满载的 agent 系统应该对新运行快速失败,而不是拖住每个已在运行的 agent 的步骤。运行中的循环里一步被延迟,比一次被拒绝的开始更糟,因为循环被它卡住。

Mooncake 也把优先级次序摆对了。大多数在线服务系统从批处理出发,事后才把缓存加上去。Mooncake 从 cache 出发,让其他一切都围着它调度。对 agent 工作负载来说,这个反转不是细节,它就是架构。

EAGLE:在短而结构化的步骤上投机

EAGLE(Li 等人,2024)是一种投机解码方法。一个小的草稿头在特征层面预测接下来的 token,目标模型并行验证草稿,被接受的 token 几乎不花成本。对 LLaMA2-Chat 70B,论文报告延迟加速 2.7x 到 3.5x,吞吐量大约翻倍。

投机解码通常被当作普遍的延迟收益来推销,但它在 agent 循环中尤其有价值。大多数 agent 步骤是结构化输出:JSON、函数参数、短标签。这些是高度可预测的 token 序列,正好是草稿容易被接受的场景。四十步循环上每步 3x 的加速,不只是更好看的 demo。它是循环感觉瞬间完成与感觉卡住之间的区别,而用户体验到的是整次运行,它的每一次生成。

第二个角度是:草稿头便宜到值得保持温热。在循环中间步骤短而频繁的在线服务设置中,草稿模型的状态可以跨轮持续。冷启动的草稿头在一个三 token 输出上会放弃大部分收益,所以跨轮的温热必须从一开始就设计进去。

投机解码不触及预填充问题。长规划轮仍然要处理整条轨迹。但它改变了常见情形的每步成本,而在 agent 工作负载中,常见情形主导墙钟时间。

前缀缓存与 KV 压缩:更广的工具箱

SGLang(Zheng 等人,2023)引入了 RadixAttention,把 KV cache 存在基数树中,让前缀在请求之间和请求内部共享,并报告在包含 agent 控制的任务上,吞吐量比最先进的推理系统最高提升 6.4x。对 agent 循环来说,这是自然的数据结构:重试、并行分支、重复的工具调用格式都共享前缀,基数树把每个前缀只存一次。如果今天让我设计 agent 在线服务系统,跨轮前缀复用会是硬性要求,而不是一个优化。轨迹是一棵树;像树一样缓存它。

KV 压缩针对的是内存这一侧。H2O(Zhang 等人,2023,NeurIPS 2023)表明一小撮「高频」token 主导注意力,一个保留这些加上最近 token 的淘汰策略能把 cache 大幅缩小,报告在 OPT 模型上比某些基线最高 29x 的吞吐量提升。KIVI(Liu 等人,2024,ICML 2024)把 cache 量化到 2 bits,key 按通道、value 按 token,并报告在真实工作负载上峰值内存降低约 2.6x、吞吐量提升 2.35x 到 3.47x,无需调参。

压缩对 agent 之所以重要,是因为 cache 随运行单调增长,而在固定 GPU 预算下,一次长运行先变成内存问题,再变成计算问题。但它与前缀缓存有一个真实的相互作用:为省内存淘汰 token,可能毁掉下一轮正要复用的共享前缀,悄悄把一次缓存命中变成一次完整预填充。教训是:淘汰与复用必须由同一个策略协调。先量化,只在轮边界淘汰,并告诉调度器哪些前缀死了。

agent 在线服务栈的三个决策

如果今天让我从零为 agent 构建推理,有三个决策支撑着设计,而且我希望每个权衡都摆上台面,而不是藏在脚注里。

第一,前缀缓存:要,无条件地要。一棵调度器最先查询的基数树 cache 是核心数据结构;SGLang 证明了这种形态,而 agent 轨迹是树,所以重试与分支天然共享前缀。权衡在于:cache 变成一个一致性问题。淘汰与复用必须由一个策略协调:运行中途量化 KV 而不是淘汰,只在轮边界淘汰,并告诉调度器哪些前缀死了。一个悄悄把命中变成完整预填充的 cache,比没有更糟,因为调度器会围绕它得不到的命中来做规划。

第二,分离式预填充与解码:在我的规模上不做。DistServe 和 Mooncake 是数据中心模式;它们要划算,需要有值得调成合适规模的独立预填充与解码机群,以及能吸收 KV 流量的互连。在单个多 GPU 节点上——这正是 PiPlan.ai 和 VerifierForge 实际运行的规模——vLLM 的分块预填充把长预填充切成与正在进行的解码共享步骤的碎片,吸收了大部分同样的干扰,而且完全没有跨网络的 KV 传输。当预填充长且突发到值得设立专用 worker 类别、且 cache 流量比空闲的解码容量更便宜时,分离才开始划算。我还没有测过自己的工作负载跨过这条线,所以我会构建的切分发生在路由层:把长规划轮升级到前沿容量;GPU 机群切分可以等。

第三,执行路径上的投机解码:有道理,但未经测试。理论是对的;JSON 块、工具调用和短标签都是可预测的 token 序列,而循环的感受就是每步延迟。但接受率因工作负载而异,我还没有在自己工具调用步骤上测过草稿接受率。在那之前,EAGLE 式草稿留在实验清单上,不进设计。

准入控制从这份清单的更长版本中存活下来:像 Mooncake 那样提前拒绝新运行,而不是让它从运行中的循环偷走 SLO。优化的单位是运行,不是请求。agent 在线服务的 benchmark 应该报告端到端的运行完成情况、跨轮缓存命中率和每步 p95 延迟,因为这才是循环真正感受到的数字。

PiPlan.ai:两层服务层

PiPlan.ai 的服务层是一个自托管的多 GPU vLLM 节点,带一层动态路由:常规 agent 步骤在本地运行,复杂规划升级到前沿 API。用上面的透镜来读,这是阶段切分在产品层面的版本。

agent run
   |
   v
dynamic routing layer -----------------------+
   | routine steps                           | complex planning
   v                                         v
local multi-GPU vLLM node          frontier API, cold start
(cache stays warm across turns)    (no local prefix)

本地节点是执行路径:短、频繁、延迟受限的步骤跑在我们控制的硬件上,每一步都没有到外部 API 的网络往返。前沿升级是规划路径:长上下文、预填充密集的轮,去往不值得本地拥有的容量。路由层就是调度器,有趣的设计工作在它的策略里;GPU 只是底座。

有两个权衡很突出。第一,升级打断缓存连续性。当一轮升级时,本地节点丢失共享前缀,前沿 API 在长上下文上冷启动。这意味着路由决策应该在一次运行中保持稳定:每一步在本地与前沿之间来回抖动,会割裂轨迹并反复支付冷启动成本。一次错误路由调用的代价,以丢失的缓存命中来衡量,所以策略应该保守。

第二,路由层在生成开始前做决定,所以它无法在步骤中途对观察到的负载做出反应。容量规划必须提前进行,这把有趣的问题推进了工作负载预测:哪些运行会升级,何时。

同样的逻辑出现在 VerifierForge 的在线服务中,它缩容到零。它的两次真实唤醒周期,在按每小时 $0.20 计费的 RTX 4000 Ada pod 上分别用 282.14 和 266.68 秒(约 4.4 到 4.7 分钟)达到就绪,服务了 200 个真实请求(111 个 default、89 个 tuned、零回退),随后被 30 分钟空闲回收器回收,一路直到供应商库存归零。这个唤醒时间对交互式首 token 来说太慢,所以缩容到零只适用于能容忍冷启动、且以短到能装进回收窗口的突发方式到达的工作负载。对 agent 循环中间的步骤来说,这是错误的权衡。对给一批批补全打分的验证器来说,这是正确的。零成本空闲,是比假装你随时都能在本地服务冷启动更诚实的替代。

主线:为 agent 提供的在线服务不是一台更大的对话服务器。它是一个以轨迹为单位、跨轮保持 cache 温热、切分阶段、并把准入与路由决策做得显式的系统。

本文中链接的来源均已抓取并验证。