Spring AI集成阿里云通义千问:ReAct Agent工具调用实战

发布时间:2026/10/6 15:24:48
Spring AI集成阿里云通义千问:ReAct Agent工具调用实战
这个系列做到第9期Spring AI 接入阿里云通义千问的链路已经不再只是 demo 层面的玩具了。这个阶段我给项目起的代号是“或跃在渊”取自乾卦九四爻辞意思是局势到了可以向上跃起、也可以退回深渊的临界点。对应到实战里就是 ReactAgent 的核心链路已经打通让大模型不再只停留在问答而是按 ReAct 模式自主决定调用哪个工具、拿到结果之后再继续推理。如果你正在做企业客服、工单助手、运维值班机器人这类内部 Copilot这篇复盘应该能帮你少走不少弯路。我的目标读者很明确已经能跑通“调用一次通义千问”但还不知道怎么让模型真正用起来的人。或者更准确一点是想让模型去查订单、发短信、读 Redis、查云资源而不是只会聊天的人。下面我把环境搭建、提示词配置、工具定义、上线排坑这几块完整拆开讲。1. 为什么是“Spring AI 阿里云 ReactAgent”这个组合1.1 选型背景Spring AI 解决的是“多模型切换”问题如果只是做一个调用通义千问的接口直接写 HttpClient 或者用阿里云百炼 SDK 也能搞定一百行代码的事。但实际项目要的不只是聊天还要查 RDS 订单、调短信 API、验 SSL 证书、返回结构化 JSON。这些服务和模型来自不同的认证体系模型走 DashScope短信走 dysmsapiRDS 走数据库连接串。如果每个服务都写一套调用逻辑最后就是一个胶水代码大杂烩。Spring AI 的价值是把“模型调用”抽象成了类似 JDBC 的接口。你换掉底层模型实现上层 ChatClient 代码基本不用动。我在项目里最开始用的是 OpenAI 风格的接口后续切到阿里云百炼业务代码改动不超过二十行。对做企业系统的人来说这个价值比“少写几个请求方法”重要得多。因为你永远不知道下个季度公司会不会换模型供应商或者你的同一个产品要同时服务多个云环境。再说选阿里云而不是其他家。一方面通义千问的中文理解和长文本能力在真实业务场景里表现不错另一方面热词里出现的大量阿里云资源RDS、短信、OSS、SSL本身就意味着 Agent 要操作的真实系统就在阿里云上。模型和业务系统放在同一生态里工具接入的摩擦最小权限控制、审计、费用账单都能统一看。这不是说其他选项不好而是对于我这个场景它是最稳的起点。1.2 ReAct 模式从“一问一答”到“想一步做一步”ReAct 是个学术词全称是 Reasoning Acting。核心思想很简单模型在回答问题时不是一口气生成最终答案而是在“思考”和“行动”之间循环。落地到 Spring AI 里不一定非要自己写循环而是借助 function calling 协议。我习惯把调用过程拆成四步模型分析用户问题认为需要查数据时输出一个 tool_call。程序拿到调用参数执行真实方法比如查订单表。执行结果以 observation 的身份追加到对话上下文里。模型读完结果后决定是继续调用工具还是输出最终答案。我把这个循环封装成一个 ReactAgent 服务类对外只暴露一个 ask(userMessage) 接口。内部通过 ChatClient 帮你维护循环不用每轮都自己判断是文本还是工具调用。它和普通 chat 的差别就像“背答案”和“现场查资料”。普通问答适合单轮、知识型、不需要外部数据的问题Agent 适合多步骤、需要实时数据、需要操作系统的任务。越复杂的问题、越多的工具这个模式的优势越明显。1.3 “或跃在渊”阶段的目标能上线、可回退标题里的“第9掌——或跃在渊”不是玄学是我给版本阶段定的代号。乾卦九四爻辞“或跃在渊无咎”说的是处在可进可退的位置。这个阶段 ReactAgent 已经不在“能不能调用工具”上纠结了核心目标变成三件事。第一所有工具调用都必须有 traceId 级别的日志方便出问题时回溯。第二模型、工具、提示词都要做成可配置项灰度期间随时能切换。第三工具失败必须能“软失败”。比如短信超时就返回明确错误信息给模型而不是让整个 Agent 直接抛异常。做到这三点才敢把 Agent 放到生产环境里。这个思路对任何做 AI Agent 上线的团队都适用和选什么框架无关。2. 环境搭建与系统提示词配置别再被细节卡住2.1 Maven 依赖和阿里云镜像仓库配置先解决一个很实在的问题依赖拉不下来。很多 Spring AI 的新手项目卡在最开始的依赖下载上。如果你在国内网络环境Maven 中央仓库速度不稳最简单的办法是在 settings.xml 里加阿里云镜像。这个操作和 Agent 本身无关但属于每个项目都绕不开的基础设施。mirror idaliyunmaven/id namealiyun public/name urlhttps://maven.aliyun.com/repository/public/url mirrorOfcentral/mirrorOf /mirror一个容易踩的坑mirrorOf 不建议配成 *。如果配成 *Spring 的里程碑仓库也被镜像到阿里云部分 spring-ai 预览包会拉不到。所以我的建议是只镜像 central其他仓库按需单独配置。如果你用的是已发布正式版则不需要特别担心这个问题。pom.xml 里的依赖坐标我用的是 Spring AI 1.0.0 时代的 BOM 方式parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.5/version relativePath/ /parent dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement配置完仓库再看具体 starter。Spring AI 官方有一堆 model starter阿里云生态对应的是 dashscope 相关模块。如果你用的是 spring-ai-alibaba 项目那会引入spring-ai-starter-alibaba-dashscope如果你用 Spring AI 官方 BOM也可以找 dashscope 对应的 starter 坐标。不同版本命名的确有些差异我的习惯是先去看一下当前版本的官方文档确认包名再往 pom 里填。这不算麻烦反而能避免很多照抄旧博客导致版本不匹配的问题。2.2 模型接入DashScope 兼容模式最省事阿里云百炼给 Spring AI 提供了两种接入姿势。第一种是直接用 DashScope 自己的 starter配置项更贴近阿里云生态。第二种是走 OpenAI 兼容模式把 base-url 指到 DashScope 的 compatible-mode 地址。如果你的项目以前就是基于 OpenAI 写的用兼容模式迁移成本最低。我当时的做法是把 application.yml 里的 api-key 换成百炼的 API-KEYbase-url 换成兼容模式地址模型名改成 qwen-plus其他地方基本不用动。spring: ai: openai: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 chat: options: model: qwen-plus temperature: 0.7有人会问qwen-plus和qwen-max怎么选。我的经验是日常问答和工具调用qwen-plus 性价比最高涉及复杂推理、长链路规划、需要稳定遵循格式的场景用 qwen-max 更稳。temperature 不要拉太高工具调用场景我一般设在 0.2 到 0.5 之间。太高会让模型输出的 tool_call 参数飘太低又可能导致缺乏灵活性。另外注意DashScope 的 API-KEY 和阿里云账号的 AccessKey 不是一回事。百炼有独立的 API-KEY 管理页面模型调用用前者短信、OSS、RDS 这些云产品 OpenAPI 用后者。这两个东西很多人一开始会搞混后面我单独讲。2.3 系统提示词放哪里、怎么写这是热词里排名靠前的“springai系统提示词怎么配置”也是我看到新手问得最多的点。Spring AI 里配置系统提示词至少有三个位置不同位置的优先级不一样。第一种是全局默认。在构建 ChatClient 时通过 defaultSystem 设置。这样所有经由该 ChatClient 发起的对话都会默认带上这段系统提示词。String systemPrompt 你是企业服务机器人。 你的职责根据用户问题判断是否需要调用工具。如果有工具可解决问题优先使用工具。 约束 1. 只能使用下方提供的工具不要编造工具结果 2. 工具返回后必须引用返回内容回答不能编造数据 3. 回答使用中文简洁输出为带要点的文本。 ; ChatClient chatClient ChatClient.builder(chatModel) .defaultSystem(systemPrompt) .build();第二种是单次调用覆盖。在每次 prompt 时传入 system 参数它的优先级高于默认值。chatClient.prompt() .system(你现在是值班助手语气要正式。) .user(订单 A1001 发货了吗) .call() .content();第三种是外置模板。把提示词放到 resources/prompts/ 目录下的独立文件里用 ClassPathResource 读取。这样调整提示词不用改代码重新编译对线上调参特别方便。ClassPathResource resource new ClassPathResource(prompts/system-agent.st); String systemPrompt resource.getContentAsString(StandardCharsets.UTF_8);写系统提示词有三条准则和写普通 prompt 不完全一样。第一明确边界告诉模型什么时候该用工具什么时候不该用。第二给示例工具调用的输出格式、回答风格最好给一两句话的样例。第三兜底行为如果所有工具都不适用直接告诉用户“该问题需要人工处理”。我发现很多 Agent 乱调用工具不是模型不行而是提示词里没写清“你不应该做什么”。3. ReactAgent 核心链路落地定义工具、注册回调、跑通循环3.1 用 Tool 注解定义业务工具真正让 Agent 变得有用的是工具。我这里的“工具”指的不是 Spring AI 框架里的类而是你暴露给模型的业务能力。Spring AI 提供Tool注解可以直接把一个 Spring Bean 的方法变成模型可调用的工具。看一个实际例子。假设我们现在要做一个订单助手Agent 需要能查订单状态、能发短信于是定义两个方法。Component public class OrderTools { Tool(description 根据订单号查询订单状态入参 orderId 是字符串) public String queryOrderStatus(String orderId) { // 这里可以查 RDS也可以调订单中心接口 if (A1001.equals(orderId)) { return 订单已发货物流单号 SF123456; } return 未查询到订单 orderId; } Tool(description 给指定手机号发送服务通知短信mobile 是手机号text 是短信正文) public String sendSms(String mobile, String text) { // 封装阿里云短信服务 return SmsResult.of(mobile, text).toString(); } }这里最容易忽视的点是 description。模型并不知道你的方法内部做了什么它完全靠 description 决定要不要调用、怎么传参数。description 写得不够具体会出现两种情况该调用的没调用或者不该调用的乱调用。比如queryOrderStatus如果你只写“订单查询”用户问“怎么退款”时模型可能也会去调它因为它觉得这算订单问题。好一点的描述应该带上“输入输出示例”和“适用场景”。3.2 注册工具并组装 ReactAgent 调用链定义好工具之后要把它们注册到 Spring AI 的上下文中。Spring AI 提供了ToolCallbackProvider我们可以通过MethodToolCallbackProvider把多个带Tool注解的 Bean 合并注册进去。Bean ToolCallbackProvider agentTools(OrderTools orderTools, SmsTools smsTools) { return MethodToolCallbackProvider.builder() .toolObjects(orderTools, smsTools) .build(); }注册完之后在创建 ChatClient 时把工具列表传给 defaultTools这样每次会话模型都能看到这些工具。ChatClient chatClient ChatClient.builder(chatModel) .defaultSystem(systemPrompt) .defaultTools(agentTools) .build();接下来就是调用。用户发一句话进来Agent 内部会自动决定要不要调用工具、调用哪个。String answer chatClient.prompt() .user(帮我查一下订单 A1001如果已经发货就发条短信通知我) .call() .content();运行结果大致是这样模型先调用 queryOrderStatus拿到“订单已发货”的 observation再调用 sendSms最后生成一句“订单 A1001 已发货短信通知已发送”。整个过程不需要人工介入。这里需要注意一点如果你发现工具没有被调用先别急着怀疑模型回去看一眼系统提示词和工具描述。大多数问题出在给模型的“说明书”不够清晰。3.3 流式输出、多轮记忆和超时处理生产环境不会满足于一次性返回完整答案。用户看到打字机式的输出体感会好很多。Spring AI 的 ChatClient 支持流式调用返回的是 Flux 流。FluxString stream chatClient.prompt() .user(帮我查订单 A1001) .stream() .content();流式输出在 Agent 场景里有个小细节工具调用阶段往往没有文本输出模型先憋着调工具等拿到结果才开始打字。所以前端如果只看字符串流会看到一段停顿。这个问题可以在产品层面处理比如在工具调用期间展示“正在查询订单...”的状态提示而不是干等。多轮记忆也很关键。ReAct Agent 的单次调用里模型能看到工具返回结果但如果用户接着问“那物流单号是多少”模型需要记住上一轮对话里查到的订单号。Spring AI 里可以通过 ChatMemory 和 Advisor 实现记忆管理。ChatMemory chatMemory new InMemoryChatMemory(); ChatClient chatClient ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory, 10)) .build();这里要注意不是所有模型都支持超长多轮历史同时保留工具调用结果。给 ChatMemory 设置合理的窗口比如 10 条既能保证上下文不丢也不会把 token 撑爆。个人建议不要盲目把历史全塞进去很多“Agent 越聊越傻”的问题就是上下文被无关历史污染了。工具调用本身也要做超时保护。比如短信发送接口最慢可能 3 秒但模型在等这个结果整个链路就会卡住。我给工具方法统一加了一层拦截超过 5 秒就返回“短信服务超时请稍后重试”。模型看到这个结果后能自己决定下一步怎么处理而不是整个 Agent 崩溃。4. 上线前遇到的那些坑逐条给你排一遍4.1 短信 API 发不出的检查清单这是热词里几乎每家都会碰到的问题阿里云短信 API 调不通。配合 Agent 场景更尴尬因为工具方法已经执行了返回给模型的是错误信息模型只能把错误转述给用户。我整理了一套排查顺序按照这个顺序走大多数问题十分钟能定位。排查项检查内容频率最高的坑AccessKey 权限是否授予短信服务权限只在主账号配置了 AK子账号没有短信权限签名审核短信签名是否已通过审核签名没同步到当前 AccessKey 所属账号模板审核短信模板是否通过变量格式是否正确模板变量与传入参数不匹配手机号格式是否国内手机号、是否带 86阿里云国际版与国内版规则不同调用频率是否触发一分钟一条的限流批量发送时最容易忽略错误码先看 Code 字段再去查文档只看 HTTP 状态码忽略了业务错误码举个例子如果返回isv.SMS_SIGNATURE_ILLEGAL不用看别的就是签名有问题检查签名是否审核通过以及签名名称是否和账号下配置一致。如果是isv.MOBILE_NUMBER_ILLEGAL先把手机号格式化检查一遍。我在 Agent 工具里专门写了一个预校验函数手机号不合法就直接返回给模型不走 API。省了不少流量钱。4.2 模型名、API Key、SDK 认证为什么总是搞混这绝对是我见过最多的问题。用户在 application.yml 里配了百炼的 API-KEY然后又拿阿里云 AccessKey 来当模型调用的 key结果永远是 401。实际上模型调用和云产品 OpenAPI 是两套认证体系。场景使用什么凭证通义千问/DashScope 模型调用百炼 API-KEY在百炼控制台申请短信、OSS、RDS OpenAPI 调用AccessKey ID AccessKey SecretRDS 数据库连接RDS 账号密码不走 AccessKey还有一个常见 404模型名写错。在 OpenAI 兼容模式下base-url 写成了https://dashscope.aliyuncs.com/api/v1模型名写成了gpt-3.5-turbo结果自然是 404。DashScope 兼容模式的 base-url 必须带compatible-mode这个路径段模型名也必须用通义千问的 ID比如qwen-plus、qwen-max。这个地方没有那么多“智能”就是细心问题。4.3 工具返回 JSON 太大两万行根本顶不住热词里有一条很接地气“阿里 json.parsearray转换对象有两万行扛得住吗”。在普通 Java 解析里两万行 JSON 只要不递归解析性能是可以接受的。但在 Agent 场景里问题不是解析而是上下文爆炸。如果你的工具返回了两万行 JSON这些内容会被塞进对话上下文模型无法在里面快速定位关键信息token 消耗直接起飞响应延迟也很明显。更严重的是多数模型的上下文窗口有限回答问题时会胡编乱造。解决思路是在工具侧做聚合而不是把原始数据交给模型。比如查订单列表工具内部先把数据加工成一个摘要ObjectNode summary objectMapper.createObjectNode(); summary.put(totalCount, 20000); summary.put(pageCount, 20); summary.put(hasError, false); summary.put(latestOrderStatus, 已发货); return summary.toString();模型只需要看总数、是否有错误、关键状态就可以决定下一步动作。如果确实需要原始明细工具应该支持分页参数让模型分批获取。我在项目里给所有查询类工具都加了一个limit和page参数默认只返回 20 条。这不仅保护了上下文也保护了下游数据库。4.4 系统提示词不生效、工具被乱调怎么排查系统提示词不生效最常见的原因有两个。第一你在 ChatClient 构建时设置了 defaultSystem但又在前面某个 advisor 或者 prompt 里传入了新的 system后者会把前者整个覆盖掉。第二你的提示词里写了工具边界但模型还是乱调说明描述不够具体或者模型本身就是小模型理解不了复杂的指令。我查过的一个案例工具描述写的是“查询订单信息”系统提示词写的是“当用户咨询物流问题时优先使用工具”。结果用户问“什么时候发货”模型去调了查询订单工具这没问题。但当用户问“你们营业时间是几点”模型依然去调了这个工具因为提示词里“优先使用工具”给模型的压力太大了。后面我把描述改成了带条件的“仅当用户提供订单号或追问订单状态时才调用此工具”。效果立竿见影。另外提醒一个技术细节如果同时配置了defaultTools和.tools()有可能会造成工具重复注册模型收到的工具列表里出现两份同名工具。排查方法是打开日志找到 Spring AI 打印的 tool list数一下数量。工具数量不是越多越好暴露给模型的每个工具都会消耗上下文窗口只注册当前场景真正需要用到的工具。4.5 SSL 证书免费续期和 Agent 的可观测运维热词里还有一条“阿里云ssl证书免费续期”。这个和 Agent 关系不算大但我在项目中刚好做了一个运维工具顺便说一下思路。Agent 服务如果暴露在公网HTTPS 证书到期是必然事件。免费证书的有效期现在一般是一年续期需要重新申请、下载、替换。传统做法是写个定时脚本但有了 Agent 后可以让运维机器人定期检查证书剩余天数。方法很简单用一个 Scheduled 注解每 7 天扫一遍 Nginx 或 SLB 上的证书到期前 30 天把提醒发到钉钉群或者短信通知。这个功能也可以反向暴露给 Agent运维人员问“证书还剩多少天”Agent 自动调用查证书工具。这样整个运维链路就闭环了。不要觉得这样做很重实际项目中这个工具比很多花哨的 AI 能力都实用。最后再分享一点个人体会做到第9个版本我最想强调的一件事是不要把 Agent 当成黑魔法它本质上还是“模型 工具 流程”的组合。模型负责决策工具负责执行提示词负责约束边界。真正影响生产体验的往往不是模型本身的智商而是工具描述是否清晰、错误处理是否完整、上下文管理是否克制。或跃在渊说的是跃起之前先想好退路。你的 Agent 工具调用必须可观测、可终止、可回退。这个思路比任何框架技巧都重要。