
这个标题一看就是老江湖了。“降SpringAI”系列一路打过来能走到第9掌说明前面基础该会的都会了而我个人一直觉得这一掌是整套系列里真正迈过分水岭的一掌——或者换个说法学到这里才算是从“调接口”正式进入了“做Agent”的阶段。这个项目本身做的事其实很具体用SpringAI整合阿里云的通义千问大模型基于ReAct模式实现一个可以“思考调用工具自我纠错”的ReactAgent然后落在一个真实生产场景上——内容审核。为什么选审核而不是什么花哨的Demo因为审核场景对准确性、可控性、实时性的要求都够高是检验Agent能力的好样板而且这个场景里的问题你躲不掉模型胡说怎么办、工具调用错了怎么办、并发上来怎么扛。如果你正好在调研SpringAI能做多深、想看看ReAct Agent在Java生态里到底怎么落地、或者被“系统提示词怎么配置”这种问题卡住过这篇内容很适合你。下面全部基于本次项目的实际开发过程来写代码可以跑到有坑也一块儿说了。1. 先聊聊“或跃在渊”这层境界Agent开发才是真正的分水岭1.1 从Prompt调用到Agent认知上的一次跃迁很多人第一次接触大模型开发都是这样的写一个Prompt调一次接口拿到一段JSON完事。这套流程写起来确实快但它本质上只是在做“单轮问答”模型的推理能力完全没被调度起来。到了第二个阶段大家开始接触Function Calling。模型可以根据对话内容决定“要不要调用某个函数”比如查天气、查订单。这一层已经很有用了但它仍然是被动的——模型调用一次工具拿到结果回答完就结束了。没有反思没有多步规划更没有“调用失败了换个方式再试”的能力。而ReactAgentReAct模式即Reasoning Acting跟前两者有一个本质区别它把“思考”和“行动”变成了一个循环。打个比方单轮问答就像你去餐厅点菜告诉服务员吃什么他端上来就完事Function Calling像是你让服务员去后厨问一句有没有活鱼他回来告诉你结果而ReAct Agent则是一位真正会做饭的厨师他先看你菜单发现缺食材自己去冷藏库拿拿到一看不新鲜又换一种做法最后把成品端到你面前。整个过程它有判断、有动作、有观察、有修正。这也就是我为什么把它称为“或跃在渊”——这一层你在空中也在水中可进可退是真正的跃迁关口。做得好Agent能顶好几个人的重复劳动做不好你只是在写一堆脆弱的if-else。1.2 为什么选SpringAI而不是裸调API或另起炉灶先说结论Java生态里做AI应用2024年后我会优先选SpringAI。原因不复杂。第一我们团队的技术栈本身就是Spring BootSpringAI的集成成本最低。它不需要你额外引入一套全新的异步框架或消息体系依赖一加配置一写就能和现有的Controller、Service、MyBatis无缝共存。这一点在项目周期紧的时候值千金。第二SpringAI提供了一套相对统一的抽象层。不管是OpenAI还是通义千问在你代码里看到的接口是接近的。这意味着如果某天大模型供应商调整价格或策略你可以以较小的成本切换模型通道。这种灵活性对于上了生产的项目很重要。第三也是很多团队忽略的一点SpringAI的对话历史管理、Tool调用上下文、结构化输出比如JsonSchema这些坑它已经替你趟过了一遍。你要是自己裸调API这些全得手搓光是一个“如何正确地把工具调用过程和最终答案塞进上下文里”就能耗掉你两三天。至于为什么不选LangChain4j这类框架——不是说它不好而是Spring AI Alibaba这个子项目对国内模型的适配更“原生”。既然用的是通义千问那我直接对接百炼平台API Key一配就能跑少掉很多中间层的转换和版本对齐问题。顺便把选型对比放这儿大家自己看方案接入成本与Spring生态契合度工具调用支持国内模型适配裸调HTTP接口中低需自研需自研LangChain4j中中好一般需额外适配SpringAI 阿里云百炼低高好原生适配2. 环境准备与工程骨架能不能打得漂亮先看马步扎得稳不稳2.1 依赖引入与版本选型版本问题单拎出来说因为SpringAI的迭代速度真的不算慢。如果版本选错后面会遇到很多诡异问题比如ChatClient没有这个Bean、某个API类型对不上白白浪费时间。本次项目采用的是Spring Boot 3.2.x JDK 17 Spring AI Alibaba的稳定版本组合。依赖配置如下dependencyManagement dependencies dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-bom/artifactId version1.0.0-M3.1/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies这段配置是什么意思spring-ai-alibaba-bom帮你统一管理了SpringAI相关依赖的版本避免你手动指定版本互相冲突。starter则是把通义千问的客户端、自动装配全部带进来你不需要自己写那些网络连接的底层代码。2.2 配置文件的几个关键点然后是配置文件。SpringAI阿里的接入结构非常简单核心就是API Key和模型名spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.2这里有两个点在实际项目里容易踩坑。第一个是API Key别写成明文。${DASHSCOPE_API_KEY}这种方式是从环境变量里取值密钥不会进到Git仓库里。谁也不想因为一次代码泄露把大模型账单也搭进去。第二个是temperature这个参数。很多人第一次配的时候会忽略它但它在审核场景里非常关键。temperature即随机性数值越高模型回答越天马行空数值越低越稳定保守。审核任务需要的是稳定所以我把它压到了0.2。如果你做的是创意写作或头脑风暴Agent反而要把温度调高到0.7以上。这些配置是起始参数不是一次定死的。后面真正开始做审核Agent的时候你会发现光是这套基础配置就为后续调用工具、维护会话上下文提供了必要的骨架。2.3 为什么单单强调“连接层”设计这里多说一句。实际开发中我习惯在Service层和AI模型之间加一层自己的封装而不是直接在所有地方调用ChatClient。Service public class AiChatGateway { private final ChatClient chatClient; public AiChatGateway(ChatClient.Builder builder) { this.chatClient builder.build(); } public String call(String systemPrompt, String userMessage) { return chatClient.prompt() .system(systemPrompt) .user(userMessage) .call() .content(); } }这层Gateway的价值在于集中管理模型调用、统一处理异常、统一记录日志。等到排查线上问题的时候你会发现所有AI调用都有日志可查是多么幸福的一件事。3. ReactAgent核心机制拆解让模型“边想边做”而不是“一口吃成胖子”3.1 ReAct范式里到底在循环什么ReAct这个词来自论文《ReAct: Synergizing Reasoning and Acting in Language Models》它的核心思想是让模型在推理过程中交替输出三种内容Thought模型的思考过程比如“我需要先检查敏感词列表”Action模型决定调用某个工具比如“调用checkSensitiveWords方法”Observation工具执行后的返回结果被模型观察、解读这三者构成一个循环直到模型认为自己已经收集了足够的信息再输出Final Answer。这个循环的价值在于它把“决策”和“验证”分开。模型不会因为“我觉得这句话有问题”就直接下结论而是要调用工具、拿到数据、观察结果之后才能给出最终判断。3.2 在SpringAI里实现ReactAgent到底要写什么SpringAI本身提供了对工具调用的支持也就是Tool注解。被这个注解标记的方法会注册成模型可调用的“函数”。然后通过ChatClient的连接实现一个完整的Agent循环。我需要先定义好提供给模型使用的工具。比如在审核场景里一个典型的工具就是“查询敏感词列表”Component public class AuditTools { Tool(name querySensitiveWords, description 查询文本中包含哪些敏感词返回命中列表) public String querySensitiveWords(String text) { ListString hits sensitiveWordService.match(text); if (hits.isEmpty()) { return NO_HIT; } return SENSITIVE_WORDS: String.join(,, hits); } }这里工具有一个足够清晰、中文描述完整的description。这一点极其重要因为模型在决定调用哪个工具时依据就是方法名和描述。描述写得含糊模型就会在几个相似的函数之间犹豫甚至跳过这个工具直接瞎猜。然后需要把这个工具注册给模型调用。SpringAI中可以用ChatClient的tool方法传入工具类的实例Service public class ReactAgentService { private final ChatClient chatClient; private final AuditTools auditTools; public ReactAgentService(ChatClient.Builder builder, AuditTools auditTools) { this.chatClient builder.build(); this.auditTools auditTools; } public String audit(String text) { String systemPrompt 你是一个严格的UGC内容审核Agent。 你必须按以下顺序行动 1. 调用querySensitiveWords检查文本敏感词 2. 调用checkForbiddenPatterns检查违规格式 3. 基于工具返回结果输出最终审核结论。 如果没有调用工具就直接下结论视为违规。 ; return chatClient.prompt() .system(systemPrompt) .user(请审核文本 text) .tools(auditTools) .call() .content(); } }这个tools(auditTools)调用就是整个Agent循环的开关。当你把工具传进去之后SpringAI会处理模型端发起的Tool Call请求执行实际Java方法并将结果返回给模型然后模型继续推理。多轮工具调用、中途失败重试、最终生成结论这些过程被整合在了“一次call()”里面。从代码层面看你只是多传了一个参数但这个行为模式已经完全改变了——模型现在是个有手有脚的执行者而不只是一个会说话的脑袋。3.3 模型“频繁调用工具但拿不到结论”的问题处理实际运行时你还会发现一种典型情况模型会几次三番调用工具但迟迟不给最终结论。这在语义复杂、工具较多的场景里会更明显。针对这个问题比较有效的做法有两个。第一个是在系统提示词里给模型明确的“循环退出条件”。比如上面的提示词最后一句“如果没有调用工具就直接下结论视为违规”看似是在约束它不要跳过工具但我也习惯补一句反向约束“只要已经完成所有必要的工具调用必须立即输出最终结论不要做额外检查。”第二个做法是控制工具轮数上限。在SpringAI里部分场景需要你在代码层做兜底比如给每次Agent调用设置一个超时时间或者在提示词中限定“最多尝试3次”。这种边界控制越早设计越好否则模型在极端情况下会把Token消耗拖得很高影响成本和响应速度。4. 智能审核场景落地从一个想法到一套能上线的系统4.1 审核需求拆解Agent到底要接哪些能力我选的是UGC内容审核场景最典型的痛点社区每天产生大量用户评论和帖子内容安全要求高但纯靠人工审不过来。纯关键词过滤又太死板——一些违规内容会故意规避敏感词而一些正常内容又会被误伤。我把需求拆成了四个能力点对应四个工具。这个拆解过程本身就能帮你想清楚Agent的边界工具方法职责数据来源querySensitiveWords本地敏感词匹配本地敏感词库checkForbiddenPatterns检测广告引流、乱码、短链等异常格式规则引擎checkSemanticRisk语义层面的风险判断调用大模型pushManualReview推入人工复审队列数据库/消息队列工具设计的顺序是有讲究的。越廉价、越确定的能力越前置只有前两个工具拿不到确定的结论时才会走到调用大模型做语义判断这一步。这样设计是为了控制成本。4.2 审核链路的核心代码Agent怎么把整个流程串起来工具定义有了下一步是把它们织进Agent里。这里我给出一段更贴近实际生产的写法Component public class AuditTools { Tool(name checkForbiddenPatterns, description 检查文本中是否包含广告词、外部链接或异常字符返回异常类型) public String checkForbiddenPatterns(String text) { Pattern adPattern Pattern.compile((加微信|加QQ|扫码|http[s]?://)); Matcher matcher adPattern.matcher(text); ListString types new ArrayList(); while (matcher.find()) { types.add(AD_LINK); } if (text.length() 500 || containsGarbledCode(text)) { types.add(ABNORMAL_LENGTH_OR_GARBLED); } return types.isEmpty() ? NO_PATTERN : PATTERN_FOUND: String.join(,, types); } }最关键的部分是让Agent能根据这些工具结果做“分级处置”而不是简单粗暴地给一个“通过/拒绝”。我在系统提示词里明确了以下分级决策规则如果命中多个高置信度敏感词属于高风险直接拒绝并给出原因如果只有可疑模式但没有确凿命中进入语义风险判断如果语义判断依然无法确认推送人工复审队列。这一套分级逻辑让Agent的表现非常接近人工审核员的思路。我之前试过把决策规则全部写死在Java代码里遇到变体写法就漏也试过把所有判断交给大模型自由发挥结果误杀率奇高。现在让“大模型做推理决策、Java工具做确定性判断”准确率和可控性都上来了。4.3 结果结构化让模型输出能直接被生产消费的JSONAgent如果只是返回一段自然语言结论下游服务会很痛苦因为没法直接根据一段话去决定“要不要删除这条评论”。所以我在提示词里强制要求输出JSON格式{ verdict: PASS, riskLevel: LOW, reasons: [未命中敏感词], reviewedBy: react-agent }在代码里利用SpringAI的ChatClient配合EntityResponse来处理结构化输出可以直接把JSON映射到Java实体类省去手写JSON解析的麻烦。record AuditResult(String verdict, String riskLevel, ListString reasons, String reviewedBy) {} public AuditResult auditWithStructuredResult(String text) { return chatClient.prompt() .system(AUDIT_SYSTEM_PROMPT) .user(text) .tools(auditTools) .call() .entity(AuditResult.class); }即便如此模型偶尔还是会输出不合法JSON。所以我在生产代码里又加了一道兜底如果反序列化失败就按“人工复审”处理。宁可多审不可漏审。4.4 并发与异步不能让Agent成为系统的瓶颈审核系统往往要接入高并发的UGC流不可能每条内容都同步等待模型返回。串行调用肯定不行我加了线程池来隔离这个阻塞操作。这里也顺带说明模型的IO操作是耗时的一定要放在工作线程中执行避免阻塞Tomcat请求线程。Bean public ThreadPoolTaskExecutor auditExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(32); executor.setQueueCapacity(200); executor.setThreadNamePrefix(audit-agent-); executor.initialize(); return executor; }对于紧急、高优先级的风险内容可以走同步审核对于普通用户评论则先写入待审核队列异步执行Agent审核再回调更新状态。这套结构上线后很稳。5. 系统提示词配置从“玄学”变成可以工程化的东西5.1 系统提示词的基础架构追热搜搜“springai系统提示词怎么配置”的人不少这个问题确实常被问。其实从SpringAI的角度配置系统提示词就是.system(...)这一行调用但真正难的是提示词本身的内容结构。在本次项目里我把审核Agent的系统提示词设计成五个固定段落角色定义、任务边界、工具调用流程、输出格式、兜底策略。每个段落之间用空行隔开便于模型区分。这里直接放一个完整模板你们可以在自己的项目里改成自己的领域内容你是内容安全审核Agent服务于UGC社区平台。 你的职责是对用户发布的内容进行合规性判断。 每一步审核都必须按下面的工具流程来执行 第一步调用querySensitiveWords检查文本是否命中敏感词 第二步调用checkForbiddenPatterns检查文本是否包含引流、短链、异常格式 第三步如果前两步无法确定结论调用checkSemanticRisk做语义判断 第四步如果语义判断仍无法确定调用pushManualReview推送人工审核。 只有当全部必要工具调用完成之后才能输出最终结论。 不要输出任何工具调用过程之外的解释。 输出必须是JSON字段包括verdict、riskLevel、reasons、reviewedBy。 verdict只能取PASS或REJECT或MANUAL_REVIEW。这套模板的关键在于它不仅告诉模型“能做什么”还规定了“先做什么、再做什么、什么时候停”。这就是把Agent的推理路径约束在了业务允许的范围内。对比一下常见的错误写法——只是笼统地写“请审核这段内容”那模型有可能先做语义判断、不做敏感词检查还有可能干脆把工具结果当背景故事然后自己编一个结论。5.2 阿里通义千问对中文提示词的适配细节既然用了阿里云通义千问就得提一下它在中文提示词上的表现。几个实测下来的点千问系模型对“中文角色设定”的理解很稳所以不要为了追求“高级感”把提示词的关键部分用英文写直接中文反而效果更好。tool description也用中文写和模型的中文指令风格保持一致能明显提升工具选择的准确率。temperature建议在0.1到0.3之间因为做过审核任务随机性高了会带来不可控的判断波动尤其多条相似内容审核时尽量保持判断标准一致性。5.3 提示词版本管理与回归验证提示词不是一次性写好的它在项目生命周期里一定是要持续迭代。我习惯把每次变的提示词都保留一个版本配合一小批“黄金样本”做回归测试。所谓黄金样本就是同一批你知道该怎么判定的文本。每调整一次提示词就用同一批样本跑一遍对比判定结果的变化率。如果某次改动导致大量原先判PASS的内容变成REJECT说明提示词约束过紧要回退或调整。这样做的好处是提示词的优化有了数据评估标准而不是每次上线全靠感觉。个人经验把提示词单独存成配置文件或数据库记录不要直接硬编码在Java类里。等到你需要修一个小问题却不能改代码发版时就知道这个设计有多值钱了。6. 上线前后的坑与性能实测这部分才是真正值回票价的地方6.1 最坑的一次模型无视工具返回自己脑补结果上线初期我遇到过一次特别诡异的现象本地敏感词工具明明返回了NO_HIT模型却在最终结果里写“检测到敏感词建议拒绝”。后来把完整的Thought/Action/Observation日志拉出来看才知道是因为工具描述写得不够清晰模型在第一步“查询敏感词”之后又脑补了一次“语义判断”。修正方式是双管齐下一方面把工具描述写得足够明确标注“此函数仅返回机器匹配结果不包含模型推理”另一方面在系统提示词里明确规定“所有判断必须基于工具返回的文本不允许凭空补充工具未给出的信息”。这事的教训是在Agent项目里模型推理结果的可解释性不是可选项而是必须项。所以我后来把所有Agent交互的完整链路日志都存了下来包括模型每一次Thought文本、Action调用参数、工具返回结果。没有这套日志排这种问题基本靠猜。6.2 通义千问在SpringAI中的几个兼容性细节SpringAI Alibaba对通义千问的适配整体很顺畅但不是什么毛病没有。我记录下两个比较典型的。一个是模型名称的大小写和格式问题。DashScope平台上的模型名有qwen-plus、qwen-max这些写法配置里必须和平台完全一致多一个空格或后缀都不行。另一个是长时间运行后的连接池问题。默认HTTP客户端在长时间运行下可能出现连接复用失效这个不算SpringAI特有但AI服务的响应时间通常较长连接被提前关闭的概率更大。后来我在超时配置里做了调整把connectTimeout和readTimeout都适当加大连接池的空闲检测也做了优化才稳定下来。6.3 性能实测与成本控制的一组数据项目稳定运行一段时间后我整理了一组实测数据可以给正在规划类似项目的同学做一个参考审核方式平均单条耗时准确率参考单条成本量级纯关键词规则15ms较低误伤率高极低单次大模型调用1.5s中等中ReactAgent多工具轮询3s左右高误伤显著降低较高Agent 规则前置最快80ms前置阶段高大幅下降最后一个组合是我强烈建议关注的优化思路把规则引擎和关键词语料放在Agent的前置阶段进行初筛只有初筛存疑的内容才会走完整的ReactAgent链路。这个策略能把80%的明显合规内容挡在外部大幅降低模型调用量。我实际跑出来的成本数据里接入前置规则之后模型Token消耗大约下降了60%以上而整体准确率没有明显变化。这是本次项目里我个人最满意的一个能耗优化。6.4 日志、监控与召回机制的三件套最后再唠叨一句生产的可持续性。Agent系统上线之后你不能不管必须设计监控。我在项目里采用了三件套第一件事日志记录Agent完整的思考与工具调用链第二件事监控看板实时展示审核通过率、拒审率、人工复审率、平均耗时第三件事召回机制一旦线上发现漏审/误审案例立刻把它加入黄金样本集并触发一次提示词迭代。这三件套的最重要作用是让Agent系统的“演进”有一个闭环。AI应用和普通CRUD应用最大的区别就在这里CRUD系统上线后状态基本固定而Agent系统的判断规则会随着攻击手法、业务形态的变化慢慢失效。你不主动让它进化它就会被动退化。从我的实际体会来说做ReactAgent项目最关键的其实不是模型选多大、框架用多新而是你要始终知道自己想让Agent解决什么问题、它的判断边界在哪里、出问题时能不能快速定位和干预。把这些基础打牢了SpringAI这样的工具才能真正为你所用而不是给你添乱。最后分享一个我一直保留的小惯例开发阶段的每次Agent调用务必记录下完整的模型请求和响应报文。等哪天真出问题了这些记录就是你排查故障的唯一线索。别嫌日志量大Agent应用的可观测性决定了这项目是能陪你睡个好觉还是半夜爬起来查Bug。