Spring AI Alibaba工具调用实战:Tool、ReAct Agent与Memory详解(附源码)
Tool Calling:让 AI 不只是会说,还能动手
-
“今天天津天气怎么样” -> 调天气接口 -
“查询订单 123 状态” -> 查数据库 -
“帮我发邮件给张三” -> 调发送接口
@Componentpublic class WeatherTool {@Tool(description = "查询指定城市的实时天气信息")public String getWeather(@ToolParam(description = "城市名称,例如北京、上海") String city) {return String.format("{\"city\":\"%s\", \"temperature\":\"25C\", \"weather\":\"晴\", \"humidity\":\"60%%\"}",,city);}}
@Beanpublic ChatClient chatClient(ChatModel chatModel) {return ChatClient.builder(chatModel).defaultSystem("你是一位专业的 Java 技术顾问").defaultTools(weatherTool).build();}
@GetMapping("/chat6")public String chat6(@RequestParamString message) {return chatClient.prompt().user(message).call().content();}

-
@ToolParam描述要写清楚 -
返回值尽量结构化,JSON 更合适 -
工具调用会增加 Token 消耗 -
涉及敏感操作一定要做权限控制
ReAct Agent:让 AI 自己规划步骤
-
先获取数据 -
再分析数据 -
最后整理输出
-
Model Node 负责思考 -
Tool Node 负责执行工具 -
Hook Node 负责插入自定义逻辑
@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.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) {}@Overridepublic String apply(SearchRequest request, ToolContext toolContext) {String query = request == null ? "" : request.query();// 返回结构化结果(建议 JSON,这里简化演示)return "搜索结果:" + query + " 股票价格是 988元";}}

Memory:让 Agent 记住上下文
-
记住用户偏好 -
记住历史对话 -
支持连续多轮交互
-
MemorySaver:负责当前会话 -
MemoryStore:负责跨会话持久化
-
相同 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会完全隔离
-
开发阶段先用 MemorySaver -
生产环境更适合 RedisSaver这类持久化方案 -
不同用户一定要不同 threadId -
同一用户一定要保持同一个 threadId
Hook:让上下文别无限膨胀
-
容易超出模型上下文限制 -
Token 成本会上升 -
历史信息太多会影响回答质量
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;@Overridepublic String getName() {return "message_trimming";}@Overridepublic 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:调用后直接删旧消息
@HookPositions({HookPosition.AFTER_MODEL})public class MessageDeletionHook extends MessagesModelHook {@Overridepublic String getName() {return "message_delete";}@Overridepublic 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);}}
-
MessageTrimmingHook:调用前临时瘦身 -
MessageDeletionHook:调用后真正删除
-
顺序是 BEFORE_MODEL -> 模型调用 -> AFTER_MODEL -
修剪和删除可以一起用 -
修剪时尽量保持 user / assistant成对 -
生产环境更适合 RedisSaver + MessageTrimmingHook这样的组合


夜雨聆风