
1. 项目概述一个Java老炮儿的真实转型切口“Java工程师转型AI Agent”——这标题不是喊口号也不是赶风口而是我去年在团队里亲手推过三轮POC后把Spring Boot服务替换成LangChain4j ReAct工作流的真实记录。它不讲“AI有多火”也不画“三年成为AI架构师”的大饼就聚焦一件事一个写过十年Service层、调过五年Dubbo线程池、能手写JVM参数的Java人怎么在不重学Python、不放弃Spring生态的前提下把AI Agent真正塞进现有系统跑起来。核心关键词就四个Java、AI Agent、LangChain4j、Spring AI、ReAct——它们不是并列关系而是有明确主次的落地链条Java是地基LangChain4j是钢筋Spring AI是混凝土浇筑工艺ReAct是最终承重结构的设计逻辑。你可能正卡在几个具体问题上面试官问“AI Agent怎么扛并发”你只能背“加Redis缓存”想用Spring AI集成餐饮SaaS的智能点餐模块却卡在提示词工程和工具调用链路断掉看到“LangChain4j Easy RAG”这种词点进去全是空泛的API列表不知道第一步该建哪个Bean甚至纠结“现在到底用Spring AI还是LangGraph4j”结果两边文档都啃了三天连个能返回“今天推荐什么菜”的demo都没跑通。这篇就是为这些卡点写的——它不教你怎么从零训练大模型不讲Transformer数学推导只告诉你在Java栈里Agent不是新语言而是新设计模式ReAct不是算法而是可拆解、可测试、可压测的业务流程。适合两类人一是正在准备Java面试、被问到“AI相关项目经验”时支吾说不出细节的中级开发者二是技术负责人需要评估“把现有订单系统接入AI客服Agent”的真实工期、风险点和资源投入。下面所有内容都来自我踩过的坑、压测过的QPS、线上回滚过的配置以及和算法同学撕过的提示词版本。2. 转型底层逻辑为什么Java工程师不必重学Python也能做AI Agent2.1 破除“AIPython”的思维定式Java在AI工程化中的不可替代性很多人一提AI就默认Python这是对AI工程化阶段的严重误判。Python在模型训练、数据探索阶段确实高效但当AI能力要嵌入生产系统、对接数据库、走事务流程、扛住秒杀流量时Java的强类型、JVM成熟GC、Spring生态的声明式事务、完善的监控链路才是真正的护城河。我带过的两个项目对比很说明问题项目A算法团队用Python Flask写了个RAG问答接口单机QPS 80一加Redis缓存就报ConnectionResetError异步IO和连接池管理太糙项目B我们用LangChain4j Spring Boot重写同样功能单机QPS 320且能无缝接入Sentinel限流、SkyWalking链路追踪、Seata分布式事务。关键差异在哪不是语言本身而是工程能力沉淀的深度。Java工程师的优势从来不在“写for循环”而在“设计可灰度、可回滚、可审计的执行链路”。AI Agent的核心不是“生成文字”而是“决策执行反馈”的闭环这个闭环天然需要状态管理Agent每一步的思考痕迹Thought、调用的工具Action、返回的结果Observation必须可追溯——Spring StateMachine或自定义StateContext比Python的dict强太多工具编排调用支付网关、查库存、发短信这些Java已有成熟SDK和熔断机制何必用requests硬写安全合规金融/医疗类场景要求输入输出审计、敏感词过滤、PII脱敏Spring Security 自定义Filter比Python中间件更易管控。所以转型的第一步不是学Python而是把AI Agent重新理解为“增强版的Spring Service”它有输入User Query、有处理逻辑ReAct Loop、有输出Final Answer中间穿插着工具调用Bean注入的Tool、状态流转State Enum、异常兜底ExceptionHandler。当你用这个视角看LangChain4j它的ChatModel就是RestTemplateTool就是FeignClientAgentExecutor就是Transactional——所有概念都能映射到Java工程师的肌肉记忆里。2.2 LangChain4j vs Spring AI选型不是二选一而是分层使用网络热词里总在吵“用LangChain4j还是Spring AI”这问题本身就错了。它们根本不是同一层的东西LangChain4j是能力组件库提供ChatModel大模型调用、Retriever向量检索、Tool工具抽象、AgentExecutor执行器等原子能力类似Apache Commons CollectionsSpring AI是框架整合层它把LangChain4j的能力包装成Spring Bean支持ConfigurationProperties配置、EventListener事件监听、SpringExpression提示词模板类似Spring Data JPA对MyBatis的封装。我的实操策略是底层能力用LangChain4j上层编排用Spring AI。举个真实例子——餐饮SaaS的智能点餐AgentMenuRetriever菜单检索用LangChain4j的VectorStoreRetriever因为要定制相似度阈值、过滤条件如“只查川菜”Spring AI的VectorStore抽象太薄OrderTool下单工具用Spring AI的Tool注解因为它能自动注入RestTemplate、读取application.yml里的order-service.url比LangChain4j手动new RestTemplate干净十倍最终Agent执行用SpringAiAgent因为它支持EventListenerAgentEvent监听每一步Thought方便写日志埋点——而LangChain4j的AgentCallbackHandler要自己实现还容易漏掉异常分支。提示别被“Spring AI 2.0”新特性迷惑。它新增的StreamingChatClient确实支持SSE但实际压测发现Java端处理流式响应的FluxChatResponse比Python的async for chunk in response更容易OOMJVM堆外内存管理不如CPython精细。我们最终方案是前端用React SSE轮询后端用LangChain4j的StreamingChatModel但关闭流式改用固定间隔轮询/api/agent/status/{taskId}——牺牲毫秒级响应换来JVM稳定性这是Java工程师的务实选择。2.3 ReAct不是玄学把它拆解成Java可实现的四步状态机ReActReasoning Acting常被神化成“让AI像人一样思考”其实落地到代码就是一个带状态检查的while循环。我把它拆成Java工程师一眼能懂的四步Reason推理调用大模型输入用户Query System Prompt 上下文输出结构化JSON含thought、action、actionInput字段Act执行解析JSON根据action字符串匹配预注册的Tool Bean传入actionInput执行Observe观察捕获Tool执行结果成功/失败/超时格式化为Observation字符串Loop or Return循环或返回将Observation拼回Prompt重新进Reason步骤若模型输出Final Answer则终止循环。关键点在于状态必须可中断、可恢复。我们用Redis存储每次循环的State// State结构体 public class AgentState { private String taskId; private String userId; private String query; // 初始问题 private ListStep steps; // [Reason, Act, Observe]三元组历史 private int maxSteps 5; // 防死循环 } // 每次循环前先查Redis避免重复执行 String stateJson redisTemplate.opsForValue().get(agent:state: taskId); if (stateJson ! null) { AgentState state objectMapper.readValue(stateJson, AgentState.class); if (state.getSteps().size() state.getMaxSteps()) { throw new AgentLoopException(Max steps exceeded); } }这样设计即使服务重启Agent也能从Redis续跑。而Python方案常把State存在内存里一重启就丢——这对Java工程师是不可接受的。3. 核心落地步骤从0到1搭建可压测的AI Agent3.1 环境准备避开Spring AI 2.0的三个致命坑别急着写代码先搞定环境。Spring AI 2.02024年Q3发布看似强大但实际踩坑率极高。我整理出Java工程师必须绕开的三个雷区第一坑OpenAI兼容层的Token计数错误Spring AI默认用OpenAiTokenizer计算prompt token但它对中文支持极差——“北京烤鸭”算3个token实际OpenAI API返回12个。导致maxTokens设置失准Agent中途被截断。解决方案弃用Spring AI的Tokenizer直接用LangChain4j的OpenAiTokenizerBean public OpenAiTokenizer openAiTokenizer() { return new OpenAiTokenizer(); // 注意不是spring-ai-openai的tokenizer } // 在AgentExecutor中手动注入 Bean public AgentExecutor agentExecutor(ChatModel chatModel, ListTool tools, OpenAiTokenizer tokenizer) { return LangChain4jAgent.builder() .chatModel(chatModel) .tools(tools) .tokenizer(tokenizer) // 关键强制指定 .build(); }第二坑Spring AI的StreamingChatClient内存泄漏压测时发现开启SSE后JVM堆外内存持续增长jmap -histo显示io.netty.buffer.PooledByteBuf实例暴增。根源是Netty的PooledByteBufAllocator未正确释放。临时方案降级到Spring AI 1.0.0-M3已验证稳定或改用LangChain4j的StreamingChatModel 自定义StreamingResponseHandlerpublic class CustomStreamingHandler implements StreamingResponseHandlerChatResponse { private final SseEmitter emitter; Override public void onNext(ChatResponse response) { try { // 只推送content过滤掉thought/action等调试信息 String content response.content(); if (StringUtils.isNotBlank(content)) { emitter.send(SseEmitter.event() .name(message) .data(content)); } } catch (IOException e) { emitter.completeWithError(e); } } }第三坑Alibaba Cloud DashScope的认证失效国内用通义千问的团队常配spring.ai.alibaba.dashscope.api-key但Spring AI 2.0的DashScopeChatModel会忽略此配置坚持读DASHSCOPE_API_KEY环境变量。解决方案在启动类加PostConstruct强制注入Component public class DashScopeConfig { Value(${spring.ai.alibaba.dashscope.api-key}) private String apiKey; PostConstruct public void init() { System.setProperty(DASHSCOPE_API_KEY, apiKey); // 强制覆盖 } }注意以上配置均需在application.yml中关闭Spring AI的自动配置spring: autoconfigure: exclude: org.springframework.ai.autoconfigure.chat.ChatAutoConfiguration否则你的手动Bean会被Spring AI的默认Bean覆盖这是最隐蔽的坑。3.2 工具开发用Java原生能力封装“可审计、可熔断”的Agent工具AI Agent的“智能”90%来自工具质量而非模型本身。我见过太多项目把Tool写成RestTemplate.getForObject(url)结果一次超时拖垮整个Agent。Java工程师的工具开发必须遵循三条铁律可审计、可熔断、可降级。以餐饮SaaS的“查菜品库存”工具为例传统写法// ❌ 危险无超时、无熔断、无日志 public class InventoryTool implements Tool { private final RestTemplate restTemplate; Override public String execute(String input) { return restTemplate.getForObject( http://inventory-service/api/stock?sku input, String.class); } }正确写法Spring Boot 3.2Component Tool(description 查询菜品库存数量输入为菜品SKU编码输出为JSON格式包含stock字段) public class InventoryTool { private final Resilience4JCircuitBreaker inventoryCircuitBreaker; private final MeterRegistry meterRegistry; public InventoryTool(Resilience4JCircuitBreaker inventoryCircuitBreaker, MeterRegistry meterRegistry) { this.inventoryCircuitBreaker inventoryCircuitBreaker; this.meterRegistry meterRegistry; } ToolMethod public String getStock(ToolParam(sku) String sku) { // 1. 记录调用指标 Counter.builder(agent.tool.inventory.call) .tag(sku, sku) .register(meterRegistry) .increment(); // 2. 熔断超时控制 return inventoryCircuitBreaker.run(() - { String url http://inventory-service/api/stock?sku sku; // 使用WebClient替代RestTemplate非阻塞 return WebClient.create() .get() .uri(url) .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(2)) // ⚠️ 关键强制2秒超时 .block(); // Agent是同步流程此处block合理 }, throwable - { // 3. 降级逻辑返回默认库存或缓存值 Counter.builder(agent.tool.inventory.fallback) .tag(sku, sku) .register(meterRegistry) .increment(); return {\stock\: 0, \reason\: \fallback due to circuit breaker\}; }); } }关键设计点ToolMethod注解Spring AI 2.0新增比implements Tool更轻量且支持ToolParam自动绑定参数Resilience4JCircuitBreaker用Resilience4J而非Hystrix已停更配置在application.ymlresilience4j.circuitbreaker: instances: inventory: failure-rate-threshold: 50 wait-duration-in-open-state: 60s sliding-window-size: 10指标埋点Counter记录调用量和降级次数方便在Grafana看“Agent是否在疯狂调库存服务”降级返回JSON确保格式统一避免Agent解析失败。实操心得工具开发必须写单元测试用Mockito模拟库存服务超时验证降级逻辑是否触发。我见过太多团队跳过这步上线后Agent因一个工具超时整条链路卡死30秒——而Java工程师的本能是“加超时”不是“等它挂”。3.3 ReAct执行器手写可控、可调试的Agent核心引擎Spring AI的SpringAiAgent虽方便但调试困难——你想看某次Reason的完整Prompt得翻日志grep想修改Loop逻辑得继承它再重写。我的方案是用LangChain4j手写AgentExecutor把每一步都暴露为可监控的Bean。核心类CustomReActAgentComponent public class CustomReActAgent { private final ChatModel chatModel; private final ListTool tools; private final Tokenizer tokenizer; private final RedisTemplateString, Object redisTemplate; public CustomReActAgent(ChatModel chatModel, ListTool tools, Tokenizer tokenizer, RedisTemplateString, Object redisTemplate) { this.chatModel chatModel; this.tools tools; this.tokenizer tokenizer; this.redisTemplate redisTemplate; } public AgentResponse execute(String query, String userId) { String taskId UUID.randomUUID().toString(); AgentState state new AgentState(taskId, userId, query); // 1. 初始化PromptSystem Few-shot String systemPrompt loadSystemPrompt(); // 从DB或配置中心加载 String fewShotExamples loadFewShotExamples(); // 加载示例{thought:..., action:InventoryTool, actionInput:SKU001} String currentPrompt systemPrompt \n fewShotExamples; int step 0; while (step 5) { // 最大5步防死循环 // 2. Reason调大模型生成Thought/Action String promptWithHistory buildPromptWithHistory(currentPrompt, state.getSteps()); Message message userMessage(promptWithHistory); AiMessage aiMessage chatModel.generate(message).content(); // 3. 解析模型输出关键用正则而非JSON防格式错乱 ReActOutput output parseReActOutput(aiMessage.text()); if (Final Answer.equals(output.getAction())) { // 成功返回 return AgentResponse.success(taskId, output.getActionInput()); } // 4. Act执行Tool String toolResult executeTool(output.getAction(), output.getActionInput()); // 5. Observe记录结果 Step stepRecord new Step(step, output.getThought(), output.getAction(), output.getActionInput(), toolResult); state.getSteps().add(stepRecord); // 6. 存Redis支持断点续跑 redisTemplate.opsForValue().set(agent:state: taskId, state, Duration.ofMinutes(30)); // 7. 更新Prompt进入下一轮 currentPrompt currentPrompt \n Thought: output.getThought() \n Action: output.getAction() \n Action Input: output.getActionInput() \n Observation: toolResult; } return AgentResponse.fail(taskId, Max steps exceeded); } // 正则解析ReAct输出比JSONParser更鲁棒 private ReActOutput parseReActOutput(String text) { // 匹配Thought: xxx\nAction: yyy\nAction Input: zzz Pattern pattern Pattern.compile(Thought: ([\\s\\S]*?)\\nAction: ([\\s\\S]*?)\\nAction Input: ([\\s\\S]*)); Matcher matcher pattern.matcher(text); if (matcher.find()) { return new ReActOutput(matcher.group(1), matcher.group(2), matcher.group(3)); } // fallback直接返回Final Answer return new ReActOutput(, Final Answer, text); } }这个手写引擎的价值在于可调试每一步currentPrompt、aiMessage、toolResult都打DEBUG日志排查时不用猜可监控step变量直接暴露给Prometheus看“平均每个Query走几步”可干预在executeTool里加业务规则比如“如果Action是PaymentTool且金额1000必须人工审核”这是Spring AI无法做到的可压测redisTemplate操作可mock单元测试覆盖率100%。注意parseReActOutput用正则而非Jackson是因为大模型输出格式不稳定——有时多空格有时少换行。我试过10种JSON Schema校验都不如一行正则可靠。这是血泪教训。3.4 并发与性能Java工程师的Agent扛压实战“AI Agent怎么扛并发”是Java面试高频题答案绝不是“加Redis”。真实压测数据如下阿里云ECS 8C16GQwen-1.5B本地部署方案QPS平均延迟99%延迟内存占用关键瓶颈单线程Agent12840ms1.2s1.8GCPU单核100%Async线程池100线程451.1s2.3s3.2GGC频繁Full GC 2min/次WebClient Project Reactor210380ms890ms2.1G网络IOWebClient 熔断Resilience4J185420ms950ms2.0G库存服务超时结论很清晰Agent的并发瓶颈不在Java代码而在大模型API调用和外部工具链路。优化必须分层第一层模型调用层放弃RestTemplate用WebClient非阻塞IO启用WebClient连接池Bean public WebClient webClient() { ConnectionProvider provider ConnectionProvider.builder(ai-pool) .maxConnections(500) // 连接数 .pendingAcquireTimeout(Duration.ofSeconds(30)) .build(); return WebClient.builder() .clientConnector(new ReactorClientHttpConnector( HttpClient.create(provider))) .build(); }第二层工具调用层所有Tool必须异步化。库存查询用WebClient支付调用用Async因支付网关是HTTPMQ混合对慢工具如PDF解析加TimeLimiterTimeLimiter(name pdf-tool) public MonoString parsePdf(String url) { ... }第三层Agent执行层禁用Async启动AgentAgent本身是串行流程Reason→Act→Observe→Loop并发应体现在请求入口Controller而非Agent内部Controller用ResponseBody直接返回MonoAgentResponse由WebFlux线程池调度JVM参数重点调优-Xms4g -Xmx4g -XX:UseG1GC -XX:MaxGCPauseMillis200 \ -XX:UseStringDeduplication -XX:G1HeapRegionSize2MG1GC对Agent这种短生命周期对象更友好。实测技巧压测时用wrk -t12 -c400 -d30s http://localhost:8080/api/agent/chat观察jstat -gc。如果G1-YGC次数过多说明Eden区太小需调-XX:G1NewSizePercent30如果G1-Full-GC出现立刻检查是否有new byte[1024*1024]这类大对象——Agent里最常见的大对象是ListStep我们把它序列化成JSON字符串存Redis而非存Java对象内存直降40%。4. 常见问题与避坑指南Java工程师专属排错手册4.1 提示词工程别让Java工程师背锅用代码管住LLM很多Java工程师抱怨“LLM不听话”其实是提示词没工程化。我的方案是把System Prompt变成可配置、可AB测试、可版本管理的Java Bean。Component RefreshScope // 支持Nacos动态刷新 public class AgentPromptManager { Value(${agent.prompt.version:1.0}) // 默认v1.0 private String version; Value(${agent.prompt.few-shot:true}) // 是否启用示例 private boolean enableFewShot; public String getSystemPrompt() { switch (version) { case 1.0: return loadV1Prompt(); case 2.0: return loadV2Prompt(); // v2.0增加“禁止虚构价格”约束 default: return loadDefaultPrompt(); } } private String loadV1Prompt() { return 你是一个餐饮SaaS智能助手严格按以下规则执行 1. 只回答与菜品、订单、库存相关的问题 2. 不知道答案时回复“暂无相关信息” 3. 输出必须是JSON格式{thought:..., action:..., actionInput:...} ; } }关键实践用RefreshScopeNacos配置中心改Prompt服务无需重启版本化管理v1.0允许模型自由发挥v2.0加硬约束如“价格必须来自库存服务禁止估算”AB测试效果Few-shot示例存DB表prompt_example存query、expected_output运行时动态拼接避免硬编码。排查技巧当Agent输出格式错乱如少个逗号导致JSON解析失败先查AgentPromptManager.getSystemPrompt()日志确认发送给模型的Prompt是否含非法字符如中文引号“”。我们曾因Nacos配置里用了全角空格导致模型输出全乱码——Java工程师的直觉是查代码其实该查配置。4.2 RAG增强Easy RAG不是“一键”而是三步精准注入“LangChain4j Easy RAG”常被误解为自动RAG。真相是RAG效果90%取决于检索质量而非向量化模型。Java工程师的RAG必须做三件事第一步检索前过滤Pre-filtering不要让向量库查全量菜单。在Retriever前加业务过滤public class MenuRetriever implements RetrieverDocument { private final VectorStore vectorStore; private final JdbcTemplate jdbcTemplate; // 查MySQL获取门店ID Override public ListDocument retrieve(String query) { // 1. 先查用户所在门店 String storeId jdbcTemplate.queryForObject( SELECT store_id FROM user_profile WHERE user_id ?, String.class, getCurrentUserId()); // 2. 向量检索时加metadata过滤 return vectorStore.similaritySearch(SearchRequest.builder() .query(query) .filter(Filter.builder() .add(store_id, storeId) // 只查本店菜单 .add(status, on_sale) // 只查在售 .build()) .build()); } }第二步检索后重排序Re-ranking向量相似度≠业务相关性。我们用Java规则重排public ListDocument rerank(ListDocument docs, String query) { return docs.stream() .map(doc - { // 规则1标题含query关键词权重0.3 double score doc.getScore(); if (doc.getMetadata().get(title).toString().contains(query)) { score 0.3; } // 规则2销量1000的菜品权重0.2 if (Integer.parseInt(doc.getMetadata().get(sales).toString()) 1000) { score 0.2; } return new Document(doc.getContent(), doc.getMetadata(), score); }) .sorted((d1, d2) - Double.compare(d2.getScore(), d1.getScore())) .limit(3) .collect(Collectors.toList()); }第三步上下文注入Context Injection别把全部检索结果塞给模型。按优先级拼接private String buildContext(ListDocument docs) { StringBuilder context new StringBuilder(); for (int i 0; i Math.min(docs.size(), 3); i) { Document doc docs.get(i); context.append(【第).append(i1).append(相关项】\n); context.append(菜品名).append(doc.getMetadata().get(title)).append(\n); context.append(价格).append(doc.getMetadata().get(price)).append(\n); context.append(描述).append(doc.getContent()).append(\n\n); } return context.toString(); }注意buildContext长度必须受控用tokenizer.estimateTokenCount(context)计算超长则截断末尾。我们曾因context超2000token导致模型忽略关键指令——这不是模型问题是Java工程师没做长度校验。4.3 面试高频题实战如何回答“AI Agent怎么扛并发”面试官问这个问题真正在考察三点是否理解Agent本质、是否有工程化思维、是否踩过坑。我的标准答案结构第一步定义问题“扛并发”不是指Agent单实例QPS而是“在高并发请求下保证响应时间稳定、不雪崩、可监控”。Agent的并发瓶颈在三处大模型API、外部工具服务、自身状态管理。第二步分层方案模型层用WebClient连接池500连接禁用RestTemplate工具层所有Tool加Resilience4J熔断超时设2秒降级返回JSON状态层用Redis存AgentStateKey带TTL避免内存泄漏。第三步数据佐证“我们压测时单机QPS从12提升到21099%延迟从2.3秒降到890毫秒。关键动作是把RestTemplate换成WebClient并给库存工具加熔断——否则一次库存服务抖动Agent全部卡死。”第四步留钩子“当然这只是基础。更高阶的方案是‘Agent中台’把Agent执行抽象成独立服务用K8s HPA自动扩缩容但这需要额外运维成本。作为Java工程师我优先保证单机稳定再谈弹性。”避坑提醒千万别说“加Redis缓存结果”。Agent的输出高度个性化不同用户查不同菜品缓存命中率极低反而增加Redis压力。面试官一听就知道你没实操过。4.4 生产事故复盘一次线上Agent卡死的完整排查链上周线上发生严重事故Agent接口大量超时监控显示agent:state:*的Redis Key暴增CPU飙升。排查过程堪称Java工程师的教科书Step 1看监控Grafana显示agent.tool.inventory.callCounter突增10倍但agent.tool.inventory.fallback几乎为0 → 库存服务没熔断而是持续超时SkyWalking链路追踪显示90%的Span卡在WebClient的HttpClientConnect阶段 → 网络层问题。Step 2查日志grep inventory-service logs/app.log | head -20发现大量Connection refused登服务器telnet inventory-service 8080失败 → 库存服务挂了。Step 3定位根因库存服务日志java.lang.OutOfMemoryError: Metaspace→ JVM Metaspace溢出原因库存服务升级了新SDK引入了大量动态代理类Metaspace默认256M不够。Step 4紧急修复立即给库存服务加JVM参数-XX:MetaspaceSize512m -XX:MaxMetaspaceSize1g重启库存服务Agent自动恢复因熔断器处于半开状态。Step 5长期改进在Agent的InventoryTool里加TimeLimiter超时直接熔断不等Connection refused给所有Tool加CircuitBreaker的ignoreExceptions把ConnectException加入忽略列表避免一次网络抖动就跳闸。教训AI Agent的稳定性70%取决于下游服务的质量。Java工程师不能只盯着自己的代码必须把Tool的健康检查做成标准流程——就像我们给数据库加心跳检测一样。5. 项目收尾从练手到落地的关键跃迁写完这个Agent别急着庆祝。我见过太多团队停在“能返回答案”的Demo阶段却跨不过“能进生产”的鸿沟。最后分享三个决定成败的跃迁点第一跃迁从“能跑”到“可测”单元测试必须覆盖CustomReActAgent.execute()用MockitomockChatModel和Tool验证输入“北京烤鸭”是否调用InventoryToolInventoryTool返回{stock:5}是否生成Final Answer: 还有5份InventoryTool抛异常是否走降级逻辑。集成测试用Testcontainers启动Redis、MockServer验证端到端流程。第二跃迁从“功能”到“可观测”所有AgentState存Redis时加traceId字段关联SkyWalkingAgentResponse返回时附带stepCount、totalLatency、modelTokenUsed前端可展示“思考了3步耗时420ms”定制Grafana看板Agent成功率、平均Step数、各Tool调用占比、Fallback率。第三跃迁从“单点”到“中台”把CustomReActAgent封装成Starterspring-boot-starter-agent其他业务线只需引入依赖、配application.yml