
“降龙十八掌”系列写到第9掌这一篇来聊聊 SpringAI 里的 ReactAgent。先说结论ReactAgent 并不是某个独立框架而是ReAct 模式在 Java 侧的落地形态核心是让大模型在一个“思考—行动—观察”的循环里自主决策而不是一次性返回结果。这个系列的命名一直有点武侠味“或跃在渊”出自乾卦九四意思是“审时度势可进可退”刚好能对上 Agent 运行时的真实状态——既要敢于调用工具往前走也要懂得在置信度不足、工具结果冲突的时候收手降级。这篇博文不打算讲玄学围绕“springai 智能审核”这个真实场景来拆我会把 ReactAgent 的循环原理、Java 工具调用细节、系统提示词怎么配置、审核业务怎么落地以及我在生产环境里踩过的坑全部摊开讲。无论你是刚接触 SpringAI 的 Java 工程师还是已经在用 ChatClient 做功能、想再往前一步搭 Agent这篇都适合你。1. 思路拆解ReactAgent 不是“跑个循环”那么简单1.1 从“工具调用”到“Agent 决策闭环”很多同学一听到 Agent 就以为是在 for 循环里反复调模型这是最容易踩的误区。早期我们做 LLM 应用最常见的模式是function calling模型根据用户问题输出一个结构化的工具调用指令应用层解析后执行执行完把结果再发给模型模型给出最终回答。一次对话基本是“一问一答”或“一问一工具一答”链条很短。ReAct 模式则是把链条拉长成闭环整个过程类似一个员工处理工单先读需求Reason判断需要查阅哪些资料然后去系统里查Act查到结果后分析结论Observation如果资料不够再发起下一轮查阅直到信息足够才输出结论。这个循环里模型是“决策者”Java 侧是“执行者”二者通过结构化消息来回协作。我个人的理解是传统function calling是“程序教模型怎么调用”而 ReactAgent 是“模型知道自己要调用什么”。这带来的好处是能处理多步骤、动态依赖的任务比如一次审核流程里先查文本、再查图片、最后综合打风险分坏处是循环次数不可控工具调用可能出错模型也可能在某个环节反复打转。所以构建 ReactAgent 时真正要设计的不是循环本身而是循环的边界和兜底策略。1.2 为什么叫“或跃在渊”一种带护栏的跃迁“或跃在渊”这四个字放在工程里我理解成两层含义。第一层是“跃”Agent 的核心价值就是主动行动模型在循环里可以连续调用多个工具逐步逼近问题答案。如果所有步骤都让业务代码写死那就没必要上 Agent 了。第二层是“渊”Agent 要敢于“停在原地”当检测到自身能力不足、工具返回异常、或用户问题超出了既定边界时主动终止、转人工或给出兜底回答而不是硬着头皮继续瞎猜。这个设计理念落到代码层面就是三个东西终止条件、护栏开关、降级路径。终止条件决定“最多循环几轮”护栏开关决定“什么情况不能继续执行”降级路径决定“Agent 失败后业务该怎么办”。我在后面几个章节会逐一展开这里先记住一个原则Agent 的强不在于它能无限循环而在于它知道自己什么时候该停下来。1.3 整体架构与模块划分先给大家一个整体视图。整套 ReactAgent 在 SpringAI 里由三大块拼起来模型接入层通过 Spring AI Alibaba 接入通义千问这类兼容 OpenAI tool calling 协议的模型。我选通义千问的原因是国内服务稳定、上下文较长而且支持并行工具调用后续 Spring AI Alibaba 对 ChatClient 的封装也比较统一。工具注册层把 Java 方法通过Tool注解暴露给模型框架自动生成 JSON Schema模型看到的是“工具菜单”真正执行的是 Java 方法。Agent 编排层用一个循环把 ChatClient 的请求发出去解析模型返回的toolCall执行工具把结果回填进对话上下文再继续下一轮直到模型不再请求调用工具或达到迭代上限。用一句话概括流程用户请求 → Agent 编排层把上下文交给模型 → 模型决策直接回答 or 调用工具 → Java 执行工具并返回结果 → 结果回到模型 → 循环或退出。理解了这条链路后面所有代码都只是往这条链路上填充细节。2. 核心实现细节Java 侧怎么把“工具箱”交给模型2.1 Spring AI 里的 Tool Calling 机制先聊 Tool Calling 的基础机制。Spring AI 之所以能把 Java 方法变成模型可调用的工具关键在ToolCallingManager。框架启动时会扫描注册过的ToolCallback把它们转换成一个 JSON 数组随请求一起送给模型。模型在推理时如果觉得自己需要外部信息就会在返回内容里带上tool_call里面包含name和参数 JSON框架收到后定位到对应的 Java 方法执行再把执行结果封装成tool类型的消息追加到上下文重新发给模型。在 Spring AI 1.0.x 里把方法暴露给模型最简单的方式就是加Tool注解下面是审核场景里一个很典型的文本检查工具Component public class ReviewTools { Tool(name check_text_risk, description 检查一段文本的违规风险返回风险等级和命中规则。输入为需要审核的文本内容。) public String checkTextRisk(String text) { // 这里调用规则引擎、敏感词库、广告法违规词库 ListString hits ruleEngine.scan(text); int score computeRiskScore(hits); // 务必返回 JSON 字符串而不是 Java 对象 return {riskLevel: %s, score: %d, hits: %s} .formatted(score 80 ? HIGH : score 40 ? MEDIUM : LOW, score, hits); } }组装ChatClient时把工具注册进去就好ToolCallback textTool new MethodToolCallback( MethodToolCallbackMetadata.builder() .toolDefinition(ToolDefinition.builder() .name(check_text_risk) .description(检查一段文本的违规风险返回风险等级和命中规则) .inputSchema(toolSchema) .build()) .method(ReviewTools.class.getMethod(checkTextRisk, String.class)) .toolCallbacks(new DefaultToolCallbacks()) .build() );实际项目里更简单的做法是注入一个ToolCallbackProvider让 Spring 自动收集所有标注了Tool的方法。如果不想手动拼 Schema可以直接继承ToolCallback接口并把Tool标注放在实现类上Spring AI 会帮你生成 input schema。2.2 工具函数的设计规范返回值与异常处理很多团队在跑通 Agent 后都会遇到同一个问题模型经常不按预期调用工具或者工具明明执行成功了模型却“理解不了”结果。我复盘下来90% 的原因出在工具设计上而不是模型能力上。第一个大坑是参数类型复杂度。Tool方法的参数不要用嵌套的对象类型比如ReviewRequest这种带多个字段的 POJO。模型虽然能输出 JSON但在生成参数时偶尔会出现字段名对不上、给错默认值的情况。我建议参数全部用String或基本类型复杂入参就传 JSON 字符串方法内部自己解析Tool(name audit_full, description 对完整内容执行综合审核入参为JSON包含title、description、imageUrl三个字段) public String auditFull(String payload) { AuditRequest req JsonUtils.parse(payload, AuditRequest.class); // ... }第二个大坑是返回值格式。工具返回值是给模型看的不是给前端看的。不要直接返回 Java 对象序列化后的完整 JSON模型会被一堆无关字段干扰。我习惯统一返回一个精简的 JSON 结构{code:0,data:{...},msg:success}业务结果全部塞进data。上面checkTextRisk就只返回风险等级和命中规则不给模型其它噪音。第三个大坑是异常必须吞掉并转成错误信息。工具方法里不要直接把异常抛出去一旦异常抛出整个 Agent 循环可能会直接中断或者模型收到一条非常规的错误消息开始胡言乱语。正确做法是把异常捕获后返回错误 JSON让模型知道“这个工具不可用或结果异常”它可以决定换工具或直接终止try { return callExternalService(text); } catch (Exception e) { return {\code\:500,\msg\:\文本检查服务暂时不可用\,\data\:null}; }还有一个细节description 是写给模型看的说明书不是给人看的。尽量写清楚输入是什么、输出包含哪些字段、典型用法是什么。很多团队把 description 写成“文本审核工具”模型根本不知道该传什么参数。把描述改成“检查一段文本的违规风险返回风险等级和命中规则用于内容发布前的安全审核”模型才能精准匹配。2.3 核心 Agent 循环编排工具就绪后最重要的就是编排层。Spring AI 官方其实有ChatClient的call()方法但它默认是“一次调用返回一个结果”不会自动处理模型的 tool_call 请求。所以 ReactAgent 的核心循环需要我们手动写。我这里给出一个简化但可落地的循环版本基于 Spring AI 1.0.x 的 APIpublic AgentResult run(String userInput, ListToolCallback tools) { ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(tools) .build(); ListMessage messages new ArrayList(); messages.add(new SystemMessage(systemPrompt)); messages.add(new UserMessage(userInput)); int maxIterations 5; for (int i 0; i maxIterations; i) { ChatClient.ChatClientRequestSpec spec chatClient.prompt() .messages(messages) .options(GenerationOptions.builder() .temperature(0.2) .build()); ChatResponse response spec.call(); String content response.getResult().getOutput().getText(); // 如果模型没有请求调用工具说明已经给出最终答案退出循环 if (!response.getResult().hasToolCalls()) { return parseFinalResult(content); } // 执行模型请求的工具调用 ListToolExecutionResult results toolCallingManager.executeToolCalls( response.getResult().getToolCalls(), tools); messages.add(new AiMessage(content, response.getResult().getToolCalls())); for (ToolExecutionResult r : results) { messages.add(new ToolResponseMessage(r.getOutput(), r.getId())); } } // 达到最大迭代次数走兜底逻辑 return fallbackResult(AgentStatus.MAX_ITERATION_EXCEEDED); }代码不长但有几个点值得反复确认。首先ToolResponseMessage的id必须对应模型返回的toolCallId否则模型会认为工具结果和它发起的调用对不上轻则重复调用重则自行编造结果。其次每轮循环都要把AiMessage和ToolResponseMessage追加到messages保证模型能看到完整上下文。最后循环必须强制设置最大迭代次数否则遇到一个“死脑筋”的模型它会一直重复调用某个工具白白烧掉 token。配置层面如果用的通义千问可以在application.yml里控制spring: ai: dashscope: chat: options: model: qwen-max temperature: 0.2qwen-max是我在审核场景里测试下来最稳定的型号给复杂工具调用留了足够推理空间。如果你用qwen-plus或qwen-turbo注意它们在非常复杂的多工具链路上偶尔会跳步生产环境要先小流量灰度。3. 智能审核场景落地让 Agent 当成“审核员”3.1 为什么内容审核要上 Agent“springai 智能审核”最近讨论度很高因为传统审核方案长期面临一个尴尬规则引擎覆盖面广但语义理解弱人工审核准确但成本高。给你看一个我实际遇到过的场景用户发布一条动态内容是“全网最低价加微信拿内部渠道资源”。规则引擎很容易命中“最低价”这个广告法违规词但“加微信拿内部渠道”这类诱导性表达如果没有预置规则它完全发现不了。可你不可能把所有话术都列进规则库黑灰产永远在用新话术绕过。Agent 审核的思路在这里就有价值了让模型把审核工具当成“外挂眼”先用规则引擎扫一遍硬性违规再自己理解语义、评估上下文最后给出一个带依据的审核结论。它比规则引擎聪明又比纯模型调用稳定因为它每一步都有工具结果作为证据不会凭空臆断。但我要强调一句Agent 不是用来完全替代规则引擎和人工的。我落地时用的是“三层漏斗”结构第一层规则引擎秒级过滤明显违规第二层 ReactAgent 处理灰色地带第三层人工复核处理模型置信度低的 case。这样既控制了成本也保住了召回率。3.2 审核工具集与审核执行流程针对审核业务我拆了四个工具给 Agent 用大家可以根据业务调整check_text_risk(text)扫描文本命中敏感词、广告违禁词、联系方式引导语返回风险分和命中规则列表。check_image_risk(imageUrl)对图片做不适宜内容检测返回风险等级和识别标签。get_content_meta(contentId)获取完整的元信息比如作者历史违规次数、内容发布时间、投诉记录用于判断是否累犯。request_human_review(contentId, reason)把内容转人工审核队列返回工单号。Agent 的审核路径大致是这样拿到待审内容后先并行调用check_text_risk和check_image_risk如果两个工具都显示低风险再拉一次get_content_meta看作者历史最后综合输出结论如果文本风险高就直接走request_human_review或建议驳回不再浪费步骤。我把工具编排说得更细一点。模型本身不知道“先查什么”所以我们要在系统提示词里给出建议调用顺序和决策规则。比如“如果 check_text_risk 返回 HIGH不要再调用其他工具直接输出拒绝建议并附上命中规则”。这种显式约束能明显降低模型的随机性。工具的结果都以 JSON 字符串返回模型会把多个工具的 JSON 拼在一起推理最终输出一个结构化的审核结论。为了让解析稳定我要求模型输出固定格式后面提示词章节会详细展开。3.3 结果回写与人工复核闭环Agent 的输出不能直接作为“删除内容”“封禁账号”这种高危行为的判决依据这是我在设计架构时定的硬规矩。Agent 输出三类结果就是建议PASS、REJECT、REVIEW。落到业务表后REJECT 的内容进入“待人工确认”状态24 小时内有人审核确认后才会真正触发处置动作PASS 的内容直接进入线上但如果用户投诉率异常上升系统能把内容撤回并重新送到 Agent 通道复检。这个闭环设计有双重意义一是为 Agent 提供“事后反馈数据”每次人工确认结果都会回流为评估样本我们定期统计 Agent 判断与人工判断的一致率二是保住业务底线Agent 判断错了最多是延迟内容上线不会造成不可逆的违规事故。如果你想在生产环境跑 Agent 审核这条“建议 vs 处置”的隔离是最值得先做好的。4. 系统提示词怎么配置才有实战价值4.1 系统提示词不是“拼接模板”最近总被问到“springai 系统提示词怎么配置”很多人以为就是把角色设定、业务规则复制到 SystemMessage 里结果跑起来效果很差。这里面的关键问题在于提示词不是给人看的是给模型看的要围绕“模型在循环里如何决策”来设计。我的系统提示词固定分成六个区块角色定位你是内容安全审核助理只负责输出审核建议不负责执行处置。任务流程先调用文本和图片检查工具再综合风险分最后输出结论。工具使用规范哪些工具可以并行调用、哪些情况必须转人工。输出格式必须输出 JSON包含 status、riskScore、reasons、suggestedAction。边界约束不确定时输出 REVIEW绝不自行猜测违规依据。降级策略工具不可用时直接转人工不得编造结果。我建议用 TextBlock 写模板再用 Spring AI 的SystemPromptTemplate做变量注入这样一套逻辑可以复用到不同审核场景String systemPrompt 你是内容安全审核助理。你的任务是根据工具返回的检查结果对给定内容输出审核建议。 请严格按照以下流程执行 1. 先调用 check_text_risk 和 check_image_risk 获取风险证据。 2. 如果任一工具返回 riskLevelHIGH直接输出 REJECT并附上命中规则。 3. 如果风险中等调用 get_content_meta 查看作者历史记录后再决策。 4. 如果工具不可用输出 REVIEW。 输出必须是 JSON格式如下 {status:PASS/REJECT/REVIEW,riskScore:0-100,reasons:[...],suggestedAction:...} ;4.2 多场景切换与动态注入审核不是一个单一场景。内容社区里有图文动态、有评论、有私信、有商品详情同一套提示词在不同场景下会水土不服。我在项目里搞了一个PromptManager把不同场景的提示词模板放在配置中心按bizType动态选择MapString, String promptTemplates configCenter.getPromptTemplates(); String scenePrompt promptTemplates.get(bizType); ChatClient chatClient ChatClient.builder(chatModel) .defaultSystem(scenePrompt) .build();这里要小心一件事提示词里只要用到变量就要用SystemPromptTemplate显式渲染不要让模型自己去做“填空”。比如在商品详情场景里模板里写了{category}就必须在代码里替换成实际类目不然模型要么忽略要么自己猜一个造成结果不可控。提示词演进我推荐走版本管理每版提示词记录版本号、改动点、影响指标如通过率、转人工率。我踩过最大的坑是同时改了提示词和采样参数结果效果变好后根本分不清是哪个改动起了作用。所以现在改成“一次只改一个变量”改提示词就不动 temperature改参数就不动提示词。4.3 采样参数与迭代上限的调参经验调参这块给大家一个参考基准以审核场景为例。temperature直接决定模型输出的发散程度审核这种高风险场景我压到 0.1~0.3太低会让模型过于机械偶尔漏判具象化表述太高则容易让模型输出格式不稳定。maxIterations我通常设 3~5实测下来 80% 的任务能在 3 轮内完成4 轮以上的 case 往往不是工具调用失败就是问题本身已经超出 Agent 能力边界继续跑只会烧钱。我把常见调参现象整理成一个对照表方便大家快速定位参数设置值观察到的现象建议temperature 0.7输出偶尔不按规定格式返回偶尔出现字段缺失、JSON 解析失败审核场景压到 0.2maxIterations 2多步骤工具链常被打断模型刚调用完文本检查还没来得及看图片就输出结论提到 3~5maxTokens 太小结论被截断模型输出了半个 JSON无法解析给到模型最大输出的 80% 以上工具返回结果过长上下文被塞满模型开始重复读取同一工具结果压缩工具返回只保留字段摘要5. 常见问题与排查技巧实录5.1 高频问题速查表我把生产环境里遇到的高频问题整理成一张速查表对照起来排查效率会高很多现象根因处理办法模型偶尔不调用工具直接回答description 太模糊模型没意识到需要工具重写工具 description提供典型使用示例模型重复调用同一个工具工具返回结果太长模型没“记住”结论压缩返回结果增加“当前已获取结果摘要”到上下文输出 JSON 无法解析temperature 偏高、输出指令不完整降低 temperature提示词里明确 JSON schema 和起止标记工具执行异常导致循环中断工具方法主动抛异常所有工具方法 catch 异常并返回错误 JSON达到迭代上限后结果质量差循环终止后没有兜底策略迭代超限走指定降级逻辑比如转人工用了 qwen-turbo 后多工具调用不稳定小型号推理能力不足换 qwen-max 或降低单轮工具复杂度模型返回空字符串输入内容可能触发内容安全拦截检查请求内容有必要时拆分输入或重试一次如果模型回复里出现了“抱歉我无法回答该问题”这类内容极大概率不是 Agent 代码问题而是模型内容安全策略拦截了输入。这时候别急着调提示词先看请求文本里有没有命中安全词换个表述再测一次。5.2 稳定性与成本控制经验最后分享几条花了很长时间才总结出来的稳定性经验比跑通 Demo 重要得多。第一工具返回结果要“压缩”。模型上下文窗口是有限的工具返回的 JSON 太长既烧 token又干扰模型注意力。我把工具结果默认只保留核心字段去掉冗余日志单个工具返回控制在 200 字以内。第二给循环加一个“重复检测”。如果模型连续两轮输出了完全一样的思考内容大概率是陷入死循环直接强制终止并转降级路径。我在代码里对比相邻两轮的 message hash相同就 break。第三生产环境必须做灰度。Agent 上线不能一把梭我先放了 5% 流量进新链路统计通过率、转人工率、误杀率三个指标和旧规则链路对照跑了一周确认指标不劣化后再逐步放开。这个过程看着保守但能帮你避开“模型幻觉导致误杀暴涨”这种灾难性问题。第四费用要看“单次审核成本”。同样一轮审核工具返回大、迭代次数多成本能差三倍。我每轮会把累计 token 打点上报设定单次审核成本上限超了就自动转人工。这个“成本熔断”机制比任何调优都管用。我个人的体会是ReactAgent 的工程化核心不在 Agent 本身而在它周边的“护栏体系”。把工具设计好、提示词管好、成本监控好模型的表现自然稳定。你不需要真的把所有业务判断都交给 Agent而是让它在可控的舞台上前进、在边界处停下这就是“或跃在渊”给我的最大启发。