如果你正在做 Java 后端的 AI 应用,最近大概率被几个名词反复刷屏:Spring AI、Spring AI Alibaba、ReactAgent、Workflow。它们听起来像是一套东西,但又不像传统 Web 框架那样有明确边界。笔者在尝试把大模型接入业务系统时,也被“智能体到底怎么落地”、“工作流编排到底是框架能力还是自己写”这类问题绕了好几圈。这篇文章会把 Spring AI Alibaba 生态下的智能体(ReactAgent)与工作流(Workflow)扩展体系拆开讲清楚,包含可复制的代码示例、配置思路和我在实践中遇到的坑。
1. 从一次智能体开发说起:为什么要涉及 Spring AI Alibaba
先说结论:Spring AI Alibaba 解决的是“Java 后端如何稳定、低成本地接入大模型”的问题。它不是一套全新的 AI 框架,而是在 Spring AI 的抽象体系之上,针对阿里云百炼平台的模型服务做了一层适配与扩展。
当你在业务中需要做以下事情时,Spring AI Alibaba 就是最自然的选型:
- 在 Spring Boot 服务中调用通义千问 / 百炼平台上的大模型;
- 把 LLM(大语言模型)能力封装成可以被业务代码统一调用的 Service;
- 让模型能够调用你已有的 Java 方法、HTTP 接口或者内部服务;
- 把“用户提问、模型思考、工具调用、结果组装”的过程编排成可维护的流程。
从这个角度看,Spring AI Alibaba 并不是一个花哨的玩具,而是把大模型接入、模型切换、多工具调用、提示词模板、记忆管理等琐碎能力收敛成统一 API 的工程化框架。
这篇文章适合三类读者:
- 正在做 Spring Boot 后端,想在项目中接入大模型能力的开发者;
- 对 Agent(智能体)概念感兴趣,但不知道在 Java 中如何实现推理、工具调用闭环的读者;
- 接触过 Dify / Coze 等工作流工具,想在代码层面复现同类编排能力的研发同学。
读完本文,你将掌握:Spring AI Alibaba 的基础接入方式,ReactAgent 的基本实现思路(ReAct 模式的 Java 落地),以及一种不依赖额外编排引擎的业务工作流组织方式。
2. 四个关键概念先分清
在进入代码之前,有几个高频出现但又容易被混用的概念,值得先单独梳理。
2.1 Spring AI:Java 生态的大模型抽象层
Spring AI 是 Spring 官方推出的 AI 应用开发框架。它的目标是把接入大模型这件事统一抽象成类似 JDBC、Spring Data 那样的规范。
// 典型使用方式:不关心底层是 qwen 还是其它模型 ChatResponse response = chatClient.prompt() .user("用一句话介绍 Spring AI") .call();这段代码后面,无论接的是阿里云百炼、OpenAI 还是其它兼容协议的服务,业务代码本身不需要大改。Spring AI 提供了:
- ChatClient / ChatModel:统一对话入口;
- ChatOptions:模型参数(温度、最大 Token、模型名等)抽象;
- Message:用户消息、系统消息、工具消息的标准化结构;
- Tool / Function Calling:统一工具注册与调用机制。
所以,Spring AI 解决的是“不同模型接入方式不一致”的问题,让 AI 能力像数据源一样可以被标准化使用。
2.2 Spring AI Alibaba:把百炼模型接入 Spring 生态的适配层
Spring AI Alibaba 是阿里巴巴开源的 Spring AI 适配组件。它做的事情可以概括为:
- 把阿里云百炼平台的大模型服务适配成 Spring AI 的 ChatModel;
- 提供基于百炼平台的配置文件与自动装配能力;
- 将阿里云模型特有的能力(例如某些工具的返回结构、流式输出格式)映射到 Spring AI 统一模型上。
如果你只走 OpenAI 兼容协议,Spring AI 本身也能工作。但如果你希望稳定接入通义千问系列模型,并使用百炼平台的密钥管理、模型路由能力,Spring AI Alibaba 是更直接的选择。
有读者可能会问:“Spring AI Alibaba 停更了吗?”这其实是一个版本变迁造成的误解。早期 spring-ai-alibaba 作为 Spring AI 仓库中的子模块维护,后来又独立为单独仓库与版本线演进,所以如果你在主仓库里搜不到新增模块,不要急着判断项目停止维护,建议直接看官方独立仓库的发布记录。
2.3 ReactAgent:基于 ReAct 范式的自定义智能体
ReactAgent 目前并不是 Spring AI 官方库中开箱即用的一个具体类,而是社区基于 ReAct(Reasoning + Acting)范式实现的智能体模式。
ReAct 的核心思路是:让大模型不只是“直接回答”,而是在回答之前先经历一个循环:
- 接收用户问题;
- 结合已有工具列表进行推理(Reasoning),判断是否需要调用工具;
- 如果需要,发起工具调用(Acting);
- 拿到工具结果后,再次推理;
- 直到模型认为信息足够,生成最终回答。
在 Java 中,你可以用 Spring AI 的 ChatClient 和 Function Calling 能力把这个循环封装成自己的 ReactAgent 类。这就是标题里“ReactAgent 智能体”的本质:它并不是一个神秘框架,而是一个可自行实现的编码模式。
2.4 Workflow:把多个原子能力编排成业务步骤
Workflow(工作流)在 AI 应用中有两种含义:
- 一类是 Dify、Coze 这类产品提供的可视化流程编排:节点包括 LLM 调用、知识库检索、代码执行、条件分支、HTTP 请求等;
- 另一类是你在代码里自己实现的业务编排:把“意图识别、参数抽取、工具调用、答案生成”串成一个可复用流程。
在 Spring AI Alibaba 的场景里,我们通常不必为了做一个简单 Agent 而引入重量级流程引擎。大多数业务场景下,用责任链模式、简单状态机或者一个 Workflow 编排类就能实现清晰可控的工作流。
3. 环境准备与依赖引入
3.1 技术栈与版本说明
本文示例以常见稳定组合为例:
- JDK 17 或 JDK 21
- Spring Boot 3.2.x 及以上(Spring AI 1.0 需要 Boot 3.x)
- Maven 3.8+
- Spring AI Alibaba 2.0.1(以实际仓库发布版本为准)
- 阿里云百炼 API Key
版本说明:Spring AI 演进速度较快。如果你在接入时发现某个类不在预期包路径下,优先检查你是否把 Spring AI Alibaba 与 Spring AI 的版本拉齐。建议以官方 release 页面标注的兼容版本为准,不要自行混用大版本。
3.2 创建 Spring Boot 工程
推荐直接使用 Spring Initializr 创建工程,手动添加依赖。下面是一个最小化的 pom.xml 核心片段:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <spring-ai-alibaba.version>2.0.1</spring-ai-alibaba.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter</artifactId> <version>${spring-ai-alibaba.version}</version> </dependency> </dependencies>如果你的工程当前没有可用的 Spring AI BOM,也可以单独引入 Spring AI 核心依赖,再叠加 Alibaba 适配层。具体依赖坐标建议以官方文档为准,因为 AI 框架的改动频率远高于普通中间件。
3.3 配置阿里云百炼模型接入
在application.yml中配置百炼平台的 API Key 和默认模型:
spring: application: name: spring-ai-alibaba-demo ai: dashscope: api-key: ${AI_DASHSCOPE_API_KEY:your-api-key} chat: options: model: qwen-plus这里有几个关键点:
AI_DASHSCOPE_API_KEY是环境变量。不要直接把 Key 硬编码到代码或配置仓库中,尤其是生产环境;model指定默认的大模型名称,比如qwen-plus、qwen-max。不同模型的能力与价格差异较大,建议根据业务场景切换;- 如果某些工具调用场景中
plus版本模型报错而max模型正常,通常与模型对 Function Calling 参数的支持差异有关,可以先用max验证链路,再针对plus调整工具描述或参数结构。
完成依赖引入和基础配置后,写一个简单的 Controller 验证链路是否通:
// 文件路径:src/main/java/com/example/agentdemo/ChatController.java @RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/chat") public String chat(@RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }启动项目后访问/chat?message=你好,如果能返回模型结果,说明环境已经就绪。
4. ReactAgent 智能体实战:让模型学会“边想边做”
4.1 ReAct 模式的执行流程
ReAct 模式本质上是一个循环,这个循环可以用下面的步骤表示:
- 组装 System Prompt:告诉模型它有哪些工具可用、应该按什么规则执行;
- 将用户问题与历史消息发送给模型;
- 模型返回结果。如果结果包含工具调用请求,进入第 4 步;如果直接返回自然语言答案,则结束;
- 执行对应工具,拿到真实结果;
- 将工具结果作为 Tool 消息追加到上下文中,再次发送给模型;
- 重复步骤 3-5,直到模型给出最终回答。
用文字描述显得抽象,但在 Spring AI 中,Function Calling 已经帮你处理了工具调用请求的解析与回传,你真正要做的是“允许多轮工具调用”的组织逻辑。
4.2 基于 ChatClient 封装一个最小 ReactAgent
在 Spring AI 1.x 中,ChatClient 原生支持工具调用。下面这个类演示了如何把一个带工具调用的对话操作封装成可复用 Agent。
// 文件路径:src/main/java/com/example/agentdemo/agent/ReactAgent.java @Component public class ReactAgent { private final ChatClient chatClient; public ReactAgent(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem(""" 你是一个智能助手。你可以使用下方工具完成用户请求: - getCurrentWeather: 根据城市名查询实时天气 当用户询问天气时,你必须调用工具获取数据,再根据工具结果回答。 """) .build(); } public String execute(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }注意defaultSystem并不触发工具自动调用,它只是给模型提供了行为规则。真正让模型具备“调用动作”能力的是注册工具。
4.3 注册外部工具:让 Agent 能够“动手”
在 Spring AI 中,使用@Tool注解即可让普通 Java Bean 方法暴露为大模型可调用的工具。
// 文件路径:src/main/java/com/example/agentdemo/tool/WeatherTool.java @Component public class WeatherTool { @Tool("根据城市名查询当前天气") public String getCurrentWeather(String city) { // 实际项目中可以替换为真实天气服务 return switch (city) { case "北京" -> "晴,12℃"; case "上海" -> "小雨,17℃"; default -> city + ":暂无数据"; }; } }然后在构建 ChatClient 时把工具实例传进去:
// 文件路径:src/main/java/com/example/agentdemo/config/ChatClientConfig.java @Configuration public class ChatClientConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder, WeatherTool weatherTool) { return builder .defaultTools(weatherTool) .build(); } }这样 ReactAgent 在收到“北京天气怎么样”时,大模型会先决定调用getCurrentWeather工具,拿到真实结果后再组织语言回答。
4.4 多轮工具调用的可靠性问题
单工具调用很简单,但真实业务往往是“多工具连续调用”:比如先查订单状态,再根据状态查询物流,最后汇总回复。
在多轮调用场景下,你可能会遇到两个典型问题:
- 模型在工具结果返回后没有继续追问,而是直接结束。这种情况通常是 System Prompt 中没有说明“根据工具结果继续推理”;
- 工具返回的数据过于复杂,占满上下文。建议在工具方法内部就做字段裁剪,只返回必要信息。
一个稳妥的做法是在 System Prompt 里显式声明:“你可以连续调用多个工具,每次调用完工具后,根据结果决定是否需要继续调用。”
从实践来看,qwen-max在复杂工具调用上通常比qwen-plus表现更稳定。如果调试时发现plus模型对某个复杂工具描述理解不到位,优先优化工具描述文本,而不是盲目换常用的随机参数。
5. Workflow 工作流编排实战
5.1 为什么不能只用“单次 Prompt”实现业务需求
业务中的 AI 功能很少是“问一句答一句”。
典型场景:用户说“帮我查一下今天北京天气,如果下雨就提醒我带伞”。
这条请求其实包含:意图识别(查天气 + 条件判断)、参数抽取(北京)、工具调用(查天气)、条件分支(是否下雨)、结果生成(提醒带伞)。如果只靠一次 Prompt,模型的随机性会让整个链路不可控。你需要把这些步骤显式地编排起来。
这里的核心思路是:确定性步骤用代码控制,不确定性步骤交给 LLM。
5.2 在 Java 中实现一个轻量 Workflow
不引入额外流程引擎,用一个简单编排类就能组织多个处理节点。
先定义一个通用节点接口:
// 文件路径:src/main/java/com/example/agentdemo/workflow/WorkflowStep.java public interface WorkflowStep<T> { /** * 执行当前节点并返回上下文或结果 */ T execute(WorkflowContext context); }上下文对象用于在节点之间传递数据:
// 文件路径:src/main/java/com/example/agentdemo/workflow/WorkflowContext.java public class WorkflowContext { private final Map<String, Object> data = new ConcurrentHashMap<>(); public void set(String key, Object value) { data.put(key, value); } public <T> T get(String key) { return (T) data.get(key); } public String getInput() { return (String) data.get("userInput"); } public void setInput(String input) { data.put("userInput", input); } }接下来,把业务拆成三个节点:
// 节点1:意图识别(调用 LLM 判断用户想做什么) @Component public class IntentRecognitionStep implements WorkflowStep<WorkflowContext> { private final ChatClient chatClient; public IntentRecognitionStep(ChatClient chatClient) { this.chatClient = chatClient; } @Override public WorkflowContext execute(WorkflowContext context) { String userInput = context.getInput(); String intent = chatClient.prompt() .system("你是意图识别助手。只输出以下枚举值之一:WEATHER、ORDER、OTHER") .user(userInput) .call() .content(); context.set("intent", intent); return context; } }// 节点2:参数抽取(从文本中抽取城市名等参数) @Component public class ParameterExtractStep implements WorkflowStep<WorkflowContext> { private final ChatClient chatClient; public ParameterExtractStep(ChatClient chatClient) { this.chatClient = chatClient; } @Override public WorkflowContext execute(WorkflowContext context) { String userInput = context.getInput(); String params = chatClient.prompt() .user("从这句话中抽取城市:\n" + userInput) .call() .content(); context.set("params", params); return context; } }// 节点3:工具执行与回答生成 @Component public class ToolExecuteStep implements WorkflowStep<WorkflowContext> { private final ReactAgent reactAgent; public ToolExecuteStep(ReactAgent reactAgent) { this.reactAgent = reactAgent; } @Override public WorkflowContext execute(WorkflowContext context) { String input = context.getInput(); String answer = reactAgent.execute(input); context.set("answer", answer); return context; } }最后用一个 Workflow 类把这些节点串成流程:
// 文件路径:src/main/java/com/example/agentdemo/workflow/WeatherWorkflow.java @Component public class WeatherWorkflow { private final List<WorkflowStep<WorkflowContext>> steps; public WeatherWorkflow(IntentRecognitionStep intentStep, ParameterExtractStep extractStep, ToolExecuteStep toolStep) { this.steps = List.of(intentStep, extractStep, toolStep); } public String process(String userInput) { WorkflowContext context = new WorkflowContext(); context.setInput(userInput); for (WorkflowStep<WorkflowContext> step : steps) { step.execute(context); } return context.get("answer"); } }这个设计的好处是:
- 步骤之间通过
WorkflowContext解耦; - 新增节点只需要实现
WorkflowStep接口并调整步骤列表; - 关键逻辑是显式代码,不依赖模型的随机表现。
5.3 Dify 工作流如何映射到 Java 实现
很多团队先用 Dify / Coze 验证流程,再要求后端用 Java 重写。这里给一个可参考的映射关系:
| Dify 节点 | Java 实现方式 |
|---|---|
| LLM 节点 | ChatClient.prompt() 调用 |
| 工具节点 | @Tool 注解方法 或 HTTP Client 调用 |
| 条件分支 | if/switch 或 Spring 的 @ConditionalOnExpression |
| 代码节点 | 普通 Service 方法 |
| 知识库检索节点 | 向量数据库 client 调用 |
| 变量赋值 | WorkflowContext.set() |
| 回答节点 | return 结果字符串 |
如果你正在做 Dify 工作流转 Java 代码的工作,建议先画出节点 DAG,再按“顺序节点 + 条件节点 + 并行节点”三类结构映射到代码。不要让业务逻辑散落在 Controller 中。
6. Spring AI 扩展的三个高频方向
6.1 扩展自定义 ChatModel
默认接入百炼模型已经够用,但有的场景需要同时对接公司内部的私有化模型服务。此时可以自定义ChatModel:
// 示例思路:实现 Spring AI 的 ChatModel 接口 public class CustomChatModel implements ChatModel { @Override public ChatResponse call(ChatRequest request) { // 将 request 中的消息转发给私有模型服务 // 将私有模型返回内容封装为 ChatResponse return null; } @Override public ChatResponse stream(ChatRequest request) { return call(request); } }这里不要求完整实现,重点是理解扩展点:只要实现ChatModel接口并注册为 Spring Bean,你的 ReactAgent 和 Workflow 代码就不需要感知底层模型差异。
6.2 扩展自定义 Tool
除了@Tool注解方式,ToolCallback接口也允许你动态定义工具描述与执行逻辑,很适合从配置中心读取工具列表的场景。
// 文件路径:src/main/java/com/example/agentdemo/config/DynamicToolConfig.java @Configuration public class DynamicToolConfig { @Bean public ToolCallback dynamicTool() { return ToolCallbacks.from("lookup_order", "根据订单号查询订单状态", """ { "type": "object", "properties": { "orderId": {"type": "string", "description": "订单号"} }, "required": ["orderId"] } """, json -> { // 解析参数并执行业务查询 return "订单已发货"; }); } }ToolCallbacks.from是 Spring AI 提供的便捷构造方法,不同版本参数顺序略有差异,建议以当前版本源码为准。
6.3 扩展对话记忆
默认ChatClient每次调用都是独立上下文。生产环境需要把历史对话保存下来,常用方案有两种:
- 使用
MessageWindowChatMemory在内存中维护最近 N 条消息; - 使用 Redis 存储按会话维度的消息列表。
Spring AI 提供了ChatMemory抽象,你可以把历史消息持久化到任何存储中,然后组合进ChatClient。这样 ReactAgent 才能真正具备连续多轮对话能力。
7. 高频问题与排查清单
以下问题来自我实际使用和社区反馈,按出现频率排序:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 请求报错 400 | 模型名称不存在或当前账号未开通对应模型 | 检查百炼平台是否开通 qwen-plus / qwen-max 服务 |
| plus 版本调工具报错,max 正常 | 不同模型对 Function Calling 的 JSON Schema 支持不一致 | 先用 max 验证,调整工具参数描述后再测 plus |
| 工具结果返回后模型不继续推理 | System Prompt 未声明多轮工具调用规则 | 在 System Prompt 明确“根据工具结果决定是否继续调用工具” |
| Spring AI Alibaba 依赖拉不下来 | 仓库地址或版本号错误 | 检查独立仓库 release 页面,确认阿里云 Maven 仓库已配置 |
| 上下文过长导致费用激增 | 工具结果未裁剪、历史消息未控制 | 工具方法只返回摘要字段,设置 ChatMemory 窗口大小 |
| 模型回答不稳定 | 相同输入在不同时间得到不同结果 | 将温度调低,固定 System Prompt,必要时用流式输出展示过程 |
排查建议清单
如果你在使用过程中遇到 400 报错,推荐按下面步骤排查:
- 确认
spring.ai.dashscope.chat.options.model的值是否与百炼平台开通模型一致; - 临时在日志中打印完整请求报文,观察是否是工具参数格式问题;
- 检查 API Key 是否有权限调用目标模型;
- 换成
qwen-max做交叉验证,排除模型本身限制; - 查看 Spring AI Alibaba 版本更新记录,确认是否存在已知 Bug。
8. 从 Demo 到生产:工程化最佳实践
8.1 密钥与配置安全
不要把百炼 API Key 直接写死在application.yml。推荐使用环境变量、KMS 或配置中心。如果用的是配置中心,注意密钥的密文存储与权限隔离。
8.2 超时、重试与限流
大模型接口的响应时间波动较大。建议在调用 ChatClient 时设置合理的超时时间,并针对网络抖动增加重试机制。但重试时要小心:生成类接口不是幂等的,重试可能导致重复扣费,建议只在超时或连接类错误时重试,不要对正常返回的结果做无理由重试。
同时,百炼 API 有 QPS 限制。如果你在 Workflow 中频繁调用模型,建议在内部加入简单的令牌桶限流,防止某个上游流量突然打满配额。
8.3 可观测性
生产环境必须能观测到完整的调用链路:
- 记录每次 ChatClient 调用的模型名称、输入 token 数、输出 token 数、耗时;
- 记录 ReactAgent 每轮「推理 -> 调用工具 -> 拿到结果」的循环信息;
- 如果搭配 Redis 存储 ChatMemory,还需要统计会话 Redis 访问耗时;
- 对异常响应(400、限流、超时)做结构化日志输出,方便后续告警。
8.4 版本演进与兼容性
Spring AI Alibaba 与 Spring AI 的版本兼容关系是生产环境必须关注的点。建议:
- 锁定一个已验证的版本组合,而不是每次用最新版;
- 升级前阅读官方 changelog,重点关注 ChatClient API 是否变化、工具调用方式是否变化;
- 为模型接入层编写单元测试和集成测试,确保升级后行为不回归。
8.5 明确 AI 能力的确定性边界
最后也是最关键的一条:Agent 与 Workflow 的处理范围必须分清。
- Agent 适合开放式任务:用户意图不明确、工具组合动态变化;
- Workflow 适合确定性任务:业务步骤固定、输入输出结构清晰;
- 生产中优先用 Workflow 包住核心业务链路,把 Agent 限制在意图识别和内容生成环节。
这样既能发挥大模型的灵活性,也能把不可控风险锁在一个可控范围内。
9. 写在最后
Spring AI Alibaba 的价值在于:它让 Java 开发者不需要离开 Spring 生态就能完成大模型接入、工具调用与智能体编排。ReactAgent 不是神秘架构,它就是 ReAct 模式在 Java 中的一个落地封装;Workflow 也不一定非要重量级引擎,几个接口加一个上下文类,就能把业务步骤编排得清清楚楚。
下一步,你可以尝试:
- 把自己已有的一个查询型接口改造成
@Tool,写一个能调用它的 ReactAgent; - 把日常中固定流程的业务(例如请假审批、工单分类)用
WorkflowStep组织起来; - 阅读 Spring AI Alibaba 官方仓库中的示例项目,对照新版本 API 调整自己的实现。
在你动手过程中遇到最多的坑,往往是版本和模型参数问题而不是架构问题。所以我的建议是:先确保最小对话链路能跑通,再逐步叠加工具、记忆和流程编排。这样哪怕出问题,排查范围也是可控的。
如果本文对你搭建 Spring AI Alibaba 应用有帮助,可以先收藏备用。后续我会继续更新智能体调用、工作流并发编排以及百炼模型调优相关的实战笔记。