风格说明:这一篇不走"先背概念"的路子。咱们直接从需求出发,代码一步步长出来,遇到知识点在代码旁边顺势讲。你只要会 Spring AI 的
ChatClient就能跟下来。
① 先别学概念,接个活儿
假设你今天接到个需求:做个公司内部的「天气小助手」------员工问"北京今天天气咋样",它去调一个天气接口把结果带回来,还能接着聊。
你会 Spring AI,这活儿闭眼能撸:ChatClient 配个工具,完事。但今天咱们不这么干,改用 Spring AI Alibaba 的 ReactAgent 来撸。目的不是"换个 API 写一遍",而是让你亲手跑一遍之后,自己体会它和 ChatClient 到底差在哪儿。
先把依赖加上(版本 1.1.2.0,两个要对齐):
xml
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
<version>1.1.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>1.1.2.0</version>
</dependency>
API Key 走环境变量(别硬编码进仓库):
bash
export AI_DASHSCOPE_API_KEY=你的key
② 最小能跑的 Agent,十来行
先不管原理,把下面这段跑起来------它定义了个天气工具,塞给 Agent,问一句,把答案打出来。
java
// 1. 先有个模型(DashScope 的 ChatModel)
DashScopeApi dashScopeApi = DashScopeApi.builder()
.apiKey(System.getenv("AI_DASHSCOPE_API_KEY"))
.build();
ChatModel chatModel = DashScopeChatModel.builder()
.dashScopeApi(dashScopeApi)
.build();
// 2. 定义工具:一个函数,输入城市名,返回天气
public static class WeatherRequest { public String location; }
public class WeatherTool implements BiFunction<WeatherRequest, ToolContext, String> {
@Override
public String apply(WeatherRequest req, ToolContext toolContext) {
return req.location + " 永远是晴天!";
}
}
ToolCallback weatherTool = FunctionToolCallback.builder("get_weather", new WeatherTool())
.description("根据城市名查询天气")
.inputType(WeatherRequest.class)
.build();
// 3. 把模型 + 工具 交给 ReactAgent
ReactAgent agent = ReactAgent.builder()
.name("weather_agent")
.model(chatModel)
.tools(weatherTool)
.systemPrompt("你是一个乐于助人的天气助手")
.saver(new MemorySaver())
.build();
// 4. 跑
AssistantMessage response = agent.call("旧金山天气怎么样");
System.out.println(response.getText());
跑起来,控制台会打出类似 旧金山 永远是晴天! 的结果。
到这儿你八成想问:这跟 ChatClient.tools() 有啥区别?不都是"给个工具、问一句、它自己调工具回答"吗?
问得对。区别不在"能不能调工具"------Spring AI 的 ChatClient 早就能自动跑工具循环了,不用自己手写。区别得等咱们把这个 Agent 再往前推两步,才看得出来。
③ 推进一步:让它记事儿(多轮对话)
"San Francisco 天气咋样"之后,员工又问"那明天呢?"。在 ChatClient 里你要么靠 ChatMemory advisor 把历史塞回去,要么自己维护消息列表。ReactAgent 这边,你只要一个 threadId:
java
ReactAgent agent = ReactAgent.builder()
.name("weather_agent")
.model(chatModel)
.tools(weatherTool)
.saver(new MemorySaver()) // 就这行在管记忆
.build();
// 同一个 threadId,串起一整段对话
RunnableConfig config = RunnableConfig.builder().threadId("user_123").build();
agent.call("今天旧金山天气怎么样", config);
agent.call("那明天呢", config); // 模型知道"明天"指的是旧金山
你发现没------表面上看,ChatClient + ChatMemory 也能干一样的事 。所以"多轮记忆"压根不是 ReactAgent 的独门绝技。那它 saver 存的到底多了啥?先卖个关子,第 ⑤ 节揭晓(这是全文最关键的一点,也是很多人第一次用会迷糊的地方)。
④ 推进两步:看清它在"想"啥(stream 看过程)
再补一刀:"你们这玩意儿像个黑盒,我都不晓得它到底调没调工具、调了啥,排查问题抓瞎。"
ChatClient 给你的是最终一句回复,中间"模型思考 → 决定调工具 → 工具返回 → 再思考"这串过程你看不到。ReactAgent 用 stream() 把这串过程全吐出来:
java
Flux<NodeOutput> stream = agent.stream("旧金山天气怎么样", config);
stream.subscribe(output -> {
String node = output.node(); // 当前执行完的节点名,如 agent 的模型节点 / 工具节点
boolean isStart = output.isSTART();
boolean isEnd = output.isEND();
// output.state() 能直接拿到完整运行时状态(消息、工具 IO 等都在里面)
});
每个 NodeOutput 代表图里"一个节点跑完"。node() 给你节点名(比如模型节点、工具节点),isSTART() / isEND() 标出首尾,state() 能直接拿到完整的运行时状态------消息、工具输入输出全在里面。这相当于给 Agent 装了"链路追踪",你一眼能看出它哪一步在调工具、哪一步跑完了,排查"它怎么乱调工具"这种问题价值巨大。

