上一篇我们先把 Spring AI Alibaba 最基础的调用链跑通了:

`Message -> Prompt -> ChatModel -> ChatClient`

这一篇继续往下走,看看怎么让 AI 不只是回答问题,而是真的开始调用工具、规划步骤、记住上下文。

如果说上一篇解决的是“怎么把一次请求发好”,那这一篇解决的就是“怎么让 AI 真正开始做事”。

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

工具调用的本质,就是让大模型去调用外部函数。

你可以把它理解成给 AI 配了一套工具箱,然后由模型自己判断什么时候用哪个工具。

典型场景:

  • “今天天津天气怎么样” -> 调天气接口

  • “查询订单 123 状态” -> 查数据库

  • “帮我发邮件给张三” -> 调发送接口

在 Spring AI 里,工具调用通常分三步。

① 定义工具

@Component
public class WeatherTool {
    @Tool(description = "查询指定城市的实时天气信息")
    public String getWeather(
            @ToolParam(description = "城市名称,例如北京、上海") String city) {
        return String.format(
              "{\"city\":\"%s\", \"temperature\":\"25C\", \"weather\":\"晴\", \"humidity\":\"60%%\"}",,
                city
        );
    }
}

    ② 注册到 ChatClient

    @Bean
    public ChatClient chatClient(ChatModel chatModel) {
        return ChatClient.builder(chatModel)
                .defaultSystem("你是一位专业的 Java 技术顾问")
                .defaultTools(weatherTool)
                .build();
    }

      ③ 正常调用(AI 自动决定是否用工具)

      @GetMapping("/chat6")
      public String chat6(@RequestParam String 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(@RequestParam String 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.SearchRequest, String> 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.SearchRequest, ToolContext, String> {
          
              // 定义结构化请求参数(推荐写法)
              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 用起来”。

                为了方便大家直接上手,我把本文完整可运行项目源码打包好了,包含依赖、配置、启动类全套,导入就能跑

                如果你对 Java + AI 实战、Spring AI 落地、RAG、MCP、Agent、AI 支付这些内容感兴趣,关注我的技术号,想领取 Spring AI 入门代码的话,关注后后台回复:SpringAI入门 即可。

                Logo

                AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

                更多推荐