这两年Agent项目从demo走向生产,我最头疼的不是功能跑不通,而是性能完全没法预估。同一个Agent,单机调试时响应挺快,一旦挂到真实业务上,几十个并发用户在群里一拥而上,整个系统就像被按住了暂停键:有的用户迟迟等不到回复,有的用户等了半天只收到半句话,更离谱的是,偶尔还会出现两个用户互相“串台”,A用户的问题答案跑到了B用户的会话里。
后来我把问题拆开一层一层排查,才意识到一件事:Agent的性能测试跟传统Web压测根本不是一回事。传统压测关心的是“接口每秒能扛多少请求”,而Agent项目关心的是“一个多轮会话完整跑下来要多久、每一步的Token和时间都花在哪”。所以我们内部搭建了一套专门的Agent性能测试框架,核心思路就是三层模型:LLM推理层、Agent编排层、应用集成层,一层一层压,一层一层算账。这篇文章就聊聊这个框架是怎么落地的,以及在真实项目中踩过的坑。
1. 先说清楚:Agent性能测试为什么和传统压测不一样
1.1 传统Web压测模型在Agent场景下失效的三个原因
做后端的人对压测都不陌生:拿JMeter或者Locust打一个HTTP接口,看QPS、P95延迟、错误率,压出瓶颈后扩容。这套方法论我用了很多年,但套到Agent项目上,第一周就发现完全不灵。
第一个原因,Agent的核心交互对象不是固定接口,而是模型。模型接口的延迟不是稳定值,随输入Token数、生成长度、排队情况浮动很大。同一个请求,输入短一点可能500毫秒返回,输入变长后直接飙到5秒。传统接口的“固定响应时间”假设不成立。
第二个原因,老压测模型没有“状态”概念。一个Agent会话往往包含多轮对话,每一轮都要携带历史上下文,上一轮的输出决定下一轮的输入。传统压测脚本是线性打请求,而Agent压测必须模拟一个会话的完整生命周期,否则压出来的数字完全失真。
第三个原因,成本是性能的一部分。传统压测里成本和性能是分开的,但Agent场景下,模型调用按Token计费,一次失败的调用重试后会成倍放大Token消耗。如果不把Token消耗和重试率纳入性能指标,就漏掉了最核心的优化空间。
我们就是在这些认知下开始重新设计压测框架的。
1.2 什么是“三层模型”:从LLM到编排到业务的拆解思路
所谓三层模型,是把我压测时关注的指标按照调用链拆分到三个层级,每一层有独立的指标体系,也有独立的压测方法和优化策略。
第一层是LLM推理层,指真正的模型推理过程,包括单次推理延迟、首Token延迟(TTFT)、Token吞吐量、每秒请求数上限。这一层解决的核心问题是:模型API到底能扛多少并发,单次调用消耗多少时间和Token,模型被限流时的表现如何。
第二层是Agent编排层,指Agent框架内部的工作,包括规划、工具调用、Prompt组装、上下文管理、循环判断等逻辑。这一层解决的核心问题是:Agent框架本身在每个会话上浪费了多少时间,工具调用是否频繁导致请求放大,上下文处理是否成为性能瓶颈。
第三层是应用集成层,指Agent服务对外暴露的整体能力,包括API网关、流式响应、会话管理、数据库和向量库的读写、K8s资源占用等。这一层解决的核心问题是:端到端的P95延迟是多少,系统在目标并发下是否稳定,资源消耗和用户数的比例关系。
三层模型的关键价值在于定位问题。压测跑完,如果端到端延迟超标,我会立刻看数据:是LLM推理慢了,还是编排逻辑耗时长,还是网关排队了。每一层有独立的埋点之后,性能问题不再是“一团模糊”,而是能用数据直接指出,瓶颈具体出在哪一段。
2. 三层模型的核心设计:每一层测什么、怎么测
2.1 第一层:LLM推理层——延迟、吞吐、Token成本
这层指标是所有Agent性能的地基,不测清楚这层,上层无论压出什么数字都没法归因,所以我通常第一件事就是先给模型API单独建一套压测脚本。
指标上抓四件事:
- 单次完整响应时间(Total Latency):从发出请求到收到完整响应的时长,包括队列等待时间。
- 首Token延迟(Time To First Token, TTFT):对于流式输出尤其关键,用户感知的“响应快不快”实际由这一项决定。
- 输出吞吐(Tokens per Second):完整响应总Token数除以响应时间,评估模型生成效率。
- 每分钟请求数(Requests Per Minute, RPM):模型API允许的调用上限,注意这里经常有并发数和每分钟请求数两个独立限制,都要测。
具体操作时我会把温度设为生产环境相同的值,关闭流式和开启流式分别压一组数据。开启流式后,首Token延迟会大幅降低,但总耗时可能反而上升,因为业务侧拼接流式片段也有开销,这层测试要能区分二者。
另外一个容易被忽略的点是Token成本统计。每个请求的输入Token、输出Token都要记录。压测时如果跑了一个长会话,输入Token可能不断累积,导致成本随轮次指数增长。在后面编排层和集成层压测里我会合并统计这笔账,但在第一层,至少要把“每Token延迟”这个指标建出来。
2.2 第二层:Agent编排层——规划、工具调用、循环决策的耗时拆解
这一层的核心是搞明白Agent框架本身消耗了多少时间。现在市面上主流的Agent框架大多遵循类似RelAct的循环:接收用户消息,构建Prompt,调用LLM,解析输出,执行工具调用,再次调用LLM,直到得出最终答案。这个循环里的每一步都可能拖慢整体响应。
我在这一层要测的东西分为三类:
- 规划与思考耗时:Agent框架在调用LLM之前和之后,解析输出、决定下一步动作、组装下一次Prompt所花费的时间。很多框架是Python实现的,这里的Json解析、工具Schema约束、状态更新,在高并发下都会出现CPU竞争,稍有疏忽延迟就能多出200到500毫秒。
- 工具调用放大比例:一个用户问题往往会触发多次工具调用,每次工具调用都伴随一次或多次LLM调用。我需要统计平均每个会话触发多少次LLM调用、多少次工具调用。这个数值乘上第一层的单次响应延迟,就是编排层的主要时间开销。
- 循环轮数分布:Agent可能会因为决策错误、结果不满足条件,在循环里多次迭代。正常情况2到3轮就能出结果,但遇到复杂问题时可能跑到7到8轮。如果P95循环轮数过高,说明Agent的规划策略存在效率问题,这是绝对不能靠扩容解决的瓶颈。
编排层压测的方法,我的习惯是准备一个专门的测试Agent,内置一个模拟LLM和一组模拟工具,不连接真实模型,用固定延迟的假模型替代真实模型,专门压测框架逻辑的开销。这样跑出来的数字不包含模型波动,能非常干净地暴露出框架本身的耗损。
2.3 第三层:应用集成层——端到端并发、会话状态与资源消耗
第三层才是我说的“真正意义上的端到端压测”,用户从API网关进来,穿过Agent服务,打到Mock或真实LLM,再落回数据库和向量库,整个过程完整走一遍。这一层重点不是拆解单次请求的耗时,而是验证系统在目标并发下的整体稳定性。
目标并发怎么定?很多同学上来就拍脑袋,说“我们要支持1000并发”,结果我问他业务场景时,他说不清楚。正确思路是从业务侧反推。举个例子,假设客服Agent的目标是支持100个坐席同时在线,每个坐席一分钟处理2个用户问题,每个问题触发1次主调用加0.5次工具回溯调用,那一分钟内LLM调用总量是100乘以2乘以1.5等于300次,峰值按2倍冗余计算是600次调用每分钟,也就是10 TPS左右。数据反推完后,再去设置压测并发,才有说服力。
第三层需要额外抓两个指标:
- 会话状态隔离正确率:并发压测时,Agent的会话上下文是否准确绑定到对应用户。我会在压测脚本里给每个虚拟用户注入特殊的“指纹信息”,要求Agent在回复里原样返回来做校验。一旦串话,这个校验立即失败。
- 资源消耗的线性度:监控压测过程中CPU、内存、GPU、网络带宽的变化。如果并发从20升到40,内存占用翻倍,这正常;但如果并发只升了20%,内存却暴涨50%,很可能存在会话状态泄漏。
这一层的输出是一张总表,每项指标对应到三层中的某一层,同时标注端到端P95延迟,满足业务线约定的SLA才放行上线。
3. 我在实际项目中的落地过程:从0搭建一套Agent性能测试框架
3.1 工具选型:为什么用Locust加自定义虚拟用户
聊完理论,说点实操层面的事。
框架搭建时我对比过JMeter、k6和Locust,最后选了Locust加Python自定义虚拟用户。原因有三个:
第一,JMeter对HTTP协议场景很顺手,但Agent会话是多轮带状态场景,JMeter的线程组模型表达起来很别扭,我得写一堆JS或Java采样器去维护会话状态。第二,k6性能极佳,默认VU模型也适合做带状态压力,但它的脚本语言是JavaScript,当我需要塞入复杂的Agent业务逻辑时,比如动态组装Prompt、解析流式响应、根据Agent返回值决定下一步走向,写起来非常痛苦。第三,Locust虽然并发能力不如k6,但它用的是纯Python,我可以直接把生产环境的Agent客户端SDK粘过来,改造一下就能当虚拟用户,开发成本最低。
我的框架分三层模块:压力发生器(基于Locust)、指标采集器(基于OpenTelemetry)、结果看板(基于Prometheus加Grafana)。压力发生器负责模拟多轮会话和设置并发梯度;指标采集器负责在每一层的关键节点埋点,统一上报;看板负责把三层指标汇总成一张可读的报表。
3.2 核心实现:三层指标采集与场景编排
先说埋点设计。我在框架里定义了一个统一的结构化指标对象,所有层都往这个对象里写数据,格式长这样:
json复制{
"layer": "llm",
"session_id": "session_123456",
"stage": "llm_call",
"milestone": "first_token_ms",
"duration_ms": 820,
"tokens_in": 1280,
"tokens_out": 46,
"timestamp": 1710000000000
}
layer字段区分当前指标来自哪一层,stage字段区分是单次调用的哪个环节,milestone字段记录阶段性时刻。这样每个请求的所有耗时信息都能被拆到毫秒级,后续做归因分析时直接按layer分组。
压力发生器的核心是一个多轮会话的虚拟用户类,简化逻辑如下:
python复制class AgentSessionUser(HttpUser):
wait_time = between(0.5, 2.0)
@task
def multi_turn_conversation(self):
context = []
max_turns = random.randint(3, 8)
for turn in range(max_turns):
payload = {
"session_id": self.session_id,
"messages": context + [{"role": "user", "content": self.next_question()}]
}
with self._record_llm_stage():
resp = self.client.post("/agent/chat", json=payload, timeout=60)
resp_json = resp.json()
context.append({"role": "user", "content": payload["messages"][-1]["content"]})
context.append({"role": "assistant", "content": resp_json["reply"]})
if resp_json.get("finished"):
break
压测之前,先单独跑一组LLM推理层的脚本,记录延迟和吞吐基线;再跑编排层脚本,用假LLM和假工具,得到框架开销;最后跑集成层脚本,使用真实模型和真实数据库,得到端到端SLA数据。三层数据对齐后,瓶颈定位就非常快了。
3.3 一个真实的压测结果:让数据告诉我们瓶颈在哪
用一个真实项目举例。那是一个企业内部的客服Agent,上线前定的目标是50个并发在线用户,每个用户平均会话持续时长为10分钟,期望中间的单轮响应P95延迟不超过3秒。
第一轮端到端压测跑完,结果很不理想:
| 指标 | 并发20用户 | 并发50用户 | 目标值 |
|---|---|---|---|
| LLM层单次P95延迟 | 1.2秒 | 1.4秒 | 不超过2秒 |
| 编排层单次P95延迟 | 0.6秒 | 1.9秒 | 不超过1秒 |
| 端到端P95延迟 | 3.4秒 | 9.8秒 | 不超过3秒 |
从这里能清晰看到,并发从20升到50,LLM层的延迟只增加了0.2秒,问题不大;但编排层从0.6秒涨到1.9秒,涨了3倍多。这时候如果只看端到端数字,可能第一反应是“模型API是不是被打爆了”,但三层数据告诉我,问题出在编排层的框架逻辑上。
进一步看埋点数据,发现高并发下Agent框架在Prompt序列化这个环节消耗了大量CPU时间。因为每个会话每次调用LLM时,框架都会把全部历史消息重新序列化一遍,50个并发会话,每分钟触发上千次序列化,Python进程的GIL直接成了瓶颈。最后我们把编排层的序列化优化成增量计算,只维护每次追加的增量部分,这一项优化直接让并发50下的编排层P95延迟从1.9秒降到0.7秒。
加压过程里还要注意一个细节:逐步上调并发,而不是一步到位。我从10并发开始,每5分钟涨10个,直到目标并发数。每上调一档,稳定观察5分钟再继续。这样能清晰地捕捉到系统从稳定到恶化的拐点。
4. 常见问题与排查技巧实录
4.1 LLM API限流与重试导致的假超时
压测过程中最迷惑人的问题就是“假超时”。
我遇到过一种场景:压测跑到一半,错误率飙升,端到端延迟从3秒涨到20秒,看起来像是下游模型API彻底死掉。但单独去查询模型API的监控,发现远没有达到它文档标注的RPM上限。后来排查发现,是某个Agent环节的调用触发了模型API的限流机制,模型侧返回429后,Agent框架默认做了指数退避重试。
问题在于重试逻辑没有设置统一的全局上限。一个用户请求在LLM调用失败后,会带着整个历史上下文重新发起,历史上下文已经是几千Token的规模,一次重试顶得上好几次正常调用。当多个并发会话同时触发重试时,重试流量反而占满了上游的额度,造成整个系统的雪崩效应。
排查了很久才读出这个问题的根因,因为从指标上看,LLM层的成功率在90%以上,但Token消耗总量却比预估多出将近一倍。最后我把重试次数上限写死为2次,并且加了全局并发令牌桶,在源头控制请求峰值。压测数据瞬间好看了很多。这里建议所有Agent项目都建立Token消耗预算表:正常推理消耗多少,重试消耗多少,工具调用额外消耗多少,每项单独建监控。
4.2 Agent循环逻辑导致请求放大与成本失控
第二个常见问题是Agent的循环逻辑没有上限。压测时遇到过一个知识库问答Agent,表面上功能正常,但Token消耗远超预期。跟踪后发现在某些边缘问题上,Agent会反复调用同一个工具,尝试用不同的查询词重试,一次会话里最多循环了11次才给出最终回答。
从三层模型的视角看,这不是模型性能问题,是编排层策略问题:工具调用的结果不满足“确定性”条件时,Agent会选择再次查询。这个策略在单用户时无感,但并发压测时,等于把用户请求放大了好几倍去消耗LLM资源。
解决办法有两个方向。技术层面,在Agent框架里加最大工具调用轮数和最大LLM调用轮数双重上限,超限后强制转入兜底回复。产品层面,把准确率要求从“每个问题都必须有答案”调整为“优先保证响应时间,复杂问题引导用户转人工”。这个改动对性能的提升立竿见影。
4.3 会话状态隔离问题和并发下的上下文漂移
并发压测还暴露出一个让人冒冷汗的问题:上下文漂移。
在一次50并发压测中,我发现A用户的会话里混进了B用户的历史消息。排查后发现是Agent服务在会话标识的映射上用了错误的方案:某些请求没有正确携带session_id,网关层默认分配了同一个“默认会话”处理所有消息,导致多个用户共享同一份上下文。
这个问题从功能测试阶段完全无法发现,因为单线程调试永远只会命中一个用户。只有压测到多并发时,才会因为并发交叉触发上下文错乱。
我的排查方法是压力脚本里内置“指纹校验”:每个虚拟用户注册时生成一个独一无二的token,并在第一条消息里要求Agent原样回复该token。如果某条回复里的token不是当前用户的,立即记录为上下文漂移事件。这个机制后来成了框架的标配检查项,所有Agent项目上线前都要跑一遍指纹校验。
三是压测机资源隔离也很重要。第一次压测时,我在本机起压测脚本,同时在同一个进程里又开了监控脚本对Prometheus做高频查询,结果压测脚本本身的CPU被监控脚本吃掉大半,导致压测数据整体偏移。后来把压测机和监控看板分开部署,数据才回归正常。
5. 复盘与建议:三层模型能不能直接抄作业
5.1 什么样的项目适合直接套用
三层模型不是银弹,但它覆盖的场景其实很广。凡是有用户交互状态的Agent项目,包括客服机器人、Copilot类助手、知识库问答系统,都适合直接套用这套框架。因为这类项目的性能指标天然跟LLM推理、编排逻辑、端到端SLA三层强相关,三层模型能帮你把性能问题归因到具体模块。
但如果你是做一个纯数据流水线式的Agent Batch任务,比如离线批量处理一批文档,没有严格的在线SLA,那这套模型就有些重了。Batch场景更关注吞吐和成本,建议简化为两层:LLM推理层和任务处理层,省略掉会话状态和准实时延迟分析。
5.2 我踩过的一些坑和后续扩展方向
最后分享几个吃亏后的经验。
第一个经验是埋点一定要尽早做,不要想着一开始就做到很完美。我在项目早期为了省事,用普通的日志模块记录耗时,结果压测数据散落在不同文件里,汇总统计时花了两天写解析脚本。后来乖乖接入OpenTelemetry,结构化的指标才让分析变得流畅。如果重来一次,我会在项目第一天就把三层埋点设计好,开发测试框架的同时,也让业务代码把运行数据同步埋好。
第二个经验是压测结果要保留上下文信息。只看最终数字很容易被误导,比如P95延迟超标,但如果超标的原因是某几个会话疯狂重试拖高了平均值,而不是整体性能不行,优化方向就完全不同。所以我在看板上会额外展示延迟分布直方图,定位是普遍变慢还是长尾恶化。
第三个经验是三层模型可以继续向下扩展。比如LLM推理层内部还可以再拆成Prompt组装、队列等待、网络传输、模型推理、Token校验几个子环节,粒度越细,优化越有方向。目前我们正在把OpenTelemetry的Span按这个模型做自动关联,期望做到“一次压测,自动生成三层性能报告”,减少人工分析和解读的工作量。
回到开头那个问题:Agent项目的性能测试到底怎么做?我的体会是,别把一个Agent会话当成一个普通HTTP请求来压,把它拆成LLM推理层、Agent编排层、应用集成层三层来分析,每一层用独立指标、独立脚本去压,再合起来看端到端SLA,性能问题就会从一团迷雾变成一张清晰的账目表。这套框架现在已经成为我们团队所有Agent项目上线的必经流程,如果你也在做Agent性能建设,可以从一个最小可用的埋开始写起。
