当前位置: 首页 > news >正文

* Spring AI 的Tool Calling 工具调用

Function Calling:让大模型拥有“动手能力”:

https://blog.csdn.net/weixin_55772633/article/details/160636233?spm=1011.2415.3001.5331

官网地址:https://docs.spring.io/spring-ai/reference/api/tools.html




一、什么是 Tool Calling?

Tool Calling(工具调用,早期称为 Function Calling)是 AI 应用中的一种常见模式,它允许大语言模型与一组外部 API 或工具进行交互,从而突破模型自身的知识局限。工具主要用在两种场景中:

信息检索型:模型无法实时获取当前日期、天气等动态信息,通过工具可以从外部数据源获取实时数据,弥补模型知识的不足。例如,查询给定地点的当前天气。

执行动作型:在软件系统中执行实际操作,如发送邮件、在数据库中创建新记录、提交表单、触发工作流等。

⚠️重要安全认知:工具调用的实际执行逻辑由客户端应用程序提供,模型本身只能请求调用工具并提供输入参数,永远无法真正访问工具的底层 API。这一点对于保障系统的安全性至关重要。

二、工作原理:四阶段流程

Spring AI 的工具调用遵循以下标准流程:

  1. 将工具的定义(名称、描述、输入参数的 JSON Schema)加入聊天请求

  2. AI 模型分析用户输入,自主判断是否需要调用某个工具

  3. 如果模型决定调用工具,它会返回一个包含工具名和输入参数的响应

  4. 应用收到请求后,在本地执行对应的 Java 方法

  5. 将方法执行结果返回给模型

  6. 模型基于工具返回的结果,生成最终的自然语言回复

三、快速入门:动手写一个工具

下面我们从一个简单的例子开始,在 Spring AI 中实现并调用一个工具。

3.1定义Tool返回结果类

为了让大模型理解返回的字段,也方便前端处理,我们定义一个CourseInfo类,并添加@JsonPropertyDescription注解。

package com.edu.tools; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CourseInfo { @JsonPropertyDescription("课程id") private Long id; @JsonPropertyDescription("课程名称") private String name; @JsonPropertyDescription("课程价格,单位为元,货币为人民币") private double price; @JsonPropertyDescription("课程学习有效期,单位:月") private Integer validDuration; @JsonPropertyDescription("适用人群,例如:初学者") private String usePeople; @JsonPropertyDescription("课程详细介绍") private String detail; }

3.1.1 @JsonPropertyDescription在 Spring AI 内部的实际效果:

当你在@Tool方法返回一个包含@JsonPropertyDescription注解的类时,Spring AI 在构建工具调用请求时,会将该类的 JSON Schema 传递给 AI 模型。模型在解析工具返回值时,这些描述信息可以帮助它:

  • 理解每个字段的业务含义

  • 决定是否需要在最终回复中重新组织或转述这些字段

3.1.2其他常用注解(增强字段描述)

除了@JsonPropertyDescription,你还可以组合使用:

注解作用示例
@JsonProperty(access = Access.READ_ONLY)标记字段只用于返回,不接受模型传入自动生成的courseId
@JsonFormat(pattern = "yyyy-MM-dd")指定日期字段的格式@JsonFormat(pattern = "yyyy-MM-dd") private Date createDate;
@JsonProperty(required = true)标记字段在返回时必定存在关键业务字段

3.2用@Tool注解定义工具

