ARTICLE DETAIL

资讯详情

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

Spring AI ReactAgent实战:让模型自主调用工具完成内容审核

Spring AI ReactAgent实战:让模型自主调用工具完成内容审核 Spring AI 的实战系列写到现在终于到了第 9 篇。这篇的主角是ReactAgent或者说是在 Spring AI 项目里真正把“让模型自己决定调用什么工具”这件事落地的完整方案。如果你已经在项目里用过 Spring AI 的 ChatClient但总感觉它差点意思——只能一问一答没法根据业务场景自动去查库、调接口、做判断——那这篇就是写给你看的。这一篇我会拿“内容审核”作为贯穿案例因为它对工具调用的要求非常典型需要模型理解规则、调工具做判定、再基于工具结果给出结论。而且“springai 智能审核”这个话题本身就很容易踩坑系统提示词怎么配置、工具参数怎么设计、模型为什么一直不肯调工具这些问题我都会一一展开讲。无论你是刚接触 Spring AI还是已经写过几个 demo 想往业务方向走这篇都能给你一套可以直接照搬的落地思路。1. 先搞清楚 ReactAgent 是什么再谈怎么搭很多同学看到“ReactAgent”这个名词会懵一下以为是 React 前端框架的什么东西。其实这里的React 是 ReAct 的缩写来自论文《ReAct: Synergizing Reasoning and Acting in Language Models》核心思想一句话概括让大模型在“推理”和“行动”之间交替进行——先思考当前情况再决定调用哪个工具看到工具结果后继续思考下一步直到得出最终答案。这里面最关键的转变在于从“我给你一段话你给我一个回复”变成“我给你一个目标你自己拆解步骤并调用工具去完成”。1.1 它解决的核心问题让模型学会“自己决定下一步”普通的大模型调用是什么样的用户发一句“这个商品评论是广告吗”模型凭训练时的知识做一个猜测式回答。这种模式在开放域闲聊里没问题但在业务系统里致命模型不知道你们公司的审核规则、不知道历史封禁记录、不知道某条关键词的权重它只能“瞎猜”。ReAct 模式完全不同。模型遇到问题后会先推理“这个问题需要判断是否存在广告嫌疑我应该调用内容审核工具传入这条文本。” 工具返回命中关键词、信用分、风险等级之后模型再拿着这些结构化的结果做二次推理“工具返回命中 3 个高风险关键词且风险等级为高所以我判定这条内容不通过。”这个过程放大看就是循环Thought思考→ Action调用工具→ Observation观察结果→ Thought继续思考。Spring AI 在 OpenAi 的函数调用机制之上封装了这一套循环开发者只需要定义好工具、配置好模型剩下的循环由框架自动完成。这就是为什么我说 ReactAgent 是 Spring AI 项目从“玩具”走向“生产力工具”的分水岭。之前你用 ChatClient 的时候所有的逻辑分支都得自己在 Java 代码里写 if-else到了 ReactAgent 阶段决策分支开始由模型接管而动作执行仍然由你的代码控制。安全性、可控性和灵活性同时拿到了。1.2 为什么到了第 9 章才讲“或跃在渊”“或跃在渊”出自易经乾卦意思是你已经积累到一定阶段可以试着往上跳一步了——但前提是有把握、看准了再动。这个形容放在 Spring AI 的学习路径上非常贴切。前几章如果讲的是“潜龙勿用”也就是 Spring AI 的基础配置、普通对话、系统提示词调优那这一章就是你开始“跃”的阶段。所谓“跃”体现在两个层面第一从单次调用跃迁到带状态的循环调用。模型不再只回答一次而是在一轮请求里多次推理、多次调用工具、多次修正判断。这对请求链路、日志追踪、异常处理的要求都更高了。第二从纯文本跃迁到结构化业务结果。ReactAgent 的输出往往要对接下游系统——比如审核结果要入库、要通知人工复核、要触发限流。这意味着你不能再拿一个字符串当结果得设计对象结构、定义判定阈值、约定工具返回格式。这一章我建议你先别急着追求花哨的多 Agent 编排而是把单 Agent 的“推理行动”循环吃透。因为后面所有复杂架构到最后拆解下来都是这一个循环的变体。2. 动手前必须想清楚的三件事模型、工具与提示词我在网上看到不少人问“springai 系统提示词怎么配置”问完提示词又问“为什么我的工具不被调用”。这两个问题其实属于同一个问题你还没有把模型、工具、提示词三者当作一个整体来设计。ReactAgent 的成功率不取决于单一环节而是三者之间的契约是否清晰。2.1 选对模型和 Spring AI 版本别让 Agent 输在起跑线上先聊版本。Spring AI 从 0.8.x 到 1.0.x 有一个很大的 API 变动尤其是函数调用相关封装。我建议你直接使用Spring AI 1.0.x 的稳定版本最好是 1.0.0 GA 之后的版本因为早期的 1.0.0-M 系列里Tool注解和ToolCallback的行为还不够稳定。当前主流的做法是引入spring-ai-openai-spring-boot-starter模型使用 OpenAI 兼容接口也可以接 Ollama 或者国内各家提供 OpenAI 兼容 endpoint 的模型。选模型这件事比选版本更影响实际效果。我的经验是函数调用能力强的模型ReactAgent 的成功率直接上一个台阶。如果你用的模型在普通对话里表现挺好但一旦给它两个工具它就频繁出现“假装调用工具但传参格式错误”、“答非所问”的情况那大概率是模型的 function calling 能力不够。这种情况下先别急着改提示词先换模型实测一轮你会发现世界清爽很多。在application.yml里最基础的配置长这样spring: ai: openai: base-url: ${AI_BASE_URL:https://api.openai.com} api-key: ${AI_API_KEY:sk-xxx} chat: options: model: ${AI_MODEL:gpt-4o-mini} temperature: 0.2 max-tokens: 4096注意temperature我刻意调到了 0.2。审核类任务追求稳定性和可复现性温度越低模型每次给出的判断越保守、越不容易发散。如果是做创意类任务可以调高但 Agent 工具调用场景我不建议超过 0.5否则模型可能在一个工具结果上衍生出各种奇怪的解读。2.2 工具函数的命名与参数设计直接影响 Agent 的“开火率”很多人在工具调用上犯的第一个错误是把工具方法当普通 Java 方法来写——名字叫check、参数叫String s也没有在Tool注解里写清楚这个工具什么时候该用。结果模型面对几十个工具时根本不知道该选哪个逻辑直接退化成一问一答。工具方法本质上是在给模型写“说明书”。你需要从模型的角度去审视三个东西方法名要包含明确的业务动作。比如checkContentRisk(String content)就比check(String content)好得多。模型通过方法名判断工具意图命名越具体命中率越高。参数描述要写清楚类型、范围、含义。尤其是当你用 record 或 DTO 做参数对象时字段的注释必须完整。我见过一个案例工具参数里有一个threshold字段描述只写了“阈值”模型调用时完全不知道该传什么。后来改成“审核阈值范围 0-100低于该值判定为低风险建议默认 60”模型的调用准确率立刻上来了。返回值要结构化。不要返回一个超长的 JSON 字符串模型解析起来费劲还容易截断。尽量返回一个精简的对象包含riskLevel、hitKeywords、score这几个关键字段即可。下面是我在项目里常用的一个工具方法写法可以直接参考Component public class ContentReviewTools { Tool(description 对用户提交的文本进行风险审核返回风险等级与命中的关键词列表) public ReviewResult checkContentRisk(String content) { ListString hitKeywords keywordRuleEngine.match(content); int score calculateRiskScore(content, hitKeywords); String riskLevel score 80 ? HIGH : (score 50 ? MEDIUM : LOW); return new ReviewResult(riskLevel, score, hitKeywords); } }这里的关键是Tool注解里的 description。Spring AI 会把这个描述连同方法签名一起发给模型描述写得越贴近业务语言模型越容易在正确的地方调用它。别写成“检查”要写成“对用户提交的文本进行风险审核返回风险等级与命中的关键词列表”这样模型才知道这个工具的适用场景。2.3 系统提示词怎么配置审核类任务的典型写法系统提示词在 ReactAgent 里的权重比普通对话场景高得多原因很简单普通对话里模型只需要回答但 Agent 场景里模型要做决策。你必须在系统提示词里把决策规则、工具使用边界、输出格式约束都交代清楚。我遇到过一种情况模型明明有工具但它就是不用自己凭感觉写了一个审核结论。排查后发现问题出在系统提示词里我只写了“你是审核助手”这种空话完全没告诉模型必须调用工具才能下结论。一个审核类 Agent 的系统提示词最少要包含四部分角色定义你是谁你在什么场景下工作你的责任边界是什么。工具使用规则什么情况下必须调用哪个工具禁止在未调用工具的情况下直接给出业务结论。判定规则拿到工具结果后怎么解读比如高分关键词命中多少条算高风险。输出格式最终回复要包含哪些字段比如审核结论、风险等级、是否需要人工复核。具体到提示词模板我会这样写你是一名内容审核助手。你必须基于工具返回的结果进行判断禁止凭空臆造审核结论。 工作流程 1. 收到用户内容后先调用 checkContentRisk 工具获取风险评分。 2. 根据工具返回的 riskLevel 给出最终审核结论。 3. 如果 riskLevel 为 HIGH给出拒绝通过的处理意见并标记需要人工复核。 输出要求 以 JSON 格式输出{decision:PASS/REJECT,riskLevel:...,reason:...}在 Spring AI 里系统提示词通过.system(...)方法传入。这里有一个容易被忽略的细节如果你的工具返回结果足够结构化你甚至不用在提示词里写太多判定规则因为模型会基于上下文里的工具输出自行推理。但如果你对审核标准有硬性要求就必须明确写出来否则模型会用自己训练数据里的标准来判。3. 实操从零实现一个内容合规审核 Agent前面把理论和设计原则讲清楚了下面进入完整的实操环节。我会按“创建工程 → 定义工具 → 组装 Agent → 验证链路”的顺序走一遍你跟着做就能得到一个可运行的 ReactAgent 示例。我以 Spring Boot 3.2 Spring AI 1.0.x Maven 为例Java 版本要求 17 以上。示例场景是文本内容风险审核用户提交一段文字Agent 调用审核工具基于关键词规则和历史数据给出“通过/驳回/人工复核”的结论。3.1 创建工程与基础依赖配置第一件事在pom.xml里引入基础依赖。如果你是直接用 start.spring.io 生成的项目只需要补上 Spring AI 的 starter 即可。注意 Spring AI 的 BOM 加上之后版本号会由 BOM 统一管理不容易冲突。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency /dependencies然后配置application.yml我把模型参数单独拎出来用环境变量覆盖方便在不同环境切换spring: application: name: react-agent-demo ai: openai: api-key: ${AI_API_KEY} base-url: ${AI_BASE_URL:https://api.openai.com} chat: options: model: ${AI_MODEL:gpt-4o} temperature: 0.2 max-tokens: 4096配置好之后写一个简单的ChatClient测试接口确认模型连通正常再往下继续。千万不要跳过这步直接搞 Agent不然后面分不清是模型问题还是代码问题。3.2 实现核心审核工具类工具类是这个 Agent 的“手”。我把审核逻辑单独放在一个ContentReviewTools类里用Tool注解暴露给模型。实际业务中你可以在这里面接规则引擎、查数据库、调外部审核 APISpring AI 不限制工具内部实现只看入参和返回值。我先定义一个结果对象public record ReviewResult(String riskLevel, int score, ListString hitKeywords, boolean needManualReview) {}再定义工具类这里用一个简化版的本地关键词规则引擎。为了演示效果我加了一个模拟分数命中高权重关键词就加 40 分命中普通关键词加 15 分累计超过 80 判为高风险。Component public class ContentReviewTools { private static final MapString, Integer KEYWORD_WEIGHT Map.of( 免费领取, 40, 加微信, 25, 点击链接, 30, 稳赚不赔, 45 ); Tool(description 对用户提交的文本进行风险审核返回风险等级、风险分数、命中的关键词列表以及是否需要人工复核) public ReviewResult checkContentRisk(String content) { if (content null || content.isBlank()) { return new ReviewResult(LOW, 0, List.of(), false); } ListString hitKeywords new ArrayList(); int score 0; for (Map.EntryString, Integer entry : KEYWORD_WEIGHT.entrySet()) { if (content.contains(entry.getKey())) { hitKeywords.add(entry.getKey()); score entry.getValue(); } } // 模拟长度异常等额外风险因子 if (content.length() 200) { score 10; } String riskLevel score 80 ? HIGH : (score 50 ? MEDIUM : LOW); boolean needManualReview score 50 score 80; return new ReviewResult(riskLevel, score, hitKeywords, needManualReview); } }注意这里我把风险阈值直接写死在工具里。这样做的优点是模型不需要自己做数学计算工具返回什么就是什么判定结果可复现。缺点则是每次调阈值都要改代码。如果你希望更灵活可以把阈值作为工具参数传进去但我个人建议能用参数传递的别用自然语言表达因为模型传参的随机性会让结果变得不稳定。3.3 组装 ReactAgent 并实现流式接口工具定义好之后剩下就是组装。在 Spring AI 1.0.x 里ChatClient的构建器支持.defaultTools(...)方法传入工具对象后框架会自动把工具方法转换成模型可识别的 function calling 格式并在对话过程中自动完成“推理 → 调用 → 观察 → 再推理”的循环。下面这个配置类就是核心装配逻辑Configuration public class ReactAgentConfig { Bean ChatClient chatClient(ChatClient.Builder builder, ContentReviewTools reviewTools) { return builder .defaultSystem( 你是一名内容审核助手。你必须基于工具返回的结果进行判断禁止凭空臆造审核结论。 工作流程 1. 收到用户内容后先调用 checkContentRisk 工具获取风险评分。 2. 根据工具返回的 riskLevel 给出最终审核结论。 3. 如果 riskLevel 为 HIGH直接给出拒绝通过的处理意见。 ) .defaultTools(reviewTools) .build(); } }这里有两个细节值得说。第一defaultSystem里明确写了“先调用工具再给结论”这句话在 ReactAgent 里等于给模型下了一道死命令。如果去掉模型很可能跳过工具自己编结论尤其在你用的模型比较“自信”的时候。第二defaultTools接收的是对象实例Spring AI 会扫描对象里所有带Tool注解的方法。你也可以用ToolCallback接口手动构造一个 callback但日常开发用注解方式最省事。接下来写一个 REST 接口接收待审核文本返回模型生成的审核结论RestController public class ReviewController { private final ChatClient chatClient; public ReviewController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping(/review) public String review(RequestBody ReviewRequest request) { return chatClient.prompt() .user(request.content()) .call() .content(); } }到这里一个最简版的 ReactAgent 审核服务已经能跑了。你发一段带“免费领取”之类的文本过去模型会先调用checkContentRisk拿到riskLevelHIGH之后再输出“拒绝通过”的审核意见。3.4 用真实请求验证整个链路接口写完后我用 curl 做一组真实的验证测试。先测高风险文本curl -X POST http://localhost:8080/review \ -H Content-Type: application/json \ -d {content:点击链接免费领取大礼包加微信领红包}正常情况下的输出会是一个 JSON{ decision: REJECT, riskLevel: HIGH, reason: 工具检测到命中关键词免费领取、加微信、点击链接风险分数为95判定为高风险建议不予通过 }再测低风险文本curl -X POST http://localhost:8080/review \ -H Content-Type: application/json \ -d {content:今天天气不错适合出去走走}这个时候模型应该调用工具返回LOW风险然后输出{ decision: PASS, riskLevel: LOW, reason: 未检测到风险关键词内容正常 }验证的时候重点关注两件事一是日志中是否出现了 tool call 记录二是模型是否真的基于工具结果做结论。如果你发现模型输出里没有引用任何工具返回的字段说明它没有真正“看到”工具结果这时候要回去检查系统提示词和工具描述。4. 排查实录ReactAgent 最常见的问题与对策ReactAgent 的坑和普通接口调用完全不同。接口调用里报错会立刻暴露Agent 场景里模型往往“一本正经地犯错”而且错误方式千奇百怪。我把自己踩过的坑整理成了一份排查清单每一类都给出我实测有效的解决方式。4.1 工具触发率低先看这四处如果模型始终不调用工具或者偶尔调用偶尔不调优先按这个顺序排查第一处看系统提示词。有没有明确告诉模型“必须调用工具才能回答”如果没有补上这句话触发率会立刻上升。实测中“禁止凭空臆造结论”这句话权重极高模型会为了不违反指令而主动找工具。第二处看工具描述。你的Tooldescription 是否让模型理解了使用场景我之前把描述写成“检查内容”模型死活不用改成“对用户提交的文本进行风险审核”同一套配置马上就正常了。模型判断工具意图主要靠 description别吝啬字数。第三处看入参类型。如果你的工具参数是对象类型但字段描述模糊模型可能在构造参数时犹豫最后干脆不走工具调用。解决办法是把参数对象设计得扁平化字段少、描述清晰任何字段都给出取值范围。第四处看模型能力。换一个 function calling 能力更强的模型做 A/B 测试。如果你的模型在工具选择上反复无常大概率是模型本身不行别在提示词上浪费时间了。4.2 模型“自问自答”不调工具我做了什么这是第二个高频坑系统提示词里写了“必须调用工具”模型也理解了但它就是不发出 tool call而是把工具执行结果用自然语言模拟了一遍——比如“根据审核工具这条文本属于高风险”。听起来没错但它调用的工具是它编出来的不是真实代码执行。出现这种情况说明模型把“工具调用”当成了“可以脑补的动作”它认为只要描述了工具的行为就等价于执行了工具。这时候我推荐一个组合拳方案在系统提示词中强调工具返回结果的可信度明确写“只有工具返回值才是唯一可靠的判断依据”。升级模型版本。部分旧模型的 function calling 实现不够严格容易出现模拟行为。把工具的副作用做重。比如在工具方法里打日志、记录调用次数Response 里返回一个自增的requestId。观察输出里是否包含真实工具返回的 requestId就知道它到底有没有调用。我在项目里用第三种方法做过验证效果非常直观。工具每次被真实调用都会生成一个调用流水号模型如果引用了这个流水号说明它确实拿到了工具结果如果没有引用八成是它在自问自答。4.3 上下文爆炸与循环调用的兜底策略ReactAgent 的多轮推理天然会消耗更多 token一次请求可能包含多次工具调用产生的中间结果。如果模型在判断上反复横跳比如“再调一次工具确认一下”一轮对话就能烧掉几万个 token。这在生产环境是不可接受的。应对方案有两个层面。第一层是静态限制。给模型设置max-tokens上限防止单次生成过长。但对于多轮工具调用导致的 token 累积max-tokens只能限制单次生成管不了整个对话历史。第二层是对话记忆裁剪。Spring AI 提供了ChatMemory和MessageWindowChatMemory可以把历史消息限制在一个窗口内。ReactAgent 场景里前面的工具调用记录如果已经完成了使命就没必要全部保留。我在审核 Agent 里通常把窗口设在 20 条消息以内既能保证足够的上下文又不会让 token 消耗失控。对于循环调用我一般会在工具结果里埋一个“终止信号”。比如我的ReviewResult里有一个needManualReview字段当它为 true 时模型应该结束思考并输出“需要人工复核”。在系统提示词里我会明确写一旦工具返回needManualReviewtrue立即停止进一步调用工具转人工处理。这个规则比单纯限制循环次数更可靠因为它给了模型一个清晰的结束条件。# 另外Spring AI 支持通过配置限制对话轮次但不同版本参数名有差异 # 如果你的框架版本较新可以查找 maxAttempts 相关配置如果确实需要在代码层面限制循环轮次更稳妥的办法是自己在工具方法里维护一个计数器超过指定次数直接返回一个“已达最大调用次数请基于现有信息作答”的结果。这个方案不依赖框架版本任何场景都通用。5. 最后再分享几个我实际踩过的经验这一篇从 ReactAgent 的原理讲到内容审核 Agent 的完整实现最后再聊几个实操层面的心得体会。第一ReactAgent 的调试核心是观察模型的选择逻辑。不要把它当普通接口来测只关心输入输出。你要打开日志看模型每一步 thought 的走向是它判断需要调用工具但选错了工具还是它根本没想到调工具。这两种问题的解法完全不同。第二提示词里任何时候都不要写死“必须返回 JSON”这种话除非你同时规定了 JSON 的结构。我在很长一段时间里只写了“以 JSON 格式输出”结果模型有时候给我包一层 markdown 代码块有时候输出数组解析逻辑写到我怀疑人生。后来我严格规定了字段名、字段类型和示例解析才稳定下来。第三也是最想强调的一点别让模型决定所有事。审核、风控这类场景敏感词规则、封禁阈值这类确定性的逻辑应该放进工具方法里用 Java 代码处理模型只负责“理解意图”和“组织表达”。把不稳定的决策留给模型本身就是一种风险。所谓“或跃在渊”就是知道自己能跳多高也知道什么时候该收手。工具函数是你的手模型是你的脑你要做的是让手更稳而不是让脑去干手的活。按这个思路去做ReactAgent 在 Spring AI 项目里就不只是炫技是真能扛业务的。
返回列表