
🪁🍁 希望本文能给您带来帮助,如果有任何问题,欢迎批评指正!🐅🐾🍁🐥
文章目录
- 一、背景
- [二、Tool Calling](#二、Tool Calling)
-
- [2.1 信息检索](#2.1 信息检索)
- [2.2 执行操作](#2.2 执行操作)
- 三、工具调用实战
-
- [3.1 方法作为工具](#3.1 方法作为工具)
-
- [3.1.1 声明式:@Tool](#3.1.1 声明式:@Tool)
-
- [3.1.1.1 向 ChatClient 添加工具](#3.1.1.1 向 ChatClient 添加工具)
- [3.1.1.2 向 ChatClient 添加默认工具](#3.1.1.2 向 ChatClient 添加默认工具)
- [3.1.1.3 向 ChatModel 添加工具](#3.1.1.3 向 ChatModel 添加工具)
- [3.1.1.4 向 ChatModel 添加默认工具](#3.1.1.4 向 ChatModel 添加默认工具)
- [3.1.2 编程式:MethodToolCallback](#3.1.2 编程式:MethodToolCallback)
- [3.1.3 方法工具限制](#3.1.3 方法工具限制)
- [3.2 函数作为工具](#3.2 函数作为工具)
-
- [3.2.1 编程式:FunctionToolCallback](#3.2.1 编程式:FunctionToolCallback)
-
- [3.2.1.1 向 ChatClient 添加工具](#3.2.1.1 向 ChatClient 添加工具)
- [3.2.1.2 向 ChatClient 添加默认工具](#3.2.1.2 向 ChatClient 添加默认工具)
- [3.2.1.3 向 ChatModel添加工具](#3.2.1.3 向 ChatModel添加工具)
- [3.2.1.4 向 ChatModel添加默认工具](#3.2.1.4 向 ChatModel添加默认工具)
- [3.2.2 动态规范:@Bean](#3.2.2 动态规范:@Bean)
- [3.2.3 函数工具限制](#3.2.3 函数工具限制)
- 四、工具调用流程图
- 五、工具调用源码解析
-
- [5.1 工具规范](#5.1 工具规范)
-
- [5.1.1 工具回调](#5.1.1 工具回调)
- [5.1.2 工具定义](#5.1.2 工具定义)
- [5.1.3 JSON 模式](#5.1.3 JSON 模式)
-
- [5.1.3.1 描述](#5.1.3.1 描述)
- [5.1.3.2 必需/可选](#5.1.3.2 必需/可选)
- [5.1.4 结果转换](#5.1.4 结果转换)
- [5.1.5 工具上下文](#5.1.5 工具上下文)
- [5.1.6 直接返回](#5.1.6 直接返回)
- [5.2 工具解析](#5.2 工具解析)
-
- [5.2.1 ToolCallbackResolver](#5.2.1 ToolCallbackResolver)
- [5.2.2 DelegatingToolCallbackResolver](#5.2.2 DelegatingToolCallbackResolver)
- [5.2.3 SpringBeanToolCallbackResolver](#5.2.3 SpringBeanToolCallbackResolver)
- [5.2.4 StaticToolCallbackResolver](#5.2.4 StaticToolCallbackResolver)
- [5.3 工具执行](#5.3 工具执行)
-
- [5.3.1 框架控制的工具执行](#5.3.1 框架控制的工具执行)
- [5.3.2 用户控制的工具执行](#5.3.2 用户控制的工具执行)
- [5.4 异常处理](#5.4 异常处理)
-
- [5.4.1 异常处理过程](#5.4.1 异常处理过程)
- [5.4.2 异常处理案例](#5.4.2 异常处理案例)
- 六、总结
导航参见:
Spring AI实战:SpringBoot项目结合Spring AI开发------ChatClient API详解
Spring AI实战:SpringBoot项目结合Spring AI开发------提示词(Prompt)技术与工程实战详解
Spring AI实战:SpringBoot项目结合Spring AI开发------模型参数及ChatOptions API详解
Spring AI实战:SpringBoot项目结合Spring AI开发------结构化输出(StructuredOutputConverter)
Spring AI实战:SpringBoot项目结合Spring AI开发------聊天记忆(ChatMemory)源码及实战详解
Spring AI实战:SpringBoot项目结合Spring AI开发------增强器Advisor详解与实战
Spring AI实战:SpringBoot项目结合Spring AI开发------Tool Calling(工具调用)源码及实战详解
一、背景
大模型的强大生成能力改变了我们人类和机器交互的方式,通过一个对话框写下一个问题,很快就能得到想要的答案。但是大模型自身也是有局限性的,比如没法获取实时信息,如天气;也没法获取个人或者企业的私有数据,也没法执行操作,比如发邮件等。我们当然希望模型越智能越好,可以帮我们做这些事情,这就需要大模型能够代替人类(代理)跟其他系统或者API交互。那么怎么实现呢?这些系统通常具有特定的输入模式;例如,API 通常具有所需的输入参数结构。这种需求带来了"工具调用"的概念。通过工具调用,大模型就有了动手的能力,可以实时获取信息、也能像人一样浏览网页,也能写代码,还能执行动作,能更好的完成人类任务。
二、Tool Calling
工具调用(也称为函数调用)是一种在 AI 应用中常见的模式,允许模型与一组 API(或工具)交互,从而增强其能力。
工具主要用于
-
信息检索: 此类工具可用于从外部来源检索信息,例如数据库、Web 服务、文件系统或 Web 搜索引擎。其目标是增强模型的知识,使其能够回答原本无法回答的问题。因此,它们可用于检索增强生成(RAG)场景。例如,工具可用于检索给定位置的当前天气、检索最新新闻文章或查询数据库以获取特定记录。
-
执行操作: 此类工具可用于在软件系统中执行操作,例如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流。其目标是自动化原本需要人工干预或显式编程的任务。例如,工具可用于为与聊天机器人交互的客户预订航班、填写网页上的表单,或在代码生成场景中根据自动化测试 (TDD) 实现 Java 类。
尽管我们通常将工具调用称为模型的能力,但实际上是由客户端应用提供工具调用逻辑。模型只能请求工具调用并提供输入参数,而应用负责根据输入参数执行工具调用并返回结果。

① 当我们希望向模型提供一个工具时,我们会将其定义包含在聊天请求(提示词)中,并调用 ChatModel API 将请求发送至 AI 模型。
② 当模型决定调用工具时,它会发送一个包含工具名称及根据定义的模式建模的输入参数的响应(ChatResponse)。
③ ChatModel 将工具调用请求发送至 ToolCallingManager API。
④ ToolCallingManager 负责识别要调用的工具并使用提供的输入参数执行该工具。
⑤ 工具调用的结果返回给 ToolCallingManager。
⑥ ToolCallingManager 将工具执行结果返回给 ChatModel。
⑦ ChatModel 将工具执行结果(ToolResponseMessage)发送回 AI 模型。
⑧ AI 模型使用工具调用结果作为额外上下文生成最终响应
一次工具调用时,会调用模型两次。
2.1 信息检索
AI 模型无法访问实时信息。询问当前日期或天气预报等信息的问题都无法由模型回答。但是,可以提供一个可以检索这些信息的工具,让模型在需要访问实时信息时调用这个工具。
当我们直接问大模型的时候,它并不知道明天的时间:
java
@SpringBootTest
public class ToolCallingTests {
@Resource(name = "deepSeekChatModel")
private ChatModel chatModel;
@Test
void test_information_retrieval_demo() {
String response = ChatClient.create(chatModel)
.prompt("明天是几号?")
.call()
.content();
System.out.println(response);
}
}
结果如下:

在 DateTimeTools 类中实现一个工具来获取用户时区的当前日期和时间,该工具不需要参数。Spring Framework 的 LocaleContextHolder 可以提供用户的时区。该工具将定义为带有 @Tool 注解的方法。为了帮助模型理解是否以及何时调用此工具,下面提供demo,运行环境、配置同前面文章。
java
public class DateTimeTools {
@Tool(description = "获取当前的日期和时间")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
测试类:
java
@SpringBootTest
public class ToolCallingTests {
@Resource(name = "deepSeekChatModel")
private ChatModel chatModel;
@Test
void test_information_retrieval_demo() {
String response = ChatClient.create(chatModel)
.prompt("明天是几号?")
.tools(new DateTimeTools())
.call()
.content();
System.out.println(response);
}
}
结果如下:

2.2 执行操作
AI 模型可用于生成实现某些目标的计划。例如,模型可以生成预订前往丹麦旅行的计划。但是,模型没有执行该计划的能力。这就是工具的作用所在:它们可用于执行模型生成的计划。
在上一个示例中,我们使用了工具来确定当前日期和时间。在此示例中,我们将定义第二个工具,用于在特定时间设置闹钟。目标是设置一个从现在起 10 分钟后的闹钟,因此我们需要将这两个工具都提供给模型来完成此任务。
我们将新工具添加到与之前相同的 DateTimeTools 类中。新工具将接受一个参数,即 ISO-8601 格式的时间。然后,该工具将向控制台打印一条消息,指示已为给定时间设置了闹钟。与之前一样,该工具被定义为一个使用 @Tool 注解的方法,我们还使用它来提供详细描述,以帮助模型理解何时以及如何使用该工具。
java
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);
}
}
测试类:
java
@SpringBootTest
public class ToolCallingTests {
@Resource(name = "deepSeekChatModel")
private ChatModel chatModel;
@Test
void test_information_retrieval_demo() {
String response = ChatClient.create(chatModel)
.prompt("请帮我设置一个从现在起 20 分钟后的闹钟")
.tools(new DateTimeTools())
.call()
.content();
System.out.println(response);
}
}
结果如下:

三、工具调用实战
Spring AI 提供了内置支持,用于以两种方式从方法中指定工具(即 ToolCallback):
-
声明式,使用
@Tool注解 -
编程式,使用
MethodToolCallback实现。
3.1 方法作为工具
3.1.1 声明式:@Tool
你可以通过用 @Tool 注解方法来将方法转换为工具。
java
public class DateTimeTools {
@Tool(description = "获取当前的日期和时间")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
下面是 @Tool 源码:

@Tool 注解允许您提供关于工具的关键信息
name:工具的名称。如果未提供,将使用方法名称。AI 模型在调用工具时使用此名称进行识别。因此,同一类中不允许存在名称相同的两个工具。对于特定聊天请求,该名称在模型可用的所有工具中必须是唯一的。description:工具的描述,模型可以使用此描述来理解何时以及如何调用该工具。如果未提供,将使用方法名称作为工具描述。但是,强烈建议提供详细描述,这对于模型理解工具目的及使用方法至关重要。未提供良好的描述可能导致模型在应该使用时未使用工具,或者使用错误。returnDirect:工具结果是直接返回给客户端,还是传回给模型。resultConverter:用于将工具调用结果转换为要发送回 AI 模型的 String 对象的ToolCallResultConverter实现。
注意:
1.@Tool标注的方法可以是静态方法或实例方法,并且可以具有任何可见性(public、protected、package-private 或 private)
2.如果方法返回一个值,则返回类型必须是可序列化类型,因为结果将被序列化并发送回模型
Spring AI 将自动为 @Tool 注解方法的输入参数生成 JSON 架构。该架构由模型用来理解如何调用工具和准备工具请求。可以使用 @ToolParam 注解来提供有关输入参数的额外信息,例如描述或参数是必需还是可选的。默认情况下,所有输入参数都被视为必需的。
java
@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.1 向 ChatClient 添加工具
上述demo中就是通过ChatClient的tools()方法来传递工具类的实例。
java
String response = ChatClient.create(chatModel)
.prompt("明天是几号?")
.tools(new DateTimeTools())
.call()
.content();
在底层,ChatClient 将从工具类实例中的每个 @Tool 注解方法使用.tools()方法生成 ToolCallback,并将它们传递给模型。

如果你希望自己生成 ToolCallback(s),可以使用 ToolCallbacks 工具类。
java
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
java
@Test
void test_information_retrieval_toolcallback_demo() {
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
String response = ChatClient.create(chatModel)
.prompt("明天是几号?")
.toolCallbacks(dateTimeTools)
.call()
.content();
System.out.println(response);
}
3.1.1.2 向 ChatClient 添加默认工具
使用注解式方法时,你可以通过将工具类实例传递给 defaultTools() 方法来向 ChatClient.Builder 添加默认工具。 如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
java
@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.1.3 向 ChatModel 添加工具
使用注解式方法时,你可以通过将工具类实例传递给用于调用 ChatModel 的 ToolCallingChatOptions 的 toolCallbacks() 方法来向 ChatModel 添加工具。此类工具仅对添加它们的特定聊天请求可用。
java
@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.1.4 向 ChatModel 添加默认工具
使用注解式方法时,你可以通过将工具类实例传递给用于创建 ChatModel 的 ToolCallingChatOptions 实例的 toolCallbacks() 方法来向 ChatModel 添加默认工具。 如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
java
@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.1.2 编程式:MethodToolCallback
除了注解外,您可以通过编程方式构建MethodToolCallback 将方法转换为工具然后将其添加到 ChatClient中,其他的几种构建方式可参考前文注解形式。
java
class DateTimeTools {
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
java
@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);
}
不管是注解的方式还是编程方式,其实本质都是转换成了核心类MethodToolCallback。
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.1.3 方法工具限制
以下类型目前不支持作为方法参数或返回类型:
-
Optional -
异步类型(例如
CompletableFuture、Future) -
反应类型(例如
Flow、Mono、Flux) -
功能类型(例如
Function、Supplier、Consumer)。
3.2 函数作为工具
Spring AI 提供内置支持,用于从函数指定工具,无论是使用低级 FunctionToolCallback 实现以编程方式实现,还是作为在运行时解析的 @Bean 动态实现。
3.2.1 编程式:FunctionToolCallback
通过构建FunctionToolCallback,实现(Function、Supplier、Consumer 或 BiFunction)等接口,来转换成工具,具体例子如下:调用和风天气开放API接口获取指定城市的天气。
java
public class WeatherService implements Function<WeatherService.WeatherRequest, WeatherService.WeatherResponse> {
private static final String BASE_URL = "自己申请的host";
private static final String API_KEY = "自己申请的apikey";
private static final RestTemplate restTemplate = new RestTemplate();
@Override
public WeatherResponse apply(WeatherRequest weatherRequest) {
// 1.调用天气工具获取城市ID
String cityId = getCityIdByName(weatherRequest.cityName);
// 2.调用天气工具获取天气数据
return getWeatherData(cityId);
}
private String getCityIdByName(String cityName) {
if (StringUtils.isBlank(cityName)) {
throw new RuntimeException("输入城市名为空!");
}
String geoUrl = BASE_URL + "/geo/v2/city/lookup?location=" +
cityName + "&adm=" + cityName + "&lang=zh" + "&key=" + API_KEY;
HttpHeaders headers = new HttpHeaders();
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<byte[]> geoResponse = restTemplate.exchange(geoUrl, HttpMethod.GET, entity, byte[].class);
if (geoResponse.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException("获取城市信息失败!");
}
// 手动解压Gzip数据
String jsonString = unCompressData(geoResponse.getBody());
JSONObject jsonObject = JSON.parseObject(jsonString);
JSONArray locationArray = jsonObject.getJSONArray("location");
List<CityResponse> cityResponseList = Optional.ofNullable(locationArray).map(it -> it.toJavaList(CityResponse.class)).orElse(Collections.emptyList());
return cityResponseList.stream().filter(it -> cityName.equals(it.name)).findFirst().map(CityResponse::id).orElse(null);
}
private WeatherResponse getWeatherData(String cityId) {
if (StringUtils.isBlank(cityId)) {
throw new RuntimeException("城市ID为空!");
}
String weatherUrl = BASE_URL + "/v7/weather/now?location=" +
cityId + "&key=" + API_KEY;
HttpHeaders headers = new HttpHeaders();
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<byte[]> weatherResponse = restTemplate.exchange(weatherUrl, HttpMethod.GET, entity, byte[].class);
if (weatherResponse.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException("获取天气信息失败!");
}
// 手动解压Gzip数据
String jsonString = unCompressData(weatherResponse.getBody());
JSONObject jsonObject = JSON.parseObject(jsonString);
return jsonObject.getObject("now", WeatherResponse.class);
}
private String unCompressData(byte[] compressedData) {
try (ByteArrayInputStream bis = new ByteArrayInputStream(compressedData);
GZIPInputStream gzip = new GZIPInputStream(bis);
BufferedReader reader = new BufferedReader(
new InputStreamReader(gzip, StandardCharsets.UTF_8))) {
StringBuilder result = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException("解压Gzip数据失败", e);
}
}
public record WeatherRequest(String cityName) {
}
public record CityResponse(String name, String id) {
}
public record WeatherResponse(String temp, String text) {
}
}
@Test
void test_functionToolCallbacks_client_tool() {
ToolCallback toolCallback = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("获取指定位置的天气信息")
.inputType(WeatherService.WeatherRequest.class)
.build();
String content = ChatClient.create(chatModel)
.prompt("三亚的天气如何?")
.toolCallbacks(toolCallback)
.call()
.content();
System.out.println(content);
}
执行完上述代码后,测试效果如下图:

接下来分析一下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:结果是否应该直接返回给客户端或传回给模型。默认否
函数输入和输出可以是 Void 或 POJO。输入和输出 POJO 必须是可序列化的,因为结果将被序列化并发送回模型。函数以及输入和输出类型必须是公共的。
3.2.1.1 向 ChatClient 添加工具
当使用编程规范方法时,您可以将 FunctionToolCallback 实例传递给 ChatClient 的 toolCallbacks() 方法。该工具将仅对它所添加的特定聊天请求可用。
java
@Test
void test_functionToolCallbacks_client_tool() {
ToolCallback toolCallback = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("获取指定位置的天气信息")
.inputType(WeatherService.WeatherRequest.class)
.build();
String content = ChatClient.create(chatModel)
.prompt("三亚的天气如何?")
.toolCallbacks(toolCallback)
.call()
.content();
System.out.println(content);
}
3.2.1.2 向 ChatClient 添加默认工具
当使用编程规范方法时,您可以通过将 FunctionToolCallback 实例传递给 defaultToolCallbacks() 方法来向 ChatClient.Builder 添加默认工具。如果同时提供了默认工具和运行时工具,则运行时工具将完全覆盖默认工具。
java
@Test
void test_functionToolCallbacks_client_defaultTool() {
ToolCallback toolCallback = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("获取指定位置的天气信息")
.inputType(WeatherService.WeatherRequest.class)
.build();
String content = ChatClient.builder(chatModel)
.defaultToolCallbacks(toolCallback)
.build()
.prompt("三亚的天气如何?")
.call()
.content();
System.out.println(content);
}
3.2.1.3 向 ChatModel添加工具
当使用编程规范方法时,您可以将 FunctionToolCallback实例传递给 ToolCallingChatOptions 的 toolCallbacks() 方法。该工具将仅对它所添加的特定聊天请求可用。
java
@Test
void test_functionToolCallbacks_model_tool() {
ToolCallback weatherTool = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("获取指定位置的天气信息")
.inputType(WeatherService.WeatherRequest.class)
.build();
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(weatherTool)
.build();
Prompt prompt = new Prompt("三亚的天气如何?", chatOptions);
ChatResponse call = chatModel.call(prompt);
System.out.println(call.getResult().getOutput().getText());
}
3.2.1.4 向 ChatModel添加默认工具
当使用编程规范方法时,您可以通过将 FunctionToolCallback 实例传递给用于创建 ChatModel 的 ToolCallingChatOptions 实例的 toolCallbacks() 方法来在构建时向 ChatModel 添加默认工具。如果同时提供了默认工具和运行时工具,则运行时工具将完全覆盖默认工具。
java
@Test
void test_functionToolCallbacks_model_defaultTool() {
ToolCallback weatherTool = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("获取指定位置的天气信息")
.inputType(WeatherService.WeatherRequest.class)
.build();
DeepSeekApi deepSeekApi = DeepSeekApi.builder()
.apiKey("your api-key")
.build();
ToolCallback[] dateTimeTools = ToolCallbacks.from(weatherTool);
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.2 动态规范:@Bean
您可以通过将工具定义为 Spring bean,而不是以编程方式指定工具,并让 Spring AI 使用 ToolCallbackResolver 接口(通过 SpringBeanToolCallbackResolver 实现)在运行时动态解析它们。此选项使您能够使用任何Function、Supplier、Consumer 或 BiFunction bean 作为工具。bean 名称将用作工具名称,Spring Framework 的 @Description 注解可用于提供工具的描述,供模型理解何时以及如何调用工具。如果您不提供描述,方法名称将用作工具描述。但是,强烈建议提供详细描述,因为这对于模型理解工具的目的和如何使用至关重要。未能提供好的描述可能导致模型在应该使用工具时没有使用,或者使用不正确。
java
@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;
}
}
使用ChatModel去调用该方法
java
@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);
}
执行上述代码,测试的结果如下图:

就算工具定义里入参数只有一个,模型也能回答出多城市天气的问题,因为模型会解读到工具定义的参数,下图是解析到的过程:

3.2.3 函数工具限制
目前不支持以下类型作为用作函数的输入或输出类型:
-
原始类型
-
Optional -
集合类型(例如
List、Map、Array、Set) -
异步类型(例如
CompletableFuture、Future) -
响应式类型(例如
Flow、Mono、Flux)。 -
原始类型和集合通过基于方法的工具规范方法受支持。
四、工具调用流程图
这里以向 ChatClient 添加工具过程来整理了工具调用的流程图,我们把这个流程走熟悉即可,其他方式进行工具调用从源码流程上来说区别不大。

五、工具调用源码解析
5.1 工具规范
5.1.1 工具回调
ToolCallback 接口提供了一种定义可由 AI 模型调用的工具的方法,包括定义和执行逻辑。当您想从头开始定义工具时,它是要实现的主要接口。例如,您可以从 MCP 客户端(使用模型上下文协议)或 ChatClient 定义 ToolCallback(以构建模块化的代理应用程序)。
该接口提供以下方法:
java
public interface ToolCallback {
Logger logger = LoggerFactory.getLogger(ToolCallback.class);
ToolDefinition getToolDefinition();
default ToolMetadata getToolMetadata() {
return ToolMetadata.builder().build();
}
String call(String toolInput);
default String call(String toolInput, @Nullable ToolContext toolContext) {
if (toolContext != null && !toolContext.getContext().isEmpty()) {
logger.info("By default the tool context is not used, "
+ "override the method 'call(String toolInput, ToolContext toolcontext)' to support the use of tool context."
+ "Review the ToolCallback implementation for {}", getToolDefinition().name());
}
return call(toolInput);
}
}
Spring AI 为工具方法(MethodToolCallback)和工具函数(FunctionToolCallback)提供内置实现。
5.1.2 工具定义
ToolDefinition 接口提供了 AI 模型了解工具可用性所需的信息,包括工具名称、描述和输入模式。每个 ToolCallback 实现都必须提供一个 ToolDefinition 实例来定义工具。
java
public interface ToolDefinition {
String name();
String description();
String inputSchema();
static DefaultToolDefinition.Builder builder() {
return DefaultToolDefinition.builder();
}
}
ToolDefinition.Builder 允许您使用默认实现(DefaultToolDefinition)构建 ToolDefinition 实例。
java
ToolDefinition toolDefinition = ToolDefinition.builder()
.name("currentWeather")
.description("Get the weather in location")
.inputSchema("""
{
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string",
"enum": ["C", "F"]
}
},
"required": ["location", "unit"]
}
""")
.build();
5.1.3 JSON 模式
当向 AI 模型提供工具时,模型需要知道用于调用工具的输入类型的模式。该模式用于理解如何调用工具并准备工具请求。Spring AI 通过 JsonSchemaGenerator 类提供了内置支持,用于为工具生成输入类型的 JSON 模式。该模式作为 ToolDefinition 的一部分提供。
JsonSchemaGenerator 类在底层用于为方法或函数的输入参数生成 JSON 模式,使用方法作为工具和函数作为工具中描述的任何策略。JSON 模式生成逻辑支持一系列注解,您可以在方法和函数的输入参数上使用这些注解来自定义生成的模式。
本节描述了在为工具输入参数生成 JSON 模式时可以自定义的两个主要选项:描述和必需状态。
5.1.3.1 描述
除了为工具本身提供描述外,您还可以为工具的输入参数提供描述。描述可用于提供有关输入参数的关键信息,例如参数应采用何种格式,允许哪些值等等。这有助于模型理解输入模式以及如何使用它。Spring AI 提供内置支持,可以使用以下注解之一为输入参数生成描述:
-
Spring AI 的 @ToolParam(description = "...")
-
Jackson 的 @JsonPropertyDescription(description = "...")
-
Swagger 的 @Schema(description = "...")

5.1.3.2 必需/可选
默认情况下,每个输入参数都被认为是必需的,这强制 AI 模型在调用工具时为其提供一个值。但是,您可以通过使用以下注解之一使输入参数可选,优先级顺序如下:
-
Spring AI 的 @ToolParam(required = false)
-
Jackson 的 @JsonProperty(required = false)
-
Swagger 的 @Schema(required = false)
-
Spring Framework 的 @Nullable

此方法适用于方法和函数,您可以递归地将其用于嵌套类型。
java
class CustomerTools {
@Tool(description = "Update customer information")
void updateCustomerInfo(Long id, String name, @ToolParam(required = false) String email) {
System.out.println("Updated info for customer with id: " + id);
}
}
为输入参数定义正确的必需状态对于降低幻觉风险并确保模型在调用工具时提供正确的输入至关重要。在前面的示例中,email 参数是可选的,这意味着模型可以在不提供值的情况下调用工具。如果参数是必需的,模型在调用工具时必须为其提供值。如果没有值,模型可能会编造一个,导致幻觉。
5.1.4 结果转换
工具调用的结果使用 ToolCallResultConverter 进行序列化,然后发送回 AI 模型。ToolCallResultConverter 接口提供了一种将工具调用结果转换为 String 对象的方法。
java
@FunctionalInterface
public interface ToolCallResultConverter {
/**
* Given an Object returned by a tool, convert it to a String compatible with the
* given class type.
*/
String convert(@Nullable Object result, @Nullable Type returnType);
}
结果必须是可序列化的类型。默认情况下,结果使用 Jackson 序列化为 JSON(DefaultToolCallResultConverter),但您可以通过提供自己的 ToolCallResultConverter 实现来自定义序列化过程。
Spring AI 依赖于方法和函数工具中的 ToolCallResultConverter。
5.1.5 工具上下文
Spring AI 支持通过 ToolContext API 将额外的上下文信息传递给工具。此功能允许您提供额外的、用户提供的数据,这些数据可以在工具执行期间与 AI 模型传递的工具参数一起使用。

ToolContext 中提供的任何数据都不会发送到 AI 模型。
5.1.6 直接返回
默认情况下,工具调用的结果作为响应发送回模型,然后,模型可以使用结果继续对话。在某些情况下,您宁愿将结果直接返回给调用者,而不是将其发送回模型。例如,如果您构建了一个依赖 RAG 工具的代理,您可能希望将结果直接返回给调用者,而不是将其发送回模型进行不必要的后处理。或者您可能有某些工具应该结束代理的推理循环。每个 ToolCallback 实现都可以定义工具调用的结果是直接返回给调用者还是发送回模型。默认情况下,结果会发送回模型。但您可以为每个工具更改此行为。
ToolCallingManager 负责管理工具执行生命周期,它负责处理与工具关联的 returnDirect 属性。如果该属性设置为 true,则工具调用的结果将直接返回给调用者。否则,结果将发送回模型。

-
当我们需要使工具可供模型使用时,我们会在聊天请求中包含其定义。如果希望工具执行结果直接返回给调用者,则将 returnDirect 属性设置为 true。
-
当模型决定调用工具时,它会发送一个包含工具名称和根据定义架构建模的输入参数的响应。
-
应用程序负责使用工具名称识别工具并使用提供的输入参数执行工具。
-
工具调用的结果由应用程序处理。
-
应用程序将工具调用结果直接发送给调用者,而不是将其发送回模型。
当使用声明式方法从方法构建工具时,您可以通过将 @Tool 注解的 returnDirect 属性设置为 true,将工具标记为直接将结果返回给调用者。
java
class CustomerTools {
@Tool(description = "Retrieve customer information", returnDirect = true)
Customer getCustomerInfo(Long id) {
return customerRepository.findById(id);
}
}
如果使用编程方法,您可以通过 ToolMetadata 接口设置 returnDirect 属性,并将其传递给 MethodToolCallback.Builder。
java
ToolMetadata toolMetadata = ToolMetadata.builder()
.returnDirect(true)
.build();
5.2 工具解析
5.2.1 ToolCallbackResolver
将工具传递给模型的主要方法是在调用 ChatClient 或 ChatModel 时提供 ToolCallback,使用方法作为工具和函数作为工具中描述的策略之一。但是,Spring AI 还支持使用 ToolCallbackResolver 接口在运行时动态解析工具,ToolCallbackResolver 由 ToolCallingManager 内部使用,以在运行时动态解析工具,支持框架控制的工具执行和用户控制的工具执行。比如前文中3.2.2 动态规范:@Bean章节介绍的WeatherService就是根据其在运行时解析出来的。

ToolCallbackResolver 接口的源码如下:

5.2.2 DelegatingToolCallbackResolver
默认情况下,Spring AI 依赖于一个 DelegatingToolCallbackResolver,它将工具解析委托给一个 ToolCallbackResolver 实例列表:SpringBeanToolCallbackResolver 和StaticToolCallbackResolver,它执行的时候会循环遍历这个列表,依次执行对应的resolve方法。

那DelegatingToolCallbackResolver是什么时候初始化的呢?为什么说它委托的实例列表里包含SpringBeanToolCallbackResolver 和StaticToolCallbackResolver?我们可以直接定位到自动化配置类ToolCallingAutoConfiguration。

5.2.3 SpringBeanToolCallbackResolver
SpringBeanToolCallbackResolver 从类型为 Function、Supplier、Consumer 或 BiFunction 的 Spring bean 解析工具。

5.2.4 StaticToolCallbackResolver
StaticToolCallbackResolver 从 ToolCallback 实例的静态列表解析工具。当使用 Spring Boot 自动配置时,此解析器会自动配置应用程序上下文中定义的所有 ToolCallback 类型的 bean。

5.3 工具执行
工具执行是指使用提供的输入参数调用工具并返回结果的过程。工具执行由 ToolCallingManager 接口处理,该接口负责管理工具执行生命周期。

如果您正在使用任何 Spring AI Spring Boot Starter,DefaultToolCallingManager 是 ToolCallingManager 接口的自动配置实现。您可以通过提供自己的 ToolCallingManager bean 来定制工具执行行为。

5.3.1 框架控制的工具执行
当使用默认行为时,Spring AI 将自动拦截来自模型的任何工具调用请求,调用工具并将结果返回给模型。所有这些都由每个 ChatModel 实现使用 ToolCallingManager 透明地为您完成,比如下面DeepSeekChatModel的internalCall方法内容。

目前,与模型交换的关于工具执行的内部消息不会暴露给用户。如果您需要访问这些消息,您应该使用用户控制的工具执行方法。
确定工具调用是否符合执行条件的逻辑由 ToolExecutionEligibilityPredicate 接口处理。默认情况下,工具执行资格由检查 ToolCallingChatOptions 的 internalToolExecutionEnabled 属性是否设置为 true(默认值)以及 ChatResponse 是否包含任何工具调用来确定。
java
public class DefaultToolExecutionEligibilityPredicate implements ToolExecutionEligibilityPredicate {
@Override
public boolean test(ChatOptions promptOptions, ChatResponse chatResponse) {
return ToolCallingChatOptions.isInternalToolExecutionEnabled(promptOptions) && chatResponse != null
&& chatResponse.hasToolCalls();
}
}
5.3.2 用户控制的工具执行
上面介绍的是框架控制的工具执行,当然也可以由用户自行控制工具执行,需要自己实现ToolExecutionEligibilityPredicate等接口,但是这里不做详细介绍。
5.4 异常处理
当工具调用失败时,异常将作为 ToolExecutionException 传播,可以捕获它来处理错误。ToolExecutionExceptionProcessor 可用于处理 ToolExecutionException,并有两种结果:生成要发送回 AI 模型的错误消息,或抛出异常由调用者处理。
java
@FunctionalInterface
public interface ToolExecutionExceptionProcessor {
/**
* Convert an exception thrown by a tool to a String that can be sent back to the AI
* model or throw an exception to be handled by the caller.
*/
String process(ToolExecutionException exception);
}
如果您正在使用任何 Spring AI Spring Boot Starter,DefaultToolExecutionExceptionProcessor 是 ToolExecutionExceptionProcessor 接口的自动配置实现。

5.4.1 异常处理过程
默认情况下,RuntimeException 的错误消息会发送回模型,而检查异常和错误(例如 IOException、OutOfMemoryError)总是抛出。DefaultToolExecutionExceptionProcessor 构造函数允许您将 alwaysThrow 属性设置为 true 或 false。如果为 true,则会抛出异常,而不是将错误消息发送回模型。

ToolExecutionExceptionProcessor 由默认的 ToolCallingManager(DefaultToolCallingManager)内部使用,用于在工具执行期间处理异常。

5.4.2 异常处理案例
我们可以通过上面的设置闹钟代码案例来看它的异常处理情况:
java
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);
}
}
@Test
void test_information_retrieval_demo() {
String response = ChatClient.create(chatModel)
.prompt("请帮我设置一个从现在起 20 分钟后的闹钟")
.tools(new DateTimeTools())
.call()
.content();
System.out.println(response);
}
可能大家感觉这个代码没有任何问题,但是具体看过它的执行过程后就能发现它有异常,只不过异常被捕获到了然后被大模型给处理掉了,所以模型给出了正确的结果,没有把异常暴露出来。
第一次调用工具getCurrentDateTime时,获取到了当前时间,第二次调用工具会调用setAlarm设置闹钟,输入的时间参数是模型返回的值时间是23:12:28。

我们可以看到执行到setAlarm这个方法的工具执行时,异常被ToolExecutionExceptionProcessor类捕获处理了然后存到了工具响应里回传给了大模型。

大模型收到了工具调用的报错信息后然后自己处理了一下,然后又重新调整了时间格式进行工具调用:

很明显还是不能正常调用这个工具,因为工具里方法时间解析格式是DateTimeFormatter.ISO_DATE_TIME

最后经过不断的试错,模型还是找到了最终的时间格式成功对工具进行了调用:

注意:通过上面案例我们能发现一个问题,就是我们最好是给工具调用的方法入参一个清晰的定义,以便大模型能够给出正确的参数格式。这样不仅可以提高模型回答用户问题的准确度,还能减少因工具调用失败而多次调用大模型的次数,提高响应时间,降低成本。
六、总结
本章讲解了Spring AI中的Tool Calling,文中不仅介绍了方法(Method)和函数(Function)两种模式,而且还分析了相应的源码。由于工具调用是后续文章将介绍的MCP内容的基石,因此这篇文章还特地对具体的案例画了流程图,分析了数据的流转过程,其他工具调用的执行过程也相差无几。
创作不易,如果有帮助到你的话请给点个赞吧!我是Wasteland,下期文章再见!