package com.edu.tools; import cn.hutool.core.lang.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.util.Random; @Component @Slf4j @RequiredArgsConstructor public class CourseTools { @Tool(description = "根据课程名称查询课程详细信息") public CourseInfo queryCourseByName(@ToolParam(description = "课程名称") String courseName) { log.info("调用根据课程名称查询课程详细信息工具,课程名称:{}", courseName); //todo 实际从数据库查询,这里直接模拟一个数据 CourseInfo courseInfo = CourseInfo.builder() .id(1L) .name(courseName + "AI大模型应用开发" + UUID.randomUUID()) .price(new Random(1000).nextDouble()) .validDuration(new Random(10).nextInt()) .usePeople("开发者") .detail("学好AI大模型应用开发,月入百万") .build(); log.info("返回课程详细信息:{}", courseInfo); return courseInfo; } }

@Tool注解提供了几个常用属性:

属性说明
name工具名称,不指定时默认为方法名。同一类中不能有同名工具
description工具描述,强烈建议提供详细描述,这是模型理解工具用途的关键
returnDirect是否直接将工具结果返回给客户端而不再经过模型处理
resultConverter自定义工具调用结果的转换器

@ToolParam支持以下属性:

  • description:参数的描述,帮助模型理解参数的含义和格式要求

  • required:参数是否必需,默认为true。如果参数被标注为@Nullable,默认会被视为可选(除非明确指定required=true

3.3注册Tool到ChatClient

@Autowired private CourseTools courseTools; @Override public Flux<ChatEventVO> streamChatJson(String question, String sessionId) { // 大模型输出内容的缓存器,用于在输出中断后的数据存储 var outputBuilder = new StringBuilder(); return chatClient .prompt() .advisors(MessageChatMemoryAdvisor.builder(redisChatMemory) .conversationId(sessionId) .build()) .tools(courseTools) // 添加工具 .system(x->x.text(aiPromptResources.getSystemChatMessage()) .param("now", LocalDateTime.now() .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) .user(question) .stream() .chatResponse() .doFirst(() -> GENERATE_STATUS.put(sessionId, true)) // 开始生成时置为 true .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) .doOnComplete(() -> GENERATE_STATUS.remove(sessionId)) .doOnCancel(() -> { // 当输出被取消时,保存输出的内容到历史记录中 redisChatMemory.add(sessionId, new AssistantMessage(outputBuilder.toString())); }) .takeWhile(response -> GENERATE_STATUS.getOrDefault(sessionId, false)) // 关键:控制流是否继续 .map(chatResponse -> { var text = chatResponse.getResult().getOutput().getText(); outputBuilder.append(text); // 累积文本 return ChatEventVO.builder() .eventType(ChatEventTypeEnum.DATA.getValue()) .eventData(text) .build(); }) .concatWith(Flux.just(ChatEventVO.builder() .eventType(ChatEventTypeEnum.STOP.getValue()) //结束标识 .build())); }

3.4测试基础查询

四、实现课程卡片展示

4.1问题分析

前端需要接收结构化的JSON数据才能渲染卡片。而大模型返回的只是自然语言文本。我们需要在流式输出的最后,额外追加一段约定好的JSON数据,格式如下:

{ "eventType": 1003, "eventData": { "courseInfo_1589905661084430337": { "id": "1589905661084430337", "name": "可能是史上最全的微服务技术栈课程", "price": 199.0, "validDuration": 9999, "usePeople": "有一定的Java开发基础...", "detail": "可能是史上最全的微服务技术栈课程..." } } }

4.2解决方案:ToolResultHolder + requestId

  1. 生成请求唯一标识:每次对话请求生成一个requestId,通过ToolContext传递给Tool。

  2. Tool执行后存储结果:Tool执行完毕后,将课程数据以requestId为key存放到全局容器ToolResultHolder中。

  3. 流输出末尾读取并追加:在Flux输出流的最后,根据requestId从容器中取出数据,如果存在则构造卡片事件追加到流中。

4.3代码实现

4.3.1. 定义ToolResultHolder

package com.edu.holder; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ToolResultHolder { private static final Map<String, Map<String, Object>> HANDLER_MAP = new ConcurrentHashMap<>(); public static void put(String key, String field, Object result) { HANDLER_MAP.computeIfAbsent(key, k -> new HashMap<>()).put(field, result); } public static Map<String, Object> get(String key) { return key == null ? null : HANDLER_MAP.get(key); } public static void remove(String key) { HANDLER_MAP.remove(key); } }

4.3.2在chatClient中传递requestId

4.3.3 修改CourseTools:接收ToolContext并存储结果

4.3.4末尾追加卡片数据

public Flux<ChatEventVO> streamChatJson(String question, String sessionId) { // 大模型输出内容的缓存器,用于在输出中断后的数据存储 var outputBuilder = new StringBuilder(); // 生成唯一请求id var requestId = IdUtil.fastSimpleUUID(); return chatClient .prompt() .advisors(MessageChatMemoryAdvisor.builder(redisChatMemory) .conversationId(sessionId) .build()) .tools(courseTools) // 添加工具 .toolContext(Map.of("requestId",requestId)) .system(x->x.text(aiPromptResources.getSystemChatMessage()) .param("now", LocalDateTime.now() .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) .user(question) .stream() .chatResponse() .doFirst(() -> GENERATE_STATUS.put(sessionId, true)) // 开始生成时置为 true .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) .doOnComplete(() -> GENERATE_STATUS.remove(sessionId)) .doOnCancel(() -> { // 当输出被取消时,保存输出的内容到历史记录中 redisChatMemory.add(sessionId, new AssistantMessage(outputBuilder.toString())); }) .takeWhile(response -> GENERATE_STATUS.getOrDefault(sessionId, false)) // 关键:控制流是否继续 .map(chatResponse -> { var text = chatResponse.getResult().getOutput().getText(); outputBuilder.append(text); // 累积文本 return ChatEventVO.builder() .eventType(ChatEventTypeEnum.DATA.getValue()) .eventData(text) .build(); }) .concatWith(Flux.defer(() -> { // 流结束后,检查是否有工具结果 var map = ToolResultHolder.get(requestId); if (CollUtil.isNotEmpty(map)) { ToolResultHolder.remove(requestId); // 清理 var cardEvent = ChatEventVO.builder() .eventData(map) // 此处map结构为 { "courseInfo_xxx": CourseInfo对象 } .eventType(ChatEventTypeEnum.PARAM.getValue()) // 约定事件类型1003 .build(); return Flux.just(cardEvent, ChatEventVO.builder() .eventType(ChatEventTypeEnum.STOP.getValue()) //结束标识 .build()); } return Flux.just(ChatEventVO.builder() .eventType(ChatEventTypeEnum.STOP.getValue()) //结束标识 .build()); })); }

4.3.5测试

五、Tool额外数据丢失问题修复实录

课程查询和预下单功能,给前端返回的数据中,包含了eventType1003的数据,这个叫作额外数据,给前端提供,前端是不会显示到页面的,正常对话是没问题的,但是,数据存储到Redis是没有保存进去的,如下:

5.1问题分析

数据流向回顾

一次完整调用中,数据流如下:

  1. 请求层:生成requestId,通过ToolContext传递给Tool

  2. Tool层:查询课程数据,将结果存入ToolResultHolder(key为requestId

  3. 输出层:流结束时,从ToolResultHolder.get(requestId)取出卡片数据,构造eventType=1003事件发送给前端

  4. 存储层RedisChatMemoryRepository负责将对话消息保存到Redis

问题出在第4步:保存消息时,只保存了UserMessageAssistantMessage的文本内容、metadata、toolCalls等标准字段,完全没有涉及ToolResultHolder中的卡片数据

5.2根本原因

RedisChatMemoryRepository在序列化消息时,并不知道ToolResultHolder中存了什么。它只能拿到Spring AI框架提供的Message对象,而AssistantMessage中没有params字段来存放额外的结构化数据。

我们需要做两件事:

  1. 在保存助手消息时,将对应的卡片数据(params)也持久化到Redis

  2. 在读取历史消息时,能恢复这些params并返回给前端

关键在于:如何将ToolResultHolder中临时存储的数据,绑定到具体的某条助手消息上?

5.3 Bug解决

这个bug的解决思路就是,在RedisChatMemoryRepository中保存数据时,获取到ToolResultHolder中的数据,将数据保存到params中即可。

但是,ToolResultHolder中的数据,是与requestId关联的,requestId是我们自己生成的,在RedisChatMemory中是没有的,所以,这个问题的关键就是如何获取到requestId了,只要有了requestId就可以获取到数据,进行保存了。

如何传递requestId

其实,同样也是可以借助于ToolResultHolder来完成,我们可以把ToolResultHolder看作是一个通用的容器,可以放Tool的结果,也可以放其他的内容,只要及时的删除即可。

有了这个思路,问题就好解决了,解决代码如下:

5.3.1 映射消息id和请求id

public Flux<ChatEventVO> streamChatJson(String question, String sessionId) { // 大模型输出内容的缓存器,用于在输出中断后的数据存储 var outputBuilder = new StringBuilder(); // 生成唯一请求id var requestId = IdUtil.fastSimpleUUID(); return chatClient .prompt() .advisors(MessageChatMemoryAdvisor.builder(redisChatMemory) .conversationId(sessionId) .build()) .tools(courseTools) // 添加工具 .toolContext(Map.of("requestId",requestId)) .system(x->x.text(aiPromptResources.getSystemChatMessage()) .param("now", LocalDateTime.now() .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) .user(question) .stream() .chatResponse() .doFirst(() -> GENERATE_STATUS.put(sessionId, true)) // 开始生成时置为 true .doOnError(throwable -> GENERATE_STATUS.remove(sessionId)) .doOnComplete(() -> GENERATE_STATUS.remove(sessionId)) .doOnCancel(() -> { // 当输出被取消时,保存输出的内容到历史记录中 redisChatMemory.add(sessionId, new AssistantMessage(outputBuilder.toString())); }) .takeWhile(response -> GENERATE_STATUS.getOrDefault(sessionId, false)) // 关键:控制流是否继续 .map(chatResponse -> { // 对于响应结果进行处理,如果是最后一条数据,就把此次消息id放到内存中 // 主要用于存储消息数据到 redis中,可以根据消息di获取的请求id,再通过请求id就可以获取到参数列表了 // 从而解决,在历史聊天记录中没有外参数的问题 var finishReason = chatResponse.getResult().getMetadata().getFinishReason(); if (StrUtil.equals("STOP", finishReason)) { var messageId = chatResponse.getMetadata().getId(); ToolResultHolder.put(messageId, "requestId", requestId); } var text = chatResponse.getResult().getOutput().getText(); outputBuilder.append(text); // 累积文本 return ChatEventVO.builder() .eventType(ChatEventTypeEnum.DATA.getValue()) .eventData(text) .build(); }) .concatWith(Flux.defer(() -> { // 流结束后,检查是否有工具结果 var map = ToolResultHolder.get(requestId); if (CollUtil.isNotEmpty(map)) { ToolResultHolder.remove(requestId); // 清理 var cardEvent = ChatEventVO.builder() .eventData(map) // 此处map结构为 { "courseInfo_xxx": CourseInfo对象 } .eventType(ChatEventTypeEnum.PARAM.getValue()) // 约定事件类型1003 .build(); return Flux.just(cardEvent, ChatEventVO.builder() .eventType(ChatEventTypeEnum.STOP.getValue()) //结束标识 .build()); } return Flux.just(ChatEventVO.builder() .eventType(ChatEventTypeEnum.STOP.getValue()) //结束标识 .build()); })); }

5.3.2在MessageUtil中增加params参数的处理:

public static String toJson(Message message) { var myMessage = BeanUtil.toBean(message, MyMessage.class); // 设置消息内容 myMessage.setTextContent(message.getText()); if (message instanceof AssistantMessage assistantMessage) { myMessage.setToolCalls(assistantMessage.getToolCalls()); // 通过 messageId 获取 requestId,再通过 requestId 获取参数列表,如果有,就存储起来 // 最后,删除 messageId 对应的数据 var messageId = Convert.toStr(assistantMessage.getMetadata().get("id")); var requestId = Convert.toStr(ToolResultHolder.get(messageId).get("requestId")); var params = ToolResultHolder.get(requestId); if (ObjectUtil.isNotEmpty(params)) { myMessage.setParams(params); } ToolResultHolder.remove(messageId); } if (message instanceof ToolResponseMessage toolResponseMessage) { myMessage.setToolResponses(toolResponseMessage.getResponses()); } return JSONUtil.toJsonStr(myMessage); }

5.3.3在反序列化Message对象时,无法给AssistantMessage设置params属性,所以需要继承AssistantMessage来扩展params属性:

package com.tianji.aigc.memory; import lombok.Getter; import lombok.Setter; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.content.Media; import java.util.List; import java.util.Map; @Setter @Getter public class MyAssistantMessage extends AssistantMessage { private Map<String, Object> params; public MyAssistantMessage(String content, Map<String, Object> properties, List<ToolCall> toolCalls, List<Media> media, Map<String, Object> params) { super(content, properties, toolCalls, media); this.params = params; } }

MessageUtil:
package com.edu.memory; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONUtil; import com.edu.holder.ToolResultHolder; import org.springframework.ai.chat.messages.*; /** * 消息转换工具类,提供消息对象与JSON字符串之间的转换功能,主要用于Redis存储格式转换 */ public class MessageUtil { /** * 将Message对象转换为Redis存储格式的JSON字符串 * * @param message 需要转换的原始消息对象 * @return 符合Redis存储规范的JSON字符串 */ public static String toJson(Message message) { var myMessage = BeanUtil.toBean(message, MyMessage.class); // 设置消息内容 myMessage.setTextContent(message.getText()); if (message instanceof AssistantMessage assistantMessage) { myMessage.setToolCalls(assistantMessage.getToolCalls()); // 通过 messageId 获取 requestId,再通过 requestId 获取参数列表,如果有,就存储起来 // 最后,删除 messageId 对应的数据 var messageId = Convert.toStr(assistantMessage.getMetadata().get("id")); var requestId = Convert.toStr(ToolResultHolder.get(messageId).get("requestId")); var params = ToolResultHolder.get(requestId); if (ObjectUtil.isNotEmpty(params)) { myMessage.setParams(params); } ToolResultHolder.remove(messageId); } if (message instanceof ToolResponseMessage toolResponseMessage) { myMessage.setToolResponses(toolResponseMessage.getResponses()); } return JSONUtil.toJsonStr(myMessage); } /** * 将Redis存储的JSON字符串反序列化为对应的Message对象 * * @param json Redis存储的JSON格式消息数据 * @return 对应类型的Message对象 * @throws RuntimeException 当无法识别的消息类型时抛出异常 */ public static Message toMessage(String json) { var myMessage = JSONUtil.toBean(json, MyMessage.class); var messageType = MessageType.valueOf(myMessage.getMessageType()); switch (messageType) { case SYSTEM -> { //return new SystemMessage(myMessage.getTextContent()); return SystemMessage.builder() .text(myMessage.getTextContent()) .build(); } case USER -> { return UserMessage.builder() .text(myMessage.getTextContent()) .metadata(myMessage.getMetadata()) .media(myMessage.getMedia()) .build(); } case ASSISTANT -> { /*return AssistantMessage.builder() .content(myMessage.getTextContent()) .media(myMessage.getMedia()) .toolCalls(myMessage.getToolCalls()) .build();*/ return new MyAssistantMessage(myMessage.getTextContent(), myMessage.getMetadata(), myMessage.getToolCalls(), myMessage.getMedia(), myMessage.getParams()); } case TOOL -> { return ToolResponseMessage.builder() .responses(myMessage.getToolResponses()) .metadata(myMessage.getMetadata()) .build(); } } throw new RuntimeException("Message data conversion failed."); } }

5.3.4测试结果

http://www.jsqmd.com/news/781824/

相关文章:

  • 如何高效管理中文文献:Zotero Jasminum插件的终极解决方案
  • Stratix III FPGA的DPA电路与rx_dpa_locked信号解析
  • 基于MediaPipe与Python的虚拟鼠标:手势识别与坐标映射实战
  • 如何免费解锁原神帧率限制?2025终极指南让游戏画面丝滑如镜
  • Oracle数据库PL/SQL中执行存储过程_oracle手动执行存储过程
  • 09:字符菱形
  • 一致性哈希终极指南:分布式系统设计的核心算法解析
  • 关于interface(接口继承)extends(接口)interface的问题_interface extends
  • Zed编辑器Cursor深色主题移植:设计解析与深度定制指南
  • 从OpenAI技能存档到AI Agent实战:解析与构建结构化AI能力蓝图
  • 水的数据库之道,老子一句话照进 SAP HANA 开发现场
  • 系统质量属性与架构评估
  • LitElement事件处理终极指南:构建高性能交互Web组件的10个最佳实践
  • VMware macOS 终极解锁方案:在普通PC上免费运行苹果系统
  • 告别仓库混乱!用Excel手把手教你做EIQ-ABC分析,快速定位核心客户与爆款商品
  • Universal Pokemon Randomizer完全手册:3步打造你的专属宝可梦世界
  • Windows驱动存储终极管理指南:DriverStore Explorer专业使用手册
  • Yeti实体关系图构建指南:如何可视化威胁活动与攻击者关联
  • Nuxt.js Token管理完全指南:JWT、刷新令牌和安全存储的最佳实践 [特殊字符]
  • LeetCode--已知前序遍历和中序遍历构造二叉树_已知一棵树的前序遍历和中序遍历
  • ComfyUI节点冲突深度解析:5种系统化解决方案与最佳实践
  • OpenCode与Cursor Pro深度整合:无限制提示词与完整工具调用实战
  • Claude Stacks:AI开发环境一键打包与共享的CLI工具实战
  • Rune语言社区贡献指南:如何参与开源项目开发的完整教程
  • 第二篇:Redis的过期删除与内存淘汰——数据过期了怎么删?内存满了怎么办?
  • Blueboat性能优化秘籍:让你的JavaScript应用运行速度提升300%
  • 树莓派Zero W打造开源电子宠物:软硬结合与低功耗设计实战
  • 视频转文字的方法有哪些?2026年视频转文字工具推荐完全对比
  • Vale编译器构建系统详解:跨平台编译与依赖管理终极指南
  • LitElement自定义渲染根终极指南:解锁Shadow DOM的高级配置