ARTICLE DETAIL

资讯详情

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

Spring AI实战:ReAct模式Agent如何实现商品标题智能审核

Spring AI实战:ReAct模式Agent如何实现商品标题智能审核 博主圈里的朋友都叫我阿里这个“降SpringAI”是我自己从零啃Spring AI的系列笔记命名有点中二用降龙十八掌当代号硬着头皮写到了第九篇。第九掌是“或跃在渊”出自乾卦意思是一个人或者一条龙已经到了临界点既可以往上蹿也可以先沉住气潜水进可攻退可守。我越琢磨越觉得这个状态拿来形容ReAct模式的Agent太贴切了上一秒它还在按固定流程问答下一秒它就自己决定调什么工具、查什么数据、继续推理然后又调用下一个动作——这就是从“聊天机器人”往“真Agent”跃迁的那个临界位置。所以这篇准备把ReactAgent彻底讲透。先明确一件事这里的React不是前端那个React它是Reasoning加Acting的合称也就是“推理与行动循环”。放在Spring AI项目里它意味着你不再只是写死一段Prompt让大模型回答而是让模型在每一轮自己产生Thought、发起Tool Call、拿到Observation后再继续推理直到给出最终答案。围绕这个模式我会把系统提示词怎么配置、工具怎么暴露给模型、商品标题智能审核的完整实现、还有我在实际项目里踩过的坑全部写出来。适合已经能跑通ChatClient基础调用、想让Spring AI项目真正具备Agent能力的人也适合正准备做AI自动审核、自动工单、智能问答这类项目的同学。1. “或跃在渊”的定位ReactAgent到底解决了什么问题1.1 武侠命名的另一层意思为什么要用“或跃在渊”降龙十八掌一共十八掌第九掌正好在中间不上不下。这个位置特别容易让人焦虑说它是高深招式吧前面还有九掌说它基础吧它又已经跳出了“见龙在田”那种练基本功的阶段。“或跃在渊”这句话给我的感觉是你已经具备往上跳的能力但还没到“飞龙在天”那种稳赢的局面所以这时候最重要的事情不是炫技而是把节奏控制住——该推理的时候推理该调用工具的时候调用工具一步错就可能满盘皆输。拿Spring AI项目来对应这个阶段其实就是“从单轮Prompt跳到自主Agent”的过渡。如果你只写一个ChatClient接口用户提问、模型回答那是“潜龙勿用”很安全但能力有限。如果你一上来就搞多Agent编排互相传消息、共享记忆、自动路由那是“飞龙在天”看起来很猛但没把地基打牢线上分分钟翻车。ReactAgent恰好是中间那条路模型自己决定下一步动作但整个决策循环还是线性的、可控的、能加日志、能限流、能兜底。我建议任何一个想把Spring AI用到生产环境的人都先从这个模式练起。还有一层意思藏在“或跃在渊”的“渊”字上。渊代表底层的数据和工具代表你给模型接好的那些Java方法。模型不是凭空做判断的它每一次“跃”都需要一个支撑点这个支撑点就是工具调用返回的真实数据。没有这个“渊”Agent只能靠大模型内部知识硬编那不是Agent那是高级猜谜。所以这篇文章标题里的“或跃在渊”我理解成让模型在推理和工具之间反复跳跃这才是第九掌真正要练的东西。1.2 ReAct循环和普通Function Calling哪里不一样很多人刚接触Spring AI的Tool Calling时会误以为“模型能调函数”就是Agent了。真不是。普通Function Calling更像一个“单次问答”模型在生成回答时发现需要外部数据于是它返回一个工具调用请求程序执行完把结果塞回去模型给出最终答案结束。整个过程就是一轮“问-答”或者两轮“问-工具-答”。ReactAgent则是一个循环核心逻辑可以概括成四个字母Thought、Action、Observation、Answer。模型先想一下当前任务缺什么信息然后决定调用哪个工具接着观察工具返回结果再根据这个结果继续想下一步。这个循环可以一直持续直到模型觉得信息足够了才输出Answer。用修水管来类比最直观你不会一上来就换零件而是先观察哪里漏水再决定关哪个阀门关完看一眼水停了没有停了就找原因补漏没停就继续排查。每一步行动都依赖上一步的观察这就是ReactAgent的运转方式。同样是把工具暴露给模型Function Calling是“按需查询”ReactAgent是“链路决策”。举个例子审核一条商品标题如果只是查一次敏感词Function Calling完全够用但如果你希望模型先查敏感词发现命中率达到阈值后再查发布者历史风险等级最后根据这两条信息决定是直接拒绝还是转人工这就不再是单次函数调用而是一条决策链。ReactAgent的价值就在于模型自己能根据上一步工具返回的内容决定要不要走下一步走哪一步甚至可以在多步之间来回修正。1.3 Spring AI对ReactAgent提供了什么以及没提供什么先说结论Spring AI官方并没有一个叫ReactAgent的类你搜Spring AI的源码也搜不到这个类名。但Spring AI的ChatClient在配置了ToolCallback之后底层已经自动实现了React的循环逻辑。也就是说框架帮你把“模型返回工具调用-执行工具-再把结果和对话历史一起发回给模型-检查是否还有工具调用”这个循环封装好了你只需要把工具准备好、把系统提示词设置对然后正常调用一次ChatClient它就能自动多轮运转下去。Spring AI提供了三个非常关键的能力。第一个是Tool注解你可以直接在任意Spring Bean的方法上标注这个注解框架会通过MethodToolCallbackProvider自动把Java方法包装成模型可识别的工具。第二个是ChatClient的toolCallbacks入口发起请求时可以把工具列表传进去让模型在本次对话中拥有调用权限。第三个是Advisor机制可以在每次请求前后插入自定义逻辑做日志、注入提示词、统计工具调用次数都很方便。没提供的是什么呢是完整的Agent状态管理、记忆窗口上限、严格的工具调用次数限制、多Agent路由这些官方目前都还没有现成的“一键配置”。所以我在文章里建议的做法是用Spring AI原生能力搭好循环底座然后在业务层用Advisor、用自定义工具、用数据库状态把缺的部分补上。这也是为什么我说ReactAgent是“或跃在渊”框架已经把渊挖好了跃多高、怎么跃取决于你自己怎么设计提示词和工具。这也是整篇文章最核心的思路。2. 开工前先给模型立规矩系统提示词与工具注册2.1 Spring AI系统提示词怎么配置四个入口一个模板“系统提示词怎么配置”是很多Spring AI项目新手第一个卡住的地方因为同一个问题至少有四种写法每种写法生效范围还不一样。我先按使用频率把这四个入口列一遍。第一个入口是ChatClient.Builder.defaultSystem(...)也就是在构建ChatClient的时候设置一个默认系统提示词。这种方式适合整条业务链路共享一套身份设定比如“你是某电商平台的内容审核助手”所有通过这个ChatClient发起的请求都会自动带上这段System Prompt。第二个入口是ChatClient.prompt().system(...)它只对当前这一条请求生效适合每个请求需要切换不同审核规则、不同业务背景的场景。第三个入口是用Prompt对象或者SystemMessage类在更底层的ChatModel调用中直接拼消息列表适合你已经不用ChatClient、想自己控制完整多轮消息序列的场景。第四个入口是写一个Advisor在aroundCall里往请求体注入或者覆盖systemText适合按用户维度、按租户维度动态路由提示词的场景。把入口搞清楚之后真正麻烦的是系统提示词本身怎么写。我的建议是三层结构第一层是角色与目标用一两句话说清楚“你是谁、你要完成什么任务”第二层是决策规则把判断标准、输出格式、不能做什么写得明明白白第三层是工具使用策略明确告诉模型“有哪些工具可用、什么情况下必须调用、工具返回后怎么处理”。贴一个我在商品标题智能审核项目里用的模板你可以按这个骨架改成自己的场景你是电商商品标题审核助手负责检查标题是否存在平台违规内容、夸大宣传、违禁品或低俗营销表述。 工作流程 1. 当你认为文本需要核验时必须先调用checkSensitiveWords工具禁止凭知识猜测。 2. 如果checkSensitiveWords返回的score大于等于60必须继续调用getUserRiskLevel查询发布者风险等级。 3. 根据前两步工具返回值再调用auditAction提交最终处置动作动作只能是PASS、REVIEW或REJECT。 4. 所有判断完成后只输出一个JSON不要输出任何多余解释。 输出格式 {decision:PASS,reason:简短理由,matchedRules:[规则1,规则2]} 禁止事项 - 禁止编造工具返回值。 - 禁止不调用工具直接输出审核结论。 - 禁止在工具已经返回结果后忽略数据自行判断。 - 如果工具返回异常输出REVIEW并说明原因。这里最容易被忽略的是“禁止编造工具返回值”和“不调用工具直接输出结论”这两句话。大模型天生有极强的补全倾向你问它一个商品标题它哪怕没查词库也会凭训练数据里的记忆给你一个看似合理的结果。三级结构里如果不把“工具优先”写死你后面会在线上看到大量没查库就下结论的“人工智障”。2.2 用Tool把Java方法变成Agent的手脚系统提示词负责告诉模型“什么时候应该动手”工具注册则负责给模型“能动手的武器”。在Spring AI 1.0正式版里最舒服的方式是Tool注解加MethodToolCallbackProvider。你不需要手写ToolCallback的实现类只要在一个Spring管理的Bean上写一个普通Java方法加上Tool注解框架就会把你的方法签名、参数名、方法上的description一起注册成模型可调用的工具描述。看代码先定义一个工具BeanComponent public class AuditTools { private final SensitiveWordService sensitiveWordService; private final UserRiskService userRiskService; private final AuditRecordService auditRecordService; public AuditTools(SensitiveWordService sensitiveWordService, UserRiskService userRiskService, AuditRecordService auditRecordService) { this.sensitiveWordService sensitiveWordService; this.userRiskService userRiskService; this.auditRecordService auditRecordService; } Tool(description 检查商品标题是否命中违规词库返回命中词列表与0到100的评分。当评分大于等于60时需要继续调用getUserRiskLevel) public String checkSensitiveWords(String title) { ListString matchedWords sensitiveWordService.match(title); int score sensitiveWordService.score(matchedWords, title); return { \matchedWords\: matchedWords , \score\: score }; } Tool(description 根据用户ID查询发布者历史风险等级返回RISK_LOW、RISK_MEDIUM或RISK_HIGH) public String getUserRiskLevel(String userId) { String riskLevel userRiskService.queryRiskLevel(userId); return { \riskLevel\: \ riskLevel \ }; } Tool(description 提交最终审核动作action只能传PASS、REVIEW或REJECT返回是否保存成功) public boolean auditAction(String contentId, String action) { auditRecordService.save(contentId, action); return true; } }这三个方法合起来就是一条审核链路先查词库再查人最后落库。Tool注解里description的写法非常关键它不是给人看的注释而是给模型看的使用说明书。模型只能通过description来理解“这个工具是干嘛的、什么时候该调用、参数是什么含义”。我见过很多人把description写得很随意比如“查询敏感词”模型根本不知道阈值是多少、返回结构是什么结果要么不调用要么乱调。你在description里多写一句“当评分大于等于60时需要继续调用getUserRiskLevel”模型就有据可循了。然后是注册配置把工具挂到Spring上下文里Configuration public class AgentConfiguration { Bean public ToolCallbackProvider auditToolCallbackProvider(AuditTools auditTools) { return MethodToolCallbackProvider.builder() .toolObjects(auditTools) .build(); } }这里有一个特别容易踩的坑如果你的项目不是基于Spring Boot Parent管理的Maven编译时可能没有开-parameters参数那么Tool注解的方法参数名会变成arg0、arg1模型调用工具时不知道应该往arg0里填什么工具调用成功率会直线下降。Spring Boot的父POM默认会开启parameterstrue但如果你是自己搭的Maven工程记得在maven-compiler-plugin里补上这个编译参数。这个坑我至少帮三个同事排查过。2.3 ChatClient如何自动完成“思考-行动-观察”工具注册完了接下来就是怎么让ChatClient把循环跑起来。Spring AI的做法是你在发起请求时把工具回调传进去框架会把工具描述塞进这次请求模型在生成过程中一旦决定调用工具就会返回一个ToolCall对象框架自动执行对应的Java方法然后把工具结果作为一条消息追加到对话历史里再次调用模型模型看了结果之后可能会继续调下一个工具也可能觉得信息够了直接给最终答案。这个循环是框架自动完成的不需要你手写while循环。所以在日常开发里你写的代码其实很短String result chatClient.prompt() .system(systemPrompt) .user(请审核这条商品标题 title) .toolCallbacks(toolCallbackProvider.getToolCallbacks()) .call() .content();你可以把toolCallbacks()这一步理解成“给模型发武器”。不发武器模型就是个只会聊天的嘴炮发了武器模型才能在需要的时候“伸出手”去查工具、查数据。ChatClient内部会自动判断返回结果里还有没有工具调用请求有就继续循环没有就把content返回给你。还有一个对生产环境特别有用的点是Advisor。你可以写一个简单的自定义Advisor在每次调用前后打印请求和响应摘要这样就能完整看到ReactAgent每一轮走了什么工具、模型说了什么、工具返回了什么。把这段日志拉出来是排查“模型为什么不调工具”的最快路径。别相信感觉先看日志里有没有ToolCall再看系统提示词是不是没把工具触发条件说清楚这是我一贯的排查顺序。3. 智能审核实战把ReactAgent落进Spring AI项目3.1 场景设计给商品标题做智能审核我选“商品标题智能审核”作为实战场景是因为它非常典型地体现了ReactAgent的优势而且逻辑足够清楚。一个电商平台每天要新增几万条商品标题如果全靠人工看成本高、速度慢如果只靠规则引擎查词又容易漏掉语义上的夸大宣传、擦边低俗。最合理的方案是大模型负责语义判断规则引擎负责硬性词库命中用户历史风险等级负责提供上下文三者结合才能给出一个让人放心的审核结果。这个场景里的决策链是什么呢第一步先让模型调用checkSensitiveWords工具对标题做规则词库扫描这一步是硬性检查命中就是命中大模型不需要猜。第二步如果词库评分超过阈值比如60分说明标题大概率有问题但要不要直接拒绝还得看发布者是谁于是模型应该调用getUserRiskLevel查询发布者历史风险等级。第三步模型综合词库评分和用户风险等级调用auditAction提交处置动作高风险加高评分就REJECT低风险加边缘词就先REVIEW转人工没问题就PASS。第四步模型把审核结果和理由整理成一个JSON输出给调用方。这个过程如果用普通Function Calling做你得在Java代码里手工编排这三步写一堆if-else模型只能在固定位置插一句“我可能需要查一下”。一旦业务规则变化比如想加一个品牌黑名单检查你又得改代码。用ReactAgent做模型根据系统提示词和工具description自己决定链路顺序代码可以不动只需要在系统提示词里加一行“如果命中平台黑名单品牌直接REJECT”就能扩展。3.2 完整代码工具类、配置类与审核接口把上面的设计落成完整可运行的代码。工具类在2.2已经写了这里补上配置类和对外接口。先写配置类把ToolCallbackProvider注册为BeanConfiguration public class AgentConfiguration { Bean public ToolCallbackProvider auditToolCallbackProvider(AuditTools auditTools) { return MethodToolCallbackProvider.builder() .toolObjects(auditTools) .build(); } Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder.build(); } }然后写审核接口。这里我用一个ChatClient实例在每次请求时动态传入系统提示词和工具回调保证不同审核场景可以复用同一个ClientRestController RequestMapping(/api/audit) public class AuditController { private final ChatClient chatClient; private final ToolCallbackProvider toolCallbackProvider; public AuditController(ChatClient chatClient, ToolCallbackProvider toolCallbackProvider) { this.chatClient chatClient; this.toolCallbackProvider toolCallbackProvider; } PostMapping(/title) public ReviewResult auditTitle(RequestBody TitleAuditRequest request) { String systemPrompt 你是电商商品标题审核助手负责检查标题是否存在平台违规内容、夸大宣传、违禁品或低俗营销表述。 必须先调用checkSensitiveWords当评分大于等于60时继续调用getUserRiskLevel最后调用auditAction。 只输出JSON格式包含decision、reason和matchedRules三个字段。 禁止编造工具返回值。 ; String content chatClient.prompt() .system(systemPrompt) .user(请审核这条商品标题 request.title()) .toolCallbacks(toolCallbackProvider.getToolCallbacks()) .call() .content(); return parseReviewResult(content); } private ReviewResult parseReviewResult(String content) { // 实际项目建议用Jackson解析这里先做空指针保护 if (content null || content.isBlank()) { return new ReviewResult(REVIEW, 模型未返回有效审核结果, List.of()); } return objectMapper.readValue(content, ReviewResult.class); } }这里还有一个很重要的设计点为什么不让auditAction在工具里直接返回“成功”而还要让模型最后再输出一个JSON因为工具调用和最终答案是两回事。工具负责执行模型负责归纳。你很可能会在同一个工具调用里拿到多条信息比如词库命中词、评分、用户风险等级这些信息分散在不同轮次里必须由模型在最后一轮把它们汇总成一条结构化结论。你把这一步砍掉接口返回的就是工具调用过程而不是审核结论协作关系就乱了。3.3 细节打磨幂等、限额、结构化输出能跑通只是第一步上生产之前有三个细节必须处理。第一个是幂等。ReactAgent在自动循环时模型可能会因为推理不严谨对同一个contentId重复调用auditAction。所以工具方法内部不要无条件新增记录最好先查一下这个contentId是否已经有审核结论有就直接返回已有结果没有才写入。幂等键是contentId不是标题文本因为同一个标题可能被不同的商品反复提交。第二个是工具调用限额。虽然Spring AI会在模型不再返回ToolCall时自动结束循环但你没法保证模型不会在某个边界样本上反复调用同一个工具。我的建议是二选一要么在Advisor里统计工具调用次数超过上限后强制给模型追加一句“请基于已有信息直接输出最终结论不要再调用工具”要么就不用ChatClient的自动循环改用底层ChatModel自己写一个带最大迭代次数的循环。我实际项目中用的是第一种因为改动最小也不会破坏框架自带的能力。第三个是结构化输出。System Prompt里写了输出JSON模型大概率会配合但不是百分百。你还要在代码层面对返回字符串做兜底解析解析失败就回退到REVIEW转人工而不是抛异常让整个审核链路挂掉。在审核这种场景里模型出错了最差的后果就是多一条人工审核记录但绝对不能因为返回格式不对导致商品漏审。4. 高频问题排查ReactAgent不是配好就能跑4.1 模型就是不调用工具怎么办这是ReactAgent上线后出现频率最高的问题。现象很一致接口返回正常内容也通顺但一看日志模型从头到尾都在凭自己的知识回答一个ToolCall都没有。原因通常不是代码写错而是模型“觉得没必要查那么多”。它的训练目标决定了它倾向于尽快给出像样的回答而不是启动一堆外部工具。解决办法分三步走。第一步检查系统提示词里是否明确给了触发条件光说“你需要时调用工具”等于没说要改成“先调用checkSensitiveWords当评分大于等于60时继续调用getUserRiskLevel最后调用auditAction”把调用顺序和触发条件写成操作指令。第二步检查工具description是否足够丰富模型看到checkSensitiveWords的description里写着“当评分大于等于60时需要继续调用getUserRiskLevel”它就知道这不是可选项而是流程的一部分。第三步在系统提示词里加一句“所有判断必须基于工具返回数据禁止直接用常识回答”这句话对大多数模型都很管用。如果三步都做了还是不调用就先把问题从“提示词”转移到“工具注册”上用日志输出当前请求里到底传入了几个ToolCallback名称是什么。有时候是ToolCallbackProvider没有正确注入工具本来就是空的模型想调也调不了。4.2 工具结果回来后被模型“篡改”了怎么办我见过一个特别典型的翻车场景工具明明返回了“命中词空评分12”但模型在最终结论里写“标题包含违规词建议拒绝”完全无视了工具结果。原因有两层一层是模型自身的“幻觉惯性”它觉得这个标题看起来像违规就不愿意被数据说服另一层是工具返回的内容太长或者太乱模型根本就没读进去。解决思路是给工具返回“瘦身”并且“结构化”。工具返回的字符串要短只包含关键字段不要拖着一大串原始日志、调试信息、数据库字段。能返回JSON就返回JSON{score:12,matchedWords:[]}比“查询完成共扫描了125条记录没有发现命中词评分很低”要清晰得多。与此同时在系统提示词里把“以工具数据为准”写死甚至可以加一句“如果工具评分小于60结论必须是PASS或REVIEW禁止使用REJECT”。这就是用规则把模型的自由发挥空间约束住。4.3 排查速查表与避坑清单我把这段时间踩过的坑整理成一张速查表部署在Spring AI项目上遇到同类问题可以直接对着查现象常见根因排查方向模型全程不调用工具提示词没有写触发条件 / 工具没注册成功检查日志中的ToolCallback数量与名称强化工具描述工具参数变成arg0Maven编译没开-parameters在pom中给maven-compiler-plugin配置parameters工具返回后被模型忽略返回内容过长 / 输出格式混乱压缩返回结果为短JSON并在提示词中强调以工具数据为准同样一个动作被调用多次模型复读工具调用 / 重试机制工具方法做幂等校验以contentId为幂等键输出JSON反复解析失败模型输出里混入了解释文字系统提示词给出few-shot输出样例代码层做兜底解析循环长时间不结束模型一直想补一轮工具调用用Advisor限制调用次数超过后追加截止指令还有一个容易被忽视的点工具方法里的耗时操作。ReactAgent的循环不是一次调用就完事如果每个工具都要查数据库或者调远程接口一次完整审核的耗时会成倍上涨。你把checkSensitiveWords写成在内存词库上做匹配把getUserRiskLevel的查询结果做本地缓存整个Agent循环会快很多。毕竟模型“思考”的时间通常只有几秒而工具执行的耗时会直接拖累整体时延优化工具调用比优化提示词更见效。5. 下一步从单Agent到可观测的复杂编排5.1 多Agent编排把“单一循环”升级为“多角色协作”ReactAgent的单循环能力稳定之后很自然就会想往上走一步既然一个模型实体可以自己决定“查什么、做什么”那我能不能让多个Agent各管一段比如初审Agent只判断文本命中舆情Agent只分析用户历史评价复审Agent综合两者结果这就是多Agent协作也是“飞龙在天”的方向。在Spring AI体系里多Agent通常还是基于ChatClient加Advisor实现的只不过每个Agent拥有自己的系统提示词、自己的工具集合、自己的记忆窗口。比如前面商品标题审核的例子可以拆成一个“词库Agent”、一个“用户画像Agent”、一个“决策Agent”词库Agent拿到标题后输出命中情况用户画像Agent输出历史风险决策Agent拿到两者产出再决定最终动作。每一步都可以独立出日志、独立做限流、独立替换规则不会因为一个Agent的异常拖垮整条链路。但我多提醒一句如果业务场景还不需要多角色别急着拆。多Agent意味着多一次大模型调用意味着更多Token消耗和更长的延迟。ReactAgent单循环能解决的事先让它解决只有当你确实需要不同角色使用不同工具集、各自维护上下文时再拆。5.2 可观测性别让Agent黑盒化我在项目里吃过最大的亏就是让Agent成为一个黑盒。从外面看一个请求进去一个结果出来中间模型到底调用了哪几个工具、每轮返回了什么、为什么最后选了这个决策全都没有记录。等到线上出了漏审或者误杀你连定位问题都无从下手。所以我的做法是从第一天就给ReactAgent加上可观测性。最简单的方式是写一个自定义Advisor在aroundCall里记录本轮输入、模型返回的工具调用列表、工具执行结果按请求ID串起来最后落一份结构化日志。不需要很复杂一个requestId加上每次调用的时间戳就够用了。等你发现某个审核结论特别离谱的时候把这串日志拉出来你会看到是模型自己跳过了工具检查还是工具返回的数据本身就不对。这种日志的价值比任何监控指标都直接。最后再说点实话这个“降SpringAI”系列写到现在第九掌“或跃在渊”是我觉得最值得单独拎出来讲的一篇。普通Function Calling和完整Agent之间的差距不是代码量的差距而是思维方式的变化你要开始习惯“把决策权交给模型”同时又要用系统提示词、工具描述、Advisor这些手段给模型画一条安全边界。ReactAgent就是这两个目标之间的平衡点进可攻、退可守非常像“或跃在渊”给我的感觉。最后再分享一个小技巧如果你第一次把一个新工具接进ReactAgent不要直接丢到生产环境先用一个固定问题去试比如“请检查这个标题并调用工具完成审核”然后在日志里数一下模型到底调用了几次工具、每次参数是什么。这个简单的冒烟测试能帮你提前发现八成以上的工具注册和描述问题。工具链路一旦跑顺后面再叠加业务规则、多Agent协作都会顺畅很多。
返回列表