跳到主要内容

为 Agent 进行容量规划:为什么并发数而非 QPS 才是你的真实衡量单位

· 阅读需 9 分钟
Tian Pan
Software Engineer

当智能体(Agent)产品在生产环境中第一次崩溃时,值班工程师通常会查看请求率图表,发现它平稳得令人乏味。每秒 40 次请求,和昨天一样。没有峰值,没有波动,也没有明显的诱因。与此同时,系统却已火烧连营:到处都是超时,供应商返回 429 错误,队列深度直线飙升。那个本该解释故障原因的图表看起来完全健康,因为图表测量的指标错了。

QPS(每秒查询数)是我们从过去十年无状态 Web 服务中继承下来的单位。一个请求进来,你处理几十毫秒,发送响应,请求结束。在这种模式下,请求率和负载几乎是同一个数字,容量规划基本上就是“单台机器每秒能处理多少请求,再乘以机器数量”。这种思维模式根深蒂固,以至于我们不假思索地沿用。但对于智能体来说,它在悄悄地欺骗你。

它撒谎的原因在于,智能体请求并不会立即离开。一条简单的用户消息会开启一个循环:模型推理、调用工具、等待工具、读取结果、再次推理、调用另一个工具,如此循环往复 30 秒、3 分钟,或者——在出错时——直到超时强制中断。在整个时间窗口内,该请求都处于 处理中(in flight) 状态。它占用着槽位、LLM 连接、Token 预算、上下文窗口内存,而且极有可能在每次工具调用时都占用一个下游数据库连接。每秒 10 次请求听起来微不足道。但如果每秒 10 次请求且每个请求都持续 2 分钟,那就是完全另一回事了。

你真正需要考量的指标是并发量

