ARTICLE DETAIL

资讯详情

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

Spring AI实战:ReactAgent+阿里云百炼构建Java智能体工具调用闭环

Spring AI实战:ReactAgent+阿里云百炼构建Java智能体工具调用闭环 我把这套Spring AI实战系列写到第九篇主题是“或跃在渊”主线终于从单轮对话推进到了带自主推理与工具调用闭环的ReactAgent。这一掌打完之后前面几篇攒下来的模型接入、提示词结构、输出解析能力会全部串起来项目里那些“让模型自己决定下一步干嘛”的需求算是有正经解法了。写这篇博文的动机很简单我最近在给一个内部项目加AI能力需求从“问答”直接跳到了“让AI帮用户查订单、查库存、改配置”如果还停留在普通ChatClient调用的层面根本撑不住这种多步骤任务。ReactAgent刚好补上这块加上Spring AI对阿里云百炼DashScope的OpenAI兼容接入一个纯Java技术栈的小团队也能把智能体跑起来不必为了Agent去单独引入Python那套生态。这篇尽量把从零搭到能跑通全讲透适合已经在Spring生态里写CRUD、又想给系统加智能体能力的开发者。1. 这一掌到底在打什么ReactAgent在Spring AI里的定位1.1 从“对话机器人”到“智能体”的分水岭“或跃在渊”这四个字放在这里其实很贴切前八篇练的都是底层的“气息”——模型怎么连、提示词怎么写、JSON输出怎么稳定解析都属于基本功到了这一篇龙要从深渊里往上跃了关键是它得有“判断力”。传统对话机器人和智能体的本质区别在于会不会主动行动。普通对话是“你问一句、模型答一句”模型再聪明也只能输出文字改不了系统的任何状态。而智能体最核心的能力是决策与执行解耦模型先分析用户目标决定需要调用哪个工具Action拿到工具返回的结果Observation再决定下一步干什么如此循环直到任务闭环。这个“推理-行动-观察”的循环就是ReAct模式Reason Act的核心。ReactAgent在Spring AI里就是这一模式的落地实现。它不像LangChain那样给你一堆抽象难啃的链式API而是用Spring Boot开发者最熟悉的方式——定义Bean、编写Tool注解的方法然后交给Agent调度。对Java团队来说这是把LLM塞进现有业务系统最顺的一条路。1.2 Spring AI为什么不直接照搬LangChain的方案市面上做Agent的框架不少LangChain是最出名的但Python技术栈和Java团队之间有一道无形的墙引入LangChain意味着要么起独立的Python服务要么搞跨语言调用日志、部署、监控全都要多一套。Spring AI的定位就很巧妙它选择成为“Java生态的AI基础设施”API设计上保留了Spring Boot一贯的约定优于配置依赖注入、自动配置、Actuator监控全都能复用现有经验。具体到ReactAgent实现上Spring AI没有把Agent做成一个神秘的“黑盒调度器”而是开放了清晰的接口ToolCallback负责封装工具逻辑和描述信息ToolCallingManager负责管理工具注册与调用结果回填ChatClient负责底层的模型对话。这些组件之间是标准接口组合关系你可以替换任何一层而不影响整体。这种设计的好处用生活场景类比就是你不是雇了一个全能的“神秘管家”而是搭建了一套“前台接待-专家顾问-记录员”协作机制。每个角色职责单一出了问题一眼就能定位。1.3 阿里云百炼在这套体系里的角色标题里提到的“阿里”指的是阿里云百炼平台。为什么选它做模型服务商原因很实际一是我所在的业务场景里部分数据必须走国内合规链路二是百炼平台提供了OpenAI兼容的接口规范接入Spring AI时只需要改一行Base URL配置之前为OpenAI写的业务代码几乎零改动。百炼在Architecture里的身份就是模型供给层负责提供基座模型能力和API的稳定接入。你可以在上面选择通义千问系列不同规格的模型基础问答用qwen-plus、复杂推理用qwen-max都通过在application.yml配一个模型名切换。在整套ReactAgent体系中百炼负责“思考”部分而工具执行、流程控制、状态管理全部由Spring AI和你的业务代码负责。2. 环境准备与基础配置先把模型服务稳稳接进来2.1 依赖清单与版本选择Spring AI的版本演进很快1.0.0 GA之后API已经相对稳定我建议新项目直接基于1.0.0及以上版本开发。Maven仓库坐标如下dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-react-agent/artifactId /dependency /dependencies需要注意spring-ai-react-agent这个模块是独立发布的只引入spring-ai-openai不会自动带上Agent能力必须显式添加。另外很多社区初学者会忘记配置Spring AI的仓库地址因为早期版本依赖在Maven中央仓库同步不及时我习惯一并加上官方仓库repositories repository idspring-milestones/id nameSpring Milestones/name urlhttps://repo.spring.io/milestone/url snapshots enabledfalse/enabled /snapshots /repository /repositories版本选型上有个心得不要盲目追新。Agent相关API在1.0.x小版本之间有过微调比如工具回调的包名可能从model.tool调整到tool目录下一旦踩到这种坑去翻阅对应版本的官方文档比到处搜博客靠谱得多。锁定一个版本后尽量保持统一避免团队成员各自升级依赖导致行为不一致。2.2 application.yml配置指向阿里云百炼的OpenAI兼容端点阿里云百炼的OpenAI兼容模式接入点Base URL是https://dashscope.aliyuncs.com/compatible-mode/v1。在Spring AI里配置非常简单spring: application: name: react-agent-demo ai: openai: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.3 max-tokens: 2048DASHSCOPE_API_KEY放在环境变量里不要写进配置文件提交到代码仓库。这个Key在阿里云百炼控制台的“API-KEY管理”页面创建一个账号可以建多个Key方便按项目隔离权限和配额。配置里有两个细节会影响Agent行为第一temperature不容易调大。Agent场景下模型要依据工具返回结果做判断太高的随机性会导致同样的输入跑来跑去建议控制在0.2到0.4之间。第二max-tokens要给足。ReAct循环中模型不仅要输出最终答案还要在中间步骤输出思考和工具调用意图token配额太小会导致Agent“说到一半就断气”甚至输出不完整的工具调用JSON。2.3 系统提示词怎么配置热搜词里“springai系统提示词怎么配置”这个需求确实是最容易被忽略又最影响Agent成败的一环。在Spring AI里配置系统提示词有两种方式。方式一在ChatClient构造时直接指定String systemPrompt 你是一个智能客服助手负责处理用户的天气查询需求。 规则 1. 当你需要获取实时数据时必须调用可用工具。 2. 工具返回结果后用简洁友好的语言回答用户。 3. 如果工具没有返回有效数据明确告知用户暂时无法获取。 ; ChatClient chatClient ChatClient.builder(chatModel) .defaultSystem(systemPrompt) .build();方式二更推荐的做法是把提示词模板化动态拼接业务上下文Value(classpath:prompts/agent-system.st) Resource systemResource; // 在构建Agent时引用模板文件 ChatClient chatClient ChatClient.builder(chatModel) .defaultSystem(systemResource) .build();使用外部模板文件的好处是运营同学可以随时调整角色设定和规则不用动Java代码重新发版。Spring AI支持Spring的Resource抽象classpath、文件系统甚至远程URL都能加载。系统提示词里到底要写什么后面第五节有专门拆解这里先记住一个原则提示词不负责教模型“怎么推理”只负责划定“能做什么、不能做什么”。推理能力是模型自带的你把边界和规则写清楚模型自然知道该在哪条路线上走。3. 核心机制拆解ReAct循环到底是怎么转起来的3.1 ReAct循环的三步思考、调用、观察ReAct这个名称来自Reasoning与Acting的组合麻省理工学院和Google的研究者提出这个概念时核心想法就是让大模型在推理过程中能够“停下来去查资料再继续推理”。一次完整的ReAct循环如下Reason模型接收到用户问题后内部展开一段思考链判断自己是否具备回答这个问题的能力。如果不具备它会在响应中声明需要调用某个工具。ActSpring AI将模型声明的工具调用解析成结构化的Function Call请求ToolCallingManager根据工具名称找到对应的ToolCallback执行业务方法。Observe工具执行完成后返回值被送回模型作为新一轮对话消息。模型把观察结果和初始问题放在一起再次进行推理。如果结果已经足够回答用户就生成最终回复如果还不够就继续发起新的工具请求。这个循环不是无限进行的。Spring AI内置了最大迭代次数的控制默认情况下Agent会在若干轮后强制结束避免模型陷入死循环。这也是ReactAgent区别于“用ChatClient手动循环调用工具”的关键——你不需要自己维护状态机框架替你处理了循环终止、历史消息累积、异常中断这些边界问题。3.2 Spring AI的工具调用架构ToolCallback与ToolCallingManager要真正理解ReactAgent这三样东西的关系必须捋清楚ToolCallback是单个工具的统一抽象。它负责两件事一是向模型暴露工具说明包括工具名称、描述、入参JSON Schema二是真正执行工具逻辑并返回结果字符串。ToolCallingManager是工具调用的总调度器。Spring AI官方文档里把它定位成“管理所有工具回调的注册与调用”负责把模型返回的工具调用请求分发给对应ToolCallback同时在每次循环后更新对话上下文。ReactAgent则是把上面两者串起来的顶层入口。它持有ChatClient做模型交互持有ToolCallingManager做工具分发然后在内部实现ReAct循环状态机。日常开发中你打交道最多的是ToolCallback因为它是你写业务代码的地方。最小实现是一个函数式接口入参是工具调用描述出参是执行结果字符串。但更推荐用Spring AI提供的Tool注解直接在任意Bean的方法上标注框架自动帮你生成ToolCallback并注册。3.3 为什么“工具描述”比“工具实现”更影响成败这一点是我在项目里踩了最多坑之后才想明白的。模型不读你的Java代码它对你工具的全部认知来自ToolCallback暴露出去的那段description和参数Schema。工具描述就是你在“招聘简介”里的岗位描述“会写代码”和“精通Java并发编程熟悉Spring事务机制”对候选人模型的判断完全不同。看一个实际对比// 反面例子描述太模糊 Tool(description 获取一些信息) public String handleSearch(String keyword) { // ... } // 正面例子描述具体并说明适用场景 Tool(description 根据关键字搜索商品信息适用于查询商品库存、价格、上架状态的场景。输入商品名称返回JSON格式数据) public String searchProduct(String name) { // ... }模糊描述会让模型在“是否需要调用这个工具”上犹豫不决导致它宁可用自己的知识瞎编也不敢去调用你的接口。描述具体的工具则会被模型高置信度地命中。给ReactAgent写工具描述我总结了一条实用心法先说自己帮什么忙再说自己不管什么事最后说明输入输出的样子。三者齐全模型的调用准确率会大幅提升。4. 实操从零构建一个能查天气和时间的ReactAgent4.1 定义两个工具基于Tool注解本节的示例目标很明确让Agent能回答“上海的天气怎么样”和“现在几点了”这类单靠模型知识回答不了的问题让它自己去调用真实接口。import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Map; Component public class WeatherAndTimeTools { // 模拟天气服务真实项目中替换为第三方天气API private final MapString, String weatherMap Map.of( 上海, 多云气温26度东南风三级, 北京, 晴气温22度北风二级, 广州, 雷阵雨气温30度南风四级 ); Tool(description 查询指定城市当前天气情况输入城市名称如上海、北京) public String getWeather(ToolParam(description 城市名称) String city) { String weather weatherMap.get(city); return weather ! null ? city 当前天气 weather : 未找到该城市的天气数据; } Tool(description 获取当前服务器时间无需输入参数返回带时区的完整时间字符串) public String getCurrentTime() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); } }这段代码有三个细节值得注意。第一ToolParam的description通常会被当作参数Schema的一部分传给模型。写清楚参数含义模型才知道把用户问题里的“上海”正确填入city字段。第二工具方法返回的是普通字符串但在真实项目中建议返回固定结构的JSON字符串这样模型解析和后续处理更稳定。如果返回自由文本模型也能理解但结构化数据能显著降低解析歧义。第三工具执行失败时不要让方法抛异常。ReAct循环中工具异常如果直接抛出Agent可能直接中断给用户报错。更稳的做法是捕获异常后返回“查询失败xx原因”的字符串让模型自己决定是重试还是向用户解释。4.2 构建ReActAgent并跑通第一个任务有了工具类之后核心工作就是把它装配进Agent。Spring AI提供构建器模式import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.tool.ReactAgent; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class ReactAgentConfig { Bean public ReactAgent myReactAgent(ChatModel chatModel, ToolCallingManager toolCallingManager, WeatherAndTimeTools tools) { return ReactAgent.builder(chatModel, toolCallingManager) .name(weather-assistant) .description(一个能查询天气和当前时间的智能助手) .tools(tools) .build(); } }如果你的Spring AI版本较新ReactAgent的包路径可能从org.springframework.ai.model.tool调整为org.springframework.ai.tool类名和构造方式基本一致。遇接口变动时最有效的方式是直接查看本地Maven仓库里jar包的源码或者依赖IDE自动提示。完成装配后写一个最简单的Controller验证流程RestController public class AgentController { private final ReactAgent reactAgent; public AgentController(ReactAgent reactAgent) { this.reactAgent reactAgent; } GetMapping(/agent) public String chat(RequestParam String question) { return reactAgent.call(question); } }访问http://localhost:8080/agent?question上海的温度怎么样预期返回内容类似于“我帮你查了一下上海当前天气多云气温26度东南风三级”。我实测跑通后观察到的调用链路是模型先输出工具调用意图ToolCallingManager定位到WeatherAndTimeTools.getWeather执行后把结果作为Observation消息回传模型读取后生成最终回答。整个流程在日志里每一步都清晰可见排障很舒服。4.3 完整可复现的调用链路与观察记录下面这条日志摘自我本地运行时控制台输出关键部分已做简化处理[1] User question: 上海的温度怎么样 [2] Model response: ToolCall(toolNameWeatherAndTimeTools.getWeather, args{city:上海}) [3] Tool execution: executing: city上海 [4] Tool result: 上海当前天气多云气温26度东南风三级 [5] Send observation back to model... [6] Model final response: 我为您查询了上海当前的天气情况多云26度东南风三级。这条链路值得反复看几遍因为ReactAgent能不能稳定工作就看[2]模型输出工具调用、[3]工具执行、[4]结果回填这三个环节是否顺畅。实际项目中我生产日志里也会特意打印工具执行细节一旦Agent行为异常可以先判断是模型判断错误[2]环节、工具代码错误[3]环节、还是结果回传被截断[4]环节很快就能锁定嫌疑人。运行过程中有两个明显体感一是即使qwen-plus这类模型对于“调用哪个工具”的判断准确性很高基本不会出现拿着天气问题去调时间工具的情况二是当问题涉及多个条件时例如“北京明天天气和现在几点了”Agent会依次发起两次工具调用逐个问题解决最后汇总回答。这就是ReAct模式并行能力的基础表现。5. 控制系统提示词与Agent行为边界5.1 提示词里该有什么角色、边界、输出格式、不允许做的事工具决定Agent的“能力上限”提示词决定它的“行为底线”。在ReactAgent场景下玩法比普通对话更复杂因为模型每轮都会产生中间行为如果没有明确约束它可能做出你不想让它做的事。一个经过实践检验的Agent系统提示词结构包含四块核心角色定位你是XX系统的智能助手负责…… 任务边界你只能处理与XX相关的问题其他问题一律回答“不在服务范围” 工具使用规则当用户问题需要实时数据时你必须调用对应工具禁止使用内部知识猜测 输出要求回答需简洁当调用工具失败时直接告知失败原因不要编造数据我在项目里吃过一个亏没有在第3条写死“禁止猜测”结果某次工具接口超时返回了“查询失败”字符串模型却自作主张回答“该商品库存充足”。后来把“工具失败即告知用户失败”写进提示词这类现象彻底消失。模型顺从系统性约束的概率远高于单次对话里的临时要求。5.2 限制Agent的循环深度防止“嘴上跑火车”ReAct循环并非越深越好。在复杂任务中如果模型每一步都只做微小进展十轮二十轮转下去不仅延迟高、token费用也扛不住还可能因为上一轮工具结果导致后续推理走偏。Spring AI的ReactAgent提供了限制循环的控制方式在构建Agent时指定maxIterations参数ReactAgent reactAgent ReactAgent.builder(chatModel, toolCallingManager) .name(assistant) .description(业务助手) .tools(tools) .maxIterations(5) .build();我建议根据任务复杂度把上限压在5到10轮之间。简单查询任务5轮足够复杂多工具联动再放宽到10轮。同时在系统提示词里补一句“当所有工具都无法解决问题时直接告诉用户暂时无法完成不要反复尝试”等于给Agent一个主动停手的体面通道能显著减少无效循环。5.3 参数调优temperature、maxTokens对Agent行为的影响Agent场景下最容易踩的调参坑就是temperature设置过高。普通对话场景0.7、0.8都很常见但Agent需要的是“按计划走流程”随机性越强越可能出现两种情况一种是不调用工具直接凭记忆回答另一种是调用了工具却在解析结果时“自由发挥”把返回的26度给你说成30度。对ReactAgent我实测中temperature设在0.2到0.3范围内既能保证回答自然度又能让推理路径保持稳定。maxTokens的影响同样被低估。模型在ReAct循环里的输出包含思考片段和工具调用声明这两部分都占token。如果maxTokens设得太小模型可能输出不完整的JSON工具调用解析失败Agent直接卡在“思考但没行动”的尴尬位置。给qwen系列模型我一般设在2048以上复杂任务甚至给到4096。还有一个小技巧如果发现模型经常输出格式不规范的半截JSON可以把响应格式提示写进系统提示词例如“工具调用必须使用合法JSON格式禁止使用省略号”。多数模型对格式提示很敏感一步到位就能改善解析成功率。6. 常见问题与排查实录6.1 模型就是不调用工具怎么办这个问题的原因80%出在工具描述上。先把你的工具描述拿出来读一遍想象自己是个只会看文字、不懂代码的实习生看完这段描述能不能准确判断“这个问题归我管”。如果描述含糊模型就倾向于自己编一个答案。排查顺序建议如下第一步确认工具是否真的注册成功。打印ReactAgent持有的ToolCallback列表看有没有你定义的工具名称。第二步检查工具描述里是否包含触发场景关键词。比如天气工具的描述里要有“天气、气温、降水、预报”这类词模型才容易将用户问题关联到工具。第三步检查模型输出。在日志中看模型是否输出了“我无法获取实时天气”这类话说明它意识到了需要数据却不知道去哪取问题又在描述上。第四步尝试换qwen-max或更强模型。小模型的工具调用能力确实弱一些如果描述已经清晰但调用率依然低换模型是成本最低的验证方式。6.2 工具调用了但返回结果解析失败症状是Agent日志里工具执行成功但下一轮模型回答是“抱歉我遇到错误”或者干脆重复之前的提问。大概率是工具返回的内容模型读不懂。大量实践后我发现工具返回结果必须单独占一个完整的消息片段不要和其他文本混在一起输出。同时建议返回值是一个结构明确、字段简短的字符串格式如{status: success, data: {city: 上海, weather: 多云}, message: 查询成功}如果工具返回了超长文本模型抓取关键信息的准确率就会下降。尤其是构造过复杂的数据结构再硬转JSON字符串模型解析时极易出错。一个稳妥的习惯是工具方法里就对结果做裁剪只回传对用户问题有直接价值的信息。6.3 连不上阿里云百炼报401或403错误报错信息里如果出现401 Unauthorized几乎都是API Key的问题要么环境变量没生效要么Key本身不对。先用curl直接验证Key有效性curl https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \ -H Authorization: Bearer $DASHSCOPE_API_KEY \ -H Content-Type: application/json \ -d {model: qwen-plus, messages: [{role: user, content: hello}]}如果curl能通、但Spring AI应用里报错优先检查Base URL配置是否被其他地方覆盖。Spring Boot的配置优先级很容易踩坑环境变量、命令行参数都可能覆盖application.yml排查时先看Actuator暴露的配置环境。404则是路径错误确认Base URL末尾的/v1有没有拼对chat/completions路径是不是被框架自动拼接成双重/v1/v1。这个问题很常见而且只见于配置检查不严的新手项目。6.4 上下文越来越长、费用飙升这个问题在Agent场景尤其突出。每轮ReAct循环都会把工具调用结果、观察消息塞进上下文中随着任务数量增多token消耗呈线性甚至超线性增长费用自然水涨船高。我调优过三个方向一是压低最大循环深度前文已讲二是让工具返回值保持精简只回传必要信息三是给Agent做“对话记忆会话级隔离”每个新请求使用独立的会话ID历史消息定期裁剪或做摘要压缩。需要特别注意如果你在同一个ChatClient上复用长期对话历史那么Agent积累的工具观察消息会跟着历史消息一起发给模型不仅浪费token还可能干扰模型对当前请求的判断。6.5 问题排查速查表症状优先排查方向常用解决手段模型不调用工具工具描述、注册状态重写description确认Bean注册换更强模型调用后回答错乱工具返回值格式精简为JSON字符串裁剪无关信息401错误API Key、环境变量用curl验证检查配置覆盖404错误Base URL拼接确认后是否带/v1检查路径是否重复循环过多maxIterations设置调低上限提示词中增加止损声明费用快速增长上下文长度、循环次数精简工具返回值、会话级隔离、压缩日志记录7. 进阶玩法从“能用”到“好用”7.1 给Agent加入短期记忆让多轮对话带上上下文上面示例中的Agent每次请求都是“一次性任务”用户如果继续追问“那广州呢”它不会记得上一句讨论的是天气。给Agent加记忆最常见的方式是在调用时传递额外历史消息。public String callWithContext(String question, ListMessage history) { return reactAgent.call( new UserMessage(question, Map.of()), history.toArray(new Message[0]) ); }7.2 用白名单机制限制Agent能碰到哪些工具一个Agent实例如果注入了全部工具模型就可能在某个场景下跑偏调用不该它管的工具。最好按Agent职责拆成多个实例例如“订单Agent”只注入查询订单工具、“售后Agent”只注入退款工具。如果必须共享工具类可以通过覆盖getToolCallbacks或自定义工具注册逻辑来过滤白名单确保每个Agent的“手”伸不到不属于它的资源。7.3 多个工具并行调用的技巧Spring AI的ReactAgent在收到多个工具调用请求时能够依次执行并汇总结果。要让模型主动发起并行调用关键在于工具描述和提示词配合在系统提示词里写明“当用户问题中同时包含多个独立信息需求时同时调用对应工具”。比如“查一下上海的天气和当前时间”模型就会在一次输出里声明两个工具调用。实测中这能显著降低多问题场景的响应延迟但代价是上下文会同时多出两条观察结果建议配合第6节的精简返回值策略一起使用。我个人在项目里练这一掌时最大的体会是ReactAgent的门槛不在框架API而在“你能不能把业务意图转换成模型看得懂的工具描述”。很多团队卡在“模型不调用工具”这个坎上翻来覆去调参数最后发现就是把description写得像流水账。另外工具返回值一定要结构化、简短、可预测模型越容易解析Agent的整体行为就越可靠。这套组合拳打完之后后面再往上走的方向我心里大致有数了——多Agent协作、长期记忆、流程编排基本上都建立在今天这个“或跃在渊”的闭环之上底子打牢了后面很多事情就是水到渠成。
返回列表