ARTICLE DETAIL

资讯详情

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

Spring AI ReactAgent实战:内容智能审核Agent开发解析

Spring AI ReactAgent实战:内容智能审核Agent开发解析 这个系列拖到第9篇终于要碰 Agent 了。前8篇我们都在单模型调用和提示词工程里打转ChatClient 用得再熟本质上还是一次问答而 ReactAgent 这类实现把模型从应答者变成了执行者——它能自己决定调用哪个工具、看结果、再决定下一步。标题里的或跃在渊出自乾卦九四讲的是龙在跃起之前反复试探的状态用来形容 ReAct 循环非常贴切模型每走一步都要停下来观察再决定是继续行动还是给出答案。这篇我会围绕 Spring AI 的 ReactAgent结合通义千问生态用一个内容智能审核的完整示例把原理和代码都过一遍。适合已经跑通过 ChatClient、想把工具调用串联成真正 Agent 的开发者。1. 从ReAct范式说起ReactAgent的或跃在渊1.1 为什么普通ChatClient干不了干活的活先说一个很常见的误区。很多人觉得我已经能调用工具了为什么还需要Agent确实Spring AI 的 Function Calling / Tool Calling 已经让模型可以发起工具调用比如你定义一个getCurrentWeather()模型回答北京今天多少度时会自动请求调用它。但请注意单次对话里的工具调用通常只有一轮——模型请求调工具、你执行、把结果塞回上下文、模型再给最终回复一次调用链就结束了。真实世界的任务没那么简单。比如帮我把这批订单按风险分级先查每个客户的信用分再查历史退货率最后结合物流时效输出一个处理建议。这需要模型连续查三个不同的数据源而且第二次查询的参数很可能依赖第一次查询的结果。你如果用普通 ChatClient 手写就得在代码里硬编码先查A再查B再查C的顺序模型失去自主判断能力更麻烦的是当用户问法变了你的编排逻辑又要改。ReactAgent 要解决的正是这个把思考-调用-观察-再思考的循环交给模型自己驱动。用个生活化的类比。你向路人打听一家很难找的店正常人不是一口气背出完整路线而是走一段、看路牌、再问一次。大模型也一样它无法通过训练数据知道当下的实时库存、用户风险分、数据库里某条记录它必须靠工具去看。ReactAgent 就是那个边走边看路牌的行人。1.2 ReAct循环四要素Thought、Action、Observation、Final AnswerReAct 这个名字来自 Reasoning Acting 两词的组合早年论文里提出的思路直到今天仍是多数 Agent 框架的底座。它的循环可以拆成四步Thought思考模型描述当前状态。用户要求审核一段文本我需要先调用违禁词检查工具。Action行动模型结构化地输出要调用的工具名和参数。这一步在 Spring AI 里会被 ObservationParser 解析成具体的工具调用。Observation观察工具执行后返回结果这个结果以消息形式放回上下文作为模型下一步思考的依据。Final Answer最终回答模型认为已经拿到足够信息不再调用工具直接输出对用户问题的答复。理解这个循环有个好处你排查 Agent 问题的时候本质上就是在人工扮演这个循环——把模型每一轮的原始输出捞出来看看它 Thought 得对不对、Action 解析出来没有、Observation 是否被正确放回上下文。如果不理解这个循环你会觉得 Agent 是个黑盒出了问题只能干瞪眼。或跃在渊的意境在这里很贴切模型每完成一次 Action并不知道下一步一定对它只是跃出去试一下靠 Observation 这个渊里的反馈来校准方向。循环多次最终才敢给出 Final Answer。1.3 Spring AI 把循环封装成了 Agent 抽象Spring AI 1.0 GA 之后官方把 Agent 做了标准化抽象。你不需要自己维护 while 循环也不用自己拼 ReAct 提示词模板核心入口是Agent.builder()常见配置项有这么几个chatClientBuilder指定 Agent 内部使用的 ChatClient模型能力就由这里决定。tools注册工具集合通常是带有Tool注解的 Spring Bean 方法。memoryAgent 工作记忆的存储实现默认有SimpleMemory也可以换成按 Token 预算裁剪的TokenBudgetChatMemory。maxIterations最大循环次数防止模型陷入死循环。observationParser把模型 Action 文本解析成可执行调用的解析器Spring AI 内置了默认实现也可以替换成带日志的自定义解析器。systemPromptAdvisors/userPromptAdvisors提示词增强器用来注入系统提示词、任务说明等。需要提醒的是Spring AI 版本迭代非常快API 命名在小版本之间可能有调整。我的建议是锁定一个 GA 版本以官方文档的 API 为准下面的代码示例我会注明基于 1.1.x 的写法逻辑核心思想不变。2. 接上通义千问依赖配置与系统提示词的坑2.1 用 Spring AI Alibaba 对接 DashScopeSpring AI 项目本身不绑定任何云厂商OpenAI、Ollama、通义千问都能接。阿里生态对应的是 Spring AI Alibaba 的 DashScope Starter。引入依赖时注意用 BOM 统一管理版本避免starter之间版本冲突。大致结构如下具体坐标要以官方仓库为准dependencyManagement dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.1.0/version typepom/type scopeimport/scope /dependency /dependencyManagement dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-alibaba-starter-dashscope/artifactId /dependency配置文件里设置模型参数spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/api/v1 chat: options: model: qwen-plus模型选择上Agent 场景和单次问答的选型逻辑不一样。Agent 一次完整任务可能循环调用模型 5 到 10 次Token 成本和响应延迟都会成倍放大。实测下来qwen-plus 在性价比上最均衡做审核、信息抽取这类多轮工具调用任务完全够用如果任务需要复杂推理和长文本理解再切 qwen-max。不要一上来就追求最强模型先让链路跑通再根据效果升级。2.2 系统提示词到底该怎么配springai 系统提示词怎么配置是很多人最先遇到的问题。普通对话里你可能会写String answer chatClient.prompt() .system(你是一个严谨的审核助手) .user(请审核这段内容) .call() .content();这在单次问答里没问题但放在 ReactAgent 里就要小心。ReactAgent 内部有一套自己的 ReAct 提示词模板它要求模型输出结构化内容Thought、Action、Final AnswerAgent 靠 ObservationParser 去解析。你在自定义系统提示词里如果写了直接回答用户的问题不要调用任何工具之类的指令模型就会忘记输出 Action 格式导致解析器拿到空结果Agent 循环空转。正确做法是把自定义系统提示词当作任务角色 行为约束接入 Agent Builder而不是覆盖掉内置的 ReAct 格式指引。Spring AI 提供了systemPromptAdvisors和defaultSystem等入口示例Agent agent Agent.builder() .chatClientBuilder(builder - builder .defaultSystem( 你是一名专业的内容审核专员。 你必须使用审核规则工具逐条检查输入文本 不允许跳过工具直接给出结论。 当所有检查都完成后再输出最终审核结果。 )) .tools(auditService) .memory(new SimpleMemory()) .maxIterations(8) .build();这条系统提示词依然在但它不会破坏 ReAct 的结构化输出要求。我在实战里看到过太多人把 Agent 的系统提示词写得极其复杂恨不得把工具返回格式都写进去结果模型被绕晕Action 格式一乱整个 Agent 就废了。系统提示词只负责角色和约束格式指引交给框架内置模板这是 Agent 场景和普通 ChatClient 提示词配置最本质的区别。2.3 先验证工具是否真的暴露给了模型很多情况下 Agent 不调用工具问题不是提示词而是工具压根没注册成功。有个很实用的调试手段在启动日志里打印注册的 ToolCallback 列表或者直接用一个临时接口问模型你现在能用哪些工具。Spring AI 会把每个Tool方法转成 ToolCallback工具的名称、描述、参数 Schema 都会发给模型。如果模型回答没有任何工具可用那问题一定出在 Bean 注册或工具扫描上不要浪费时间调提示词。这类问题我遇到的频率相当高尤其是把Tool方法写在非 Spring 管理的类里时工具静默失效Agent 却表现得很蠢——它不调用任何东西凭幻觉直接给答案。检查这一步能省掉后面大量排查时间。3. 内容审核Agent实战从工具到可运行链路3.1 场景设计智能审核到底审什么为了把 ReactAgent 讲透我搭一个很典型的业务场景UGC 内容审核。运营每天要处理大量用户留言人工看不过来需要一个智能审核 Agent 帮我们判断这条内容能不能发、要不要进人工复审。这里的智能不是让模型自由发挥恰恰相反审核的关键是刚性规则和可追踪的过程。我设计了三个工具checkIllegalWords检查违禁词和风险词checkPrivacy检查隐私信息手机号、身份证号、地址等queryUserRisk查询该用户的历史违规记录。这三个工具的返回都是结构化对象Agent 拿到结果后综合判断最后输出直接通过 / 转人工复审 / 拦截三种结论。之所以拆成三个工具而不是一个大工具是为了让每个 Action 简单明确、返回体量小模型更容易做出正确的工具选择观察结果也更清晰。这个拆法在实际项目中比一个大而全的工具稳定得多。3.2 完整代码Tool定义与Agent构建工具类直接用Tool注解定义Service public class AuditService { Tool(检查文本是否包含违禁词或高风险词汇返回违规词列表与风险等级) public CheckResult checkIllegalWords(String text) { // 实际场景查词库/规则引擎 return new CheckResult(pass, List.of(), low); } Tool(检查文本是否包含手机号、身份证号、地址等个人隐私信息返回命中类型与风险等级) public CheckResult checkPrivacy(String text) { // 实际场景用正则或NLP服务 return new CheckResult(hit, List.of(phone), medium); } Tool(根据用户ID查询该用户的历史违规记录返回用户风险等级) public UserRisk queryUserRisk(Long userId) { // 实际场景查数据库/风控服务 return new UserRisk(userId, 3, high); } }Agent 的构建代码Configuration public class AuditAgentConfig { Bean Agent auditAgent(ChatClient.Builder chatClientBuilder, AuditService auditService) { return Agent.builder() .chatClientBuilder(chatClientBuilder::build) .systemPromptAdvisors(SystemPromptAdvisors.system( 你是一名严格的UGC内容审核专员。 必须依次调用checkIllegalWords、checkPrivacy、queryUserRisk三个工具 所有工具都执行完之后综合风险结论输出最终审核决定。 最终审核决定只允许输出直接通过、转人工复审、拦截。 )) .tools(auditService) .memory(new SimpleMemory()) .maxIterations(10) .build(); } }调用处非常简单String result auditAgent.chat(用户留言想加微信聊电话138xxxx1234有懂行的来。);注意maxIterations我设成了 10而不是默认值。原因后面会讲但在审核场景里模型至少要执行三次工具调用才能覆盖三个维度迭代上限设小了任务会被截断。3.3 跑通一次之后的执行链路复盘第一次跑通时我把每一轮 Agent 内部的状态打了出来下面这段是加了日志后的真实链路形态Question: 用户留言想加微信聊电话138xxxx1234有懂行的来。 Thought: 用户留言包含联系方式需要先检查是否有违禁词再检查隐私信息。 Action: checkIllegalWords(text想加微信聊电话138xxxx1234有懂行的来。) Observation: 未发现违禁词风险等级 low。 Thought: 违禁词通过继续检查隐私信息。 Action: checkPrivacy(text想加微信聊电话138xxxx1234有懂行的来。) Observation: 命中手机号风险等级 medium。 Thought: 文本包含手机号存在隐私泄露风险需要结合用户历史行为判断。 Action: queryUserRisk(userId10086) Observation: 该用户历史违规3次风险等级 high。 Thought: 综合隐私风险与用户历史风险该内容不适合直接发布。 Final Answer: 转人工复审。复审重点泄露手机号且用户历史风险高。很多人第一次看到这段日志会问模型真的想了吗其实不用纠结模型是不是真的有意图重要的是行为模式——它确实按照先查A、再看B、最后查C的顺序依赖前一步的 Observation 做决策。如果直接给一个不带工具信息的普通 ChatClient它只会回答这条留言包含手机号建议不要发布完全不会去核实用户历史这就是 Agent 和普通问答的本质区别。4. 落地过程中踩过的坑与完整排查链路4.1 模型不按套路出牌Action格式丢失我最早踩的坑是自定义系统提示词写得太聪明。我在提示词里加了如果你认为内容没问题可以直接给出通过结论不需要调用工具。结果模型真的就不调用工具了直接 Final Answer。原因很简单我主动给模型提供了一个绕过工具的路径而 ReAct 的内置模板要求它先思考再行动两者发生冲突时模型倾向于遵循更具体的指令。排查链路是这样的第一步打开 Agent 日志发现模型第一轮就输出 Final Answer没有 Action第二步检查系统提示词发现可以直接给出通过结论这句话第三步删掉这句保留所有检查完成后才能输出结论重新跑模型恢复正常调用链。永远不要在 Agent 系统提示词里给模型可以不调用工具的例外指令除非你有意设计一个轻量分支。4.2 工具返回数据把上下文撑爆了第二个坑是工具返回体量失控。最初我把queryUserRisk设计成返回该用户最近20条违规记录详情每条记录还带原文摘要。结果模型一轮循环下来上下文里多了一两千 Token 的 Observation而且随着 Agent 循环深入这些历史 Observation 还会被反复带上。费用翻倍、响应变慢还是小事更麻烦的是模型容易在大量日志文本里迷失后续 Action 参数开始出错。解决办法很简单工具在自己内部做好聚合和摘要返回给模型的永远是小而结构化的结果。queryUserRisk只返回历史违规次数 风险等级不返回明细checkPrivacy只返回命中类型 风险等级。如果确实需要明细给下游人工使用就写到数据库让工具返回一个记录ID。模型只需要足够的信息做决策不需要看原始明细。4.3 死循环与maxIterations的设参逻辑第三个坑是死循环。有段时间模型会反复调用同一个工具比如checkPrivacy连续调用三次每次返回都一样。后来我看了 Observation 才明白模型认为第一次返回的信息不够全面它期待我看到某个具体字段但返回里没有于是它再试一次。这在模型眼里是合理行为在业务上就是死循环。我的解法分两层。第一层工具返回里带上明确的是否已覆盖所有检查项字段模型一眼就能判断无需再试第二层maxIterations不能设太大也不宜用默认值。就我的经验简单任务 5 到 8 次足够复杂任务 10 到 15 次超过 15 次基本就是模型陷入某种偏执循环再给次数也只是烧钱。可以做个对照表供参考现象根因解决方案模型不调用工具直接回答系统提示词给了可直接回答的出口移除绕过工具的指令保留角色约束上下文暴涨、响应变慢工具返回大段明细历史Observation堆积工具内聚合摘要只返回决策所需字段反复调用同一工具返回结果里缺少是否完成检查的明确信号工具返回增加明确的终止标识任务进行到一半被截断maxIterations 小于所需工具调用次数根据任务轮次预估一般设8-124.4 容易被忽视的并发与Bean问题这个坑我犹豫要不要写因为太基础了但真的吃了亏。某次联调时测试同学同时开两个会话测同一个 Agent结果 A 会话的临检结果跑到了 B 会话里。定位后发现我的 Agent Bean 是单例里面用的memory也是共享的单例实例。ReactAgent 本身的设计里Agent 是配置定义memory 是会话状态两者不能混在一起。单例 Agent 没问题但每个用户会话必须持有独立的 memory或者用支持会话ID区分的 ChatMemory 实现。另外Tool方法所在 Bean 必须是 Spring 管理的且方法本身要无状态。如果你在工具方法里放了实例变量存当前用户并发环境下一定会串。工具方法只应该接收参数、查数据、返回结果绝不允许持有会话级状态。5. 生产化还要做的事记忆、观测、成本与边界5.1 工作记忆与会话记忆要分开Agent 里那个 memory我习惯叫它工作记忆——它装的是当前任务里模型自己的 Thought、Observation 等中间过程。这个工作记忆会随循环自动增长So 会话结束了就清理。但如果你要做多轮对话的 Agent用户上一轮说帮我审核一下我的留言这一轮说这个结果我看了再查一下另外一个用户你就得自己管好用户会话记忆把历史对话摘要或最近几轮消息存到 Redis 或数据库下一轮调用前塞回 Agent 的 user 上下文。最简单的做法是每次请求前重建 Agent memory把最近的用户消息放进去。不要图省事把几个月的历史全塞进去——Observation 已经占了大量 Token历史一多模型很快就会被小字淹没。5.2 把Thought/Action/Observation变成可观测日志生产环境里Agent 的输出不能只看最终结果每一步中间过程都要留痕。Spring AI 允许自定义ObservationParser我通常在默认解析器外面包一层把解析前的原始 Action 文本和解析结果都打印出来Observation 放回上下文前也截断打日志。这样线上出了争议内容你能完整复盘模型当时为什么做了这个判断——这在审核场景是硬需求。同时要看 Token 消耗。通义千问的响应里会带 usage 元数据Spring AI 的 ChatResponse 里也能取到。一个 Agent 任务的总 Token 消耗大约是所有循环轮次输入 Token 之和 输出 Token 之和。我实测上面审核案例大概 4 到 6 次循环输入累计约 1.5 万 Token输出约 5000 Token用 qwen-plus 成本很低但如果换成更贵的模型就要格外控制maxIterations和工具返回体量。5.3 安全边界审核Agent不能只靠模型自觉最后说安全。你做智能审核 Agent一定不要让模型感觉隐私风险高就拦截关键规则要在工具层刚性执行。比如手机号命中工具直接返回riskhigh模型只能基于这个 high 做判断不能自己把 high 降成 low。我的实践原则是模型负责编排和综合表达工具负责事实和规则权限边界全部下沉到工具层。违禁词、隐私信息这类强规则判断如果依赖模型自由裁量上线一周就会出事故。这个系列写到第9篇我自己最大的体会是ReactAgent 的 API 并不难难的是你愿不愿意把每一轮跃起都看清楚。模型每调用一次工具就是一次决策工具描述模糊、返回体量失控、系统提示词留了口子任何一个细节都会让循环越走越偏。把原理和排查链路吃透后面的多 Agent 协作才好继续——那才是真正飞龙在天的部分。
返回列表