工具调用(Tool Calling)
原文:https://docs.spring.io/spring-ai/reference/api/tools.html#_toolindex_api
工具调用(也称为函数调用)是AI应用中的一种常见模式,允许模型与一组API或工具进行交互,从而增强其能力。
工具主要用于:
信息检索。此类工具可用于从外部来源检索信息,例如数据库、Web服务、文件系统或Web搜索引擎。目标是增强模型的知识,使其能够回答原本无法回答的问题。因此,它们可用于检索增强生成(RAG)场景。例如,可以使用工具检索给定位置的当前天气、检索最新新闻文章或查询数据库中的特定记录。
执行操作。此类工具可用于在软件系统中执行操作,例如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流。目标是自动化原本需要人工干预或显式编程的任务。例如,可以使用工具为与聊天机器人交互的客户预订航班、在网页上填写表单,或在代码生成场景中基于自动化测试(TDD)实现Java类。
尽管我们通常将工具调用称为模型能力,但实际上由客户端应用程序提供工具调用逻辑。模型只能请求工具调用并提供输入参数,而应用程序负责根据输入参数执行工具调用并返回结果。模型永远不会访问作为工具提供的任何API,这是一个关键的安全考虑因素。
Spring AI提供了便捷的API来定义工具、解析模型的工具调用请求并执行工具调用。以下各节概述了Spring AI的工具调用能力。
查看聊天模型比较以了解哪些AI模型支持工具调用。
按照迁移指南从已弃用的FunctionCallback迁移到ToolCallback API。
快速入门
让我们看看如何在Spring AI中开始使用工具调用。我们将实现两个简单的工具:一个用于信息检索,一个用于执行操作。信息检索工具将用于获取用户时区的当前日期和时间。操作工具将用于在指定时间设置闹钟。
信息检索
AI模型无法访问实时信息。任何假设了解当前日期或天气预报等信息的提问,模型都无法回答。但是,我们可以提供一个可以检索这些信息的工具,并让模型在需要访问实时信息时调用此工具。
让我们在DateTimeTools类中实现一个工具,用于获取用户时区的当前日期和时间。该工具不接受任何参数。Spring Framework的LocaleContextHolder可以提供用户的时区。该工具将被定义为带有@Tool注解的方法。为了帮助模型理解是否以及何时调用此工具,我们将提供该工具功能的详细描述。
java
import java.time.LocalDateTime;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;
class DateTimeTools {
@Tool(description = "获取用户时区的当前日期和时间")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
接下来,让该工具对模型可用。在本例中,我们将使用ChatClient与模型交互。我们将通过tools()方法传递DateTimeTools的实例来向模型提供该工具。当模型需要了解当前日期和时间时,它将请求调用该工具。在内部,ChatClient将调用该工具并将结果返回给模型,然后模型将使用工具调用结果生成对原始问题的最终回答。
java
ChatModel chatModel = ...
String response = ChatClient.create(chatModel)
.prompt("明天是星期几?")
.tools(new DateTimeTools())
.call()
.content();
System.out.println(response);
输出将类似于:
明天是2015-10-21。
你可以再次尝试询问同样的问题。这次,不提供该工具。输出将类似于:
我是一个AI,无法访问实时信息。请提供当前日期,以便我准确确定明天是星期几。
没有该工具,模型不知道如何回答问题,因为它无法确定当前日期和时间。
执行操作
AI模型可用于生成实现某些目标的计划。例如,模型可以生成预订丹麦旅行的计划。然而,模型没有能力执行该计划。这就是工具的作用所在:它们可用于执行模型生成的计划。
在前面的示例中,我们使用了一个工具来确定当前日期和时间。在本例中,我们将定义第二个工具,用于在特定时间设置闹钟。目标是在从现在起10分钟后设置闹钟,因此我们需要向模型提供这两个工具来完成此任务。
我们将把新工具添加到与之前相同的DateTimeTools类中。新工具将接受一个参数,即ISO-8601格式的时间。然后,该工具将在控制台打印一条消息,指示闹钟已在给定时间设置。与之前一样,该工具被定义为带有@Tool注解的方法,我们还使用该注解提供详细描述,以帮助模型理解何时以及如何使用该工具。
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;
class DateTimeTools {
@Tool(description = "获取用户时区的当前日期和时间")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
@Tool(description = "为给定时间设置用户闹钟,时间格式为ISO-8601")
void setAlarm(String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("闹钟已设置为 " + alarmTime);
}
}
接下来,让这两个工具对模型可用。我们将使用ChatClient与模型交互。我们将通过tools()方法传递DateTimeTools的实例来向模型提供工具。当我们要求从现在起10分钟后设置闹钟时,模型首先需要知道当前日期和时间。然后,它将使用当前日期和时间计算闹钟时间。最后,它将使用闹钟工具来设置闹钟。在内部,ChatClient将处理模型的任何工具调用请求,并将任何工具调用执行结果发送回模型,以便模型生成最终回答。
java
ChatModel chatModel = ...
String response = ChatClient.create(chatModel)
.prompt("你能在从现在起10分钟后设置闹钟吗?")
.tools(new DateTimeTools())
.call()
.content();
System.out.println(response);
在应用程序日志中,你可以检查闹钟是否已在正确时间设置。
概述
Spring AI通过一组灵活的抽象支持工具调用,允许你以一致的方式定义、解析和执行工具。本节概述了Spring AI中工具调用的主要概念和组件。
工具调用的主要操作序列
-
当我们想要使工具对模型可用时,我们将其定义包含在聊天请求中。每个工具定义包括名称、描述和输入参数的架构。
-
当模型决定调用工具时,它会发送一个包含工具名称和根据定义架构建模的输入参数的响应。
-
应用程序负责使用工具名称识别并使用提供的输入参数执行工具。
-
工具调用的结果由应用程序处理。
-
应用程序将工具调用结果发送回模型。
-
模型使用工具调用结果作为额外上下文生成最终回答。
工具是工具调用的构建块,由ToolCallback接口建模。Spring AI为从方法和函数指定ToolCallback提供了内置支持,但你始终可以定义自己的ToolCallback实现以支持更多用例。
当使用ChatClient时,工具调用请求由ToolCallingAdvisor自动处理,该顾问在顾问链中注册,并使用ToolCallingManager接口管理完整的工具执行生命周期。
ChatModel实现以前在内部处理工具执行。该方法自2.0.0起已弃用,并将在3.0.0中移除。建议使用带有ToolCallingAdvisor的ChatClient。
ChatClient和ChatModel都接受ToolCallback对象的列表,以使工具对模型可用,并接受最终将执行它们的ToolCallingManager。
除了直接传递ToolCallback对象外,你还可以传递工具名称列表,这些名称将使用ToolCallbackResolver接口动态解析。
以下各节将更详细地介绍所有这些概念和API,包括如何自定义和扩展它们以支持更多用例。
方法作为工具
Spring AI为从方法指定工具(即ToolCallback)提供了两种内置支持:
- 声明式:使用
@Tool注解 - 编程式:使用低层级的
MethodToolCallback实现。
声明式规范:@Tool
你可以通过使用@Tool注解方法将其转换为工具。
java
class DateTimeTools {
@Tool(description = "获取用户时区的当前日期和时间")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
@Tool注解允许你提供工具的关键信息:
- name:工具的名称。如果未提供,将使用方法名称。AI模型在调用工具时使用此名称来识别工具。因此,在同一类中不允许有两个同名的工具。对于特定聊天请求,该名称在模型可用的所有工具中必须是唯一的。
- description:工具的描述,模型可以用它来理解何时以及如何调用工具。如果未提供,将使用方法名称作为工具描述。然而,强烈建议提供详细描述,因为这对模型理解工具的目的和使用方法至关重要。未能提供良好描述可能导致模型在应该使用工具时未使用,或错误使用工具。
- returnDirect :工具结果是否应直接返回给客户端还是传回给模型。参见直接返回了解更多详情。
- resultConverter :
ToolCallResultConverter实现,用于将工具调用的结果转换为字符串对象以发送回AI模型。参见结果转换了解更多详情。
方法可以是静态的或实例方法,可以具有任何可见性(public、protected、package-private或private)。包含该方法的类可以是顶级类或嵌套类,也可以具有任何可见性(只要它在你要实例化的位置可访问)。
只要包含方法的类是Spring Bean(如@Component),Spring AI就为@Tool注解的方法提供AOT编译的内置支持。否则,你需要向GraalVM编译器提供必要的配置。例如,使用@RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_METHODS)注解该类。
你可以为方法定义任意数量的参数(包括无参数),支持大多数类型(基本类型、POJO、枚举、列表、数组、映射等)。同样,方法可以返回大多数类型,包括void。如果方法返回值,返回类型必须是可序列化的类型,因为结果将被序列化并发送回模型。
某些类型不受支持。参见方法工具限制了解更多详情。
Spring AI将自动为@Tool注解方法的输入参数生成JSON架构。该架构被模型用来理解如何调用工具并准备工具请求。@ToolParam注解可用于提供有关输入参数的额外信息,例如描述或参数是必需还是可选。默认情况下,所有输入参数都被视为必需。
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
class DateTimeTools {
@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。参见[JSON Schema](#JSON Schema)了解更多详情。
将工具添加到ChatClient
当使用声明式规范方法时,你可以在调用ChatClient时通过tools()方法传递工具类实例。这些工具仅对它们被添加到的特定聊天请求可用。
java
ChatClient.create(chatModel)
.prompt("明天是星期几?")
.tools(new DateTimeTools())
.call()
.content();
在底层,ChatClient将从工具类实例中的每个@Tool注解方法生成一个ToolCallback,并将它们传递给模型。如果你更愿意自己生成ToolCallback,可以使用ToolCallbacks工具类并直接传递给tools()。
java
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatClient.create(chatModel)
.prompt("明天是星期几?")
.tools(dateTimeTools)
.call()
.content();
tools()还直接接受ToolCallback和ToolCallbackProvider实例,以及@Tool注解的POJO实例和这些类型的集合。
将默认工具添加到ChatClient
当使用声明式规范方法时,你可以通过defaultTools()方法将工具类实例传递给ChatClient.Builder来添加默认工具。如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
默认工具在由同一ChatClient.Builder构建的所有ChatClient实例的所有聊天请求中共享。它们对于跨不同聊天请求常用的工具很有用,但如果使用不当也可能很危险,有可能在不应该的时候使它们可用。
java
ChatModel chatModel = ...
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new DateTimeTools())
.build();
将工具添加到ChatModel
当使用声明式规范方法时,你可以通过ToolCallingChatOptions的toolCallbacks()方法将工具类实例传递给用于调用ChatModel的选项。这些工具仅对它们被添加到的特定聊天请求可用。
java
ChatModel chatModel = ...
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(dateTimeTools)
.build();
Prompt prompt = new Prompt("明天是星期几?", chatOptions);
chatModel.call(prompt);
直接调用ChatModel会将工具定义发送给AI模型,但响应中的工具调用不会自动执行。使用ChatClient(它会自动注册ToolCallingAdvisor)进行自动执行,或实现用户控制的工具执行来自己驱动循环。
将默认工具添加到ChatModel
当使用声明式规范方法时,你可以通过在创建ChatModel时向用于创建ChatModel的ToolCallingChatOptions实例的toolCallbacks()方法传递工具类实例来添加默认工具。如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
默认工具在由该ChatModel实例执行的所有聊天请求中共享。它们对于跨不同聊天请求常用的工具很有用,但如果使用不当也可能很危险,有可能在不应该的时候使它们可用。
java
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(OllamaApi.builder().build())
.options(ToolCallingChatOptions.builder()
.toolCallbacks(dateTimeTools)
.build())
.build();
编程式规范:MethodToolCallback
你可以通过编程方式构建MethodToolCallback将方法转换为工具。
java
class DateTimeTools {
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
MethodToolCallback.Builder允许你构建MethodToolCallback实例并提供工具的关键信息:
- toolDefinition :定义工具名称、描述和输入架构的
ToolDefinition实例。你可以使用ToolDefinition.Builder类构建它。必需。 - toolMetadata :定义额外设置(如结果是否应直接返回给客户端以及要使用的结果转换器)的
ToolMetadata实例。你可以使用ToolMetadata.Builder类构建它。 - toolMethod :表示工具方法的
Method实例。必需。 - toolObject:包含工具方法的对象实例。如果方法是静态的,可以省略此参数。
- toolCallResultConverter :用于将工具调用结果转换为字符串对象以发送回AI模型的
ToolCallResultConverter实例。如果未提供,将使用默认转换器(DefaultToolCallResultConverter)。
ToolDefinition.Builder允许你构建ToolDefinition实例并定义工具名称、描述和输入架构:
- name:工具的名称。如果未提供,将使用方法名称。AI模型在调用工具时使用此名称来识别工具。因此,在同一类中不允许有两个同名的工具。对于特定聊天请求,该名称在模型可用的所有工具中必须是唯一的。
- description:工具的描述,模型可以用它来理解何时以及如何调用工具。如果未提供,将使用方法名称作为工具描述。然而,强烈建议提供详细描述,因为这对模型理解工具的目的和使用方法至关重要。未能提供良好描述可能导致模型在应该使用工具时未使用,或错误使用工具。
- inputSchema :工具输入参数的JSON架构。如果未提供,将根据方法参数自动生成架构。你可以使用
@ToolParam注解提供有关输入参数的额外信息,例如描述或参数是必需还是可选。默认情况下,所有输入参数都被视为必需。参见[JSON Schema](#JSON Schema)了解更多详情。
ToolMetadata.Builder允许你构建ToolMetadata实例并为工具定义额外设置:
- returnDirect :工具结果是否应直接返回给客户端还是传回给模型。参见直接返回了解更多详情。
java
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
ToolCallback toolCallback = MethodToolCallback.builder()
.toolDefinition(ToolDefinitions.builder(method)
.description("获取用户时区的当前日期和时间")
.build())
.toolMethod(method)
.toolObject(new DateTimeTools())
.build();
方法可以是静态的或实例方法,可以具有任何可见性(public、protected、package-private或private)。包含该方法的类可以是顶级类或嵌套类,也可以具有任何可见性(只要它在你要实例化的位置可访问)。
只要包含方法的类是Spring Bean(如@Component),Spring AI就为工具方法提供AOT编译的内置支持。否则,你需要向GraalVM编译器提供必要的配置。例如,使用@RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_METHODS)注解该类。
你可以为方法定义任意数量的参数(包括无参数),支持大多数类型(基本类型、POJO、枚举、列表、数组、映射等)。同样,方法可以返回大多数类型,包括void。如果方法返回值,返回类型必须是可序列化的类型,因为结果将被序列化并发送回模型。
某些类型不受支持。参见方法工具限制了解更多详情。
如果方法是静态的,你可以省略toolObject()方法,因为它不需要。
java
class DateTimeTools {
static String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
ToolCallback toolCallback = MethodToolCallback.builder()
.toolDefinition(ToolDefinitions.builder(method)
.description("获取用户时区的当前日期和时间")
.build())
.toolMethod(method)
.build();
Spring AI将自动为方法的输入参数生成JSON架构。该架构被模型用来理解如何调用工具并准备工具请求。@ToolParam注解可用于提供有关输入参数的额外信息,例如描述或参数是必需还是可选。默认情况下,所有输入参数都被视为必需。
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.ai.tool.annotation.ToolParam;
class DateTimeTools {
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。参见[JSON Schema](#JSON Schema)了解更多详情。
将工具添加到ChatClient和ChatModel
当使用编程式规范方法时,你可以直接将MethodToolCallback实例传递给ChatClient上的tools()方法。该工具仅对它被添加到的特定聊天请求可用。
java
ToolCallback toolCallback = ...
ChatClient.create(chatModel)
.prompt("明天是星期几?")
.tools(toolCallback)
.call()
.content();
将默认工具添加到ChatClient
当使用编程式规范方法时,你可以通过defaultTools()方法直接将MethodToolCallback实例传递给ChatClient.Builder来添加默认工具。如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
默认工具在由同一ChatClient.Builder构建的所有ChatClient实例的所有聊天请求中共享。它们对于跨不同聊天请求常用的工具很有用,但如果使用不当也可能很危险,有可能在不应该的时候使它们可用。
java
ChatModel chatModel = ...
ToolCallback toolCallback = ...
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(toolCallback)
.build();
将工具添加到ChatModel
当使用编程式规范方法时,你可以将MethodToolCallback实例传递给ToolCallingChatOptions的toolCallbacks()方法,用于调用ChatModel。该工具仅对它被添加到的特定聊天请求可用。
java
ChatModel chatModel = ...
ToolCallback toolCallback = ...
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(toolCallback)
.build();
Prompt prompt = new Prompt("明天是星期几?", chatOptions);
chatModel.call(prompt);
直接调用ChatModel会将工具定义发送给AI模型,但响应中的工具调用不会自动执行。使用ChatClient(它会自动注册ToolCallingAdvisor)进行自动执行,或实现用户控制的工具执行来自己驱动循环。
将默认工具添加到ChatModel
当使用编程式规范方法时,你可以通过在创建ChatModel时向用于创建ChatModel的ToolCallingChatOptions实例的toolCallbacks()方法传递MethodToolCallback实例来添加默认工具。如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
默认工具在由该ChatModel实例执行的所有聊天请求中共享。它们对于跨不同聊天请求常用的工具很有用,但如果使用不当也可能很危险,有可能在不应该的时候使它们可用。
java
ToolCallback toolCallback = ...
ChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(OllamaApi.builder().build())
.options(ToolCallingChatOptions.builder()
.toolCallbacks(toolCallback)
.build())
.build();
方法工具限制
以下类型目前不支持作为用作工具的方法的参数或返回类型:
Optional- 异步类型(如
CompletableFuture、Future) - 响应式类型(如
Flow、Mono、Flux) - 函数式类型(如
Function、Supplier、Consumer)。
函数式类型使用基于函数的工具规范方法支持。参见函数作为工具了解更多详情。
函数作为工具
Spring AI为从函数指定工具提供了两种内置支持:编程式使用低层级的FunctionToolCallback实现,或动态作为在运行时解析的@Bean。
编程式规范:FunctionToolCallback
你可以通过编程方式构建FunctionToolCallback将函数式类型(Function、Supplier、Consumer或BiFunction)转换为工具。
java
public class WeatherService implements Function<WeatherRequest, WeatherResponse> {
public WeatherResponse apply(WeatherRequest request) {
return new WeatherResponse(30.0, Unit.C);
}
}
public enum Unit { C, F }
public record WeatherRequest(String location, Unit unit) {}
public record WeatherResponse(double temp, Unit unit) {}
FunctionToolCallback.Builder允许你构建FunctionToolCallback实例并提供工具的关键信息:
- name:工具的名称。AI模型在调用工具时使用此名称来识别工具。因此,在同一上下文中不允许有两个同名的工具。对于特定聊天请求,该名称在模型可用的所有工具中必须是唯一的。必需。
- toolFunction :表示工具方法的函数对象(
Function、Supplier、Consumer或BiFunction)。必需。 - description:工具的描述,模型可以用它来理解何时以及如何调用工具。如果未提供,将使用方法名称作为工具描述。然而,强烈建议提供详细描述,因为这对模型理解工具的目的和使用方法至关重要。未能提供良好描述可能导致模型在应该使用工具时未使用,或错误使用工具。
- inputType:函数输入的类型。必需。
- inputSchema :工具输入参数的JSON架构。如果未提供,将根据
inputType自动生成架构。你可以使用@ToolParam注解提供有关输入参数的额外信息,例如描述或参数是必需还是可选。默认情况下,所有输入参数都被视为必需。参见[JSON Schema](#JSON Schema)了解更多详情。 - toolMetadata :定义额外设置(如结果是否应直接返回给客户端以及要使用的结果转换器)的
ToolMetadata实例。你可以使用ToolMetadata.Builder类构建它。 - toolCallResultConverter :用于将工具调用结果转换为字符串对象以发送回AI模型的
ToolCallResultConverter实例。如果未提供,将使用默认转换器(DefaultToolCallResultConverter)。
ToolMetadata.Builder允许你构建ToolMetadata实例并为工具定义额外设置:
- returnDirect :工具结果是否应直接返回给客户端还是传回给模型。参见直接返回了解更多详情。
java
ToolCallback toolCallback = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("获取位置的天气")
.inputType(WeatherRequest.class)
.build();
函数的输入和输出可以是Void或POJO。输入和输出POJO必须是可序列化的,因为结果将被序列化并发送回模型。函数以及输入和输出类型必须是public的。
某些类型不受支持。参见函数工具限制了解更多详情。
将工具添加到ChatClient
当使用编程式规范方法时,你可以直接将FunctionToolCallback实例传递给ChatClient上的tools()方法。该工具仅对它被添加到的特定聊天请求可用。
java
ToolCallback toolCallback = ...
ChatClient.create(chatModel)
.prompt("哥本哈根的天气怎么样?")
.tools(toolCallback)
.call()
.content();
将默认工具添加到ChatClient
当使用编程式规范方法时,你可以通过defaultTools()方法直接将FunctionToolCallback实例传递给ChatClient.Builder来添加默认工具。如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
默认工具在由同一ChatClient.Builder构建的所有ChatClient实例的所有聊天请求中共享。它们对于跨不同聊天请求常用的工具很有用,但如果使用不当也可能很危险,有可能在不应该的时候使它们可用。
java
ChatModel chatModel = ...
ToolCallback toolCallback = ...
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(toolCallback)
.build();
将工具添加到ChatModel
当使用编程式规范方法时,你可以将FunctionToolCallback实例传递给ToolCallingChatOptions的toolCallbacks()方法。该工具仅对它被添加到的特定聊天请求可用。
java
ChatModel chatModel = ...
ToolCallback toolCallback = ...
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(toolCallback)
.build();
Prompt prompt = new Prompt("哥本哈根的天气怎么样?", chatOptions);
chatModel.call(prompt);
直接调用ChatModel会将工具定义发送给AI模型,但响应中的工具调用不会自动执行。使用ChatClient(它会自动注册ToolCallingAdvisor)进行自动执行,或实现用户控制的工具执行来自己驱动循环。
将默认工具添加到ChatModel
当使用编程式规范方法时,你可以通过在创建ChatModel时向用于创建ChatModel的ToolCallingChatOptions实例的toolCallbacks()方法传递FunctionToolCallback实例来添加默认工具。如果同时提供了默认工具和运行时工具,运行时工具将完全覆盖默认工具。
默认工具在由该ChatModel实例执行的所有聊天请求中共享。它们对于跨不同聊天请求常用的工具很有用,但如果使用不当也可能很危险,有可能在不应该的时候使它们可用。
java
ToolCallback toolCallback = ...
ChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(OllamaApi.builder().build())
.options(ToolCallingChatOptions.builder()
.toolCallbacks(toolCallback)
.build())
.build();
ToolCallback Bean
Spring AI可以自动发现应用程序上下文中定义的ToolCallback Bean,并按名称使其可用于解析。这允许你使用FunctionToolCallback.builder()或MethodToolCallback.builder()将工具定义为Spring Bean,并在聊天请求中显式注入它们。
java
@Configuration(proxyBeanMethods = false)
class WeatherTools {
WeatherService weatherService = new WeatherService();
@Bean
ToolCallback currentWeather() {
return FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("获取位置的天气")
.inputType(WeatherRequest.class)
.build();
}
}
当聊天请求中需要ToolCallback Bean时,直接注入它:
java
@Autowired
ToolCallback currentWeather;
// 在请求时传递给ChatClient
ChatClient.create(chatModel)
.prompt("哥本哈根的天气怎么样?")
.tools(currentWeather)
.call()
.content();
// 或通过构建器注册为所有请求的默认工具
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(currentWeather)
.build();
直接调用ChatModel会将工具定义发送给AI模型,但响应中的工具调用不会自动执行。使用ChatClient(它会自动注册ToolCallingAdvisor)进行自动执行,或实现用户控制的工具执行来自己驱动循环。
函数工具限制
以下类型目前不支持作为用作工具的函数的输入或输出类型:
- 基本类型
Optional- 集合类型(如
List、Map、Array、Set) - 异步类型(如
CompletableFuture、Future) - 响应式类型(如
Flow、Mono、Flux)。
基本类型和集合使用基于方法的工具规范方法支持。参见方法作为工具了解更多详情。
工具规范
在Spring AI中,工具通过ToolCallback接口建模。在前面的章节中,我们看到了如何使用Spring AI提供的内置支持从方法和函数定义工具(参见方法作为工具和函数作为工具)。本节将更深入地探讨工具规范以及如何自定义和扩展它以支持更多用例。
Tool Callback(工具回调)
ToolCallback接口提供了一种定义可由AI模型调用的工具的方法,包括定义和执行逻辑。当你想要从头开始定义工具时,这是要实现的主要接口。例如,你可以从MCP客户端(使用模型上下文协议)或ChatClient(构建模块化代理应用程序)定义ToolCallback。
该接口提供以下方法:
java
public interface ToolCallback {
/**
* AI模型用来确定何时以及如何调用工具的定义。
*/
ToolDefinition getToolDefinition();
/**
* 提供有关如何处理工具的额外信息的元数据。
*/
ToolMetadata getToolMetadata();
/**
* 使用给定输入执行工具并返回要发送回AI模型的结果。
*/
String call(String toolInput);
/**
* 使用给定输入和上下文执行工具并返回要发送回AI模型的结果。
*/
String call(String toolInput, ToolContext tooContext);
}
Spring AI为工具方法(MethodToolCallback)和工具函数(FunctionToolCallback)提供了内置实现。
Tool Definition(工具定义)
ToolDefinition接口为AI模型提供了了解工具可用性所需的信息,包括工具名称、描述和输入架构。每个ToolCallback实现必须提供一个ToolDefinition实例来定义工具。
该接口提供以下方法:
java
public interface ToolDefinition {
/**
* 工具名称。在提供给模型的工具集中唯一。
*/
String name();
/**
* 工具描述,AI模型用来确定工具的功能。
*/
String description();
/**
* 用于调用工具的参数的架构。
*/
String inputSchema();
}
参见[JSON Schema](#JSON Schema)了解更多关于输入架构的详情。
ToolDefinition.Builder允许你使用默认实现(DefaultToolDefinition)构建ToolDefinition实例。
java
ToolDefinition toolDefinition = ToolDefinition.builder()
.name("currentWeather")
.description("获取位置的天气")
.inputSchema("""
{
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string",
"enum": ["C", "F"]
}
},
"required": ["location", "unit"]
}
""")
.build();
方法工具定义
当从方法构建工具时,ToolDefinition会自动为你生成。如果你更愿意自己生成ToolDefinition,可以使用这个便捷的构建器。
java
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
ToolDefinition toolDefinition = ToolDefinitions.from(method);
从方法生成的ToolDefinition包括方法名称作为工具名称,方法名称作为工具描述,以及方法输入参数的JSON架构。如果方法带有@Tool注解,工具名称和描述将来自注解(如果设置)。
参见方法作为工具了解更多详情。
如果你更愿意显式提供部分或全部属性,可以使用ToolDefinition.Builder构建自定义ToolDefinition实例。
java
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
ToolDefinition toolDefinition = ToolDefinitions.builder(method)
.name("currentDateTime")
.description("获取用户时区的当前日期和时间")
.inputSchema(JsonSchemaGenerator.generateForMethodInput(method))
.build();
函数工具定义
当从函数构建工具时,ToolDefinition会自动为你生成。当你使用FunctionToolCallback.Builder构建FunctionToolCallback实例时,你可以提供用于生成ToolDefinition的工具名称、描述和输入架构。参见函数作为工具了解更多详情。
JSON Schema
当向AI模型提供工具时,模型需要知道调用工具的输入类型的架构。该架构用于理解如何调用工具和准备工具请求。Spring AI通过JsonSchemaGenerator类为工具输入类型生成JSON Schema提供了内置支持。该架构作为ToolDefinition的一部分提供。
参见工具定义了解更多关于ToolDefinition以及如何向其传递输入架构的详情。
JsonSchemaGenerator类在底层用于为方法或函数的输入参数生成JSON架构,使用方法作为工具和函数作为工具中描述的任何策略。JSON架构生成逻辑支持一系列注解,你可以将这些注解用于方法和函数的输入参数,以自定义生成的架构。
本节描述了在生成工具输入参数的JSON架构时可以自定义的两个主要选项:描述和必需状态。
描述
除了为工具本身提供描述外,你还可以为工具的输入参数提供描述。描述可用于提供有关输入参数的关键信息,例如参数应采用什么格式、允许哪些值等。这有助于模型理解输入架构以及如何使用它。Spring AI使用以下注解之一为输入参数生成描述提供了内置支持:
- Spring AI的
@ToolParam(description = "...") - Jackson的
@JsonClassDescription(description = "...") - Jackson的
@JsonPropertyDescription(description = "...") - Swagger的
@Schema(description = "...")。
该方法适用于方法和函数,并且您可以递归地将其用于嵌套类型。
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.context.i18n.LocaleContextHolder;
class DateTimeTools {
@Tool(description = "为给定时间设置用户闹钟")
void setAlarm(@ToolParam(description = "ISO-8601格式的时间") String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("闹钟已设置为 " + alarmTime);
}
}
必需/可选
默认情况下,每个输入参数都被视为必需的,这会强制AI模型在调用工具时为其提供值。但是,您可以使用以下注解之一(按优先级顺序)将输入参数设为可选:
- Spring AI 的
@ToolParam(required = false) - Jackson 的
@JsonProperty(required = false) - Swagger 的
@Schema(required = false) - Spring Framework 的
@Nullable
该方法适用于方法和函数,并且您可以递归地将其用于嵌套类型。
java
class CustomerTools {
@Tool(description = "更新客户信息")
void updateCustomerInfo(Long id, String name, @ToolParam(required = false) String email) {
System.out.println("已更新ID为 " + id + " 的客户信息");
}
}
为输入参数正确定义必需状态对于降低幻觉风险至关重要,并确保模型在调用工具时提供正确的输入。在前面的示例中,email 参数是可选的,这意味着模型可以在不提供该值的情况下调用工具。如果该参数是必需的,模型则必须在调用工具时为其提供值。如果没有值,模型很可能会编造一个,从而导致幻觉。
结果转换
工具调用的结果使用 ToolCallResultConverter 进行序列化,然后发送回AI模型。ToolCallResultConverter 接口提供了一种将工具调用的结果转换为 String 对象的方法。
该接口提供了以下方法:
java
@FunctionalInterface
public interface ToolCallResultConverter {
/**
* 给定工具返回的对象,将其转换为与给定类类型兼容的String。
*/
String convert(@Nullable Object result, @Nullable Type returnType);
}
结果必须是可序列化的类型。默认情况下,结果使用 Jackson 序列化为 JSON(DefaultToolCallResultConverter),但您可以通过提供自己的 ToolCallResultConverter 实现来定制序列化过程。
Spring AI 在方法工具和函数工具中都依赖于 ToolCallResultConverter。
方法工具调用结果转换
当使用声明式方法从方法构建工具时,您可以通过设置 @Tool 注解的 resultConverter() 属性来为该工具提供自定义的 ToolCallResultConverter。
java
class CustomerTools {
@Tool(description = "检索客户信息", resultConverter = CustomToolCallResultConverter.class)
Customer getCustomerInfo(Long id) {
return customerRepository.findById(id);
}
}
如果使用编程式方法,您可以通过设置 MethodToolCallback.Builder 的 resultConverter() 属性来为该工具提供自定义的 ToolCallResultConverter。
详细信息请参见将方法作为工具。
函数工具调用结果转换
当使用编程式方法从函数构建工具时,您可以通过设置 FunctionToolCallback.Builder 的 resultConverter() 属性来为该工具提供自定义的 ToolCallResultConverter。
详细信息请参见将函数作为工具。
工具上下文
Spring AI 支持通过 ToolContext API 向工具传递额外的上下文信息。此功能允许您提供额外的、用户提供的数据,这些数据可以在工具执行过程中与AI模型传递的工具参数一起使用。
向工具提供额外的上下文信息
java
class CustomerTools {
@Tool(description = "检索客户信息")
Customer getCustomerInfo(Long id, ToolContext toolContext) {
return customerRepository.findById(id, toolContext.getContext().get("tenantId"));
}
}
ToolContext 中的数据由用户在调用 ChatClient 时提供。
java
ChatModel chatModel = ...
String response = ChatClient.create(chatModel)
.prompt("告诉我更多关于ID为42的客户的信息")
.tools(new CustomerTools())
.toolContext(Map.of("tenantId", "acme"))
.call()
.content();
System.out.println(response);
ToolContext 中提供的数据不会发送给AI模型。
类似地,您也可以直接在调用 ChatModel 时定义工具上下文数据。
java
ChatModel chatModel = ...
ToolCallback[] customerTools = ToolCallbacks.from(new CustomerTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(customerTools)
.toolContext(Map.of("tenantId", "acme"))
.build();
Prompt prompt = new Prompt("告诉我更多关于ID为42的客户的信息", chatOptions);
chatModel.call(prompt);
如果 toolContext 选项同时在默认选项和运行时选项中设置,最终的 ToolContext 将是两者的合并,其中运行时选项优先于默认选项。
直接返回
默认情况下,工具调用的结果会作为响应发送回模型。然后,模型可以使用该结果继续对话。
在某些情况下,您可能希望将结果直接返回给调用者,而不是将其发送回模型。例如,如果您构建了一个依赖RAG工具的代理,您可能希望将结果直接返回给调用者,而不是将其发送回模型进行不必要的后处理。或者,某些工具可能应该结束代理的推理循环。
每个 ToolCallback 实现都可以定义工具调用的结果应直接返回给调用者还是发送回模型。默认情况下,结果是发送回模型的。但您可以为每个工具更改此行为。
负责管理工具执行生命周期的 ToolCallingManager 负责处理与工具关联的 returnDirect 属性。如果该属性设置为 true,则工具调用的结果将直接返回给调用者。否则,结果将发送回模型。
如果同时请求多个工具调用,则必须将所有工具的 returnDirect 属性设置为 true,才能将结果直接返回给调用者。否则,结果将发送回模型。
将工具调用结果直接返回给调用者
当我们想让模型使用某个工具时,我们会在聊天请求中包含其定义。如果我们希望工具执行的结果直接返回给调用者,我们将 returnDirect 属性设置为 true。
- 当模型决定调用工具时,它会发送一个包含工具名称和根据定义的模式建模的输入参数的响应。
- 应用程序负责使用工具名称识别并使用提供的输入参数执行工具。
- 工具调用的结果由应用程序处理。
- 应用程序将工具调用结果直接发送给调用者,而不是将其发送回模型。
方法直接返回
当使用声明式方法从方法构建工具时,您可以通过将 @Tool 注解的 returnDirect 属性设置为 true 来标记工具将结果直接返回给调用者。
java
class CustomerTools {
@Tool(description = "检索客户信息", returnDirect = true)
Customer getCustomerInfo(Long id) {
return customerRepository.findById(id);
}
}
如果使用编程式方法,您可以通过 ToolMetadata 接口设置 returnDirect 属性,并将其传递给 MethodToolCallback.Builder。
java
ToolMetadata toolMetadata = ToolMetadata.builder()
.returnDirect(true)
.build();
详细信息请参见将方法作为工具。
函数直接返回
当使用编程式方法从函数构建工具时,您可以通过 ToolMetadata 接口设置 returnDirect 属性,并将其传递给 FunctionToolCallback.Builder。
java
ToolMetadata toolMetadata = ToolMetadata.builder()
.returnDirect(true)
.build();
详细信息请参见将函数作为工具。
工具执行
工具执行是使用提供的输入参数调用工具并返回结果的过程。工具执行由 ToolCallingManager 接口处理,该接口负责管理工具执行的生命周期。
java
public interface ToolCallingManager {
/**
* 从模型的工具调用选项中解析工具定义。
*/
List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions);
/**
* 执行模型请求的工具调用。
*/
ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse);
}
如果您使用的是任何 Spring AI Spring Boot Starter,DefaultToolCallingManager 是 ToolCallingManager 接口的自动配置实现。您可以通过提供自己的 ToolCallingManager bean 来定制工具执行行为。
java
@Bean
ToolCallingManager toolCallingManager() {
return ToolCallingManager.builder().build();
}
Spring AI 支持三种工具执行生命周期管理方法。推荐的方法是框架控制的工具执行 (通过 ChatClient),它自动处理工具调用循环。对于需要定制工具调用循环的情况,可使用顾问(Advisor)控制的工具执行 。当您需要完全手动控制时,也可使用用户控制的工具执行模式。
框架控制的工具执行
当使用 ChatClient 时,Spring AI 通过 ToolCallingAdvisor 自动处理整个工具调用生命周期,该顾问始终自动注册在顾问链中(除非显式禁用)。这意味着由另一个顾问在运行时注入的动态工具也受支持,而不仅仅是在调用时静态声明的工具。所有这些都以透明的方式完成------您提供工具并提出问题;框架负责其余工作。
框架控制的工具执行生命周期:
- 当我们想让模型使用某个工具时,我们会在聊天请求中包含其定义,并调用
ChatClientAPI,将请求发送给AI模型。 - 当模型决定调用工具时,它会发送一个包含工具名称和输入参数的响应。
- 顾问链中的
ToolCallingAdvisor拦截响应,并将工具调用请求发送给ToolCallingManager。 ToolCallingManager使用提供的输入参数识别并执行工具。- 工具执行结果返回给
ToolCallingManager。 ToolCallingManager将结果返回给ToolCallingAdvisor。ToolCallingAdvisor将工具执行结果作为ToolResponseMessage发送回AI模型。- AI模型使用工具结果作为额外上下文生成最终响应,并通过
ChatClient将其返回给调用者。
java
String response = ChatClient.create(chatModel)
.prompt("明天是星期几?")
.tools(new DateTimeTools())
.call()
.content();
要全局禁用自动注册(针对自动配置的 ChatClient 的所有调用),请设置以下属性:
properties
spring.ai.chat.client.tool-calling.enabled=false
要仅为单个调用禁用它:
java
chatClient.prompt("明天是星期几?")
.tools(new DateTimeTools())
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.content();
使用 ToolCallingAdvisor 进行顾问控制的工具执行
对于需要定制工具调用循环的情况,您可以显式配置 ToolCallingAdvisor。ToolCallingAdvisor 作为顾问链的一部分实现工具调用循环,并提供了几个扩展点:
- 可观测性:链中的其他顾问可以拦截并观察每次工具调用迭代
- 与聊天记忆集成:可与聊天记忆顾问无缝协作,用于管理对话历史
- 可扩展性 :自定义
ToolExecutionEligibilityChecker和钩子方法允许对循环进行细粒度控制
当模型请求工具调用时,ToolCallingAdvisor 通过 ToolCallingManager 执行工具,并将结果发送回模型,循环直到不再需要工具调用。
java
var toolCallingAdvisor = ToolCallingAdvisor.builder()
.toolCallingManager(toolCallingManager)
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(toolCallingAdvisor)
.build();
String response = chatClient.prompt("明天是星期几?")
.tools(new DateTimeTools())
.call()
.content();
配置选项:
ToolCallingAdvisor.Builder 支持以下配置选项:
toolCallingManager:用于执行工具调用的ToolCallingManager实例。如果未提供,则使用默认实例。advisorOrder:顾问在链中应用的顺序。必须在BaseAdvisor.HIGHEST_PRECEDENCE和BaseAdvisor.LOWEST_PRECEDENCE之间。conversationHistoryEnabled:控制顾问是否在工具调用迭代期间内部维护对话历史。默认值为true。toolExecutionEligibilityChecker:一个Function<ChatResponse, Boolean>,决定模型响应是否应触发下一次工具调用迭代。默认检查chatResponse.hasToolCalls()。覆盖此选项可应用特定于提供者的停止原因逻辑(例如,除了工具调用存在性之外,还检查完成原因字段)。
对话历史管理:
默认情况下(conversationHistoryEnabled=true),ToolCallingAdvisor 在工具调用迭代期间内部维护完整的对话历史。每次后续的LLM调用都包含所有之前的消息。
默认排序将记忆顾问放在工具调用循环之外:DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER 是 HIGHEST_PRECEDENCE + 200,低于 ToolCallingAdvisor.DEFAULT_ORDER(HIGHEST_PRECEDENCE + 300)。记忆顾问在循环前加载一次历史,并在循环后仅持久化最终的 用户/助手 交换。这适用于所有 ChatMemoryRepository 实现,因为工具调用消息永远不会写入存储库。
java
// 默认:记忆顾问在工具调用循环之外;ToolCallingAdvisor 管理中间历史
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory).build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(chatMemoryAdvisor)
.build();
仅当将记忆顾问放置在循环内部(顺序高于 ToolCallingAdvisor.DEFAULT_ORDER)时,才使用 .disableInternalConversationHistory()。然后,记忆顾问在每次迭代中处理历史。这需要一个支持工具调用消息的 ChatMemoryRepository(例如 InMemoryChatMemoryRepository)。
spring-ai-session 社区项目提供了一个支持工具调用消息的会话感知记忆实现,可以在任何后端下安全地用于工具调用循环内部。请参阅 spring-ai-session 文档。
java
var toolCallingAdvisor = ToolCallingAdvisor.builder()
.toolCallingManager(toolCallingManager)
.disableInternalConversationHistory() // 循环内部的记忆顾问处理历史
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 300)
.build();
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
.advisorOrder(BaseAdvisor.HIGHEST_PRECEDENCE + 400) // 内部(在 ToolCallingAdvisor 之后)
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(chatMemoryAdvisor, toolCallingAdvisor)
.build();
直接返回:
ToolCallingAdvisor 支持"直接返回"功能,允许工具绕过LLM并直接将结果返回给客户端。当工具执行具有 returnDirect=true 时,顾问会跳出工具调用循环并直接返回工具结果。
有关 ToolCallingAdvisor 的更多详细信息,请参见递归顾问 - ToolCallingAdvisor。
用户控制的工具执行
在某些情况下,您希望自己控制工具执行生命周期------例如,将中间进度流式传输到UI、添加自定义可观测性或在迭代之间应用条件逻辑。
ChatClient 和 ChatModel 都支持用户控制的工具执行。无论哪种情况,您都负责检测 ChatResponse 中的工具调用,并使用 ToolCallingManager 执行它们。
使用 ChatClient
当使用 ChatClient 时,使用 AdvisorParams.toolCallingAdvisorAutoRegister(false) 为单个调用禁用自动注册的 ToolCallingAdvisor,然后自己驱动工具调用循环。
以下示例展示了使用 call() 路径的用户控制工具执行:
java
ChatClient chatClient = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.build();
String question = "阿姆斯特丹和巴黎的天气怎么样?";
// ToolCallingAdvisor 被禁用------不会自动运行工具循环
ChatClientResponse response = chatClient.prompt()
.user(question)
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
Prompt prompt = new Prompt(List.of(new UserMessage(question)), chatOptions);
// 手动驱动工具调用循环
while (response.chatResponse() != null && response.chatResponse().hasToolCalls()) {
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response.chatResponse());
prompt = new Prompt(result.conversationHistory(), chatOptions);
response = chatClient.prompt()
.messages(result.conversationHistory())
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
}
System.out.println(response.chatResponse().getResult().getOutput().getText());
同样的模式也适用于流式API。由于工具调用跨越多个流式块,在检查工具调用之前,必须使用 ChatClientMessageAggregator 聚合每个流式调用。每次迭代产生的 Flux 块可以转发给订阅者(例如SSE端点),同时进行聚合,让您完全控制每个步骤发出的内容:
java
ChatClient chatClient = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.build();
String question = "阿姆斯特丹和巴黎的天气怎么样?";
Prompt prompt = new Prompt(List.of(new UserMessage(question)), chatOptions);
AtomicReference<ChatClientResponse> ref = new AtomicReference<>();
new ChatClientMessageAggregator().aggregateChatClientResponse(
chatClient.prompt()
.messages(prompt.getInstructions())
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.stream()
.chatClientResponse(),
ref::set
).blockLast();
ChatClientResponse response = ref.get();
while (response != null && response.chatResponse() != null && response.chatResponse().hasToolCalls()) {
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response.chatResponse());
prompt = new Prompt(result.conversationHistory(), chatOptions);
ref.set(null);
new ChatClientMessageAggregator().aggregateChatClientResponse(
chatClient.prompt()
.messages(prompt.getInstructions())
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.stream()
.chatClientResponse(),
ref::set
).blockLast();
response = ref.get();
}
System.out.println(response.chatResponse().getResult().getOutput().getText());
使用 ChatModel
要使用 ChatModel 驱动工具调用循环,直接调用它,链中没有 ToolCallingAdvisor,并使用 ToolCallingManager 执行工具调用。
当选择用户控制的工具执行方法时,我们建议使用 ToolCallingManager 来管理工具调用操作。这样,您可以受益于 Spring AI 为工具执行提供的内置支持。但是,没有什么可以阻止您实现自己的工具执行逻辑。
以下示例演示了用户控制工具执行方法的最小实现:
java
ChatModel chatModel = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new CustomerTools()))
.build();
Prompt prompt = new Prompt("告诉我更多关于ID为42的客户的信息", chatOptions);
ChatResponse chatResponse = chatModel.call(prompt);
while (chatResponse.hasToolCalls()) {
ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse);
prompt = new Prompt(toolExecutionResult.conversationHistory(), chatOptions);
chatResponse = chatModel.call(prompt);
}
System.out.println(chatResponse.getResult().getOutput().getText());
同样的模式适用于流式API。由于工具调用跨越多个流式块,在检查工具调用之前,必须首先使用 MessageAggregator 聚合每个流式调用:
java
ChatModel chatModel = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new CustomerTools()))
.build();
Prompt prompt = new Prompt("告诉我更多关于ID为42的客户的信息", chatOptions);
AtomicReference<ChatResponse> aggregatedResponseRef = new AtomicReference<>();
new MessageAggregator()
.aggregate(chatModel.stream(prompt), aggregatedResponseRef::set)
.collectList().block();
while (aggregatedResponseRef.get().hasToolCalls()) {
ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, aggregatedResponseRef.get());
prompt = new Prompt(toolExecutionResult.conversationHistory(), chatOptions);
aggregatedResponseRef.set(null);
new MessageAggregator()
.aggregate(chatModel.stream(prompt), aggregatedResponseRef::set)
.collectList().block();
}
System.out.println(aggregatedResponseRef.get().getResult().getOutput().getText());
接下来的示例展示了用户控制的工具执行方法与 ChatMemory API 结合使用的最小实现:
java
ToolCallingManager toolCallingManager = DefaultToolCallingManager.builder().build();
ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
String conversationId = UUID.randomUUID().toString();
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new MathTools()))
.build();
Prompt prompt = new Prompt(
List.of(new SystemMessage("你是一个乐于助人的助手。"), new UserMessage("6 * 8 等于多少?")),
chatOptions);
chatMemory.add(conversationId, prompt.getInstructions());
Prompt promptWithMemory = new Prompt(chatMemory.get(conversationId), chatOptions);
ChatResponse chatResponse = chatModel.call(promptWithMemory);
chatMemory.add(conversationId, chatResponse.getResult().getOutput());
while (chatResponse.hasToolCalls()) {
ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(promptWithMemory,
chatResponse);
chatMemory.add(conversationId, toolExecutionResult.conversationHistory()
.get(toolExecutionResult.conversationHistory().size() - 1));
promptWithMemory = new Prompt(chatMemory.get(conversationId), chatOptions);
chatResponse = chatModel.call(promptWithMemory);
chatMemory.add(conversationId, chatResponse.getResult().getOutput());
}
UserMessage newUserMessage = new UserMessage("我刚才问了你什么?");
chatMemory.add(conversationId, newUserMessage);
ChatResponse newResponse = chatModel.call(new Prompt(chatMemory.get(conversationId)));
异常处理
当工具调用失败时,异常会作为 ToolExecutionException 传播,可以捕获该异常来处理错误。ToolExecutionExceptionProcessor 可用于处理 ToolExecutionException,有两种结果:要么生成错误消息发送回AI模型,要么抛出异常由调用者处理。
java
@FunctionalInterface
public interface ToolExecutionExceptionProcessor {
/**
* 将工具抛出的异常转换为可发送回AI模型的String,
* 或抛出异常由调用者处理。
*/
String process(ToolExecutionException exception);
}
如果您使用的是任何 Spring AI Spring Boot Starter,DefaultToolExecutionExceptionProcessor 是 ToolExecutionExceptionProcessor 接口的自动配置实现。默认情况下,RuntimeException 的错误消息会发送回模型,而受检异常和 Error(例如 IOException、OutOfMemoryError)总是被抛出。DefaultToolExecutionExceptionProcessor 构造函数允许您将 alwaysThrow 属性设置为 true 或 false。如果为 true,则会抛出异常,而不是将错误消息发送回模型。
您可以使用 spring.ai.tools.throw-exception-on-error 属性来控制 DefaultToolExecutionExceptionProcessor bean 的行为:
| 属性 | 描述 | 默认值 |
|---|---|---|
spring.ai.tools.throw-exception-on-error |
如果为 true,工具调用错误将作为异常抛出,由调用者处理。如果为 false,错误将转换为消息并发送回AI模型,允许其处理并响应错误。 | false |
java
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
return new DefaultToolExecutionExceptionProcessor(true);
}
如果您定义了自己的 ToolCallback 实现,请确保在 call() 方法的工具执行逻辑中发生错误时抛出 ToolExecutionException。
ToolExecutionExceptionProcessor 由默认的 ToolCallingManager(DefaultToolCallingManager)在内部使用,以处理工具执行期间的异常。有关工具执行生命周期的更多详细信息,请参见工具执行。
工具解析
向模型传递工具的主要方法是在调用 ChatClient 或 ChatModel 时提供 ToolCallback,使用将方法作为工具和将函数作为工具中描述的策略之一。
然而,Spring AI 也支持使用 ToolCallbackResolver 接口在运行时动态解析工具。
java
public interface ToolCallbackResolver {
/**
* 为给定的工具名称解析 {@link ToolCallback}。
*/
@Nullable
ToolCallback resolve(String toolName);
}
当使用此方法时,ToolCallbackResolver 实现负责将工具名称解析为相应的 ToolCallback 实例。
默认情况下,Spring AI 依赖于 StaticToolCallbackResolver,它从 ToolCallback 实例的静态列表中解析工具。当使用 Spring Boot 自动配置时,此解析器会自动配置应用程序上下文中定义的所有 ToolCallback 类型的 bean。
如果您依赖于 Spring Boot 自动配置,您可以通过提供自定义的 ToolCallbackResolver bean 来定制解析逻辑。
java
@Bean
ToolCallbackResolver toolCallbackResolver(List<ToolCallback> toolCallbacks) {
return new StaticToolCallbackResolver(toolCallbacks);
}
ToolCallbackResolver 由 ToolCallingManager 在内部使用,以在运行时动态解析工具,支持顾问控制的工具执行和用户控制的工具执行。
工具参数增强
Spring AI 提供了一个用于动态增强工具输入模式的实用程序,可添加额外参数。这允许在不修改底层工具实现的情况下,从模型捕获额外信息,例如推理或元数据。
常见用例包括:
- 内部思考/推理:在执行工具之前捕获模型的分步推理
- 记忆增强:提取见解以存储在长期记忆中
- 分析与跟踪:收集元数据、用户意图或使用模式
- 多代理协调:传递代理标识符或协调信号
快速开始
将增强参数定义为 Java Record:
java
public record AgentThinking(
@ToolParam(description = "您调用此工具的推理", required = true)
String innerThought,
@ToolParam(description = "置信度级别(低、中、高)", required = false)
String confidence
) {}
使用 AugmentedToolCallbackProvider 包装您的工具:
java
AugmentedToolCallbackProvider<AgentThinking> provider = AugmentedToolCallbackProvider
.<AgentThinking>builder()
.toolObject(new MyTools()) // 您的 @Tool 注解类
.argumentType(AgentThinking.class)
.argumentConsumer(event -> {
AgentThinking thinking = event.arguments();
log.info("工具: {} | 推理: {}", event.toolDefinition().name(), thinking.innerThought());
})
.removeExtraArgumentsAfterProcessing(true)
.build();
与 ChatClient 一起使用:
java
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(provider)
.build();
LLM 会看到包含您额外字段的增强模式。您的消费者会收到 AgentThinking 记录,而原始工具只会收到其预期的参数。
核心组件
AugmentedToolCallbackProvider<T>- 包装工具对象或提供者,使用指定的 Record 类型增强所有工具AugmentedToolCallback<T>- 包装单个ToolCallback实例AugmentedArgumentEvent<T>- 包含toolDefinition()、rawInput()和arguments(),供消费者使用ToolInputSchemaAugmenter- 用于模式操作的低级实用程序
配置
removeExtraArgumentsAfterProcessing 选项控制在调用原始工具时是否传递增强参数:
true(默认)- 在调用工具之前移除增强参数false- 在输入中保留增强参数(如果工具可以忽略额外字段)
可观测性
工具调用包含可观测性支持,带有 spring.ai.tool 观测指标,用于测量完成时间并传播追踪信息。请参见工具调用可观测性。
可选地,Spring AI 可以将工具调用参数和结果导出为 span 属性,出于敏感性原因默认禁用。详细信息:工具调用参数和结果数据。
日志记录
工具调用功能的所有主要操作都在 DEBUG 级别记录。您可以通过将 org.springframework.ai 包的日志级别设置为 DEBUG 来启用日志记录。
工具搜索工具
随着AI代理连接到更多服务(Slack、GitHub、Jira、MCP服务器),工具库迅速增长。一个典型的多服务器设置很容易拥有50多个工具,在任何对话开始之前就消耗55,000多个令牌。当模型面对30多个名称相似的工具时,工具选择的准确性也会下降。
工具搜索工具模式通过启用按需工具发现解决了这个问题:
- 模型最初只接收一个搜索工具------最小的令牌使用量。
- 当需要功能时,模型使用自然语言查询调用搜索工具。
- 匹配的工具定义动态扩展到上下文中。
- 然后模型可以正常调用发现的工具。
这在保持对大型工具目录访问的同时实现了显著的令牌节省。
工作原理
ToolSearchToolCallingAdvisor 扩展了 Spring AI 的 ToolCallingAdvisor 来实现动态工具发现。
运行时的流程是:
- 索引 --- 在对话开始时,所有注册的工具都在
ToolIndex中建立索引,但不会发送给LLM。 - 初始请求 --- 只有内置的
toolSearchTool定义发送给LLM。 - 发现调用 --- 当LLM需要某个功能时,它会使用搜索查询调用
toolSearchTool。 - 搜索与扩展 ---
ToolIndex找到匹配的工具;它们的定义会添加到下一个请求中。 - 工具调用 --- LLM看到
toolSearchTool和发现的工具定义,并可以调用实际的工具。 - 工具执行 --- 发现的工具被执行,其结果返回给LLM。
- 响应 --- LLM使用工具结果生成最终答案。
安装
使用 Spring Boot Starter 实现最简单的设置(包括 Lucene 和自动配置):
Maven:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-tool-search-advisor</artifactId>
</dependency>
或者直接使用库进行手动配置:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tool-search-advisor</artifactId>
</dependency>
快速开始
java
// 1. 配置 ToolIndex(例如,用于语义搜索的 VectorToolIndex)
@Bean
ToolIndex toolIndex(VectorStore vectorStore) {
return new VectorToolIndex(vectorStore);
}
// 2. 创建顾问
var smartToolRetrieverAdvisor = ToolSearchToolCallingAdvisor.builder()
.toolIndex(toolIndex)
.maxResults(5)
.build();
// 3. 与 ChatClient 一起使用 --- 工具已注册但最初不会发送给LLM
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new MyTools())
.defaultAdvisors(smartToolRetrieverAdvisor)
.build();
// 4. 发起请求 --- 工具按需发现
String answer = chatClient.prompt("阿姆斯特丹的天气怎么样?")
.call()
.content();
搜索策略
ToolIndex 接口抽象了搜索实现。提供了三种开箱即用的策略:
| 策略 | 实现 | 最佳用途 |
|---|---|---|
| 语义 | VectorToolIndex |
自然语言查询、模糊匹配 |
| 关键词 | LuceneToolIndex |
精确术语匹配、已知工具名称 |
| 正则表达式 | RegexToolIndex |
工具名称模式(get_*_data) |
VectorToolIndex(语义)
java
@Bean
ToolIndex vectorToolIndex(VectorStore vectorStore) {
return new VectorToolIndex(vectorStore);
}
使用基于嵌入的相似性搜索。最适合用户描述其需求的自然语言查询。
LuceneToolIndex(关键词)
java
@Bean
ToolIndex luceneToolIndex() {
return new LuceneToolIndex(); // 默认阈值为 0.25
// return new LuceneToolIndex(0.4f); // 自定义最低分数阈值
}
使用 Apache Lucene 进行基于关键词的搜索。快速且有效,适用于精确术语匹配。
RegexToolIndex(模式)
java
@Bean
ToolIndex regexToolIndex() {
return new RegexToolIndex();
}
使用正则表达式模式匹配。当工具名称遵循已知的命名约定(例如 get_* 或 database)时非常有用。
配置
ToolSearchToolCallingAdvisor.Builder 提供以下选项:
| 选项 | 描述 | 默认值 |
|---|---|---|
toolIndex(ToolIndex) |
使用的搜索实现。 | 必需 |
maxResults(Integer) |
每次搜索返回的最大工具引用数。为 null 时,由LLM决定(由工具描述提示引导,建议为5)。 |
null |
systemMessageSuffix(String) |
附加到系统消息的自定义提示后缀,用于指导模型如何使用 toolSearchTool。 |
内置模板 |
referenceToolNameAccumulation(boolean) |
当为 true 时,所有先前搜索调用中发现工具名称会被累积并注入。当为 false 时,仅使用最近的搜索结果。 |
true |
sessionIdKeyName(String) |
用于查找对话/会话ID的上下文键。当您的应用程序在自定义键下存储会话ID时,更改此值。 | "chat_memory_conversation_id"(ChatMemory.CONVERSATION_ID) |
evictionStrategy(ToolIndexEvictionStrategy) |
确定何时释放会话工具索引。参见下面的索引驱逐。 | LruEvictionStrategy(1000) |
advisorOrder(int) |
此顾问在顾问链中的位置。 | HIGHEST_PRECEDENCE + 300 |
ToolIndex API
ToolIndex接口及其配套类型(ToolSearchRequest、ToolSearchResponse、ToolReference)位于spring-ai-tool-search-tool模块的org.springframework.ai.tool.toolsearch包下。该模块还提供了内置的索引实现(LuceneToolIndex、VectorToolIndex、RegexToolIndex)。
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tool-search-tool</artifactId>
</dependency>
java
public interface ToolIndex {
void indexTool(String sessionId, ToolReference toolReference);
void indexTools(String sessionId, List<ToolReference> toolReferences); // 默认:循环调用 indexTool
ToolSearchResponse search(ToolSearchRequest request);
void clearIndex(String sessionId);
}
每个调用都通过sessionId进行作用域隔离,因此工具索引在并发会话之间是相互隔离的。
索引驱逐(Eviction)
默认情况下(LruEvictionStrategy(1000)),最多保留 1,000 个活跃会话,当超出上限时,最近最少使用的会话将被驱逐。调用 advisor.evictSession(sessionId) 可以主动释放某个会话(例如在登出时)。提供了五种内置策略:
| 策略 | 行为 |
|---|---|
LruEvictionStrategy(maxSessions)(默认) |
当活跃会话数超过 maxSessions 时,驱逐最近最少使用的会话。 |
NeverEvictStrategy.INSTANCE |
永不自动驱逐;索引会一直保留,直到显式调用 evictSession()。 |
AlwaysEvictStrategy.INSTANCE |
在每次请求前清除会话的索引,强制每轮完全重新索引。适用于测试场景,或每次请求工具集都会变化的情况。 |
TtlEvictionStrategy(duration) |
驱逐最后访问时间超过指定 TTL 的会话。在每次请求时惰性评估。 |
CompositeEvictionStrategy(strategies...) |
委托给多个策略,只要任何一个委托策略要求驱逐,就驱逐该会话。 |
java
// 默认:LRU 上限 1000 个会话(无需配置)
var advisor = ToolSearchToolCallingAdvisor.builder()
.toolIndex(toolIndex)
.build();
// 永不驱逐 ------ 自行管理会话生命周期
ToolIndexEvictionStrategy eviction = NeverEvictStrategy.INSTANCE;
// 总是驱逐 ------ 每次请求重新索引(适用于测试)
ToolIndexEvictionStrategy eviction = AlwaysEvictStrategy.INSTANCE;
// 当活跃会话超过 200 个时,驱逐最近最少使用的会话
ToolIndexEvictionStrategy eviction = new LruEvictionStrategy(200);
// 驱逐闲置超过 30 分钟的会话
ToolIndexEvictionStrategy eviction = new TtlEvictionStrategy(Duration.ofMinutes(30));
// 组合:TTL + LRU 上限
ToolIndexEvictionStrategy eviction = new CompositeEvictionStrategy(
new TtlEvictionStrategy(Duration.ofMinutes(30)),
new LruEvictionStrategy(200));
var advisor = ToolSearchToolCallingAdvisor.builder()
.toolIndex(toolIndex)
.evictionStrategy(eviction)
.build();
驱逐是在每次请求时惰性评估的 ------ 不需要后台线程。
适用场景
适合使用:
- 系统中有 10 个或更多工具。
- 工具定义消耗超过 10K token。
- 构建包含多个服务器的 MCP 驱动系统。
- 在大型工具集上遇到工具选择准确性问题。
传统方式可能更好:
- 工具库较小(少于 10 个工具)。
- 所有工具在每个会话中都被频繁使用。
- 工具定义非常精简。
Spring Boot 自动配置
spring-ai-starter-tool-search-advisor 启动器提供了零样板设置。通过一个属性即可启用:
properties
spring.ai.chat.client.tool-search-advisor.enabled=true
启用后,自动配置会注册一个 ToolSearchToolCallingAdvisor.Builder Bean,透明地替换默认的 ToolCallingAdvisor ------ 无需更改代码。同时,除非应用显式声明了 ToolIndex Bean,否则它也会自动注册一个。
ToolIndex 自动选择
设置 spring.ai.chat.client.tool-search-advisor.tool-index-type 来选择具体实现:
| 值 | 实现 | 要求 |
|---|---|---|
regex(默认) |
RegexToolIndex | 无需额外依赖 |
lucene |
LuceneToolIndex | 类路径中包含 org.apache.lucene:lucene-core(启动器已包含) |
vector |
VectorToolIndex | 应用上下文中存在 VectorStore Bean |
应用声明的自定义 ToolIndex Bean 始终优先。
配置属性参考
| 属性 | 描述 | 默认值 |
|---|---|---|
spring.ai.chat.client.tool-search-advisor.enabled |
启用该顾问。为 true 时替换默认的 ToolCallingAdvisor。 |
false |
spring.ai.chat.client.tool-search-advisor.tool-index-type |
要使用的 ToolIndex 实现:regex、lucene 或 vector。 |
regex |
spring.ai.chat.client.tool-search-advisor.max-results |
每次搜索调用返回的最大工具引用数。null 使用内置默认值。 |
null |
spring.ai.chat.client.tool-search-advisor.system-message-suffix |
附加到系统消息末尾的自定义提示后缀。null 使用内置模板。 |
null |
spring.ai.chat.client.tool-search-advisor.reference-tool-name-accumulation |
在一次对话中,累积所有搜索调用中发现的工具名称。 | true |
spring.ai.chat.client.tool-search-advisor.session-id-key-name |
顾问上下文中携带会话 ID 的键名。 | "chat_memory_conversation_id" |
spring.ai.chat.client.tool-search-advisor.advisor-order |
该顾问在顾问链中的位置。 | HIGHEST_PRECEDENCE + 300 |
spring.ai.chat.client.tool-search-advisor.eviction.lru-max-sessions |
LRU 驱逐策略保留的最大活跃会话数。 | 1000 |
spring.ai.chat.client.tool-search-advisor.eviction.ttl |
空闲会话的存活时间。设置后,将使用 LRU+TTL 组合策略。接受 java.time.Duration 格式字符串(例如 30m、1h)。 |
null |
spring.ai.chat.client.tool-search-advisor.lucene.min-score-threshold |
Lucene 结果被包含的最低分数阈值。当 tool-index-type=lucene 时生效。 |
0.25 |
示例
Lucene + 自定义阈值:
properties
spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=lucene
spring.ai.chat.client.tool-search-advisor.lucene.min-score-threshold=0.4
spring.ai.chat.client.tool-search-advisor.eviction.ttl=30m
向量搜索示例(需要 VectorStore Bean,例如来自 spring-ai-starter-vector-store-pgvector):
properties
spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=vector