【Spring AI 1.0.0】Spring AI 1.0.0框架快速入门(5)——Tool Calling(工具调用)
Spring AI框架快速入门
- Spring AI 系列文章:
- 一、前言
- 二、Tool Calling
- 2.1 信息检索
- 2.2 采取行动
- 三、方法作为工具
- 3.1 注解式:@Tool
- 3.1.1 向 ChatClient 添加工具
- 3.1.2 向 ChatClient 添加默认工具
- 3.1.3 向 ChatModel 添加工具
- 3.1.4 向 ChatModel 添加默认工具
- 3.2 编程式:MethodToolCallback
- 3.2.1 向 ChatClient 和 ChatModel 添加工具
- 3.3 方法工具限制
- 四、函数作为工具
- 4.1 编程式:FunctionToolCallback
- 4.2 动态式:@Bean ⭐️ ⭐️ ⭐️
- 4.3 函数工具限制
- 五、总结
Spring AI 系列文章:
【Spring AI 1.0.0】Spring AI 1.0.0框架快速入门(1)——Chat Client API
【Spring AI 1.0.0】Spring AI 1.0.0框架快速入门(2)——Prompt(提示词)
【Spring AI 1.0.0】Spring AI 1.0.0框架快速入门(3)——Structured Output Converter(结构化输出转换器)
【Spring AI 1.0.0】Spring AI 1.0.0框架快速入门(4)——Chat Memory(聊天记录)
【Spring AI 1.0.0】Spring AI 1.0.0框架快速入门(5)——Tool Calling(工具调用)
一、前言
现有大模型都是基于海量的数据训练出来,但这组数据有一个弊端就是不具备实时性,比如询问当前时间及天气等。这时候需要引入一组API或者工具来进行交互,来增强大模型功能,大模型通过调用工具返回的结果作为上下文进行回答问题,而Spring AI也集成了Tool Calling。这一章来详细学习Spring AI如何使用Tool Calling调用工具。
二、Tool Calling
工具调用(也称为函数调用)
是 AI 应用程序中的一种常见模式,允许模型与一组 API 或工具进行交互,从而增强其功能。
工具主要用于:
-
信息检索(Information Retrieval):此类别中的工具可用于从外部源(如数据库、Web 服务、文件系统或 Web 搜索引擎)检索信息。目标是增强模型的知识,使其能够回答其他方式无法回答的问题。因此,
它们可用于检索增强生成 (RAG) 方案
。例如,工具可用于检索给定位置的当前天气、检索最新的新闻文章或查询数据库以获取特定记录。 -
采取行动(Taking Action):此类别中的工具可用于在软件系统中执行,例如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流。目标是自动执行原本需要人工干预或显式编程的任务。例如,可以使用工具为与聊天机器人交互的客户预订航班,在网页上填写表单,或在代码生成场景中实现基于自动测试 (TDD) 的 Java 类。
Spring AI 提供了方便的 API 来定义工具、解决来自模型的工具调用请求以及执行工具调用,下面是Spring AI工具的调用流程:
- ① 当想让某个工具可供模型使用时,需要在聊天请求中包含其定义。每个工具定义都包含输入参数的名称、描述和方案。
- ② 当模型决定调用工具时,它会发送一个请求,其中包含工具名称和输入参数,这些参数在定义的架构之后建模。
- ③ 应用程序负责使用工具名称来识别和执行具有提供的输入参数的工具。
- ④ 工具调用的结果由应用程序处理。
- ⑤ 应用程序将工具调用结果发送回模型。
- ⑥ 模型将工具调用的结果作为附加上下文,生成最终的结果。
注意:之前Spring AI 有Function calling功能,这部分已经迁移到Tool calling,具体参考《官方文档》
2.1 信息检索
AI 模型无法访问实时信息。询问当前日期或天气预报等信息的问题都无法由模型回答。但是,可以提供一个可以检索这些信息的工具,让模型在需要访问实时信息时调用这个工具。
在 DateTimeTools
类中实现一个工具来获取用户时区的当前日期和时间。该工具不需要参数。Spring Framework 的 LocaleContextHolder
可以提供用户的时区。该工具将定义为带有 @Tool
注解的方法。为了帮助模型理解是否以及何时调用此工具,下面提供demo。运行环境、配置同前面文章。
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;import java.time.LocalDateTime;public class DateTimeTools {@Tool(description = "获取当前的日期和时间")String getCurrentDateTime() {return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();}}
测试类:
@SpringBootTest
@SpringBootTest
class ToolCallingTests {@Resource(name = "zhiPuAiChatModel")private ChatModel chatModel;@Testvoid test_information_retrieval_demo() {String response = ChatClient.create(chatModel).prompt("明天是几号?").tools(new DateTimeTools()).call().content();System.out.println(response);}}
结果如下:
如果注释掉.tools(new DateTimeTools())
,则结果如下:
2.2 采取行动
AI 模型可用于生成实现某些目标的计划。例如,模型可以生成定闹钟的计划。但是,模型没有执行计划的能力。这就是工具的用武之地:它们可用于执行模型生成的计划。
在前面的示例中,使用工具来确定当前日期和时间。在此示例中,将定义第二个工具,用于在特定时间设置闹钟。目标是设置从现在起 10 分钟的闹钟,因此需要向模型提供两个工具来完成此任务。
public class DateTimeTools {@Tool(description = "获取当前的日期和时间")String getCurrentDateTime() {return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();}@Tool(description = "设置一个闹钟")void setAlarm(String time) {LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);System.out.println("设置闹钟:" + alarmTime);}
}
三、方法作为工具
Spring AI 提供了内置支持,用于以两种方式从方法指定工具(即 ToolCallback(s)
):
-
声明式,使用
@Tool
注解 -
编程式,使用
MethodToolCallback
实现。
3.1 注解式:@Tool
你可以通过用 @Tool
注解方法来将方法转换为工具。下面是 @Tool
源码:
@Tool
注解允许你提供有关工具的关键信息:
-
name
:工具的名称。如果未提供,将使用方法名称。AI 模型在调用工具时使用此名称来识别工具。因此,不允许在同一类中有两个具有相同名称的工具。该名称在特定聊天请求中模型可用的所有工具中必须是唯一的。 -
description
:工具的描述,模型可以使用它来理解何时以及如何调用工具。如果未提供,将使用方法名称作为工具描述。但是,强烈建议提供详细描述,因为这对于模型理解工具的目的和使用方式至关重要。未能提供好的描述可能导致模型在应该使用时没有使用工具,或者使用不当。 -
returnDirect
:工具结果是否应该直接返回给客户端或传回给模型。 -
resultConverter
:用于将工具调用结果转换为要发送回 AI 模型的 String 对象的ToolCallResultConverter
实现。
Spring AI 将自动为 @Tool
注解方法的输入参数生成 JSON 架构。该架构由模型用来理解如何调用工具和准备工具请求。可以使用 @ToolParam
注解来提供有关输入参数的额外信息,例如描述或参数是必需还是可选的。默认情况下,所有输入参数都被视为必需的。
@Tool(description = "设置一个闹钟")
void setAlarm(@ToolParam(description = "时间,格式为 ISO 8601") String time) {LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);System.out.println("设置闹钟:" + alarmTime);
}
@ToolParam
注解允许你提供有关工具参数的关键信息:
-
description
:参数的描述,模型可以使用它来更好地理解如何使用它。例如,参数应该采用什么格式,允许什么值,等等。 -
required
:参数是必需还是可选的。默认情况下,所有参数都被视为必需的。
如果参数被注解为 @Nullable
,除非使用 @ToolParam
注解明确标记为必需,否则它将被视为可选的。
除了 @ToolParam
注解外,你还可以使用 Swagger 的 @Schema
注解或 Jackson 的 @JsonProperty
。这些后面内容会有详细说明。
3.1.1 向 ChatClient 添加工具
上述demo中就是通过ChatClient
的tools()
方法来传递工具类的实例。
String response = ChatClient.create(chatModel).prompt("明天是几号?").tools(new DateTimeTools()).call().content();
在底层,ChatClient
将从工具类实例中的每个 @Tool
注解方法使用.tools()
方法生成 ToolCallback
,并将它们传递给模型。
tools()
方法源码:
如果你希望自己生成 ToolCallback(s)
,可以使用 ToolCallbacks
工具类。
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
执行结果:
3.1.2 向 ChatClient 添加默认工具
使用注解式方法时,你可以通过将工具类实例传递给 defaultTools()
方法来向 ChatClient.Builder
添加默认工具。 如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
@Test
void test_chatClient_defaultTools() {ChatClient chatClient = ChatClient.builder(chatModel).defaultTools(new DateTimeTools()).build();String response = chatClient.prompt("明天是几号?").call().content();System.out.println(response);
}
3.1.3 向 ChatModel 添加工具
使用注解式方法时,你可以通过将工具类实例传递给用于调用 ChatModel
的 ToolCallingChatOptions
的 toolCallbacks()
方法来向 ChatModel
添加工具。此类工具仅对添加它们的特定聊天请求可用。
@Test
void test_chatModel_toolCallbacks() {ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());ChatOptions chatOptions = ToolCallingChatOptions.builder().toolCallbacks(dateTimeTools).build();Prompt prompt = new Prompt("明天是几号?", chatOptions);ChatResponse call = chatModel.call(prompt);System.out.println(call.getResult().getOutput().getText());
}
3.1.4 向 ChatModel 添加默认工具
使用注解式方法时,你可以通过将工具类实例传递给用于创建 ChatModel
的 ToolCallingChatOptions
实例的 toolCallbacks()
方法来向 ChatModel
添加默认工具。 如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
@Test
void test_chatModel_default_toolCallbacks() {DeepSeekApi deepSeekApi = DeepSeekApi.builder().apiKey("your api-key").build();ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());DeepSeekChatOptions options = DeepSeekChatOptions.builder().model("deepseek-chat").toolCallbacks(dateTimeTools).build();ChatModel chatModel = DeepSeekChatModel.builder().deepSeekApi(deepSeekApi).defaultOptions(options).build();Prompt prompt = new Prompt("明天是几号?");ChatResponse response = chatModel.call(prompt);System.out.println(response.getResult().getOutput().getText());
}
3.2 编程式:MethodToolCallback
可以通过 MethodToolCallback
程序化来将方法转换为工具。
String getCurrentDateTimeWithoutTool() {return "2024-07-28T17:26:50.055860700+08:00[Asia/Shanghai]";
}
@Test
void test_methodToolCallback_demo() {Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTimeWithoutTool");ToolCallback toolCallback = MethodToolCallback.builder().toolDefinition(ToolDefinition.builder().name("getCurrentDateTimeWithoutTool").description("获取当前时间").inputSchema("{}").build()).toolMethod(method).toolObject(new DateTimeTools()).build();String content = ChatClient.create(chatModel).prompt("明天是几号?").toolCallbacks(toolCallback) // 关键修改:使用toolCallbacks().call().content();System.out.println(content);
}
上述getCurrentDateTimeWithoutTool()方法中写死了2024-07-28T17:26:50
,所以执行结果如下:
MethodToolCallback源码:
MethodToolCallback.Builder
允许你构建 MethodToolCallback
实例并提供有关工具的关键信息:
-
toolDefinition
:定义工具名称、描述和输入架构的ToolDefinition
实例。你可以使用ToolDefinition.Builder
类来构建它。其中工具名称、描述和输入架构都为必填项。 -
toolMetadata
:定义附加设置的ToolMetadata
实例,例如结果是否应该直接返回给客户端或传回给模型,以及要使用的结果转换器。你可以使用ToolMetadata.Builder
类来构建它。 -
toolMethod
:表示工具方法的Method
实例。必需。 -
toolObject
:包含工具方法的对象实例。如果方法是静态的,你可以省略此参数。 -
toolCallResultConverter
:用于将工具调用结果转换为要发送回 AI 模型的 String 对象的ToolCallResultConverter
实例。如果未提供,将使用默认转换器(DefaultToolCallResultConverter
)。
ToolDefinition.Builder
允许你构建 ToolDefinition
实例并定义工具名称、描述和输入架构:
-
name
:工具的名称。AI 模型在调用工具时使用此名称来识别工具。因此,不允许在同一类中有两个具有相同名称的工具。该名称在特定聊天请求中模型可用的所有工具中必须是唯一的。 -
description
:工具的描述,模型可以使用它来理解何时以及如何调用工具。强烈建议提供详细描述,因为这对于模型理解工具的目的和使用方式至关重要。未能提供好的描述可能导致模型在应该使用时没有使用工具,或者使用不当。 -
inputSchema
:工具的输入参数的 JSON 架构。可以使用@ToolParam
注解来提供有关输入参数的额外信息,例如描述或参数是必需还是可选的。默认情况下,所有输入参数都被视为必需的。
ToolMetadata.Builder
允许你构建 ToolMetadata
实例并定义工具的附加设置:
returnDirect
:结果是否应该直接返回给客户端或传回给模型。默认是否。
3.2.1 向 ChatClient 和 ChatModel 添加工具
使用编程式方法时,你可以通过将 MethodToolCallback
实例传递给 tools()
方法来向 ChatClient
添加工具。 该工具仅对添加它们的特定聊天请求可用。
@Test
void test_methodToolCallback_demo() {Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTimeWithoutTool");ToolCallback toolCallback = MethodToolCallback.builder().toolDefinition(ToolDefinition.builder().name("getCurrentDateTimeWithoutTool").description("获取当前时间").inputSchema("{}").build()).toolMethod(method)
// .toolObject(new DateTimeTools()).build();String content = ChatClient.create(chatModel).prompt("明天是几号?").toolCallbacks(toolCallback) // 关键修改:使用toolCallbacks().call().content();System.out.println(content);
}
3.3 方法工具限制
以下类型目前不支持作为方法参数或返回类型:
-
Optional
-
异步类型(例如
CompletableFuture
、Future
) -
反应类型(例如
Flow
、Mono
、Flux
) -
功能类型(例如
Function
、Supplier
、Consumer
)。
四、函数作为工具
Spring AI 提供了FunctionToolCallback
4.1 编程式:FunctionToolCallback
通过构建FunctionToolCallback
,实现(Function、Supplier、Consumer 或 BiFunction)
等接口,来转换成工具,具体例子如下,返回当前温度为30.0:
public class WeatherService implements Function<WeatherService.WeatherRequest, WeatherService.WeatherResponse> {public WeatherResponse apply(WeatherRequest request) {double temperature = 0;if (request.location().contains("巴黎")) {temperature = 15;} else if (request.location().contains("东京")) {temperature = 10;} else if (request.location().contains("旧金山")) {temperature = 30;}return new WeatherResponse(temperature, 15, 20, 2, 53, 45, Unit.C);}public enum Unit {C, F}public record WeatherRequest(String location, Unit unit) {}public record WeatherResponse(double temp, double feels_like, double temp_min, double temp_max, int pressure,int humidity,Unit unit) {}
}
接下来分析一下FunctionToolCallback
源码,他和上面介绍的MethodToolCallback
比较相似:
FunctionToolCallback.Builder
允许构建 FunctionToolCallback
实例并提供有关工具的关键信息:
-
name
:工具的名称。AI 模型在调用工具时使用此名称来识别工具。因此,不允许在同一上下文中有两个具有相同名称的工具。该名称在特定聊天请求中模型可用的所有工具中必须是唯一的。 -
toolFunction
:表示工具方法(Function、Supplier、Consumer 或 BiFunction
)的功能对象。必需。 -
description
:工具的描述,模型可以使用它来理解何时以及如何调用工具。如果未提供,将使用方法名称作为工具描述。但是,强烈建议提供详细描述,因为这对于模型理解工具的目的和使用方式至关重要。未能提供好的描述可能导致模型在应该使用时没有使用工具,或者使用不当。 -
inputType
:函数输入的类型。必需。 -
inputSchema
:工具的输入参数的 JSON 架构。如果未提供,将根据 inputType 自动生成架构。可以使用@ToolParam
注解来提供有关输入参数的额外信息,例如描述或参数是必需还是可选的。默认情况下,所有输入参数都被视为必需的。 -
toolMetadata
:定义附加设置的ToolMetadata
实例,例如结果是否应该直接返回给客户端或传回给模型,以及要使用的结果转换器。你可以使用ToolMetadata.Builder
类来构建它。 -
toolCallResultConverter
:用于将工具调用结果转换为要发送回 AI 模型的 String 对象的ToolCallResultConverter
实例。如果未提供,将使用默认转换器(DefaultToolCallResultConverter
)。
ToolMetadata.Builder
允许你构建 ToolMetadata
实例并定义工具的附加设置:
returnDirect
:结果是否应该直接返回给客户端或传回给模型。默认否
ToolCallback toolCallback = FunctionToolCallback.builder("currentWeather", new WeatherService()).description("获取当前位置的天气情况").inputType(WeatherService.WeatherRequest.class).build();
函数输入和输出可以是 Void
或 POJO
。输入和输出 POJO
必须是可序列化的,因为结果将被序列化并发送回模型。函数以及输入和输出类型必须是公共的。
使用编程式方法时,可以通过将 FunctionToolCallback
实例传递给 tools()
方法来向 ChatClient
添加工具。该工具仅对添加它们的特定聊天请求可用。
@Test
void test_functionToolCallback_demo() {UserMessage userMessage = new UserMessage("旧金山、东京和巴黎的天气怎么样?以摄氏度为单位的温度返回。");List<Message> messages = new ArrayList<>(List.of(userMessage));var promptOptions = DeepSeekChatOptions.builder().model(DeepSeekApi.ChatModel.DEEPSEEK_CHAT.getValue()).toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new WeatherService()).description("获取当前位置的天气情况").inputType(WeatherService.WeatherRequest.class).build())).build();ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));System.out.println(response.getResult().getOutput().getText());
}
执行结果:
使用ToolCallingChatOptions
的toolCallbacks()
方法来向 ChatModel
添加默认工具。
@Test
void test_functionToolCallback_demo1() {ToolCallback toolCallback = FunctionToolCallback.builder("currentWeather", new WeatherService()).description("Get the weather in location").inputType(WeatherService.WeatherRequest.class).build();ChatOptions chatOptions = ToolCallingChatOptions.builder().toolCallbacks(toolCallback).build();Prompt prompt = new Prompt("旧金山、东京和巴黎的天气怎么样?以摄氏度为单位的温度返回。", chatOptions);String text = chatModel.call(prompt).getResult().getOutput().getText();System.out.println(text);
}
执行结果:
4.2 动态式:@Bean ⭐️ ⭐️ ⭐️
还可以使用@Bean注解,让 Spring AI 在运行时使用 ToolCallbackResolver
接口(通过 SpringBeanToolCallbackResolver
实现)解析它们。此选项允许你将任何 Function、Supplier、Consumer 或 BiFunction bean
用作工具。bean 名称将用作工具名称,Spring Framework 的 @Description
注解可以用于提供工具描述,模型可以使用它来理解何时以及如何调用工具。如果你不提供描述,方法名称将用作工具描述。但是,强烈建议提供详细描述,因为这对于模型理解工具的目的和使用方式至关重要。未能提供好的描述可能导致模型在应该使用时没有使用工具,或者使用不当。
例如下面配置:
@Configuration(proxyBeanMethods = false)
class WeatherTools {WeatherService weatherService = new WeatherService();public static final String CURRENT_WEATHER_TOOL = "getWeather";@Bean(CURRENT_WEATHER_TOOL)@Description("获取当前位置的天气情况")Function<WeatherService.WeatherRequest, WeatherService.WeatherResponse> currentWeather() {return weatherService;}
}
使用ChatClient去调用该方法
@Test
void test_functionToolCallback_bean_demo() {ChatOptions chatOptions = ToolCallingChatOptions.builder().toolNames("getWeather").build();Prompt prompt = new Prompt("旧金山、东京和巴黎的天气怎么样?以摄氏度为单位的温度返回。", chatOptions);String content = chatModel.call(prompt).getResult().getOutput().getText();System.out.println(content);
}
执行结果如下:
Bean进阶用法:
新增一个获取当前时间的@Bean工具
@Configuration(proxyBeanMethods = false)
class WeatherTools {WeatherService weatherService = new WeatherService();public static final String CURRENT_WEATHER_TOOL = "getWeather";@Bean(CURRENT_WEATHER_TOOL)@Description("获取当前位置的天气情况")Function<WeatherService.WeatherRequest, WeatherService.WeatherResponse> currentWeather() {return weatherService;}@Bean("getTime")@Description("获取当前时间")Function<TimeRequest, TimeResponse> getTime() {return request -> {// 模拟数据:实际项目中可调用时间APIString time = switch (request.city().toLowerCase()) {case "旧金山" -> LocalDateTime.now(ZoneId.of("America/Los_Angeles")).toString();case "东京" -> LocalDateTime.now(ZoneId.of("Asia/Tokyo")).toString();case "巴黎" -> LocalDateTime.now(ZoneId.of("Europe/Paris")).toString();default -> "未知";};return new TimeResponse(request.city(), time);};}public record TimeRequest(String city) {}public record TimeResponse(String city, String currentTime) {}
}
然后在toolNames()
方法中再去加上getWeather
和getTime
这两个bean
@Test
void test_functionToolCallback_double_bean_demo() {ChatOptions chatOptions = ToolCallingChatOptions.builder().toolNames("getWeather", "getTime").build();Prompt prompt = new Prompt("分别说出旧金山、东京和巴黎的当前时间及当前天气怎么样?以摄氏度为单位的温度返回。", chatOptions);String content = chatModel.call(prompt).getResult().getOutput().getText();System.out.println(content);
}
执行结果:
当然,你的工具中也可以做存储和查询的操作
4.3 函数工具限制
以下类型目前不支持作为函数输入或输出类型:
-
基本类型
-
Optional
-
集合类型(例如
List
、Map
、Array
、Set
) -
异步类型(例如
CompletableFuture
、Future
) -
反应类型(例如
Flow
、Mono
、Flux
)。
五、总结
本章讲解了Spring AI中的Tool Calling,并且如何定义工具并使用工具,其中有方法(Method)和函数(Function)两种模式,每种模式都有自己的注解方式和编码方式,可以让用户比较灵活实用。
下一章将会讲解最近很火的MCP服务!!!
创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️