Spring AI实战:ReActAgent让模型从“会聊天”到“会办事”

📅 发布时间:2026/10/7 13:43:23
Spring AI实战:ReActAgent让模型从“会聊天”到“会办事”
降SpringAI这个系列写到第九篇正好到了一道坎。前面几篇把 ChatClient、提示词模板、结构化输出聊了个遍模型已经能说得很好了但很多朋友卡在同一个地方——模型能聊天却办不了事。你让它查一下数据库里的风险词、判断一条评论要不要拦下来、然后把结果通知给运营它就只会给你一段你可以这样操作的建议然后什么都没有发生。这一掌叫或跃在渊。乾卦九四爻龙在深渊里积蓄力量正准备往上跃。这个状态特别像现在的 Spring AI 应用开发你说它不能用吧Demo 全跑通了你说它能上生产吧距离真正替人干活还差着一层。ReActAgent 就是那个跃的动作——让模型从回答你变成替你做。这篇文章我会从 ReAct 的原理讲起把如何在国内这套环境阿里云 Maven、RDS、短信、SSL里把一个能用的 ReactAgent 搭起来、跑起来、踩坑排掉完整过一遍。适合已经会用 ChatClient 发消息但还没搞清楚工具调用Agent 循环怎么落地的同学。1. 为什么这一掌叫或跃在渊——ReactAgent到底解决什么问题1.1 从会聊天到会办事先说一个真实场景。我最近接了一个内容审核相关的需求用户每天提交大量评论运营希望先机器过滤一遍只把可疑的推给人工。最初用 ChatClient 直接调模型让模型给评论打标效果其实还行——模型能告诉你这条评论疑似夹杂广告。但问题来了。审核不是光判断就完了你得真的去查违禁词库查用户历史行为调内容安全接口最后把审核结果写进数据库。这些事模型一样都干不了。标准做法是自己在代码里写 if-else先调用敏感词查询再调安全 API再写库。代码不难但每加一条规则就要改一次代码而且当模型说我需要查一下数据的时候你的硬编码逻辑根本接不住这句话。Agent 的思路完全不同。它把查违禁词调安全接口记录日志这些能力打包成工具交给模型。模型自己决定先查什么、查到什么结论、下一步干什么。你不用写判断逻辑只需要把工具准备好然后告诉模型你是审核助手按这个流程处理。1.2 ReAct模式想一步做一步看一步ReAct 是 Reasoning Acting 的组合思想来自 2023 年那篇经典的 ReAct 论文。它把模型的一次任务执行拆成一个循环先思考Thought再行动Action然后观察结果Observation基于观察继续思考直到给出最终答案。这个循环听起来简单却是 Agent 能力的基石。普通模型调用是你问一句它答一句ReAct 模型可以为了回答你的问题主动去调用一个工具看看返回什么再决定下一步。Spring AI 在 1.0 版本里把这套循环封装成了现成的 ReActAgent你不用自己维护模型返回了工具调用怎么办执行完工具结果怎么塞回上下文这些细节。对比一下硬编码和 Agent 的差别。硬编码是用户输入 - 先查敏感词 - 如果有词 - 标记违规 - 写库 - 返回。下次规则变了改代码。ReActAgent 是用户输入 - 模型自己决定调敏感词工具 - 看到命中结果 - 决定调安全接口再查一次 - 两个结果综合判断 - 调用写库工具 - 返回结论。代码量上硬编码确实少但灵活性不可同日而语。尤其当工具数量增加到十几个规则互相交叉时Agent 的推理价值就体现出来了。1.3 为什么说这是跃还不是飞这里必须泼一盆冷水。ReActAgent 不是万能的第九掌叫或跃在渊而不是飞龙在天是有原因的。我实测下来ReActAgent 的效果非常依赖底层模型的推理能力。模型强它能自己规划合理的工具调用顺序模型弱它会在两个工具之间反复横跳甚至把工具描述里的文字当成结果返回给你。另外工具本身的质量也直接决定 Agent 的上限一个返回垃圾数据的工具再怎么推理也得不到好结论。但不可否认这是从接口调用到自主任务最关键的一步。跨过这一步后面才有多 Agent 协作、任务编排这些更复杂的东西。2. 搭建ReactAgent前的环境准备阿里云Maven仓库与依赖版本2.1 先解决依赖拉取Maven阿里云镜像Spring AI 的模块非常碎版本发布也快依赖拉取是个实打实的痛点。默认 Maven 中央仓库在国内的下载速度时好时坏尤其上午和晚上高峰时段一个 spring-ai-bom 都能卡半天。我的做法是在 Maven 的 settings.xml 里配置阿里云镜像。打开~/.m2/settings.xml没有就新建加入mirrors mirror idaliyunmaven/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrorsmirrorOf设为central意思是对中央仓库的所有请求都走阿里云。public 这个仓库聚合了 central 和 jcenter 的内容日常开发足够了。一个容易踩的坑阿里云的仓库同步不是实时的中央仓库发布了新版本镜像可能要滞后几个小时甚至一天。如果你配了阿里云镜像后发现某个 Spring AI 新版本拉不下来可以先临时把mirrorOf改成*,!central或者直接注释掉镜像强制走一次中央仓库把依赖下到本地之后再把镜像开回来。2.2 Spring AI的BOM与关键依赖Spring AI 1.0 之后的版本变化挺大0.8.x 时代的包名和 API 在 1.0 里基本都重构了。我建议新项目直接上 1.0别在老版本上折腾。项目里加 BOM 管理dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后按需引入模块。我用到的核心是这几个dependencies !-- 模型接入的 starter这里以 OpenAI 协议为例 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency !-- ReActAgent 本体 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-agent-executor/artifactId /dependency !-- 工具调用的基础模块 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-tool-calling/artifactId /dependency /dependencies需要说明一下spring-ai-agent-executor这个模块在不同版本里的包结构有过调整。如果你用的不是 1.0.0建议去官方文档确认一下 ReActAgent 的完整类名。我这里贴的是 1.0 版本的组织方式。2.3 环境自检先让ChatClient跑通Agent 是基于聊天模型构建的环境配置有问题后面什么都跑不起来。先确认application.yml里的模型配置spring: ai: openai: base-url: https://your-endpoint.example.com api-key: ${AI_API_KEY} chat: options: model: your-model-name temperature: 0如果你用的是国内云厂商提供的 OpenAI 兼容接口base-url 指到对应的网关地址就行。temperature 在 Agent 场景我通常设成 0审核类任务要的是确定性不需要模型发挥创造力。然后写一个最简单的 ChatClient 调用验证链路SpringBootTest class SanityCheckTest { Autowired private ChatClient.Builder builder; Test void chatClientShouldWork() { ChatClient client builder.build(); String reply client.prompt(只回复两个字正常) .call() .content(); System.out.println(reply); } }能打印出内容说明模型接口、API Key、网络链路都没问题。这个自检动作别省它能帮你把 Agent 的问题和基础设施的问题隔离开。3. 从零搭一个能用的ReactAgent核心代码拆解3.1 让Agent有手有脚Tool方法注册ReActAgent 能行动的基础是工具。Spring AI 里注册工具很简单写一个 Spring Bean方法上标Tool注解即可。我这里以内容审核助手为例子定义三个工具Component public class ReviewTools { private final SensitiveWordRepository sensitiveWordRepository; private final ContentSecurityClient contentSecurityClient; private final ReviewLogRepository reviewLogRepository; public ReviewTools(SensitiveWordRepository sensitiveWordRepository, ContentSecurityClient contentSecurityClient, ReviewLogRepository reviewLogRepository) { this.sensitiveWordRepository sensitiveWordRepository; this.contentSecurityClient contentSecurityClient; this.reviewLogRepository reviewLogRepository; } Tool(description 查询输入文本命中了哪些敏感词返回命中词列表及风险等级) public String querySensitiveWords(String text) { ListSensitiveWord hits sensitiveWordRepository.findHits(text); if (hits.isEmpty()) { return 未命中任何敏感词; } return hits.stream() .map(w - w.getWord() : w.getRiskLevel()) .collect(Collectors.joining(, )); } Tool(description 调用内容安全服务对文本进行综合风险评分返回分数和建议动作) public String callContentSecurity(String text) { SecurityResult result contentSecurityClient.check(text); return String.format(风险分数%d, 建议动作%s, result.getScore(), result.getAction()); } Tool(description 记录一条审核日志接收审核结论、原始文本和命中的敏感词) public void saveReviewLog(String conclusion, String originalText, String hitWords) { reviewLogRepository.save(new ReviewLog(conclusion, originalText, hitWords)); } }这里有个很重要的经验Tool的description一定要写清楚。模型不是靠方法名理解工具的它靠的是 description。你把 description 写得太笼统比如查询敏感词模型可能不知道什么时候该调用它写得太绕模型又可能把 description 本身当作输出内容。最好的写法是包含输入什么、返回什么、什么时候用。3.2 系统提示词怎么配置Agent的行为边界系统提示词对 Agent 的重要性比普通聊天高出好几个量级。聊天场景里提示词跑偏了最多回答得不好Agent 场景里提示词不清晰模型会乱调工具甚至把敏感词库的内容直接回给用户。我把审核助手的系统提示词写成这样String systemPrompt 你是内容审核助手。你的任务是根据用户提交的文本调用可用工具输出审核结论。 工作流程 1. 调用 querySensitiveWords检查文本是否命中敏感词。 2. 如果命中了中高风险敏感词再调用 callContentSecurity做二次确认。 3. 调用 saveReviewLog把审核结论写入日志。 审核结论只能取三个值 - pass未命中敏感词或命中低风险词且数量少于2个。 - review命中中风险词或高风险词但数量为1。 - reject命中高风险词且数量大于等于2。 约束 - 必须先调用 querySensitiveWords再决定后续动作。 - 如果 querySensitiveWords 返回未命中任何敏感词不要调用 callContentSecurity直接判定 pass 并写日志。 - 不得在回复中展示敏感词本身只能返回审核结论和风险等级。 - 如果无法给出结论输出 review不要自作主张。 ;注意这里我把什么情况走哪条分支写死了。有人会觉得这不够智能但审核场景需要的就是可控性和可追溯性。你要的是模型在给定分支里做判断不是让它自由发挥。提示词的挂载方式也很关键。如果每个请求都要动态拼接文本用ChatClient的defaultSystem或者.system()都行。区别在于.system()只对当前这次 prompt 生效defaultSystem是构建 ChatClient 时统一设置的默认值。Agent 场景我建议把固定规则放defaultSystem把每次请求的待审核文本放userText。3.3 组装ReActAgent与调用工具和提示词准备好后组装 Agent 的代码很简洁Service public class ReviewAgentService { private final ReActAgent agent; public ReviewAgentService(ChatClient.Builder builder, ReviewTools reviewTools) { ChatClient chatClient builder.defaultSystem(systemPrompt).build(); this.agent ReActAgent.builder(chatClient) .name(review-agent) .description(内容审核助手) .tools(reviewTools) .build(); } public String review(String text) { return agent.call(text); } }ReActAgent 的 builder API 在不同小版本里略有差异但核心参数就这三样ChatClient、工具集合、Agent 的描述。description是为了以后多个 Agent 协作时互相识别用的单 Agent 场景不写也行但建议养成习惯。调用的时候Agent 内部会自己走 Thought - Action - Observation 的循环直到它认为可以给出最终结论。如果你开启了 Spring AI 的日志输出会看到类似这样的中间过程Thought: 用户提交了文本我需要先检查敏感词。 Action: querySensitiveWords(text) Observation: 命中词xxx, 风险等级high Thought: 命中了高风险词需要调用内容安全服务二次确认。 Action: callContentSecurity(text) Observation: 风险分数92, 建议动作reject Thought: 风险分数很高审核结论为 reject。 Action: saveReviewLog(reject, text, xxx) Observation: 日志已保存 Final: 审核结论reject风险等级high看到这个循环日志你就知道这个 Agent 是真在干活而不只是回了一段建议。我一般会把这段中间日志打到应用日志里方便排查问题。4. 实测中的性能与稳定性坑JSON解析、并发与会话4.1 工具返回两万行JSON问题不在解析在Token有一个热搜词让我印象很深阿里 json.parsearray转换对象有两万行扛得住吗。我在做审核 Agent 时还真遇到过类似的问题但结论和大家想的不太一样。当时有个工具要查询某个用户的历史评论记录为了省事我直接让工具方法返回一个ListComment转成的 JSON 字符串。结果一条 SQL 查出两万条记录转出来的 JSON 有几 MB传给模型之后整个请求直接超时。先说解析本身。用 fastjson2 或者 Jackson 解析两万行 JSON性能完全不是问题毫秒级就完成了别被网上说的大数据量 JSON 卡死吓到。真正的瓶颈在 Token——模型上下文窗口就那么大几 MB 的 JSON 塞进去既爆上下文又让模型抓不住重点还按 Token 计费烧钱。正确的做法是在工具方法内部做聚合给模型瘦身之后的结果。比如不再返回两万条记录而是返回统计摘要Tool(description 查询用户历史评论的违规统计返回总评论数、违规数和主要违规类型) public String queryUserViolationSummary(Long userId) { ListComment comments commentRepository.findByUserId(userId); long total comments.size(); long violationCount comments.stream() .filter(c - c.getFlag() 1) .count(); MapString, Long typeCount comments.stream() .filter(c - c.getFlag() 1) .collect(Collectors.groupingBy(Comment::getViolationType, Collectors.counting())); return String.format( 总评论数%d, 违规数%d, 违规类型统计%s, total, violationCount, typeCount ); }模型的推理不需要原始明细它只需要能支持决策的关键数字。这条经验在 Agent 工具设计里非常通用工具返回的数据越精炼Agent 的推理越准成本越低。另外提醒一句如果坚持用 fastjson2 做大 JSON 解析注意它默认的autoType是关闭的别为了省事全局打开。曾经出过不少因为 autoType 开启导致的漏洞安全上没必要赌这个。4.2 会话记忆与多轮审核ReActAgent 在默认情况下是无状态的。每次agent.call(text)都是一次独立的循环模型不记得上一次审核过什么。多轮审核场景下比如运营先让 Agent 审一段文本接着追问刚才那条的敏感词是什么类型的如果没有记忆Agent 会一头雾水。解决办法是给 ChatClient 挂上消息窗口顾问Advisor让最近的对话内容保留在上下文里ChatClient chatClient builder.defaultSystem(systemPrompt) .defaultAdvisors(new MessageWindowAdvisor(10)) .build();MessageWindowAdvisor(10)表示保留最近 10 条消息。这里有一个取舍窗口越大模型对上下文的感知越强但 Token 消耗也越大。审核这种偏向单次判断的场景窗口设 5 到 10 就够用场景更复杂的考虑把历史记录存到数据库按需取。还要注意会话隔离。线上系统通常用chatId区分用户Spring AI 的做法是把 chatId 绑定到请求维度。如果你不做这个区分所有用户共享同一段窗口记忆A 用户的信息可能被 B 用户的下一次请求看到这是实打实的生产事故隐患。4.3 并发与资源控制Agent 的循环天然比单次模型调用更容易出问题。一个循环里可能调好几次模型接口每次模型接口 2 秒再加上工具执行时间一次 Agent 请求就可能十秒钟QPS 稍微一高线程池就满了。控制并发我会重点盯四个地方最大迭代次数。ReActAgent 的 builder 一般都有 maxIterations 之类的参数给个 5 到 8 的上限防止模型陷入工具调用死循环白白烧钱。超时设置。包括模型接口连接超时和读取超时Spring AI 里可以配置connect-timeout和read-timeout。宁可超时失败返回请稍后重试也不要让用户一直转圈。工具内部的数据库连接池。审核工具要经常查库连接池不够时并发一高工具本身先拖垮。异步执行。如果你的项目是 WebFlux 风格建议用ReActAgent的响应式方法Web MVC 项目用同步方法就好不要混用导致线程阻塞问题。我曾遇到过一次线上问题Agent 在高峰期因工具方法里用了同步 JDBC 查询连接池耗尽导致后续所有请求都卡在获取连接上。后来给工具方法加了独立的连接池配置并把超时缩短到 3 秒才稳定下来。5. 落地部署时的阿里云相关配置从RDS到短信API5.1 Agent持久化与RDS连接审核 Agent 跑起来之后第一件事就是把审核日志存下来。我选择了阿里云 RDS原因没有多复杂——团队本来就在阿里云上RDS 的监控、备份、高可用都是现成的。Agent 连接 RDS 时几个容易踩的点逐个说。首先是白名单。RDS 默认只允许白名单内的 IP 访问。如果你在本地调试连不上先别怀疑代码去控制台看看当前出口 IP 有没有加进去。生产环境用 VPC 内网地址连接别图省事用公网地址公网连接既有安全风险也容易因为延迟导致连接超时。其次是连接参数。JDBC 连接串里建议明确指定时区和 SSL 参数spring: datasource: url: jdbc:mysql://your-instance.mysql.rds.aliyuncs.com:3306/review_db?useSSLtrueserverTimezoneAsia/ShanghaiserverTimezoneAsia/Shanghai必须加否则你写入的审核时间和数据库实际时间可能差 8 个小时。SSL 在数据敏感的场景下建议开启代价是稍微多一点握手耗时但对审核系统来说值得。最后是连接池。推荐 HikariCPSpring Boot 默认就是它。给个参考配置spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 3000connection-timeout设为 3 秒宁可快速失败也不要无限等。Agent 调用链本身已经够长了数据库这一环不能成为新的瓶颈。5.2 短信通知为什么发不出去审核 Agent 判定一条评论违规之后产品希望自动给用户发一条短信通知。这是很自然的延伸需求但短信接入的坑比想象中多。我遇到过的短信发不出去基本集中在四类原因。第一签名或模板还在审核中或者被驳回。调用阿里云短信 API 时返回的错误码如果带isv.SMS_SIGNATURE_ILLEGAL或isv.SMS_TEMPLATE_ILLEGAL基本就是签名和模板的问题。这类问题不是代码能解决的去控制台看审核状态被驳回就按要求修改。第二模板变量格式不对。比如模板里定义了变量${content}你传的参数里 content 是空字符串或者传了一个超过长度限制的文本都会失败。还有一点容易被忽略变量内容里如果包含网址或特殊字符需要提前处理否则可能被判定为违规内容拒发。第三AccessKey 权限不足。短信服务要求的权限是AliyunDysmsFullAccess如果你用的子账号只配了 ECS 或 RDS 的权限调用短信接口就会报权限错误。建子账号时要么精确授权要么干脆用一个专用账号跑短信服务。第四限流。同一号码同一内容在一分钟内只能发一条一天内有条数限制。Agent 如果在一个循环里因为判断失误重复给同一个人发短信就会触发限流。我的建议是把短信发送包装成带幂等校验的工具方法对用户ID时间窗口做去重从源头避免这个问题。排查顺序也有讲究。先看返回错误码再用同样的参数在控制台里手动发一条如果控制台能发出去说明代码里参数传递有问题如果控制台也发不出去那就是签名、模板或账号权限的问题。这个方法能帮你快速把责任范围缩小一半。5.3 SSL证书与部署的长期维护Agent 服务对外提供接口域名要配 HTTPS。阿里云的免费 SSL 证书申请很方便但有个变化要注意免费证书的有效期已经缩短到 90 天左右也就是说一年至少要续四次证书。这个每季度换证书的操作如果还是手动搞迟早会漏。我见过几次凌晨线上告警就是因为证书过期没人管。建议把证书申请、验证、下载、部署这条链路脚本化。阿里云开放了证书 API你可以在新证书签发后通过脚本自动下载并更新到 Nginx再执行 reload。Nginx 里的配置大概长这样server { listen 443 ssl; server_name your-domain.example.com; ssl_certificate /etc/nginx/ssl/your-domain.pem; ssl_certificate_key /etc/nginx/ssl/your-domain.key; }证书更新完重启 Nginx 后记得用openssl s_client -connect your-domain.example.com:443验证一下证书到期时间别等浏览器提示不安全才发现问题。这步检查十几秒能省掉一次线上事故的尴尬。6. 从或跃在渊到飞龙在天下一步还能做什么这一掌的内容到这里差不多了但我知道很多人会问ReactAgent 搭起来了然后呢从我实际做审核 Agent 的体会来说ReActAgent 解决了让模型调用工具完成任务的问题但离稳定可靠的生产级 Agent还差几步这几步才是真正区分Demo和产品的地方。第一模型选型别抠门。ReActAgent 的推理质量直接取决于模型。能力弱的模型在多工具场景下会频繁出现该调工具时不调不该调时乱调这种事你在日志里能看得很清楚。复杂业务流程我建议直接用当前梯队里推理能力靠前的模型Token 贵一点但省下的开发排查时间远超这点成本。第二把 Thought/Action 日志当成一等公民。Spring AI 的 Agent 中间步骤日志很有价值我把它们落到了单独的日志表里。遇到审核结果争议时复盘这些日志能精确还原模型当时的推理链条。没有这些记录出了问题只能黑盒猜。第三保留人工兜底。ReActAgent 即使跑得再顺本质上还是一个概率系统。审核场景里凡是 Agent 输出review的我都强制转人工复核不做全自动放行。这既是业务要求也是给系统留一条可靠的退路。最后回到开头的或跃在渊。龙在渊里跃起这一步能不能跃上去取决于前面积蓄的力量够不够。ReActAgent 本身不复杂复杂的是它周围的工程配套——清晰的任务边界、可靠的工具方法、精准的提示词、完备的监控日志。把这些做好了你的 Agent 离飞龙在天就不远了。