工具调用基础(2.0 API)
本教程共 45 篇 · 第 24 篇 · 更新于 2026-08-16 · 约 9 分钟阅读
本节目标:理解什么是工具调用、模型与代码如何分工。学完你能用 @Tool 注解定义自己的工具,通过 ChatClient.tools() 注册,并说清一次工具调用的完整流程。
24.1 什么是工具调用
工具调用(Tool Calling)也叫函数调用(Function Calling)。它让模型能够调用你写好的 API,弥补两个天生短板:拿不到实时信息,也动不了外部系统。
模型的知识有截止日期。你问”明天是星期几”,它答不上来。模型也没有执行能力。它规划好了订机票的步骤,但不会真的去点按钮。工具就是架在中间的桥。
工具的用途分两类:
- 信息检索:查数据库、调天气接口、搜网页。目的是给模型补充知识,常用于 RAG 场景。
- 执行操作:发邮件、建订单、设置闹钟。目的是把模型生成的计划真正落地。
这里要强调一个关键事实:模型本身不会执行工具。模型只会在响应里声明”我想调用某某工具,参数是什么”。真正执行的是你的应用代码。模型永远接触不到 API 本身,这是重要的安全边界。
24.2 工具调用的完整流程
一次工具调用走六步,以”问明天是星期几”为例:
- 应用把工具定义(名称、描述、参数 Schema)随请求发给模型。
- 模型判断需要实时信息,返回一个工具调用请求:工具名 + 参数。
- 应用根据工具名找到对应代码,用参数执行它。
- 应用拿到执行结果。
- 应用把结果作为一条消息回传给模型。
- 模型结合结果生成最终回答,返回给你。
整个过程对用户透明。用户只看到”明天是星期三”,看不到中间的两轮往返。
24.3 2.0 的新 API:ToolCallback
在 Spring AI 1.x 里,工具用 FunctionCallback 定义。2.0 起这套 API 被废弃,取而代之的是 ToolCallback 体系。术语也从”函数”改成”工具”,更贴近行业惯例。
所有工具都实现 ToolCallback 接口,它只要求四个方法:
public interface ToolCallback {
// 给模型看的定义:名称、描述、参数 Schema
ToolDefinition getToolDefinition();
// 附加设置:是否直接返回结果等
ToolMetadata getToolMetadata();
// 执行工具,入参是 JSON 字符串,返回结果字符串
String call(String toolInput);
// 带上下文执行
String call(String toolInput, ToolContext toolContext);
}
你不需要从零实现这个接口。Spring AI 提供了两个现成实现:
- MethodToolCallback:把任意方法变成工具。
- FunctionToolCallback:把 Function、Supplier 等函数式对象变成工具。
本章先讲方法,这也是最常用的方式。
24.4 用 @Tool 声明工具
给方法加一个 @Tool 注解,方法就变成了工具。看一个获取当前时间的例子:
import java.time.LocalDateTime;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;
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();
}
}
@Tool 注解支持四个属性:
- name:工具名,默认用方法名。模型靠它识别工具,同一请求里必须全局唯一。
- description:工具描述。强烈建议写详细,描述不到位,模型该调用时不调用、不该调用时乱调用。
- returnDirect:结果是否直接返回给调用方,默认 false。
- resultConverter:自定义结果转换器,默认用 Jackson 序列化成 JSON。
方法可以是静态的也可以是实例方法,可见性不限。参数支持基本类型、POJO、枚举、List、Map 等大多数类型。返回值要可序列化,因为结果要发回给模型。
NoteOptional、CompletableFuture、Mono/Flux、Function 这类类型不能作为工具方法的参数或返回值。函数式类型要改用 FunctionToolCallback,见第 25 章。
24.5 参数描述:@ToolParam 与 JSON Schema
模型怎么知道该传什么参数?靠 JSON Schema。Spring AI 会根据方法签名自动生成,不需要你手写。
但自动生成只解决类型问题,解决不了语义问题。比如 setAlarm(String time),模型不知道 time 是什么格式。这时用 @ToolParam 补充说明:
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 = "Set a user alarm for the given time")
void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("Alarm set for " + alarmTime);
}
}
@ToolParam 有两个属性:
- description:参数说明,告诉模型格式要求、取值范围。
- required:是否必填,默认 true。
所有参数默认必填。想让某个参数可选,按优先级可以用 @ToolParam(required = false)、Jackson 的 @JsonProperty(required = false)、Swagger 的 @Schema(required = false),或 Spring 的 @Nullable。
必填状态定义错了会引发幻觉。参数其实没有值却标成必填,模型就会编一个值凑数。第 25 章会专门讲这个坑。
24.6 注册工具:ChatClient.tools()
工具定义好了,怎么让模型用上?通过 ChatClient 的 tools() 方法,把工具类实例传进去:
ChatModel chatModel = ...; // 自动配置的模型 Bean
String response = ChatClient.create(chatModel)
.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.call()
.content();
System.out.println(response);
tools() 接收工具类实例后,内部会把每个 @Tool 方法生成一个 ToolCallback。想自己掌控生成过程,可以用工具类 ToolCallbacks:
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatClient.create(chatModel)
.prompt("What day is tomorrow?")
.tools(dateTimeTools)
.call()
.content();
tools() 还接受 ToolCallback、ToolCallbackProvider 实例以及它们的集合。这样注册的工具只对这一次请求生效。
想让工具对所有请求默认生效,用 Builder 的 defaultTools():
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new DateTimeTools())
.build();
Warning默认工具在同一 Builder 创建的所有 ChatClient 实例间共享。方便是方便,但工具会在不该出现的场景里可用,有权限风险。如果某次请求同时传了运行时工具,运行时工具会完全覆盖默认工具。
24.7 框架自动执行:ToolCallingAdvisor
第 5 章讲过 Advisor 链。工具调用的执行也挂在链上:ChatClient 会自动注册 ToolCallingAdvisor,由它管理整个执行循环。
流程是这样的:模型返回工具调用请求后,ToolCallingAdvisor 拦截响应,交给 ToolCallingManager 执行,再把结果以 ToolResponseMessage 的形式发回模型,循环直到模型不再要求调用工具。全程自动,你只负责提供工具和提问。
ToolCallingManager 是执行生命周期的核心接口,包含两个方法:
public interface ToolCallingManager {
List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions);
ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse);
}
Spring Boot Starter 会自动配置 DefaultToolCallingManager,通常不用管它。想全局关掉自动执行,改配置:
spring.ai.chat.client.tool-calling.enabled=false
只对某一次调用关闭,用 AdvisorParams:
chatClient.prompt("What day is tomorrow?")
.tools(new DateTimeTools())
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.content();
Note直接调用 ChatModel 只会把工具定义发给模型,响应里的工具调用不会自动执行。想要自动执行就用 ChatClient。这也是 2.0 推荐 ChatClient 的原因之一:ChatModel 内部执行工具的旧方式已在 2.0.0 废弃,3.0.0 将移除。
24.8 完整示例:信息检索 + 执行操作
把两个工具放在一个类里,一次演示两种用途。问”设置 10 分钟后的闹钟”,模型需要先取当前时间,再算出闹钟时间,最后调用 setAlarm:
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 = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
return LocalDateTime.now()
.atZone(LocaleContextHolder.getTimeZone().toZoneId())
.toString();
}
@Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("Alarm set for " + alarmTime);
}
}
调用代码不用变,还是 tools() 一个入口:
ChatModel chatModel = ...;
String response = ChatClient.create(chatModel)
.prompt("Can you set an alarm 10 minutes from now?")
.tools(new DateTimeTools())
.call()
.content();
System.out.println(response);
模型内部会连续调用两次工具:先 getCurrentDateTime,再 setAlarm。这就是多步工具协作的雏形,第 25 章会展开讲。
24.9 结果转换与 ToolContext
工具返回的对象会先转成字符串再发给模型。转换器是 ToolCallResultConverter 接口,默认实现 DefaultToolCallResultConverter 用 Jackson 序列化成 JSON。想自定义,声明式方案在 @Tool 里指定:
@Tool(description = "Retrieve customer information", resultConverter = CustomToolCallResultConverter.class)
Customer getCustomerInfo(Long id) {
return customerRepository.findById(id);
}
编程式方案在 MethodToolCallback.Builder 上设置 resultConverter()。
有时工具执行需要额外的上下文,比如多租户系统里的 tenantId。模型参数里不能带这些,可以用 ToolContext 传递:
String response = ChatClient.create(chatModel)
.prompt("Tell me more about the customer with ID 42")
.tools(new CustomerTools())
.toolContext(Map.of("tenantId", "acme"))
.call()
.content();
工具方法里声明一个 ToolContext 参数就能拿到:
@Tool(description = "Retrieve customer information")
Customer getCustomerInfo(Long id, ToolContext toolContext) {
return customerRepository.findById(id, toolContext.getContext().get("tenantId"));
}
ImportantToolContext 里的数据不会发给模型。它只在应用内部流转,适合放密钥、租户标识这类敏感信息。
24.10 小结
工具调用是模型与真实世界的接口。模型只负责决策,执行权永远在应用手里。2.0 的写法总结成三步:@Tool 注解声明方法、@ToolParam 描述参数、ChatClient.tools() 注册工具。执行循环由 ToolCallingAdvisor 自动完成。
下一章讲进阶:多个工具怎么组织、结果怎么处理、出错怎么办。