乐于分享
好东西不私藏

Spring AI Alibaba工具调用实战:Tool、ReAct Agent与Memory详解(附源码)

Spring AI Alibaba工具调用实战:Tool、ReAct Agent与Memory详解(附源码)

上一篇我们先把 Spring AI Alibaba 最基础的调用链跑通了:
`Message -> Prompt -> ChatModel -> ChatClient`
这一篇继续往下走,看看怎么让 AI 不只是回答问题,而是真的开始调用工具、规划步骤、记住上下文。
如果说上一篇解决的是“怎么把一次请求发好”,那这一篇解决的就是“怎么让 AI 真正开始做事”。

Tool Calling:让 AI 不只是会说,还能动手

工具调用的本质,就是让大模型去调用外部函数。
你可以把它理解成给 AI 配了一套工具箱,然后由模型自己判断什么时候用哪个工具。
典型场景:
  • “今天天津天气怎么样” -> 调天气接口
  • “查询订单 123 状态” -> 查数据库
  • “帮我发邮件给张三” -> 调发送接口
在 Spring AI 里,工具调用通常分三步。
① 定义工具
@Componentpublic class WeatherTool {    @Tool(description = "查询指定城市的实时天气信息")    public String getWeather(            @ToolParam(description = "城市名称,例如北京、上海"String city) {        return String.format(              "{\"city\":\"%s\"\"temperature\":\"25C\"\"weather\":\"\"\"humidity\":\"60%%\"}",,                city        );    }}
② 注册到 ChatClient
@Beanpublic ChatClient chatClient(ChatModel chatModel) {    return ChatClient.builder(chatModel)            .defaultSystem("你是一位专业的 Java 技术顾问")            .defaultTools(weatherTool)            .build();}
③ 正常调用(AI 自动决定是否用工具)
@GetMapping("/chat6")public String chat6(@RequestParamString message) {    return chatClient.prompt()            .user(message)            .call()            .content();}
这里不需要你手动判断是否调用工具,模型会自己决定。
这一段只记住 4 点:
  • @ToolParam 描述要写清楚
  • 返回值尽量结构化,JSON 更合适
  • 工具调用会增加 Token 消耗
  • 涉及敏感操作一定要做权限控制
不过,到了这一步,流程编排仍然主要在我们手里。
比如什么时候调工具、调几次、先做哪一步,通常还是由开发者决定。
如果这件事也想交给 AI,就该轮到 Agent 了。

ReAct Agent:让 AI 自己规划步骤

`ReAct` 来自 `Reasoning + Acting`,意思就是让 AI 一边思考,一边行动,循环执行直到任务完成。
比如用户说:
“帮我分析最近 3 个月的消费情况,并给出建议。”
它背后的过程通常是:
  • 先获取数据
  • 再分析数据
  • 最后整理输出
这已经不是单次问答,而是多步决策。
在 Spring AI Alibaba 里,`ReactAgent` 做的事,就是把“推理 + 工具调用”串成一个自动循环。
底层可以简单理解成一个 Graph:
  • Model Node 负责思考
  • Tool Node 负责执行工具
  • Hook Node 负责插入自定义逻辑
最简单的 Agent:
@GetMapping("agent1")public String agent1(@RequestParamString msg) {    ReactAgent agent = ReactAgent.builder()            .name("测试Agent")            .model(chatModel)            .systemPrompt("你是一个简历编写专家")            .build();    return agent.call(msg).getText();}
这一步的意义很直接:把单次调用升级成多步推理。
再加上工具之后,差别就更明显了:
@GetMapping("agent2")public String agent2(@RequestParam String msg) throws Exception {    // 创建工具回调(结构化参数)    FunctionToolCallback<SearchTool.SearchRequestString> toolCallback =            FunctionToolCallback.builder("search"new SearchTool())                    .description("搜索工具")                    .inputType(SearchTool.SearchRequest.class)                    .build();    ReactAgent reactAgent = ReactAgent.builder()            .name("测试Agent")            .model(chatModel)            .tools(toolCallback)            .build();    AssistantMessage assistantMessage = reactAgent.call(msg);    return assistantMessage.getText();}public class SearchTool implements BiFunction<SearchTool.SearchRequestToolContextString> {    // 定义结构化请求参数(推荐写法)    public record SearchRequest(String query) {    }    @Override    public String apply(SearchRequest request, ToolContext toolContext) {        String query = request == null ? "" : request.query();        // 返回结构化结果(建议 JSON,这里简化演示)        return "搜索结果:" + query + " 股票价格是 988元";    }}
这里的本质区别可以直接记成一句话:
`Tool Calling` 是你在编排流程,`ReAct Agent` 是 AI 开始自己规划流程。

Memory:让 Agent 记住上下文

默认情况下,`ReactAgent` 是无状态的。
也就是说,你这一轮告诉它“我叫张三”,下一轮它可能就忘了。
但真实业务里,我们通常希望它:
  • 记住用户偏好
  • 记住历史对话
  • 支持连续多轮交互
Spring AI Alibaba 提供了两层记忆:
  • MemorySaver:负责当前会话
  • MemoryStore:负责跨会话持久化
而真正控制记忆隔离的关键,是 `RunnableConfig` 里的 `threadId`:
  • 相同threadId = 同一个会话
  • 不同threadId = 完全隔离
  • 不传threadId = 每次重新开始
最简单的短期记忆示例:
@GetMapping("agent4")public void agent4() throws Exception {    ReactAgent reactAgent = ReactAgent.builder()            .name("个人小助理")            .model(chatModel)            .saver(new MemorySaver()) // 开启短期记忆            .build();    RunnableConfig config = RunnableConfig.builder()            .threadId("user_1")            .build();    AssistantMessage message1 = reactAgent.call("我的名称叫NannanWang", config);    log.info("message1: {}", message1.getText());    AssistantMessage message2 = reactAgent.call("写一首关于春天的诗词", config);    log.info("message2: {}", message2.getText());    AssistantMessage message3 = reactAgent.call("写一首诗关于苹果的", config);    log.info("message3: {}", message3.getText());    AssistantMessage message4 = reactAgent.call("我叫什么名字", config);    log.info("message4: {}", message4.getText());    // 模拟另一个用户    RunnableConfig config2 = RunnableConfig.builder()            .threadId("user_2")            .build();    AssistantMessage message5 = reactAgent.call("我叫什么名字", config2);    log.info("message5: {}", message5.getText());}
整体流程:
这段代码只是在说明两件事:
  • 同一个threadId 能记住上下文
  • 不同threadId 会完全隔离
所以这里记住 4 点就够了:
  • 开发阶段先用MemorySaver
  • 生产环境更适合RedisSaver 这类持久化方案
  • 不同用户一定要不同threadId
  • 同一用户一定要保持同一个threadId
但记忆加上之后,又会带来新问题:上下文会越来越长。

Hook:让上下文别无限膨胀

上下文一长,就会带来 3 个问题:
  • 容易超出模型上下文限制
  • Token 成本会上升
  • 历史信息太多会影响回答质量
所以 Agent 不只是要记得住,还得记得刚刚好。
这时候就需要 `Hook`,也就是在模型调用前后插入自定义逻辑,去控制上下文。
最常见的一个例子,就是 `MessageTrimmingHook`。

MessageTrimmingHook:调用前先裁一遍

它的作用一句话就能说清:
在调用模型前,先把上下文裁一遍。
常见策略也很简单:
  • 限制消息数量
  • 保留第一条关键消息
  • 保留最近几轮对话
示例代码:
@GetMapping("agent5")public void agent5() throws Exception {    ReactAgent reactAgent = ReactAgent.builder()            .name("个人小助理")            .model(chatModel)            .hooks(new MessageTrimmingHook())            .saver(new MemorySaver())            .build();    RunnableConfig runnableConfig = RunnableConfig.builder()            .threadId("user_1")            .build();    AssistantMessage message1 = reactAgent.call("我的名称叫NannanWang", runnableConfig);    log.info("message1: {}", message1.getText());    AssistantMessage message2 = reactAgent.call("写一首春节的诗", runnableConfig);    log.info("message2: {}", message2.getText());    AssistantMessage message3 = reactAgent.call("写一首端午节的诗", runnableConfig);    log.info("message3: {}", message3.getText());    AssistantMessage message4 = reactAgent.call("你有写过哪些诗", runnableConfig);    log.info("message4: {}", message4.getText());    AssistantMessage message5 = reactAgent.call("我叫什么名字", runnableConfig);    log.info("message5: {}", message5.getText());}
@HookPositions({HookPosition.BEFORE_MODEL})public class MessageTrimmingHook extends MessagesModelHook {    private static final int MAX_MESSAGE = 3;    @Override    public String getName() {        return "message_trimming";    }    @Override    public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {        // 消息数量未超限,直接返回        if (previousMessages.size() <= MAX_MESSAGE) {            return new AgentCommand(previousMessages);        }        // 保留第一条关键消息        Message firstMsg = previousMessages.get(0);        // 保留最近几条消息,尽量保证 user / assistant 成对        int keepCount = previousMessages.size() % 2 == 0 ? 3 : 4;        List<Message> recentMessages = previousMessages.subList(                previousMessages.size() - keepCount,                previousMessages.size()        );        List<Message> trimList = new ArrayList<>();        trimList.add(firstMsg);        trimList.addAll(recentMessages);        // 用裁剪后的消息替换原始上下文        return new AgentCommand(trimList, UpdatePolicy.REPLACE);    }}
它解决的核心问题只有一个:记忆该保留多少。

MessageDeletionHook:调用后直接删旧消息

如果不只是想裁剪,而是想在每轮结束后直接删掉最旧的历史消息,就可以用 `MessageDeletionHook`。
@HookPositions({HookPosition.AFTER_MODEL})public class MessageDeletionHook extends MessagesModelHook {    @Override    public String getName() {        return "message_delete";    }    @Override    public AgentCommand afterModel(List<Message> previousMessages, RunnableConfig config) {        // 如果消息数量大于2,删除最旧的两条        if (previousMessages.size() > 2) {            List<Message> newMessages = previousMessages.subList(                    2,                    previousMessages.size()            );            return new AgentCommand(newMessages, UpdatePolicy.REPLACE);        }        // 不需要删除,直接返回原消息        return new AgentCommand(previousMessages);    }}
这两个 Hook 的区别也很好记:
  • MessageTrimmingHook:调用前临时瘦身
  • MessageDeletionHook:调用后真正删除
实际使用时记住这几点就够了:
  • 顺序是BEFORE_MODEL -> 模型调用 -> AFTER_MODEL
  • 修剪和删除可以一起用
  • 修剪时尽量保持user / assistant 成对
  • 生产环境更适合RedisSaver + MessageTrimmingHook 这样的组合
最后收一下这篇的主线
上一篇我们先把最基础的调用链跑通了,这一篇继续把“AI 怎么真正做事”补齐了。
所以现在你可以把 Spring AI Alibaba 的整条链路连起来看:
`Message -> Prompt -> ChatModel -> ChatClient -> Tool -> Agent -> Memory -> Hook`
如果第一篇解决的是“怎么把请求发好”,那这一篇解决的就是“怎么把 AI 用起来”。
本文所有工具调用逻辑、配置、代码我都整理完整可运行项目,不用自己拼凑,直接导入就能测。
需要这份源码的可以关注我,后台回复[SpringAI入门]即可领取。后面我会持续更新Java AI落地实战,一起把AI真正用进业务里。