【SAA实战】第 1 篇:ReactAgent 入门——先撸个"会调工具的助手"跑起来

94 阅读6分钟

风格说明:这一篇不走"先背概念"的路子。咱们直接从需求出发,代码一步步长出来,遇到知识点在代码旁边顺势讲。你只要会 Spring AI 的 ChatClient 就能跟下来。

① 先别学概念,接个活儿

假设你今天接到个需求:做个公司内部的「天气小助手」——员工问"北京今天天气咋样",它去调一个天气接口把结果带回来,还能接着聊。

你会 Spring AI,这活儿闭眼能撸:ChatClient 配个工具,完事。但今天咱们不这么干,改用 Spring AI Alibaba 的 ReactAgent 来撸。目的不是"换个 API 写一遍",而是让你亲手跑一遍之后,自己体会它和 ChatClient 到底差在哪儿。

先把依赖加上(版本 1.1.2.0,两个要对齐):

<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 走环境变量(别硬编码进仓库):

export AI_DASHSCOPE_API_KEY=你的key

② 最小能跑的 Agent,十来行

先不管原理,把下面这段跑起来——它定义了个天气工具,塞给 Agent,问一句,把答案打出来。

// 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

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() 把这串过程全吐出来:

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 装了"链路追踪",你一眼能看出它哪一步在调工具、哪一步跑完了,排查"它怎么乱调工具"这种问题价值巨大。

image.png

⑤ 最关键的点: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)玩出花,顺便看几个真实工具怎么接外部系统。