工具调用(亦称函数调用)是 AI 应用的常见模式,允许模型通过与一组 API(即工具)交互来扩展其能力。工具可以是任何东西:网页搜索、对外部 API 的调用,或特定代码的执行等。
LLM 本身不能实际调用工具,工具调用逻辑是由客户端应用程序提供的。模型只能请求工具调用并提供输入参数,而应用程序负责根据输入参数执行工具调用并返回结果。
一、概述
Spring AI 通过一组灵活的抽象机制支持工具调用功能,这些抽象允许你以统一的方式定义、解析和执行工具。
-
当我们想让模型使用某个工具时,我们会在聊天请求中包含该工具的定义。每个工具的定义都包括名称、描述和输入参数的模式。
-
当模型决定调用工具时,它会发送包含工具名称及符合预定义模式的输入参数的响应。
-
应用程序负责根据工具名称识别对应工具,并使用提供的输入参数执行该工具。
-
工具调用的结果由应用程序进行处理。
-
应用程序将工具调用结果返回至模型。
-
模型最终利用工具调用结果作为附加上下文生成响应。
Tool
是工具调用的基础构建单元,通过 ToolCallback
接口进行建模。Spring AI 内置支持从方法和函数生成 ToolCallback
,同时你始终可以自定义 ToolCallback
实现以满足更多使用场景。
工具主要应用于以下场景:
-
信息检索。此类工具可用于从外部源检索信息,例如数据库、网络服务、文件系统或网络搜索引擎。其目的是增强模型的知识,使其能够回答原本无法回答的问题。因此,它们可用于检索增强生成(RAG)场景。例如,可以使用工具检索给定位置的当前天气、检索最新新闻文章或查询数据库中的特定记录。
-
执行操作。此类工具可用于在软件系统中执行操作,例如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流。其目的是自动化那些原本需要人工干预或显式编程的任务。例如,可以使用工具为与聊天机器人交互的客户预订航班、填写网页上的表单,或在代码生成场景中基于自动化测试(TDD)实现 Java 类。
Spring AI 支持两种工具调用的定义:方法工具
和 函数工具,
ChatClient
和 ChatModel
均可接收 ToolCallback
对象列表,既向模型提供可用工具。
一、方法工具
Spring AI 为方法转工具(即 ToolCallback
)提供两种内置支持方式:
-
声明式:通过
@Tool
注解实现。 -
编程式:通过底层的
MethodToolCallback
实现。
这里我们只看最常用的声明式:
1.声明式配置:@Tool
注解方案
1.1 @Tool
你只需为方法添加 @Tool
注解,即可将其转换为工具。
@Tool
注解允许你配置以下关键工具信息:
-
name
:工具名称。若不指定,默认使用方法名称。AI 模型通过此名称识别调用工具,因此不允许在同一类中存在同名工具。模型处理单个聊天请求时,所有可用工具的名称必须保持全局唯一。 -
description
:工具描述,用于指导模型判断何时及如何调用该工具。若未指定,默认使用方法名称作为工具描述。但强烈建议提供详细描述,因为这对模型理解工具用途和使用方式至关重要。若描述不充分,可能导致模型在该调用工具时未调用,或错误调用工具。 -
returnDirect
:控制工具结果直接返回客户端(true
)还是传回模型(false
),默认false。 -
resultConverter
:工具调用结果的转化器,默认使用 DefaultToolCallResultConverter,将结果转换为字符串。
class DateTimeTools {
@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
1.2 @ToolParam
Spring AI 将自动为 @Tool
注解方法的输入参数生成 JSON Schema。该 Schema 供模型理解如何调用工具及准备工具请求。你可使用 @ToolParam
注解为输入参数提供额外信息(如描述、是否必需等),默认情况下所有输入参数均为必需参数。
你可以为方法定义任意数量的参数(包括无参数),支持大多数类型(基本类型、POJO、枚举、List、数组、Map 等)。同样,方法可以返回大多数类型,包括 void
。若方法有返回值,则返回类型必须是可序列化类型,因为结果将被序列化并发送回模型。
@ToolParam
注解允许你配置工具参数的关键信息:
-
description
:参数描述,用于帮助模型更准确地理解如何使用该参数。例如:参数格式要求、允许取值范围等。 -
required
:指定参数是否为必需项(默认值:true
,即所有参数默认必需)。
@Component
public class TimeService {
@Tool(description = "Get time by zone id")
public String getTimeByZoneId(@ToolParam(description = "Time zone id, such as Asia/Shanghai")
String zoneId) {
ZoneId zid = ZoneId.of(zoneId);
ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
return zonedDateTime.format(formatter);
}
}
2.示例:获取当前时间
说明:AI 模型无法获取实时信息。任何需要知晓当前日期或天气预报等实时信息的问题,模型都无法直接回答。但我们可以提供能够检索这类信息的工具,当需要访问实时数据时,让模型调用这些工具。
定义并注册工具类:
@Component
public class TimeService {
@Tool(description = "Get time by zone id")
public String getTimeByZoneId(@ToolParam(description = "Time zone id, such as Asia/Shanghai")
String zoneId) {
ZoneId zid = ZoneId.of(zoneId);
ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
return zonedDateTime.format(formatter);
}
}
@Configuration
public class ToolConfig {
@Bean
@Description("时间查询工具")
public TimeService timeService() {
return new TimeService();
}
}
调用工具:
使用ChatClient
或 ChatModel 均可接收,可在配置时通过
defaultTools 或 调用时通过 tools方法传入工具类实例。
@Configuration
public class ChatClientConfig {
@Autowired
private TimeService timeService;
@Bean
public ChatClient ollamaChatClient(@Qualifier("ollamaChatModel") OllamaChatModel ollamaChatModel) {
SimpleLoggerAdvisor simpleLoggerAdvisor = new SimpleLoggerAdvisor(
request -> "请求内容: " + request.prompt().getUserMessage().getText(),
response -> "响应内容: " + response.getResult().getOutput().getText(),
0
);
return ChatClient.builder(ollamaChatModel).
defaultSystem("你是一个乐于助人的智能助手,请以{tone}的语气回答用户的问题。").
defaultTools(timeService).
defaultAdvisors(
simpleLoggerAdvisor
)
.build();
}
}
controller:
@Autowired
@Qualifier("ollamaChatClient")
private ChatClient ollamaChatClient;
@GetMapping("/chat1")
public String chat(@RequestParam(value = "question") String question) {
log.info("question: {}", question);
String content = ollamaChatClient.prompt(question)
.system(s -> s.param("tone", "邓超"))
.call()
.content();
return content;
}
调用接口:
单LLM是无法获取时间或天气等信息的。
二、函数工具
Spring AI 内置支持通过函数定义工具,既可通过底层的 FunctionToolCallback
实现以编程方式配置,也能作为运行时解析的 @Bean
动态注册。
1.编程式规范:FunctionToolCallback
1.1 Function
你可以通过编程方式构建 FunctionToolCallback
,将函数式类型(Function
、Supplier
、Consumer
或 BiFunction
)转换为工具。
public class TimeFunction implements
Function<TimeFunction.Request, TimeFunction.Response> {
@JsonClassDescription("Request to get time by zone id")
public record Request(@JsonProperty(required = true, value = "zoneId")
@JsonPropertyDescription("Time zone id, such as Asia/Shanghai") String zoneId) {
}
@JsonClassDescription("Response to get time by zone id")
public record Response(@JsonPropertyDescription("time") String time) {
}
@Override
public Response apply(Request request) {
ZoneId zid = ZoneId.of(request.zoneId());
ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
return new Response(zonedDateTime.format(formatter));
}
}
1.2 FunctionToolCallback.Builder
FunctionToolCallback.Builder
允许你构建 FunctionToolCallback
实例并提供以下关键工具信息:
-
name
:工具名称。AI 模型通过此名称识别调用工具,因此同一上下文中不允许存在同名工具。对于特定聊天请求,模型可用的所有工具名称必须保持全局唯一。(必需项) -
toolFunction
:表示工具方法的函数式对象(Function
、Supplier
、Consumer
或BiFunction
)。(必需项)
-
description
:工具描述,用于帮助模型判断何时及如何调用该工具。若未提供,将使用方法名称作为工具描述。但强烈建议提供详细描述,这对模型理解工具用途及使用方法至关重要。若描述不充分,可能导致模型在该调用工具时未调用,或错误调用工具。 -
inputType
:函数输入类型。(必需项) -
inputSchema
:工具输入参数的 JSON Schema。若未提供,将基于inputType
自动生成 Schema。你可使用@ToolParam
注解提供输入参数的额外信息(如描述、是否必需等),默认情况下所有输入参数均为必需参数。 -
toolMetadata
:定义额外设置的ToolMetadata
实例(如是否将结果直接返回客户端、使用的结果转换器等),可通过ToolMetadata.Builder
类构建。 -
toolCallResultConverter
:用于将工具调用结果转换为String
对象并返回 AI 模型的ToolCallResultConverter
实例(未配置时默认使用DefaultToolCallResultConverter
)。
ToolCallback toolCallback = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
函数输入和输出可以是 Void
或 POJO。输入和输出的 POJO 必须是可序列化的,因为结果将被序列化并发送回模型。函数及输入输出类型必须是 public
的。
1.3 示例:获取当前时间
在调用 ChatClient 时通过 .toolCallBacks()
或者在实例化 ChatClient 对象的时候通过 .defalutToolCallBacks()
传递 FunctionToolCallBack
对象:
实现Function接口:
package com.hl.springdemo.function;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Function;
public class TimeFunction implements
Function<TimeFunction.Request, TimeFunction.Response> {
@JsonClassDescription("Request to get time by zone id")
public record Request(@JsonProperty(required = true, value = "zoneId",defaultValue = "Asia/Shanghai")
@JsonPropertyDescription("Time zone id, such as Asia/Shanghai") String zoneId) {
}
@JsonClassDescription("Response to get time by zone id")
public record Response(@JsonPropertyDescription("time") String time) {
}
@Override
public Response apply(Request request) {
ZoneId zid = ZoneId.of(request.zoneId());
ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
return new Response(zonedDateTime.format(formatter));
}
}
实例化 ChatClient 对象的时候通过 .defalutToolCallBacks()
传递 FunctionToolCallBack
对象:
@Bean
public ChatClient ollamaChatClient(@Qualifier("ollamaChatModel") OllamaChatModel ollamaChatModel) {
// defaultToolCallbacks
FunctionToolCallback<TimeFunction.Request, TimeFunction.Response> timeFunction = FunctionToolCallback
.builder("timeFunction", new TimeFunction())
.description("Get time by zone id")
.inputType(TimeFunction.Request.class)
.build();
SimpleLoggerAdvisor simpleLoggerAdvisor = new SimpleLoggerAdvisor(
request -> "请求内容: " + request.prompt().getUserMessage().getText(),
response -> "响应内容: " + response.getResult().getOutput().getText(),
0
);
return ChatClient.builder(ollamaChatModel).
defaultSystem("你是一个乐于助人的智能助手,请以{tone}的语气回答用户的问题。").
defaultToolCallbacks(timeFunction).
defaultAdvisors(
simpleLoggerAdvisor
)
.build();
}
调用接口:
2.
动态规范:@Bean
开发者可以把任意实现 Function
接口的对象,定义为 Bean
,并通过 .toolNames()
或 .defaultToolNames()
传递给 ChatClient 对象。
@Configuration
public class TestAutoConfiguration {
@Bean
@Description("Get time by zone id")
public TimeFunction getTimeByZoneId() {
return new TimeFunction();
}
}
在调用 ChatClient 时,通过.toolNames()
传递函数工具的 Bean 名称,或者在实例化 ChatClient 对象的时候通过 .defalutToolNames()
方法传递函数工具:
@Bean
public ChatClient ollamaChatClient(@Qualifier("ollamaChatModel") OllamaChatModel ollamaChatModel) {
SimpleLoggerAdvisor simpleLoggerAdvisor = new SimpleLoggerAdvisor(
request -> "请求内容: " + request.prompt().getUserMessage().getText(),
response -> "响应内容: " + response.getResult().getOutput().getText(),
0
);
return ChatClient.builder(ollamaChatModel).
defaultSystem("你是一个乐于助人的智能助手,请以{tone}的语气回答用户的问题。").
defaultToolNames("getTimeByZoneId").
defaultAdvisors(
simpleLoggerAdvisor
)
.build();
}
三、返回值转换
Spring AI 框架中,工具调用的结果会通过 ToolCallResultConverter
进行处理,然后回传给 AI 模型。ToolCallResultConverter
接口提供了将工具调用结果转换为字符串对象的方法。Spring AI 默认使用 DefaultToolCallResultConverter
,将返回结果对象使用 Jackson 库转化为 JSON 字符串。ToolCallResultConverter
接口的定义为:
@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);
}
定义方法工具时,可以通过 @Tool
注解的 resultConverter
参数提供 ToolCallResultConverter
的实现类;定义方法工具和函数工具时可以通过 MethodToolCallBack.Builder
和 FunctionToolCallBack.Builder
的 resultConverter()
方法设置ToolCallResultConverter
的实现类。
四、工具上下文
Spring AI 支持通过 ToolContext API
向工具传递额外的上下文信息。该特性允许提供补充数据,比如用户身份信息。这些数据将与 AI 模型传递的工具参数结合使用。
1.ToolContext(工具上下文)
用于在函数调用(工具调用)场景下,封装和传递工具执行所需的上下文信息。它保证上下文数据不可变,便于多线程安全地传递和使用
Map<String, Object> context
:于存储和获取工具调用的消息历史
示例:
public class UserInfoTools {
@Tool(description = "get current user name")
public String getUserName(ToolContext context) {
String userId = context.getContext().get("userId").toString();
if (!StringUtils.hasText(userId)) {
return "null";
}
// 模拟数据
return userId + "user";
}
}
在调用 ChatClient 时,通过 .toolContext()
方法传递工具上下文:
String response = chatClient.prompt("获取我的用户名")
.tools(new UserInfoTools())
.toolContext(Map.of("userId", "12345"))
.call()
.content();
五、工具调用直接返回
默认情况下,工具调用的返回值会再次回传到 AI 模型进一步处理。但在一些场景中需要将结果直接返回给调用方而非模型,比如数据搜索。
每个 ToolCallback
实现均可定义工具调用结果应直接返回调用方还是传回模型。默认结果为传回模型,但你可按工具单独修改此行为。ToolCallingManager
负责管理工具执行生命周期,其会处理工具的 returnDirect
属性。若该属性设为 true
,工具调用结果将直接返回调用方;否则结果将传回模型。
-
当需要向模型提供工具时,我们将其定义包含在聊天请求中。若希望工具执行结果直接返回调用方,则将
returnDirect
属性设为true
。 -
当模型决定调用工具时,它会发送一个响应,其中包含工具名称和根据定义的模式建模的输入参数。
-
应用程序负责使用工具名称来识别并使用提供的输入参数执行工具。
-
工具调用的结果由应用程序处理。
-
应用程序将工具调用结果直接返回调用方,而非传回模型。
1.方法直接返回
使用声明式方法从方法构建工具时,你可以通过将 @Tool
注解的 returnDirect
属性设为 true
,将工具标记为直接向调用方返回结果。
class CustomerTools {
@Tool(description = "Retrieve customer information", returnDirect = true)
Customer getCustomerInfo(Long id) {
return customerRepository.findById(id);
}
}
2.函数直接返回
使用编程式方法从函数构建工具时,你可通过 ToolMetadata
接口(描述工具的元数据信息,目前仅用来控制是否直接将工具结果返回给 AI 模型)设置 returnDirect
属性,并将其传递给 FunctionToolCallback.Builder
。
调用这段代码将直接返回 TimeFunction 返回的JSON对象,而不再经过大模型加工处理。
String response = chatClient.prompt("获取北京时间")
.toolCallbacks(FunctionToolCallback
.builder("getTimeByZoneId", new TimeFunction())
.toolMetadata(ToolMetadata.builder()
.returnDirect(true)
.build())
.description("Get time by zone id")
.inputType(TimeFunction.Request.class)
.build())
.call()
.content();