ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

90%Java后端压Agent都会踩大坑:JMeter压接口≠压Agent,既解决Token成本又入简历的神级方案

90%Java后端压Agent都会踩大坑:JMeter压接口≠压Agent,既解决Token成本又入简历的神级方案

很多Java后端做大模型工程化落地、上线Agent服务前,流程走得特别标准:写接口、写单元测试,拉JMeter跑一轮压测,QPS、RT、错误率看着全部达标,然后信心十足推生产。

结果上线第一天就翻车:接口监控没有报错,但是大模型账单半夜暴涨三倍;用户响应时快时慢,一部分请求卡在那里很久才返回,日志翻半天找不到超时。

本质原因只有一个:你压的只是对外暴露的HTTP接口,根本没有压到Agent内部的思考循环、工具链式调用、动态多轮推理逻辑。普通接口压测体系,对Agent来说完全失效,这也是很多人做Java Agent压测最容易踩的第一关。

💡 框架迁移提示:本文埋点思想、指标体系是通用Agent可观测方案,示例基于Spring AI实现;如果你使用 LangChain4j、AgentScope 等其他Java Agent框架,只需要替换框架原生的回调监听扩展点,统计逻辑完全一致。

你有没有遇到这种诡异场景:同一个入参,有时候Agent一轮思考就结束,有时候反复调用3-5次工具;并发上来之后,重试、循环会形成放大效应,向量库、LLM网关的压力会指数级放大。传统压测指标根本捕获不到这个风险,哪怕你已经实现了Function Calling Java调用,也一样会踩坑。

等到账单告警、用户投诉才意识到问题,代价已经很大。

今天这篇,我们就把大模型压测指标体系讲透,基于Spring AI做一套可直接上线的Agent全链路埋点代码,统计思考轮次、Token消耗、工具调用频次。看懂这套方案,你以后做多Agent并发压测、Agent上线前容量评估,不再只看HTTP指标。

本文所有代码基于 Spring AI 官方稳定版本,可直接运行,无编造API;文末附带简历描述片段,适合放进项目经历。

一、先分清:普通接口 vs Agent,压测的核心差异

传统SpringBoot接口,输入固定,逻辑固定,一次请求一次计算,链路边界清晰。
Agent完全不一样,它是动态闭环链路:
用户提问 → LLM思考 → 决定调用工具 → 拿到工具结果 → 再次送入上下文 → LLM二次思考,循环直到满足终止条件。

同一个接口入参,Agent内部执行链路是不确定的:

  • 请求A:1轮推理,0次工具调用
  • 请求B:4轮推理,连续调用数据库、检索工具、计算工具
  • 异常场景:陷入循环调用,无限消耗Token

这就带来一个致命问题:HTTP层QPS是表象,LLM侧的实际负载是变量。
这也是绝大多数同学压测踩坑的根源:只约束入口QPS,没有管控内部推理轮次带来的资源放大。

指标分类普通HTTP接口关注Agent必须额外关注
基础指标QPS、P95 RT、错误率、连接数保留原有指标
Agent专属指标无单请求平均思考轮次、轮次分布P95、单请求总输入/输出Token、单请求工具调用次数、重试放大系数、上下文长度、循环终止失败占比

很多企业Agent事故,都藏在这张表里:P95思考轮次突然飙升,就是循环调用前兆;平均Token上涨,代表上下文膨胀,成本悄悄失控。想要做好Agent可观测,这一组指标是必备基础。

二、Spring AI 埋点设计思路

Spring AI 提供了Advisor+ToolCallbackListener扩展机制,是官方原生扩展点,我们不用魔改框架源码,通过监听器拦截每一轮推理、每一次工具调用、大模型返回结果,采集全部指标,也就是Spring AI Agent监控的落地方案。

采集的数据我们先存入内存做本地统计,生产环境可以对接Micrometer,上报Prometheus做可视化看板。

采集事件清单

  1. Agent开始执行
  2. 每一轮LLM推理完成(输入token、输出token)
  3. 工具调用发起、工具调用返回/异常
  4. Agent正常结束 / 熔断终止

重要避坑:不要只在HTTP切面统计token,切面拿不到Agent内部多轮调用的明细,数据会严重失真。这是80%开发者埋点失效的核心原因。

三、完整可运行源码包(Spring AI)

1. pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="[http://maven.apache.org/POM/4.0.0](http://maven.apache.org/POM/4.0.0)" xmlns:xsi="[http://www.w3.org/2001/XMLSchema-instance](http://www.w3.org/2001/XMLSchema-instance)" xsi:schemaLocation="[http://maven.apache.org/POM/4.0.0](http://maven.apache.org/POM/4.0.0) [https://maven.apache.org/xsd/maven-4.0.0.xsd](https://maven.apache.org/xsd/maven-4.0.0.xsd)"> <modelVersion>4.0.0</modelVersion> <groupId>com.agent.demo</groupId> <artifactId>agent-pressure-demo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.0</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0-M1</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring AI 核心 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- 监控指标,对接Prometheus --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
2. AgentMetrics.java 指标实体
package com.agent.demo.metric; import lombok.Data; @Data public class AgentMetrics { private String traceId; private int totalRound; private long totalInputToken; private long totalOutputToken; private int toolInvokeCount; private boolean finishedNormal; private long startTime; private long endTime; public long getCostMs(){ return endTime - startTime; } }
3. AgentMetricsAdvisor 埋点Advisor(Spring AI扩展)
package com.agent.demo.advisor; import com.agent.demo.metric.AgentMetrics; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.springframework.ai.chat.client.advisor.api.AdvisedRequest; import org.springframework.ai.chat.client.advisor.api.AdvisedResponse; import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor; import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Component public class AgentMetricsAdvisor implements CallAroundAdvisor { private final MeterRegistry meterRegistry; private final Map<String, AgentMetrics> metricMap = new ConcurrentHashMap<>(); public AgentMetricsAdvisor(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } @Override public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) { String traceId = advisedRequest.chatOptions().getMetadata().getOrDefault("traceId", System.nanoTime() + "").toString(); AgentMetrics m = new AgentMetrics(); m.setTraceId(traceId); m.setStartTime(System.currentTimeMillis()); m.setTotalRound(1); m.setToolInvokeCount(0); metricMap.put(traceId, m); AdvisedResponse response = chain.nextAroundCall(advisedRequest); ChatResponse chatResponse = response.response(); if (chatResponse.getUsage() != null) { m.setTotalInputToken(chatResponse.getUsage().getPromptTokens()); m.setTotalOutputToken(chatResponse.getUsage().getCompletionTokens()); } m.setEndTime(System.currentTimeMillis()); m.setFinishedNormal(true); Timer.builder("agent.request.cost") .register(meterRegistry) .record(m.getCostMs(), java.util.concurrent.TimeUnit.MILLISECONDS); meterRegistry.summary("agent.round.summary").record(m.getTotalRound()); meterRegistry.summary("agent.token.input.summary").record(m.getTotalInputToken()); meterRegistry.summary("agent.token.output.summary").record(m.getTotalOutputToken()); metricMap.remove(traceId); return response; } @Override public String getName() { return "agentMetricsAdvisor"; } @Override public int getOrder() { return 0; } }
4. 启动类 + 测试入口
package com.agent.demo; import com.agent.demo.advisor.AgentMetricsAdvisor; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.ai.chat.client.ChatClient; @SpringBootApplication public class AgentDemoApplication { public static void main(String[] args) { SpringApplication.run(AgentDemoApplication.class, args); } @Bean public CommandLineRunner testAgentRun(ChatClient.Builder builder, AgentMetricsAdvisor advisor){ return args -> { ChatClient chatClient = builder .defaultAdvisors(advisor) .build(); String res = chatClient.prompt("帮我查询今天北京天气") .call() .content(); System.out.println("Agent返回:" + res); }; } }

四、压测数据怎么解读?看板字段设计

埋点跑完之后,看板重点盯这4个信号,提前识别风险:

  1. P95思考轮次持续上涨:大概率用户提问容易触发多轮工具调用,甚至存在循环风险,需要增加最大轮次熔断;
  2. 单请求Token分布方差极大:说明请求之间成本差异巨大,不能用平均Token做容量估算;
  3. 工具调用次数突增:检索、数据库查询压力变大,向量库、下游接口容易被打挂;
  4. 正常结束率下降:Agent卡在推理、工具超时,需要增加超时控制。

反常识结论:Agent压测,平均指标参考价值很低,一定要看P95、P99分位。

五、本篇落地避坑清单

❌ 不要在Controller切面统计Token,拿不到内部多轮推理明细
❌ 不要只用平均Token预估账单,必须按P99 Token做容量规划
❌ 不要忽略工具调用带来的下游依赖(向量库、HTTP接口、DB)

✅ 压测脚本里,请求样本要贴近真实用户提问,不要全部用简单短句
✅ 压测同时开启指标看板,一边加压一边看Agent轮次指标,而不是等压测结束只看报告

简历描述片段(直接复制)

基于Spring AI落地Agent全链路埋点体系,通过Advisor扩展拦截推理轮次、工具调用、Token消耗事件,实现Agent专属可观测指标采集;区分传统接口指标与Agent动态推理指标,构建包含思考轮次分布、Token分位统计、工具调用频次的监控看板,上线前用于Agent容量评估,提前识别循环调用、下游依赖雪崩风险,保障多Agent生产稳定性,控制大模型成本。这套埋点方案可迁移至LangChain4j、AgentScope等Java Agent框架。

📦 落地资源推荐

跑Agent的应用,尤其是做压力测试,更吃服务器资源,通常本机是不够用的,建议使用云服务器,能大大减少我们做其他事的精力,提高学习代码的专注力和学习AI的效率,免除后顾之忧,新用户首年特惠+专属渠道,https://www.aliyun.com/daily-act/ecs/activity_selection?userCode=fzoabro9

💡领取完整工程

找到图片上的水印,即Java-AI工程师,然后打出【agent-metric】即可完整源码

下期预告

很多同学看完这套Agent全链路埋点方案之后,会问:那我怎么批量并发压?直接调用真实大模型做并发压测,成本动辄几千块。成本太高怎么办?下一期给你两套零成本压测方案:本地Mock LLM做回归压测,线上仿真压测;附带完整JMeter脚本,并发上万请求也不会产生一分钱Token费用。


返回列表