有一个数学公式可以精确描述这一点,它的历史非常悠久,任何靠流量服务谋生的人都不应对此感到陌生。利特尔法则(Little's Law):L = λ × W。系统中物品的平均数量 (L) 等于到达率 (λ) 乘以每个物品在系统中的平均停留时间 (W)。无论到达分布如何,它对任何稳定系统都成立,这正是它成为可靠规划工具的原因。

把智能体的数据带入公式。假设请求以每秒 10 个的速度到达,而平均每个智能体运行(端到端的推理加上所有工具调用)需要 90 秒。那么:

L = 10 req/s × 90 s = 任何时刻都有 900 个并发运行中的请求。

九百。这才是你需要预留资源的数字:九百个同时运行的推理循环,九百个活跃的 LLM 流,以及在任何给定瞬间,可能有高达九百个并发工具调用冲击你的下游服务。每秒 10 次这个数字几乎没提供任何有用信息。是 90 秒的停留时间将其转化为了负载数字,而这个乘数之所以如此残酷,正是因为智能体的延迟是以“数十秒”计的,而不是以前 Web 请求常见的 200 毫秒。

这是最重要的观念转变:延迟是并发量的乘数,而不仅仅是用户体验指标。 在 Web 服务中,延迟激增是用户体验问题。而在智能体系统中,延迟激增是 容量 问题。如果你的平均运行时间翻倍——因为工具变慢、模型思考更深、或者触发了重试循环——你的并发负载也会随之翻倍,即使没有增加任何一个额外的请求。请求到达图表保持平稳,而系统却在无声无息中需要两倍的容量。这正是为什么在 QPS 仪表盘看起来正常时,系统却会发生故障。

扇出(Fan-out)让实际数字远大于请求数

请求层面的并发仅仅是第一层。现代智能体具有扇出效应。一个用户请求可能衍生出子智能体、并行运行工具调用、分发一批检索查询,甚至可能分叉出一个规划器和多个执行器。一个入站请求在完成之前,可能会变成几十个甚至上百个下游调用。

因此,对于你的供应商、向量数据库和内部 API 而言,真正重要的并发量不是 900,而是 900 乘以每一层的扇出因子。如果每个处理中的运行平均保持 3 个并发工具调用,那么你的下游服务将看到源自每秒 10 次人工输入的 2,700 个并发调用。只盯着入口处的容量规划会漏掉这个乘数所代表的真实负载,而这个乘数在任何请求率图表上都是不可见的。

这正是故障模式变得恶毒的地方。扇出与延迟在反馈循环中相互作用。下游饱和导致下游延迟上升,进而增加了在该处发起调用的每个运行的停留时间 W。根据利特尔法则,这会提高并发量 L,意味着更多的运行会把更多的调用堆积到已经饱和的下游。系统不会沿着平滑曲线优雅地降级;它会一直保持正常,直到跨越饱和点,然后突然崩溃。因为每一单位的额外延迟都会招致更多的并发负载,从而产生更多的延迟。你不会看到预警斜坡,你只会遇到断崖。

为什么忽略这一点推理层会让你付出代价

如果你选择私有化部署推理,并发量(Concurrency)不仅仅是你的衡量单位——它是推理服务栈旋转的物理旋钮,而且它的表现是非线性的。当你向 GPU 推送更多并发请求时,系统范围内的吞吐量(所有用户的总 tokens/sec)会上升,因为批处理(Batching)让硬件保持繁忙。但单用户吞吐量会下降:每个用户的 tokens/sec 大致等于 Token 间延迟(Inter-token latency)的倒数,而随着批次增大以及请求争夺计算和内存带宽,Token 间延迟会攀升。

这里存在一个饱和点。在饱和点之下,增加并发量可以用可接受的延迟成本换取吞吐量。超过饱和点后,系统吞吐量会进入平台期,而单用户延迟却持续攀升——你增加了负载,除了更慢的响应外一无所获,而更慢的响应(再次强调)意味着更长的驻留时间(Residency),进而产生更高的并发量。整个推理服务文献都将其描述为一个三难困境(Trilemma):你无法独立地最大化吞吐量、最小化延迟和最小化成本。推动其中一个角,就会使另外两个变形。以 QPS(每秒查询数)定义的容量计划无法描述这一切,因为正在饱和的东西——运行中的批次(In-flight batch)——从未出现在每秒请求数中。

即使你不自托管,你也会通过供应商的速率限制(Rate limits)继承同样物理规律的变体。现在的限额越来越多地以并发请求和每分钟 Token 数(TPM)而不是原始 QPS 来衡量——因为供应商在规划自己的集群时,已经知道并发量才是对应到 GPU 的核心单位。

以正确的单位进行规划和运营

解决办法并不深奥;它是从估算阶段到运行时始终如一地改变分母。以下几种实践直接源自数学逻辑:

  • 按边界使用利特尔法则进行规模估算。 对于每一层——前端入口运行、LLM 调用、每个下游服务——使用该层的到达率(λ)和驻留时间(W)来估算 L = λ × W。预留并发槽位以覆盖 L 加上应对波动的余量,而不仅仅是长期平均值。最小值是底线,而不是目标。
  • 执着地测量 W,并将其作为容量信号进行告警。 追踪端到端的运行耗时以及每个工具内部的驻留时间。W 的上升是一个早期预警,表明即使 QPS 持平,你的并发负载也在攀升。将延迟退化视为容量故障,因为事实就是如此。
  • 将并发视为一种显式的、受限的资源。 为并发运行和并发工具调用设置真实的上限——使用信号量(Semaphore)、受限池(Bounded pool)或固定大小的队列——其大小取决于你的下游服务率实际能吸收的量。不受限的并发并不意味着“快”,它只是一个只要流量或延迟一上升就注定会发生的故障。
  • 应用背压(Backpressure)而不是让队列吸收一切。 当运行中的任务数达到上限时,让新工作等待或有意识地丢弃。不受限的队列看起来比拒绝请求更安全,但它只是将快速失败转化为缓慢且彻底的崩溃——队列增长,驻留时间增长,最终你会在积压了百万项任务的情况下崩溃,而不是优雅地返回一个 429。
  • 控制扇出(Fan-out)预算。 限制单次运行的生成深度和并行工具调用数量。扇出倍数从 3 变成 30,意味着在相同的入站速率下,下游并发量会产生 10 倍的变化。在有人察觉到图表异常之前,一个配置错误的循环就能让账单飙升。
  • 以并发量而非 QPS 进行容量测试。 在进行负载测试时,固定运行中的并发任务数,找出 W 和错误率开始急剧恶化的转折点。针对 Agent 后端测试每秒请求数的增长,是在测量一个并不决定故障发生的指标。

核心思想只有一句话,值得钉在仪表盘上方:在 Agent 系统中,你衡量的不是请求到达的频率,而是同时存活的请求数量。 QPS 是事物进入的速率。并发量是同时占用资源的请求数。由于 Agent 运行时间长且具有扇出特性,这个数字比过去 15 年作为默认容量规划单位的请求速率更大、波动更剧烈,也危险得多。切换分母,那场看似凭空出现的故障就会变回一个你原本就能预见的数字。

References:Let's stay in touch and Follow me for more thoughts and updates