ARTICLE DETAIL

资讯详情

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

Spring AI + 阿里云DashScope构建生产级ReactAgent:从工具调用到提示词配置全解析

Spring AI + 阿里云DashScope构建生产级ReactAgent:从工具调用到提示词配置全解析 Spring AI加上阿里云模型做一个能自己决定“先查什么、再做什么”的ReactAgent是我最近几篇文章一直在聊的主线。这个系列写到第9篇标题借了《易经》乾卦的“或跃在渊”九四爻那条龙卡在进退之间往上一步是九五飞龙在天停下来就会退回深渊。Agent项目做到这个阶段处境其实差不多——基础接入通了、提示词调过几轮、工具也能被模型调用了但离“生产可用”还差着一层窗户纸。这篇我打算把这层窗户纸捅破为什么在Java侧我会选Spring AI做Agent底座阿里云模型服务怎么用最省事的方式接进来系统提示词到底怎么配才不拖后腿以及工具调用中那些只有踩过坑才会懂的细节。适合已经跑通第一个Agent、正准备让它干点实事的Java开发同学。1. 为什么是“或跃在渊”ReactAgent的运行机制与选型思考1.1 ReAct不是新概念是“把思考变成步骤”ReAct这个叫法来自Reasoning与Acting的组合核心思想一句话让模型别急着给答案先拆任务再调工具拿到结果后继续思考直到有足够依据才输出结论。我不是第一次提这个概念但在这个系列里它值得重新讲一遍因为这个模式决定了整个系统的行为方式。拿一个实际场景举例。用户问“刚才那个订单12345发没发货如果发了就通知运营群。”如果是普通问答模型它大概率会基于训练数据里的相似订单瞎编一个状态因为模型本身不连接你的业务库。而ReactAgent会把这件事拆成两步先调用订单查询工具拿到orderId对应的真实状态发现是已发货后再调用通知工具往群组里发一条消息最后回给用户一句“已核实并完成通知”。整个过程里模型每一步都在“思考接下来该做什么”而不是凭记忆写答案。我自己的理解是ReAct本质上把“决策权”交给了模型但把“手”限制在你能控制的工具集合里。它比传统的if-else规则引擎灵活得多因为你不用提前枚举所有可能的用户表达模型自己会去组合工具。但它也不是没有代价——模型可能选错工具、传错参数、或者在一个错误结果上不停打转这些都需要靠提示词约束、工具设计和异常返回来兜底。所以“或跃在渊”这个阶段玩的就是边界设计。1.2 为什么我选择Spring AI而不是裸写HTTP很多同学第一次接大模型时都会走一条路用RestTemplate对着模型接口手写鉴权、拼Prompt、解析响应。说实话只做一次单轮问答这种方式并不差甚至比引框架更轻。但一旦进入ReactAgent的Tool Calling流程手写HTTP的痛苦指数会直线上升。我踩过的坑可以列一串tools参数要维护一套完整的JSON Schema模型返回的tool_calls和消息历史必须保持严格的顺序工具执行完要把结果以“工具角色”塞回对话里多轮下来消息列表越来越长序列化稍微一乱模型就“失忆”。这些逻辑不是不能写而是写完后基本变成一坨只有自己能看懂的业务代码换一个模型厂商又要重新适配。Spring AI在这里做的事情是把“模型无关”的抽象层做出来了。你只需要在Spring Bean里写一个带Tool注解的方法框架会通过反射自动生成工具描述组装多轮消息解析模型返回的工具调用请求甚至帮你维护会话历史。我在生产环境里对比过三种接法裸HTTP适合快速验证厂商SDK适合深度绑定某一家能让我用最少的胶水代码把通义、DeepSeek、甚至私有化模型来回切换的反而是Spring AI这类抽象层。真到了Agent要接入多个内部系统的阶段会发现这个切换能力非常值钱。1.3 阿里云DashScope在选型中的关键角色阿里云在这场选型里的位置很特殊。国内能稳定跑通工具调用的模型服务DashScope是绕不开的一个。我选择它的理由有三个。第一DashScope提供了OpenAI兼容的接口。这意味着我在Spring AI里只需要改一下base-url和api-key就能把本来为OpenAI协议设计的客户端指向通义模型不用引入额外的SDK方言。第二模型的性价比梯度清晰qwen-turbo便宜适合批量日志分析和意图初筛qwen-plus在工具调用上表现稳我做大多数Agent场景都用它qwen-max逻辑更强适合一次要串四五个工具才能完成的复杂任务但成本也高。第三阿里云在认证、日志、限流这些基础设施上做得比较完整Agent要上生产的阶段这些能力比模型本身更救命。我把三种接入方式的取舍放在一起对比过列成表格会直观一些接法上手成本工具调用支持换模型成本适合阶段裸HTTP低自己从头写高验证想法厂商SDK中较全但绑定厂商协议中单一模型深度使用Spring AI抽象层中框架内统一处理低Agent多工具、多模型2. 环境准备与依赖拉取三座大山的翻越2.1 JDK 17和Maven版本组合这一章的标题有点夸张但坦白说从零把一个Agent工程搭起来环境问题往往比代码更花时间。先说基础版本JDK建议直接用17因为Spring Boot 3和Spring AI的当前版本对JDK 8已经不太友好了。如果你还在用JDK 8不是完全不能跑但你可能会在依赖兼容性上花掉大量时间不值得。Maven建议3.9以上Maven本身没有太多版本坑但要注意IDEA里内置的Maven版本可能比较旧最好手动配一下。构建工具用Gradle也行但这个系列一直用Maven下面所有配置我都按Maven写。另外如果你的团队有统一的代码规范顺手把Maven的编码、编译参数固定住避免同事机器上因为默认编码不一样导致乱码或者编译告警。这些细节和Agent本身无关但会直接影响协作效率。2.2 Maven阿里云仓库配置让依赖下载不再卡死国内拉依赖的老问题中央仓库时快时慢Spring AI的构件又多等起来非常折磨人。解决办法很成熟用阿里云的Maven公共仓库镜像在全局settings.xml里加一段mirror配置就行。settings mirrors mirror idaliyun/id nameAliyun Public Mirror/name urlhttps://maven.aliyun.com/repository/public/url mirrorOf*/mirrorOf /mirror /mirrors /settings注意mirrorOf写了*意思是所有仓库请求都走阿里云公共仓库。一般业务项目这样没问题因为阿里云公共仓库同步了绝大多数中央仓库构件。遇到个别冷门构件拉不到时再在pom.xml里单独补充release仓库不要全局乱加。还有一个容易忽略的点Spring AI的里程碑版本和snapshot版本不在公共仓库里。如果用了里程碑版本需要在pom.xml里显式加上Spring的里程碑仓库如果你不想折腾直接用发布版省心很多。这块我在下一节展开。2.3 Spring AI版本选择与BOM管理Spring AI目前已经进入了1.x稳定阶段但它的迭代速度仍然比普通Spring生态快API会有微调。我的建议是不要追最新选一个你自己验证过的版本锁死。下面演示用的版本是1.0.0配Spring Boot 3.4.x。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.1/version relativePath/ /parent properties java.version17/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement选用spring-ai-openai-spring-boot-starter是因为DashScope的OpenAI兼容接口可以直接对接不需要再加一套阿里专用starter。依赖拉完以后看一眼IDEA右侧的Maven面板如果spring-ai-core这些构件都顺利出现环境这块就算过了。后续如果出现依赖下载失败优先检查settings.xml是否生效、镜像仓库是否配置正确再去考虑代码问题。3. 系统提示词配置是Agent的“人设天花板”3.1 提示词直接决定能不能“跃”起来这个系列里我反复强调一句话在Agent项目里系统提示词不是“写一段说明文字”而是“定义一套行为协议”。模型能不能正确调用工具、调用失败后能不能自救、最终回答会不会编数据很大程度都取决于系统提示词怎么写。刚接触Agent时我犯过两个极端错误。第一次是几乎不写系统提示词只给模型一堆工具结果它经常基于常识编造订单状态第二次是写了一篇两千字的角色设定把语气、风格、企业文化全塞进去结果模型反而忽略工具调用光顾着扮演“创意文案大师”了。后来我总结出一个可复用的公式系统提示词 身份约束 工作流程 工具边界 输出格式。四个部分都只做减法不写废话。在技术实现上Spring AI对系统提示词没有特殊限制你可以在每次请求时通过prompt().system()传入也可以做成外部资源文件统一加载。但生产环境里我强烈建议把它从代码里拆出来因为提示词的修改频率远高于代码发布频率放资源文件里才能快速热更新也好让业务方同学直接review。3.2 配置与加载方式的三种实践先看第一种最简单直接的方式写在业务代码里ChatClient chatClient builder.build(); String response chatClient.prompt() .system(你是订单处理助手必须基于工具返回结果作答禁止编造数据。) .user(查一下订单12345) .call() .content();这种方式适合本地调试但不适合Agent项目因为提示词一长Java字符串的转义和拼接会很难维护。第二种方式是把提示词放到classpath资源文件里通过Resource加载这是我在项目里最常用的方式Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(new ClassPathResource(prompts/react-agent.md)) .build(); }第三种方式是把提示词模板放在配置中心或数据库里配合Spring AI的PromptTemplate做变量渲染。比如某个Agent的提示词里需要动态注入用户角色、当前日期、可用工具列表就可以用模板变量String systemPrompt promptTemplate.render( Map.of(currentDate, LocalDate.now().toString()) );三种方式不是互斥的。官方推荐的思路是固定部分放资源文件动态部分用模板变量需要频繁调整的业务规则放配置中心。你可以根据团队情况组合使用。3.3 我常用的ReAct人设模板下面这个模板是从我自己项目里简化出来的可以直接抄。它的特点是结构清晰、约束明确尤其强调了“工具返回优先”和“失败重试边界”。你是订单处理助手运行在 ReAct 模式下。 工作流程 1. 先理解用户请求拆解为需要执行的子任务。 2. 判断哪些子任务可以通过工具完成哪些需要你推理。 3. 调用工具时参数必须严格按照工具描述填写禁止省略必填项。 4. 工具返回结果后基于结果继续推进如果结果缺失尝试换参数重试最多3次。 5. 全部任务完成后用自然语言给用户一个简洁结论。 可用工具 - queryOrderById根据订单ID查询订单状态和明细。 - sendNotify发送文本消息到指定群组。 - sendSms通过阿里云短信发送通知。 行为边界 - 订单状态、金额、库存等数据必须来自工具返回严禁根据常识编造。 - 如果工具调用全部失败明确告知用户当前无法完成不编造替代结果。 - 对于“已发货”这类状态判断以工具返回的status字段为准不要自行推断。这个模板看起来不长但它把三件最重要的事说清楚了先想再动手、数据必须来自工具、失败时不许编造。在实际测试里加了这几条之后我们项目的模型编造率明显下降。4. 核心实现让模型真正“动手干活”4.1 用Tool注解暴露业务能力Spring AI的Tool Calling机制类比一下就是你把业务系统里的能力包装成一个一个“按钮”模型根据用户请求决定按哪个按钮它不需要理解按钮背后的代码。实现方式就是在普通的Spring Bean方法上加上Tool注解。Component public class OrderAgentTools { private static final Logger log LoggerFactory.getLogger(OrderAgentTools.class); Tool(name queryOrderById, description 根据订单ID查询订单状态和明细) public String queryOrderById( ToolParam(required true, description 订单ID例如 12345) String orderId) { log.info(查询订单, orderId{}, orderId); // 真实场景这里会走 RDS 或内部服务这里用固定结果演示 if (12345.equals(orderId)) { return {\orderId\:\12345\,\status\:\SHIPPED\,\items\:[\手机\,\充电器\],\totalAmount\:4999.00}; } return {\orderId\:\ orderId \,\status\:\NOT_FOUND\}; } Tool(name sendNotify, description 发送文本消息到指定群组) public String sendNotify( ToolParam(required true, description 群组名称例如 operations) String targetGroup, ToolParam(required true, description 消息内容) String content) { // 这里可以接钉钉、企业微信等 webhook先打印模拟 log.info(发送通知到 {}: {}, targetGroup, content); return {\result\:\SUCCESS\}; } }这里有两个细节值得专门说。第一方法返回值尽量用String并且内部返回规范化的JSON字符串因为LLM接收到的本质是文本结构一致的JSON比Java对象的toString容易解析得多。第二ToolParam里的description要写得像给实习生的操作说明模型是根据描述来填参数的你写得太模糊它就会传错值。4.2 接入DashScope并配置ChatClient配置方法很简单因为是OpenAI兼容协议只需要在application.yml里把base-url和api-key指到DashScope即可。注意api-key不要硬编码在代码里用环境变量注入。spring: ai: openai: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus然后是ChatClient的封装。这里要把系统提示词和工具类一起挂上默认配置之后所有通过这个Client发起的请求都会自动携带。Configuration public class AgentConfig { Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(new ClassPathResource(prompts/react-agent.md)) .defaultTools(OrderAgentTools.class) .build(); } }注意defaultTools传的是Class不是new出来的实例。这样Spring容器会管理工具类的依赖注入比如你的工具方法里需要注入订单Service或者短信Client都能正常工作。4.3 一个完整的订单通知Agent闭环现在把所有东西串起来。需求用户用自然语言说“查一下订单12345如果已发货就通知运营群”。完整的最小闭环如下。Service public class AgentService { private final ChatClient chatClient; public AgentService(ChatClient chatClient) { this.chatClient chatClient; } public String handle(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }Controller层就不重复写了就是一个普通的POST接口。真正值得看的是模型内部的实际执行轨迹。我在日志里抓过几次完整的调用链大致是这样user: 查一下订单12345如果已发货就通知运营群 模型思考: 需要先查询订单状态 工具调用: queryOrderById({orderId:12345}) 工具返回: {orderId:12345,status:SHIPPED,items:[手机,充电器],totalAmount:4999.00} 模型思考: 订单已发货符合通知条件需要发送通知给运营群 工具调用: sendNotify({targetGroup:operations,content:订单12345已发货金额4999元包含手机和充电器}) 工具返回: {result:SUCCESS} 最终回复: 已核实订单12345状态为已发货并且已经通知运营群。这个闭环看起来不复杂但它验证了一个关键能力模型不是只调用一次工具就结束而是根据前一次工具返回的结果自主决定是否进入下一步。我们的Agent能处理这种条件分支生产价值就出来了。在生产环境我还会给ChatClient的请求设置几个参数温度调到0.2左右让模型输出更稳定超时时间放到60秒以上因为Agent链路过长时模型响应和工具执行都会变慢另外在工具方法里加上审计日志记录谁在什么时候调用了什么工具、参数是什么。Agent一旦出错这些日志是唯一能回放问题的手段。5. 常见问题与排查技巧实录5.1 工具方法抛出异常后模型“原地打转”这是我在ReactAgent里遇到的第一个大坑。工具方法内部如果直接throw RuntimeException模型拿到的错误信息往往是一大段堆栈它看完根本不知道下一步该怎么办于是就会反复调用同一个工具试图“碰运气”通过结果每次都拿到同样的异常。解决办法一句话工具方法别抛异常把错误转成结构化反馈返回给模型。Tool(name queryOrderById, description 根据订单ID查询订单状态和明细) public String queryOrderById(ToolParam(required true, description 订单ID) String orderId) { try { // 查询业务数据 return {\orderId\:\ orderId \,\status\:\SHIPPED\}; } catch (Exception e) { return {\error\:\订单查询失败\,\reason\:\数据库连接超时\,\suggest\:\请稍后重试或检查订单ID格式\}; } }这个返回里的suggest字段很关键它相当于给模型递了一根救命稻草模型看到后可能会换一种参数或换一个策略而不是原地死循环。5.2 阿里云SDK鉴权失败别急着怀疑参数我在项目接入DashScope时碰到过401当时第一反应是api-key配置错了检查了半天才发现是环境变量名冲突。很多人电脑上配置过OPENAI_API_KEY如果你的应用读取的是这个变量而实际key是DashScope的就会鉴权失败。建议统一用DASHSCOPE_API_KEY命名并且启动前确认环境变量真的生效。还有一个容易被忽略的点DashScope控制台里创建API-KEY之前需要先开通百炼服务。没有开通控制台能创建key但实际调用时会返回403。这个顺序问题我不会踩第二次每次都写进部署文档里。如果排查时想绕开代码直接用curl打兼容端点最干净curl -X POST 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:你好}]}这条通了说明key和网络都没问题问题就在代码配置上。5.3 工具返回两万行JSON模型根本吞不下有同学问过“JSON parse成对象有两万行扛得住吗”这类问题其实真正常踩的坑是把两万行JSON直接塞给模型。模型的上下文窗口再大也不该用来读这条数据。比如一个盘点工具返回了全量库存明细几万条记录模型光读系统提示词和工具返回就把上下文耗尽了后续思考质量急剧下降。正确做法是工具层先做聚合和摘要只把模型需要的关键结论返回。return { \totalCount\: 9210, \lowStockItems\: [ {\sku\:\A1001\,\stock\:2,\level\:\WARN\}, {\sku\:\A1002\,\stock\:0,\level\:\EMPTY\} ], \summary\:\共有2个SKU库存异常建议立即补货\ };模型不需要知道每个SKU的实时库存它只需要知道异常项和结论。这条经验适合所有Agent工具设计工具返回的粒度应该服务于“让模型做出正确决策”这个目的而不是服务于数据完整性。5.4 短信API直通但Agent一调就失败这个场景也很典型单独写个接口调用阿里云短信服务短信能正常发出去但通过Agent工具调用要么超时要么一直失败。我排查后发现两个原因。第一个原因是工具方法同步等待短信回执而短信服务在高峰期的最终回执可能几秒甚至几十秒才返回超过了模型调用工具的超时阈值。解决办法是发送端改成异步受理只要短信平台返回“受理成功”工具就立刻返回成功最终送达状态通过回调或异步任务跟踪。第二个原因是多线程并发下工具方法里每次new短信Client导致连接池被耗光。解决办法是把短信Client作为单例Bean注入复用连接池。这两个问题单独看都不难但叠在一起就成了“Agent那边总超时”的玄学。我把这段写出来是为了提醒你遇到Agent调外部服务失败时先按“超时、并发、连接池”这个顺序排查而不是怀疑模型。5.5 常见问题速查表现象常见原因处理办法模型调用工具时参数总是缺工具描述没说明字段含义补全ToolParam的description写清示例模型反复调用同一工具工具返回异常文本不友好捕获异常返回结构化错误并给出suggest401 Unauthorizedapi-key错误或未开通服务用curl直连验证确认环境变量Agent一调外部API就超时同步等待回执连接池耗尽异步受理复用Client单例上下文越来越长后回答变差工具返回大JSON塞满上下文工具层做摘要只返回关键结论提示词不生效默认system被后续覆盖检查是否同时使用defaultSystem和prompt().system收尾这一掌打完聊聊我自己的一点体会这个系列写到“或跃在渊”我自己最大的感受是Spring AI确实省掉了大量对接上的重复劳动但Agent能不能真正“跃”起来决定权从来不在框架而在你对边界的设计。给模型的不是工具越多越好而是把工具描述写到能让一个实习生看懂把返回格式规范到一台机器能稳定解析把所有异常都转化成模型能继续思考的反馈。我在实际项目里最后做的两件事一是给Agent加了一层完整的操作审计二是给“发消息”“下订单”这类敏感工具加了人工审批开关。如果你也正处在把自己的Agent推向生产的阶段先把这两个动作做掉后面的路会稳很多。这个系列的后半段我还会继续聊Agent的可观测性和多Agent协作欢迎一起交流。
返回列表