⑤ 最关键的点:MemorySaver 和 ChatMemory,到底差在哪
刚才 ③ 留的扣。很多人(包括我第一次用)以为 saver 就是 ReactAgent 版的 ChatMemory。其实不是一回事,差在存的东西不一样:
ChatClient + ChatMemory存的是聊天记录------谁说了啥,一张消息表。模型本身还是无状态的,历史只是作为文本重新喂回去。就像每次开会前,助理把纪要打印给你,你看了才有上下文。ReactAgent + MemorySaver / Checkpointer存的是运行时快照------除了聊天记录,还存了"工具调了啥、返回了啥、图执行到哪个节点、下一步要干啥"。
用游戏类比最准:ChatMemory 是"战绩列表"(你打过哪些怪);Checkpointer 是"存档点"(你站在地图哪个坐标、血量是几、背包有啥、任务到哪步了)。
正因为存的是"完整执行状态",你才能玩出这些花活儿(后面高级篇逐个讲):
- 暂停 / 恢复执行:工具要扣钱了,先停一下让人确认(human-in-the-loop);
- 时间旅行 / 回滚:上一步走错了,退回重来;
- 取消长任务:跑了半小时的活儿,中途喊停。
这些都不是"回放聊天记录"能实现的。在简单多轮问答场景下,两者体验几乎一样;一旦你要玩"可控、可干预、可恢复"的 Agent,区别就出来了。
⑥ Spring AI vs SAA ReactAgent,诚实对照
| 维度 | Spring AI(ChatClient) | SAA ReactAgent |
|---|---|---|
| 自动跑工具循环 | ✅ 有 | ✅ 有(一样,不是 SAA 独门) |
| 多轮记忆 | ✅ ChatMemory | ✅ saver + threadId |
| 记忆持久化 | ✅ RedisChatMemory / JdbcChatMemory | ✅ Checkpointer(Redis / Mongo 等) |
| 过程可见性 | 仅模型 token 流,看不到工具/Hook 步骤 | ✅ stream() 暴露每步节点事件 |
| 存的是 | 聊天记录 | 运行时快照 |
| 暂停 / 恢复 / 回滚 / 取消 | ❌ | ✅ |
| 护栏 | Advisor(消息级切面) | Hooks(Agent 级,如人工确认) |
| 编排(多 Agent / 工作流 / A2A) | ❌ | ✅ 框架内建 |
注意看:前三行两者都有,ReactAgent 没有"取代" ChatClient 的任何已有能力。真正的增量是后面几行------可观测、可存档、可干预、可编排。这也是整个 SAA 系列要一点点展开的东西。
⑦ 小结 & 下一篇预告
今天咱们没先背概念,而是直接撸了个会调工具的 ReactAgent,又推了两步(多轮、看过程),最后点破了它和 ChatClient 真正的分水岭:不是"能不能调工具 / 记事儿",而是"存的是聊天记录还是运行时快照,以及能不能暂停 / 干预 / 编排"。
下一篇《模型与消息》------Agent 已经跑起来了,咱们聊聊怎么给它换模型、怎么把 Messages 和运行时上下文(ToolContext / metadata)玩出花,顺便看几个真实工具怎么接外部系统。