给智能体系统做压测

9 分钟读完

O18
运维 · 智能体运维:部署与运营

给智能体系统做压测。

智能体压测的第一个决定不是选哪个工具,而是拿副作用怎么办——而每一个可选答案都会改变你测到的东西。把工具打成 mock,你就抹掉了那几秒真实延迟,而正是它主导着一条轨迹;于是你测的是模型,发出去的却是一个被自家供应商卡住脖子的系统。你真正需要的产出不是一个吞吐数字,而是你最先撞上的那道天花板叫什么名字。

STEP 1

最先饱和的,没有一样是你自己的东西。

Web 服务会耗尽 CPU。智能体系统耗尽的是别人的配额,而且是在你自家基础设施还闲着的流量水平上就耗尽了。这里的容量是并发在途任务数,不是每秒请求数;而那道天花板几乎总是下面四样之一——常规压测一样都没瞄准:

  • 厂商的每分钟 token 配额。智能体的 token 消耗对步数是平方级的,因为每一步都要重发整段对话记录,而你的配额是平的。这道线远早于每分钟请求数限制到来;算术在限流与服务商容量里。
  • 某个第三方工具的限流。通常是系统里最不慷慨的那个数字,也是没人清点过的那个。一次智能体任务可以对同一个 CRM 发出三十次调用。
  • 被长尾钉住的 worker 槽位。任务时长是长尾的,意味着 p99 的那次运行会占住一个 worker 好几分钟,而短任务在它后面排队。
  • KV 缓存内存,如果你自建推理的话:容量是并发序列数乘上下文长度,不是一个请求数——见为智能体自建推理

动手之前先用利特尔法则把测试规模定下来:并发数等于到达率乘平均任务时长。每分钟十个任务、平均四分钟,就是四十个在途——而如果你的 p99 是二十分钟,那么光是长尾就在任何时刻都占着相当一部分 worker。这个数字告诉你该造多大的负载;低于它的一切,都是披着压测名字的冒烟测试。

STEP 2

先把副作用这道题答了,并且清楚它让你付出了什么。

智能体调用的工具会下单、会发帖、会删除、会扣款。你不可能在不回答这个问题的前提下跑一千次。而每一种答案买到的是一种不同的测量。要刻意去选,而不是默认用你的测试框架最顺手的那个。

  • Mock 掉工具。安全、便宜、可重复——而且在你要测的那件事上系统性地错。真实的工具调用要几百毫秒到几秒;一个 mock 只要几微秒。轨迹结束得太快,并发根本堆不起来,于是你得出结论说自己有一份并不存在的容量。如果要 mock,就为每个工具回放一份录制下来的延迟分布,长尾也要带上,而不是一个固定的 sleep——钉住 worker 的正是那条长尾。
  • 录制回放。真实的延迟、真实的载荷体积,没有副作用。麻烦在于模型会脱稿:它一定会调用某个录制里没有的东西。要把缓存未命中记成一次被计数的失败,绝不要合成一个响应——一个编造出来的工具结果会悄悄改变轨迹,让整次运行失效。
  • 真工具,打在沙箱租户上。唯一能诚实测出下游限流的选项,也就是说,唯一能找到那道真会咬人的天花板的选项。它要花真钱、真配额。跑之前先通知厂商;来自单一账号的调用量突然涨五十倍,看上去和一次攻击一模一样,而在测试中途被限流,你学不到任何可以据以行动的东西。
  • 生产环境,只走读路径。对检索与上下文组装有用,且边界必须收紧:写工具要从工具目录里移除,而不只是在提示词里劝阻。

绝不要拿生产写路径做压测,也别指望幂等键能让这件事变安全。键给你的是按意图的恰好一次,而一次压测会生成一千个货真价实、彼此不同的意图——其中每一个都是一笔真实的下单。你要的防护是一份根本够不到生产租户的凭据。

STEP 3

造任务,而不是重复的请求。

常规压测的本能——挑一个请求、打它一万次——在这里会得出严重误导的结果,因为第二个一模一样的提示会命中一个热的提示词缓存,此后每一个都比生产环境将来会见到的任何东西更便宜、更快。你测到的是一个缓存,却把它叫作一个系统。

  • 从生产 trace 里抽任务组合。要紧的是难度的分布,因而也是步数的分布:一个 90% 三步、10% 四十步的负载,行为和它的均值毫无相似之处。去采样真实任务,不要发明一个"有代表性的"。
  • 让前缀有真实的变化。不同租户、不同工具子集、不同检索文档。缓存命中率是你容量的一项输入,所以要让它贴近生产,而不是不小心把它钉死在 100%。
  • 冷、热分开报。两者都是真实的运行状态——一次改动了系统提示的部署会同时让所有缓存失效,而那恰恰是流量没变、你的成本与延迟却一起跳起来的时刻。
  • 把上下文长度当作一根负载轴。只跑短上下文的测试会高估容量,有时高估一个数量级,因为一次运行占用的内存随它的对话记录增长。
  • 把失败路径也放进去。真实流量里有工具报错、超时和重试,而负载正是在重试处被放大的。一次工具错误率为 0% 的测试找不出重试放大,而重试放大是多数智能体系统宕机背后的那套机制。

