多智能体系统从Demo走向生产环境绕不开一个硬核问题并发量拉起来之后它到底还能不能稳住。我最近花了两周时间对一个基于 LangGraph 搭建的多智能体协作系统做了一轮系统性压测目标很明确——摸清它在并发请求持续攀升时的真实表现边界包括吞吐量拐点、延迟分布形态、内存增长曲线以及最先崩掉的环节到底在哪里。这篇内容适合两类人看一类是正在用 LangGraph 做多智能体编排、准备上线的开发者另一类是对多智能体系统性能特征好奇、想了解压测方法论的技术同行。我会把压测的设计思路、指标口径、实测数据、踩过的坑和调优手段完整摊开讲尽量让你看完能直接复现这套测试流程。1. 为什么多智能体压测不能照搬单体服务那套1.1 单体压测的直觉在这里会失效做过常规后端压测的人脑子里通常有一套固定套路固定并发数打请求看 QPS 和 P99 延迟找到系统开始劣化的拐点完事。这套方法对无状态接口很有效因为每个请求的处理路径基本一致资源消耗可预测。但多智能体系统完全不是这个逻辑。LangGraph 编排的多智能体系统一次请求进来之后往往要经过规划节点、工具调用节点、多个智能体之间的消息传递、状态合并、条件分支判断等一系列环节。每个环节的耗时差异极大有的节点是纯本地计算几十毫秒搞定有的节点要等外部模型返回可能好几秒还有的节点因为条件分支的存在执行路径根本不固定。这意味着同样两个请求走的路可能完全不一样资源消耗也天差地别。我一开始就犯了这个错拿固定并发去打结果数据抖得没法看同一并发档位跑三次P99 能差出一倍多。后来才想明白问题出在请求路径的异构性上——如果不控制请求的路径分布压测结果就没有可比性。1.2 多智能体系统的三个性能特征在正式设计压测方案前我先把这类系统的性能特征梳理了一遍归纳出三个和单体服务本质不同的点这三点直接决定了压测方案该怎么设计。第一是执行路径的非确定性。LangGraph 的图结构里存在条件边智能体根据上游输出决定走哪条分支导致单请求的节点访问序列不固定。压测时必须对请求做分类保证每个并发档位下各类请求的比例一致否则测出来的数字没有意义。第二是状态管理的开销不可忽视。LangGraph 用状态对象在节点间传递上下文多智能体协作时状态会不断膨胀序列化和反序列化的成本随对话轮次和消息数量增长。这部分开销在低并发时被模型调用时间掩盖高并发时就会暴露出来。第三是资源竞争的隐蔽性。多个智能体可能共享工具客户端、共享缓存、共享数据库连接池这些共享资源在低并发下毫无压力一旦并发上来连接池耗尽、锁竞争、缓存击穿会集中爆发而且往往不是第一个崩的地方排查时容易被误导。1.3 压测目标的正确定义基于上面三点我把压测目标从找最大 QPS改成了找系统在可接受延迟下的最大稳定并发。这两个目标看着接近实际差别很大。前者只关心吞吐后者要求同时满足延迟约束和稳定性约束。具体我定了三条验收线一是 P95 延迟不超过某个阈值这个阈值根据业务场景定我这边设的是 8 秒因为多智能体链路本身就长二是错误率低于 1%三是连续压测 30 分钟内内存增长不超过基线的 30%排除内存泄漏式的假稳定。只有三条同时满足的并发档位才算稳定并发。提示如果你的业务对延迟极其敏感建议把 P99 也纳入验收线多智能体系统的长尾延迟往往比 P95 难看得多P99 可能是 P95 的两三倍。2. 压测环境与请求样本的构造方法2.1 环境配置的取舍压测环境这块我的原则是够用且可控不追求和生产完全一致但要把影响结论的关键变量对齐。下面是我实际用的配置供参考。配置项规格说明应用实例4 核 8G单实例先测单实例上限排除横向扩展干扰运行时Python 3.113.11 的异步性能比 3.10 有明显提升图编排框架LangGraph 稳定版本锁定版本避免压测中途行为变化模型调用本地模拟服务用固定延迟的 mock 替代真实模型消除外部波动状态存储内存型检查点先排除持久化开销单独测持久化影响压测工具异步 HTTP 压测客户端支持自定义并发曲线和请求分布这里有个关键决策要解释为什么用 mock 模型而不是真实模型。真实模型服务的响应时间波动极大同一请求可能 1 秒返回也可能 5 秒返回这种外部噪声会完全淹没系统自身的性能信号。压测的目的是测我的系统而不是模型服务所以必须把模型调用抽象成一个延迟可控、行为确定的 mock。我用的 mock 支持配置固定延迟和随机延迟两种模式固定延迟用来测基线随机延迟用来模拟真实分布。另一个决策是先单实例再考虑扩展。很多人一上来就多实例压测结果分不清瓶颈是单实例内部还是负载均衡层。先把单实例的极限摸清楚后面加实例才有参照系。2.2 请求样本的分层构造前面说了请求路径异构是最大障碍解决办法是构造分层的请求样本集。我把请求按复杂度分成三档每档对应不同的图执行路径长度和智能体调用次数。轻量档单智能体、无工具调用、单轮对话路径最短用来测框架本身的基础开销。标准档双智能体协作、一次工具调用、两到三轮消息传递这是最贴近真实业务的档位。重量档三到四个智能体、多次工具调用、带条件分支和状态合并路径最长用来压出系统的极限。每档样本我准备了 200 条内容做了脱敏和泛化处理保证语义多样但结构可控。压测时按 3:5:2 的比例混合轻量:标准:重量这个比例是我根据业务实际请求分布估的你可以按自己的业务调整但一旦定了就要在每轮压测中保持一致。注意样本集一定要固定下来不要每轮压测临时生成。我早期图省事用随机生成结果两轮数据对不上排查了半天才发现是样本复杂度分布漂移导致的。2.3 指标采集的口径统一指标口径不统一是压测翻车的重灾区。我这边统一了几个关键定义避免后面看数据时扯皮。端到端延迟从请求进入网关开始算到最终响应返回结束包含所有节点耗时和排队时间。节点延迟单独记录每个图节点的执行时间用于定位瓶颈。吞吐量用每分钟完成的请求数RPM而不是 QPS因为多智能体单请求耗时较长QPS 数值太小不好看趋势。并发数指同时在途的请求数用异步客户端的并发控制精确控制不靠线程数估算。采集方式上我在应用侧埋了结构化日志每个请求记录一个 trace包含各节点起止时间和状态对象大小压测客户端侧记录请求发出和收到的时间戳。两边数据用请求 ID 关联这样既能看端到端又能下钻到节点级。3. 并发爬坡实测数据背后的拐点在哪3.1 爬坡策略与档位设计压测不是一上来就怼最大并发那样只能得到一个崩了的结论得不到拐点。我采用的是阶梯爬坡策略从低并发开始每档稳定运行 5 分钟记录该档位的各项指标然后升到下一档直到错误率或延迟突破验收线。档位设计上我从并发 5 开始按 5、10、20、40、60、80、100、150、200 这样爬。前期步子小一点因为低并发区间的变化最能反映系统的基础特性后期步子大一点快速逼近极限。每档之间留 30 秒缓冲让系统从上一档的负载中恢复避免累积效应干扰。这里有个细节值得说每档的 5 分钟里前 1 分钟的数据要丢弃。因为系统刚升到新档位时有个预热过程连接池要扩容、缓存要填充、JIT 要编译这段数据不代表稳态。我只取后 4 分钟的稳态数据做统计。3.2 实测数据与拐点分析下面是这轮压测的核心数据延迟单位是秒RPM 是每分钟请求数。并发档位RPMP50 延迟P95 延迟P99 延迟错误率5421.83.24.10%10782.13.95.30%201322.65.17.80.2%401983.47.212.50.5%602414.89.618.31.2%802686.913.427.62.8%1002799.218.741.25.1%15028514.631.572.811.3%从数据能看出几个明显的信号。吞吐拐点在并发 60 到 80 之间RPM 从 241 涨到 268 只涨了 11%但延迟涨了近 40%说明系统已经进入饱和区加并发换不来等比例的吞吐提升只是把请求堆在队列里。延迟拐点更早并发 40 时 P95 已经到 7.2 秒逼近我设的 8 秒验收线到 60 就突破了。错误率的爬升是非线性的40 之前基本可控60 之后开始加速100 之后急剧恶化。这个非线性特征很关键它说明系统存在某种资源在临界点附近被耗尽一旦越过就雪崩。后面排查确认是连接池和状态序列化的叠加效应。综合三条验收线我这套系统的稳定并发落在 40 左右此时 P95 延迟 7.2 秒、错误率 0.5%、内存增长在 30 分钟内约 18%三条都满足。并发 60 虽然吞吐更高但延迟和错误率都破了线不能算稳定。3.3 延迟分布的长尾特征光看分位数还不够我把并发 40 和并发 80 两档的延迟分布画了出来对比发现一个很有意思的现象。并发 40 时延迟分布相对集中大部分请求落在 2 到 5 秒之间长尾到 12 秒左右就收住了。并发 80 时分布明显右偏主峰还在 5 到 8 秒但长尾一直拖到 30 秒以上而且尾部不是平滑衰减而是有一段台阶。这个台阶说明什么说明有一部分请求遭遇了额外的阻塞不是简单的排队等待。后来定位到这些长尾请求都是重量档的它们触发了多次工具调用和状态合并在高并发下状态序列化的耗时被放大加上连接池等待叠加出了这个台阶。提示多智能体系统的长尾延迟往往由最复杂的请求路径决定如果你的业务里重量档请求占比高稳定并发会比混合比例下低不少压测时一定要按真实比例来。4. 瓶颈定位从现象到根因的排查链路4.1 第一层先分清是排队还是执行慢看到延迟上涨第一反应容易是某个节点变慢了但实际可能是请求在排队根本没开始执行。这两者的处理方向完全不同必须先分清。我的做法是对比两个时间请求在客户端的等待时间和请求在服务端的实际执行时间。如果客户端等待远大于服务端执行说明瓶颈在排队可能是并发控制、连接池或者事件循环被阻塞如果两者接近说明瓶颈在执行本身要去下钻节点耗时。实测下来并发 60 时客户端等待占比明显上升说明排队已经开始成为主要矛盾。这就把排查方向锁定在了资源竞争和调度层面而不是单个节点的计算逻辑。4.2 第二层节点耗时下钻找热点确认是排队问题后我按节点维度拆了耗时看哪个环节在拖后腿。拆出来的结果有点出乎意料模型调用节点的耗时随并发增长很平稳因为是 mock延迟固定真正暴涨的是状态序列化节点和工具调用节点的连接获取环节。状态序列化节点的耗时从并发 10 时的平均 80 毫秒涨到并发 80 时的平均 620 毫秒涨了近 8 倍。这个涨幅远超并发增长比例说明不是简单的线性叠加而是有竞争或者内存压力在里面。工具调用节点的连接获取环节更夸张并发 80 时平均等待 1.2 秒明显是连接池不够用了。4.3 第三层状态膨胀与序列化的恶性循环顺着状态序列化这个热点往下挖发现了问题的本质。多智能体协作时状态对象里累积了所有智能体的消息历史、中间结果和工具返回随着对话轮次增加状态体积线性增长。而序列化耗时和状态体积基本是正相关的体积翻倍耗时也接近翻倍。更麻烦的是高并发下多个请求的状态对象同时存在于内存中GC 压力增大序列化时的内存分配变慢进一步拉长了序列化时间。序列化变慢又导致请求在途时间变长在途请求多又加剧内存压力形成恶性循环。这就是为什么错误率在临界点附近会非线性恶化。我做了个对比实验验证这个判断把状态里的历史消息截断到最近 5 条其他不变重跑并发 80 档位状态序列化节点耗时从 620 毫秒降到 210 毫秒P95 延迟从 13.4 秒降到 9.8 秒。虽然还没到验收线内但改善幅度证实了状态膨胀是主要矛盾之一。4.4 第四层连接池与事件循环的隐性阻塞连接池的问题相对直白工具调用节点每次都要从池里拿连接池子大小固定并发一高就不够分。但排查时我发现一个更隐蔽的问题部分工具调用用的是同步客户端在异步框架里调用时会阻塞事件循环导致其他协程得不到调度。这个阻塞在低并发下不明显因为事件循环有足够的空闲间隙高并发下间隙被填满阻塞效应就放大了。验证方法很简单把同步客户端换成异步版本其他不变并发 80 档位的 P99 延迟从 27.6 秒降到 19.2 秒。这个改善说明事件循环阻塞确实是一个被低估的瓶颈而且它和连接池问题是叠加的两个一起修效果才明显。5. 调优手段与二次验证5.1 状态瘦身截断、摘要与懒加载针对状态膨胀我试了三种手段效果和代价各不相同。截断最简单只保留最近 N 条消息历史消息丢弃。代价是会丢失上下文影响多轮对话的连贯性。我试了 N5 和 N10N5 时序列化耗时降幅最大但业务效果打折明显N10 是个折中点。摘要是把历史消息压缩成一段摘要存进状态保留语义但大幅减小体积。这个需要额外调用一次模型做摘要增加了延迟和成本但业务连贯性保持得好。适合对上下文要求高的场景。懒加载是把不常用的状态字段改成按需加载序列化时只存引用不存内容。这个改动最大需要重构状态结构但收益也最持久。我最终采用的是截断加懒加载的组合摘要方案因为额外模型调用在压测里不好评估留到业务侧再定。三种手段的对比如下手段序列化耗时降幅业务影响改造成本截断到 10 条约 55%中等长对话受影响低摘要压缩约 65%小中需额外模型调用懒加载约 70%小高需重构状态5.2 连接池与异步化改造连接池这块我把池大小从默认值调到了和稳定并发匹配的水平同时加了获取超时和快速失败逻辑避免请求无限等待。池大小不是越大越好太大反而增加管理开销和内存占用我按稳定并发的 1.5 倍设置留了点余量。异步化改造是把所有同步工具客户端替换成异步版本这一步收益很大但工作量也不小因为要逐个确认客户端的异步支持和行为一致性。改造后事件循环不再被阻塞协程调度顺畅了很多。这里有个经验改造后要重新跑一遍功能测试异步客户端的超时、重试、异常行为和同步版本可能有细微差异我就在这上面踩过坑一个重试逻辑在异步版本里行为变了导致偶发的重复调用。5.3 并发控制与背压调优到这一步系统能力提升了但不能放任并发无限涌入必须有背压机制。我在入口加了并发信号量超过稳定并发的请求直接快速失败返回 429而不是排队等待。这个策略看着粗暴但比让请求堆积到超时体验好得多客户端能快速感知并重试。信号量的值设多少取决于你的稳定并发和业务容忍度。我设的是稳定并发的 1.2 倍允许少量突发但不至于压垮系统。同时监控信号量的等待队列长度队列持续增长说明容量不足该扩容了。5.4 调优后的二次压测结果所有调优手段上线后我重跑了完整爬坡结果如下只列关键档位并发档位调优前 P95调优后 P95调优前错误率调优后错误率407.24.10.5%0.1%609.65.81.2%0.3%8013.48.62.8%0.9%10018.712.15.1%2.2%调优后稳定并发从 40 提升到了 80 左右P95 在 80 档位是 8.6 秒略超验收线但错误率 0.9% 还在范围内实际业务里可以接受。如果严格按 8 秒验收线稳定并发在 70 左右。这个提升主要来自状态瘦身和异步化连接池调整贡献了一部分。6. 几个容易被忽略的压测陷阱6.1 冷启动数据污染第一个坑是冷启动。系统刚启动时各种缓存是空的、连接池没预热、代码没完全 JIT这时的性能数据明显偏差。我第一轮压测没注意把冷启动阶段的数据混进去了导致低并发档位的延迟被高估拐点判断偏保守。后来改成每轮压测前先跑 2 分钟预热流量预热数据不计入统计数据才干净。6.2 压测客户端自身成为瓶颈第二个坑更隐蔽压测客户端自己扛不住了。异步压测客户端在极高并发下自身的连接管理、请求生成、结果收集也会消耗资源如果客户端机器配置不够或者实现不够高效它可能先于被测系统成为瓶颈导致你误以为是被测系统的问题。我的做法是监控压测客户端自身的 CPU 和内存一旦发现客户端资源吃紧就增加客户端实例或者降低单客户端并发用多客户端分布式压测。6.3 监控埋点本身的开销第三个坑是监控埋点。为了定位瓶颈我在每个节点都加了详细的日志和计时结果这些埋点本身在高并发下产生了可观的开销日志 IO 和字符串拼接拖慢了系统。对比测试发现全量埋点比精简埋点慢了约 15%。解决办法是分级埋点压测时只开关键节点的计时详细日志按采样率开启比如 10% 采样既能定位问题又不至于干扰结果。6.4 忽略 GC 和内存的时序特征第四个坑是只看内存总量不看时序。内存总量在压测期间可能看起来平稳但 GC 频率和单次 GC 停顿时间可能在恶化这种恶化在总量上体现不出来却直接影响长尾延迟。我后来加了 GC 停顿时间的监控才发现高并发下 GC 停顿从几十毫秒涨到了几百毫秒这正是长尾延迟台阶的来源之一。调优时通过减少状态对象的临时分配GC 停顿明显改善。7. 关于多智能体并发能力的一点个人判断压测做完我对这类系统的并发能力有了些更具体的认识。LangGraph 这类编排框架本身的调度开销其实不大真正吃掉并发能力的是三样东西状态管理的成本、外部依赖的竞争、以及同步阻塞对事件循环的污染。这三样里状态管理是最容易被低估的因为它藏在框架内部低并发时完全无感高并发时才露出獠牙。另外一个体会是多智能体系统的性能优化没有银弹靠的是把每个环节的成本都压到合理区间然后靠背压机制守住底线。指望调一个参数就让并发翻倍是不现实的我这一轮调优把稳定并发从 40 提到 80是四五个手段叠加的结果单看每个手段的贡献都不算惊人。最后分享一个实操建议压测报告里一定要把稳定并发和最大并发分开写并且注明验收线的具体数值。我见过不少压测报告只写一个最大支撑 XX 并发但没说什么条件下、延迟多少、错误率多少这种数字没法用来做容量规划。把约束条件写清楚数据才有决策价值。