ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AI ReactAgent实战:打造商品发布智能审核系统

Spring AI ReactAgent实战:打造商品发布智能审核系统 如果要给“降SpringAI”系列选一个最能体现Agent魅力的章节我会毫不犹豫地选“或跃在渊”——对应到技术实现就是Spring AI下的ReactAgent模式。前八掌我们讲了提示词、结构化输出、RAG、工具调用但真正让系统“主动思考”的是从这一掌开始的模型不再是一问一答的接口而是一个能自己决定“我要先查什么、再调什么工具、拿到结果后下一步干什么”的智能体。我用Spring AI Alibaba搭过一套商品发布文本的智能审核系统核心就是ReactAgent。这篇文章把整个落地过程拆开讲包含系统提示词怎么配置、工具怎么设计、循环调用里的坑怎么填适合已经在用SpringAI做业务、想把固定流程升级成自主决策Agent的开发者。1. ReactAgent这一掌为什么值得从工具调用里单拆出来先亮个观点Spring AI里的ReactAgent并不是一个神秘的新框架它就是“ChatClient 工具注解 提示词 多轮推理循环”的组合。但组合方式不同效果天差地别。这一节我们先把它跟普通的工具调用做对比顺便说说“或跃在渊”到底对应在哪儿。1.1 “或跃在渊”和ReAct的底层逻辑降龙十八掌里“或跃在渊”是龙从深渊中跃起、蓄势待发的一式。ReAct这个词是Reason和Act拼出来的先推理Reason再行动Act观察结果后继续推理。这和一次性工具调用最大的区别在于Agent可以“走一步看一步”。拿审核场景举例。普通工具调用流程是用户输入文本 - 模型判断要不要调工具 - 调用一次工具 - 根据返回结果给出答案。如果这个判断需要“先检查联系方式再根据联系方式结果决定要不要检查竞品词”普通模式下模型可能只会调用一次工具就草率收尾。ReactAgent则不同每次工具返回后模型会把观察结果放回对话上下文重新推理下一步。这个“返回上下文继续推理”的动作就是龙跃起之后还要回到渊里蓄力——直到模型认为自己已经拿够了证据才跳出循环给出最终结论。我在Spring AI Alibaba里真正跑通这个逻辑时有种豁然开朗的感觉原来之前觉得工具调用“不够聪明”不是模型问题而是我只给了它“一次出手”的机会。1.2 ChatClient直接调工具和Agent循环的差别用一张表格说清楚差别这也是我经常跟团队解释的版本维度一次性工具调用ReactAgent循环工具调用次数通常一次可能多轮直到模型认为信息足够推理依据只有用户输入和系统提示词每轮工具返回都会作为新观察加入上下文决策能力弱适合单步判断强适合多步校验、分支判断失败恢复工具异常则整体失败模型可根据错误信息换策略或换工具调试难度低中等需要看工具调用链路适用场景分类、抽取、翻译审核、规划、检索后综合分析在Spring AI的实现里当你给ChatClient传入带Tool的方法时模型返回一个toolCall请求框架自动把工具执行结果作为tool消息追加回去再次请求模型。这个循环对业务代码是透明的你只看到一次chatClient.prompt().call()实际底层可能已经来回走了三四轮。我第一次通过日志看到这种自动循环时就知道Agent这个方向是对的。1.3 Spring AI Alibaba对ReactAgent的定位说回“阿里”这部分。我用的Spring AI Alibaba是阿里开源的Spring AI适配项目它没有把ReactAgent包成一个黑盒而是遵循Spring AI标准API底层接的是DashScope、通义系列模型上层暴露的依然是ChatClient、ChatModel、Tool这些标准化接口。所以你在Spring AI原版上学到的工具调用、提示词技巧在Spring AI Alibaba里完全通用切换模型供应商的成本也被压得很低。我选择Spring AI Alibaba的另一个理由是它对国内模型生态的适配比较顺公司内部不少模型服务走的是兼容OpenAI协议的网关用这个starter接起来非常省事。项目里只需要保证一个原则业务代码不直接依赖具体模型SDK全走Spring AI抽象层这样后续换模型、加模型都是配置级改动。1.4 适合用ReactAgent的场景以及别硬上的场景不是所有业务都需要ReactAgent。我自己踩过这个坑有一阵子把关键词过滤也交给Agent做结果模型偶尔会“理解”过滤规则漏掉纯规则就能命中的词性能还差。后来我把业务拆成两层能用正则、词表快速确定的绝不上Agent需要语义判断、需要跨多个证据综合决策的才交给ReactAgent。适合的场景有几个共性判断步骤多、依赖外部数据源词库、数据库、规则引擎、结果需要附带证据链、失败时希望模型能自我修正。典型例子就是智能审核一条文本可能同时涉及联系方式、广告违禁词、竞品提及、夸大宣传单个工具无法覆盖必须要Agent按顺序取证、综合打分。这正是“或跃在渊”的含义——宁可多回几次深渊也要把证据拿全再升空。2. 最小可运行骨架先把工具调通再谈Agent这一节直接给一个能跑起来的骨架。你别一上来就上完整审核案例先把“模型调工具-工具返回-模型继续推理”这个闭环打通后面加业务逻辑会轻松很多。2.1 依赖怎么加我用的是Spring Boot 3.x项目Maven里加上Spring AI Alibaba的starter。版本号我不写死因为官方release更新比较快建议直接用当前最新稳定版dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version使用官方最新release版本/version /dependency如果你是纯Spring AI项目不接阿里云系列模型也可以只用spring-ai-starter-model配合兼容OpenAI协议的本地模型网关。核心逻辑完全一样只是配置项改一下。2.2 配置文件示例在application.yml里做最小配置。注意Spring AI Alibaba的api-key我习惯用环境变量注入避免把密钥提交到仓库spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.1temperature我调得很低审核场景不需要模型发挥想象力低温度能让工具选择和结论稳定不少。等你要做创意内容时再调高也不迟。2.3 第一个带工具调用的Agent代码我写了一个最简版本定义两个工具一个返回当前时间一个做文本长度统计。然后让模型自己决定“先调哪个、调几次”。Service public class MinimalReactAgent { private final ChatClient chatClient; public MinimalReactAgent(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个会使用工具的助手。请根据问题自行决定调用哪个工具并把最终答案用自然语言输出。) .build(); } public String run(String userInput) { return chatClient.prompt() .user(userInput) .tools(new DemoUtils()) .call() .content(); } }Component public class DemoUtils { Tool(name getCurrentTime, description 获取当前系统时间返回格式为yyyy-MM-dd HH:mm:ss) public String getCurrentTime() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); } Tool(name countTextLength, description 统计输入文本的字符长度返回整数) public int countTextLength(String text) { return text.length(); } }这段代码背后发生了什么我强烈建议你打开日志观察一次完整链路它大概长这样模型收到用户问题 - 返回一个toolCall声明要调getCurrentTime- 框架执行工具 - 把结果作为新消息追加 - 模型看到时间后继续推理可能再次调用countTextLength- 拿到所有数据后模型组织最终答案。我第一次在自己项目里看到这条链路时特意打印了每一条message的角色和内容那种“模型真的在思考”的实感是普通接口调用给不了的。2.4 为什么这个骨架能循环起来很多人第一次看这段代码会有疑惑代码里只调用了一次call()多轮循环是谁在驱动答案是Spring AI的ChatModel内部实现了tool calling的循环处理。每次模型返回工具调用请求框架会自动执行对应方法再把结果封装成tool消息发回模型。循环结束的条件是模型不再要求调用工具返回普通文本内容。知道这点很重要。因为一旦工具方法太多、描述太笼统模型可能会陷入“反复调同一个工具”的死循环。我在后面专门有一节讲这个问题这里先记住一个结论循环虽然由框架驱动但“循环的边界”需要你在提示词和工具设计上主动控制。3. 智能审核案例让Agent同时碰三个审核工具骨架跑通后我直接上真实案例。这个案例我从一个商品发布审核项目中提炼出来核心任务是用户输入一段商品描述文本Agent自动检查联系方式、广告违禁词、竞品名称最后给出是否允许发布、风险等级、证据列表。3.1 审核场景和工具定义审核不是把全部文本丢给模型读一遍就完事而是要让模型借助确定性工具取证。我设计了三个工具工具名作用返回内容contactInfoExtract从文本中提取手机号、微信号、QQ、邮箱等联系方式命中类型和原文片段列表adWordCheck检查广告法违禁词、绝对化用语、夸大宣传词命中词列表competitorMentionCheck检查是否提到竞品品牌或对竞品的贬低表述竞品词列表和原文片段工具实现不复杂核心是正则和本地词表。这里我给出一个工具类的骨架Component public class AuditTools { Tool(name contactInfoExtract, description 从商品描述文本中提取联系方式。支持手机号、座机、微信号、QQ号、邮箱。返回ContactItem列表没有命中时返回空列表。) public ListContactItem contactInfoExtract(String text) { ListContactItem hits new ArrayList(); // 手机号 Pattern mobile Pattern.compile(1[3-9]\\d{9}); Matcher m mobile.matcher(text); int count 0; while (m.find() count 5) { hits.add(new ContactItem(MOBILE, m.group())); count; } // 微信常见关键词后面的连续英文数字 // 竞品词表同理 return hits; } Tool(name adWordCheck, description 检查文本中是否出现广告违禁词如绝对化用语、夸大宣传词。返回命中词列表没有命中时返回空列表。) public ListString adWordCheck(String text) { ListString adWords List.of(国家级, 世界级, 最高级, 最佳, 第一, 全网最低, 史无前例); return adWords.stream() .filter(text::contains) .collect(Collectors.toList()); } Tool(name competitorMentionCheck, description 检查文本中是否提到指定竞品品牌或对竞品的贬低表述。返回竞品词列表。) public ListString competitorMentionCheck(String text) { ListString competitors List.of(某竞品A, 某竞品B, 某竞品C); return competitors.stream() .filter(text::contains) .collect(Collectors.toList()); } }注意每个Tool的description都要写清楚“什么时候用、返回什么、没命中时返回什么”。模型选择工具的依据就是这段描述描述含糊模型就会乱选工具。这一点我在下一节还会展开。3.2 系统提示词这样配Agent才知道先干什么后干什么系统提示词是ReactAgent的“行动纲领”。我给审核Agent配的系统提示词经过了好几轮迭代结构基本固定成四段角色定义、工具使用顺序、证据要求、输出格式。你是商品发布内容审核助手。你的任务是对用户输入的商品描述文本进行合规审核。 工具使用顺序 1. 先调用 contactInfoExtract 检查联系方式 2. 再调用 adWordCheck 检查广告违禁词 3. 再调用 competitorMentionCheck 检查竞品提及 4. 最后基于所有工具结果综合判断。 要求 - 每一步都必须真实调用工具不能凭记忆猜测。 - 工具返回为空时在证据中注明“未命中”不要把未命中写成命中。 - 最终结论必须引用原文片段作为证据。 输出格式为JSON { passed: true或false, level: LOW/MEDIUM/HIGH, reason: 结论理由, evidence: [工具类型: 原文片段] }这里最关键的其实是“工具使用顺序”这一段。我遇到过模型跳过第二个工具、只根据第一个工具就下结论的情况把顺序写进提示词后正确率明显提升。原因也不难理解模型在没有明确流程约束时倾向于“差不多就回答”有了显式顺序它就知道每一步都要交作业。3.3 完整调用代码和结果解析工具类和提示词都准备好后Agent主体代码非常简单Service public class AuditAgent { private final ChatClient chatClient; private final AuditTools auditTools; public AuditAgent(ChatClient.Builder builder, AuditTools auditTools) { this.auditTools auditTools; this.chatClient builder .defaultSystem(PromptTemplateLoader.load(audit-system-prompt.txt)) .build(); } public AuditResult audit(String productText) { String content chatClient.prompt() .user(productText) .tools(auditTools) .call() .content(); return JsonParser.fromJson(content, AuditResult.class); } }PromptTemplateLoader是我写的一个工具类用来从resources/prompts/目录加载系统提示词模板。这样提示词可以独立于代码维护业务人员也能直接改文本。我用一个典型输入测试一下正品包包全网最低价加微信abc123详聊比某竞品B质量好太多。Agent的输出结果大概是{ passed: false, level: HIGH, reason: 文本包含联系方式、广告违禁词、竞品贬低表述属于高风险发布内容, evidence: [ contactInfoExtract: 微信 abc123, adWordCheck: 全网最低, competitorMentionCheck: 某竞品B ] }我实际跑出来的结果跟上面基本一致而且Agent是真的依次调用了三个工具不是一口气编出来的结论。这一步让我确信ReactAgent在审核场景里不是玩具是真能接生产任务的。3.4 为什么不让模型直接判断而要强制走工具有人可能会问既然模型已经知道“全网最低”是违禁词直接让它判断不就行了为什么还要绕一圈调工具我的回答是确定性业务不能交给概率。模型的知识有边界它可能不知道你们公司最新定义的竞品名单也可能把“不是第一”这种否定表述误判成违禁词。工具在这里的作用是把“业务规则”变成“确定性的、可更新的数据源”模型只负责“调度工具和综合推理”不负责“背诵规则”。这也是我把工具放在Agent里的核心原因规则变了只改工具和词表不用改提示词。审核结果的一致性、可审计性都上了一个台阶。4. 系统提示词这样配Agent才不会乱飞“SpringAI系统提示词怎么配置”是我这段时间被问最多的问题。这里我把在审核Agent上验证过的配置方法完整讲一遍并且说清楚每一段为什么存在。4.1 一份可复用的系统提示词模板我拆解成六段每段解决一个具体问题提示词段落核心内容解决的问题角色定义你是谁、负责什么任务让模型切换专业状态避免通用聊天口吻工具清单与调用顺序有哪些工具、先调哪个后调哪个避免工具选择混乱、跳过关键步骤证据要求结论必须引用原文片段防止模型编造证据、凭记忆回答负向约束禁止做的事防止模型在工具未命中时强行输出命中输出格式JSON结构和每个字段的说明保证下游能稳定解析结果边界兜底异常情况怎么处理防止工具出错时模型直接崩溃4.2 工具描述怎么写模型才不乱选工具描述里最容易犯的错是写得太空。比如检查联系方式和检查广告词模型看不出这两个工具的具体边界就可能把“微信号”既当成联系方式又当成广告词。我现在的写法是触发条件、支持的输入类型、返回结构、未命中行为四要素齐全。Tool(name adWordCheck, description 检查文本中是否出现广告法违禁词。只负责违禁词不负责联系方式或竞品。返回ListString格式的命中词列表没有命中时返回空列表。)加一句“不负责联系方式或竞品”听起来多余实际上是在帮模型做工具选择的排除法。模型对工具边界的理解越清晰工具调用准确率越高。4.3 输出格式约束的常见坑我在配置输出格式时栽过一个跟头只写了“输出JSON”没有给字段说明结果模型偶尔会多返回一个details字段或者把passed写成字符串false。后来我在提示词里加了这样的说明输出字段说明 - passedboolean类型只有true和false两种值 - level只能是LOW、MEDIUM、HIGH三个值之一 - reason不超过50个字说清楚判断依据 - evidencestring数组每一项格式为“工具类型: 原文片段”。这招很有效。模型也是“给点阳光就灿烂”的你不把边界画死它就会自由发挥。4.4 让模型先复述任务再调工具另一个提升稳定性的小技巧在系统提示词里要求模型在开始工具调用前先用一句话复述用户输入的审核要点。不要小看这一步它等于强制模型先“进入状态”减少对输入文本的漏看。我见过不少漏检案例原因就是模型拿到长文本后直接跳到结论漏掉了藏在中间的联系方式。加了“先复述再执行”之后漏检率下降得很明显。当然复述这句话不会出现在最终输出里因为它属于模型内部推理的一部分。你只需要在提示词里写清楚“你的思考过程不用呈现给用户最终输出只包含JSON”即可。5. 循环调用里的三个坑死循环、假结果、上下文爆炸ReactAgent跑起来容易跑好难。我这一节全是实际踩过的坑有些问题排查了我整整一个下午。5.1 死循环模型反复调用同一个工具有一次测试模型一直在调用contactInfoExtract连续调了五六次都不肯结束。我看日志才发现工具返回的ContactItem里有一个字段叫type模型可能是想通过反复调用拿到更多联系方式。后来我用两个手段解决一是在提示词里明确“每类联系方式最多提取一条禁止重复调用同一工具”二是在业务侧加了一个外层校验如果工具调用次数超过5次直接终止本轮并返回“审核失败请人工处理”。Spring AI的底层循环机制很强大但它没有帮你限制“循环次数”的义务这部分必须在业务层自己兜住。我建议你在封装Agent时把工具调用次数纳入监控指标。// 伪代码外层兜底控制循环次数 AtomicInteger toolCallCount new AtomicInteger(0); String content chatClient.prompt() .user(input) .tools(tools) .call() .content(); // 如果日志发现toolCallCount持续增长说明有死循环风险严格来说Spring AI内部的循环次数据说也有相关参数但不同版本API变化较快我不会把一个未来可能变更的参数名写死在这里。最稳妥的做法就是自己监控、自己兜底。5.2 上下文爆炸每轮工具调用都会累积历史Agent每次工具调用后框架都会把“工具请求”和“工具结果”追加到对话上下文。如果工具返回的是一个很大的List几轮下来上下文就可能膨胀到上万token。审核工具还好我遇到更严重的是在做RAG检索Agent时每轮检索返回几篇文档上下文很快爆掉。解决思路是让工具的返回结果“够用但不要过多”。比如contactInfoExtract最多返回5条命中每条只返回类型和原文片段不返回其他元数据。你要在工具方法里做截断不要把整个词库或整段分析结果倒给模型。5.3 假结果模型在工具没返回时强行编造这是让我最警惕的坑。有一次工具实现有BugadWordCheck返回了空列表但模型在最终结论里写了“命中广告违禁词全网最低”。我查日志发现工具结果明明是空模型为什么能“看到”违禁词因为它凭自己的知识猜了一个。从那以后我在系统提示词里加了一条硬约束证据列表中的每一项必须来自工具返回结果若工具返回为空必须写“未命中”禁止自行补充。并且在代码里加了校验最终输出的evidence字段如果出现了工具返回列表里不存在的文本就判定为异常输出转人工复核。这条硬约束在审核场景里极其重要。审核系统的价值在于可追溯模型编一条证据整个日志链就废了。5.4 工具抛异常别让整个对话中断工具方法执行过程中可能遇到词表加载失败、正则出错等情况。默认情况下异常会直接中断对话模型拿不到任何反馈用户看到的就是一个报错。我后来把所有工具方法统一改成“返回错误信息而不是抛异常”Tool(name adWordCheck, description ...) public ListString adWordCheck(String text) { try { // 业务逻辑 } catch (Exception e) { return List.of(ERROR: 词表加载失败请稍后重试); } }这样模型收到错误信息后至少有机会在最终结论里说明“本次审核工具异常结果不可靠”而不是让整个调用链直接断掉。6. 生产级优化日志、兜底和提示词版本管理骨架跑通、案例验证、坑也踩过一轮之后最后说说我把这套ReactAgent推到生产环境前做的几件事。6.1 把工具调用全链路打出来Agent跟普通接口最大的区别在于“过程不可见”。普通接口你只能看到输入输出Agent中间做了三次工具调用、每次返回是什么你不打日志根本不知道。我在生产环境把关键节点都打印出来用户输入、模型发起的工具请求、工具返回结果、最终输出。定位问题的时候这些日志比任何监控都管用。log.info(ReactAgent user input: {}, input); // 框架自动执行工具调用日志会记录toolCall log.info(ReactAgent final output: {}, content);如果你用的是Spring AI Alibaba的自动装配很多链路信息可以通过观测体系收集。项目里没有接入复杂链路追踪的话最简单的log.info就是保命手段。6.2 三层兜底低风险直放、中风险提示、高风险人工我把审核结果分成三个处理路径而不是让模型一个人拍板风险等级处理方式LOW自动通过MEDIUM自动提示修改建议同时记录日志HIGH转人工审核Agent结果仅作为参考这个兜底设计很关键。Agent再聪明也只是辅助决策不是最终决策者。尤其审核场景涉及处罚和投诉一旦模型判断失误人工介入能兜住最坏情况。6.3 提示词模板独立维护纳入版本管理这也是“系统提示词怎么配置”的终极答案不要写在Java字符串里不要散落在各个Service。我建议把系统提示词放到resources/prompts/目录用独立的文本文件管理。配置文件一多你可能会想引入配置中心但初期一个目录就够了。每次修改提示词我都要求团队在提交记录里写清楚“改了哪一段、解决什么问题、副作用是什么”。这样Agent行为变了你能快速回溯到是哪次提示词变更引起的而不是对着代码猜半天。6.4 我对ReactAgent生产落地的一点体会这套审核Agent上线跑了两个月最大的体会是ReactAgent的价值不在“灵光一现”的智能感而在“可编排的确定性”。你把工具做成业务规则的稳定底座把提示词做成行为约束的缰绳模型反而能在这套框架里展示出真正的价值。那些一上来就想让模型“自由发挥”的开发往往会被不可控输出折磨到放弃。如果你正要开始在SpringAI项目里做智能审核或类似的Agent场景我的建议是按这个顺序来先把工具函数写得让模型一眼看懂再花半天调系统提示词最后盯着日志看三轮工具调用链路。等这三步都稳了再谈“或跃在渊”龙才能真的跃起来。
返回列表