如果你要施压的是对话界面而不是任务队列,做这件事的装置已经作为一项评估技术存在了——见评估中的模拟用户

STEP 4

要对质量下断言,因为退化是无声的。

这一步是团队会跳过的,也正是他们的压测通过了而系统其实在失效的原因。饱和之下,智能体的退化方式完全不产生任何报错:它退到一个更小的模型、为了塞得下而截断上下文、缩短思考预算、放弃一次它本来会做的重试。延迟仍在 SLO 之内。错误率仍是零。答案变差了。

所以压测必须带着一份评估,而不只是一只秒表:

  • 把评估集的一个固定子集作为负载的一部分跑起来。同样的任务、同样的评分器,在空载时跑一遍、在负载下再跑一遍。这个对比就是结论——一个在 70% 标称容量处成功率掉了八个点的系统,它的真实容量就是 70%,无论延迟曲线怎么说。
  • 把每一次退化事件显式计数。备用方案被触发的次数、上下文被截断的次数、努力度预算被下调的次数、被卸载的请求数。如果降级模式不是一个你数得出来的具名状态,你就分不清一次健康的测试和一次后半程都跑在备用路径上的测试。
  • 盯步数,不只盯时长。思考预算被压缩之后,模型常常要走更多步才能到同一个地方,而这恰恰在配额紧张的时候抬高了 token 消耗。正是这个反馈回路,把一次缺口变成一次宕机。
STEP 5

去测那少数几个有意义的数字。

常规压测面板报 RPS 和 p99 响应时间,而对智能体来说两者都近乎无意义——一个持续四分钟、向下游发出四十次调用的"请求",不是任何东西饱和时所用的那个单位。改测下面这些,并且每一项都要能与一条 trace 关联:

  • 并发在途任务数,画在你第 1 步算出来的那道天花板旁边。
  • 任务时长分布——p50、p95、p99——并在它旁边放上每任务步数,因为两者同向移动,而只有其中一个是成本驱动因子。
  • 每分钟 token 数相对配额,要报余量而不是总量。余量才是那个能预告事故的数字;计时那一侧见度量智能体延迟
  • 按下游依赖分别统计的调用速率,以及按厂商、按工具分开计数的 429。一旦聚合,它们就藏起了你究竟撞上了哪道天花板——而那是整场演练最有价值的产出。
  • 重试放大系数——尝试次数除以逻辑操作数。负载下高于约 1.3,就意味着你的重试策略是问题的一部分,而不是缓解措施。
  • 队列深度与队列年龄。深度告诉你有多少活在等;年龄告诉你有没有东西在挨饿,而只有年龄能抓住那次跟在长尾后面排了四十分钟的运行。
  • 每次运行的美元数。一次有意义的智能体压测要花真钱,而这个数字本身就是一项结果:它是你在那个流量水平上、老老实实算出来的边际成本。
STEP 6

把它做成常设的,因为天花板会移动。

压测通常在上线前跑一次,然后再也不跑——对一个只在你部署时才改变容量的服务来说,这说得通。在这里它是错的,因为智能体的容量在模型提示工具目录变化时就会移动,而这三样里有两样不需要你发布任何东西就能变。厂商换上一个新快照;某家供应商改写了一句工具描述,步数随之漂移;你自己的检索开始返回更长的文档。每一件都在无声地挪动那道天花板。

  • 行为四元组一变就重跑——模型、提示、工具、策略——放进跑评估的那同一道闸门里。不必是全规模测试;固定并发下的一次固定短跑,和上一次对比,就足以抓住漂移。
  • 用实测的天花板、而不是厂商文档上的那个来设准入控制。把在途任务数封在质量开始退化处的约 70–80%——用第 4 步那个数字,不是开始报错的那个数字。
  • 把测试本身圈起来。独立凭据、硬性花费上限,以及一个你已经验证过能停下在途运行(而不只是拦住新运行)的急停开关。压测是你此生会刻意制造出来的、流量最大的一次失控智能体。
  • 把 trace 留下来。这些运行是一份语料:一次压测中最慢的那 1% 轨迹,是你能拿到的、关于"你的智能体在吃力时会做什么"的最好样本,而它们读起来和你在空载时见到的失败很不一样。

如果只做一次压测,就做这一次。从生产 trace 里采样五十个真实任务,在沙箱租户里对着真工具、按利特尔法则算出的并发跑起来,并在负载还开着的时候用你的评估集给结果打分。记下两个数字:成功率开始下滑时的并发数,以及第一个返回 429 的依赖是谁。把准入控制上限设在第一个数字的 75%,然后去为第二个谈判、加缓存或做分片。本文其余的一切,都是对这两个数字的细化。

延伸阅读:并发、队列与扩缩容,测试所探查的那套架构;幂等与重试,为何在造出任何负载之前写路径就得先有键;在循环层面控制成本,把测试自身圈住的那些上限;以及智能体的多租户,一个吵闹租户就能耗尽的那些共享池。