主题
工具调用
工具调用——即 AI 模型调用应用自定义函数并对其结果采取行动的能力——是智能体(agentic)AI 系统的基本构件。一个只会生成文本的模型是聊天机器人;一个能发现信息、采取行动并循环直至达成目标的模型则是智能体。
工具用于两大目的:
- 信息检索 —— 用来自数据库、网络服务、文件系统或搜索引擎的数据增强模型的知识。示例:获取当前天气、查询客户记录、查最新新闻。
- 采取行动 —— 在你的系统中执行操作。示例:发送邮件、预订航班、更新记录、触发工作流。
尽管我们把“工具调用”说成是一种模型能力,但拥有工具调用逻辑的是应用。模型可以请求一次工具调用并提供输入参数;应用负责执行工具并返回结果。模型永远不会获得对你工具背后 API 的直接访问——这是一个关键的安全考量。
架构概览
Spring AI 2.0 把工具调用循环变成 ChatClient 顾问链中一个一等、可组合的组件。整体流程一览:
- 你定义工具(定义工具)并把它们传给
ChatClient。 ChatClient自动注册一个驱动该循环的ToolCallingAdvisor(工具调用循环)。- 模型决定调用哪些工具;
ToolCallingManager执行它们;循环持续进行,直到模型产出一个不含工具调用的响应。 - 链中的其他顾问——记忆、可观测性、重试、自定义逻辑——通过一个统一的旋钮与循环组合:顾问顺序(记忆与工具循环)。
NOTE
这一架构取代了 Spring AI 1.x 中每个 ChatModel 各自的工具执行循环。直接调用 ChatModel 对底层用例仍受支持——参见 ChatModel 工具调用。
TIP
查看聊天模型对比以了解哪些模型支持工具调用。
快速开始
用 @Tool 注解一个方法来定义一个工具:
java
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(description = "Set a user alarm for the given time, provided in ISO-8601 format")
void setAlarm(String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("Alarm set for " + alarmTime);
}
}通过 .tools() 把工具传给 ChatClient,并问一个需要它们的问题:
java
ChatModel chatModel = ...
String response = ChatClient.create(chatModel)
.prompt("Can you set an alarm 10 minutes from now?")
.tools(new DateTimeTools())
.call()
.content();模型决定调用 getCurrentDateTime(),然后用计算出的时间调用 setAlarm(...)。Spring AI 自动处理完整的往返。
定义工具
Spring AI 提供三种定义工具的方式,从最高层到最显式:
| 风格 | 何时使用 | 章节 |
|---|---|---|
声明式 @Tool | 你拥有该方法并希望最少仪式感 | 声明式 |
MethodToolCallback | 你需要对由方法支撑的工具做程序化控制 | 程序化:方法 |
FunctionToolCallback | 你想暴露一个 Function / Supplier / Consumer lambda 或方法引用 | 程序化:函数 |
三者都产出 ToolCallback 实例,流经同一个循环。你可以自由混用各种风格。
声明式:@Tool
用 @Tool 注解任意方法——public、包私有、private;静态或实例——:
java
class WeatherTools {
@Tool(description = "Get the current weather for a given city")
public String getWeather(String city) {
return weatherService.fetch(city);
}
}@Tool 注解支持:
name—— 工具名称。默认为方法名。在提供给某个模型的工具集内必须唯一。description—— 工具做什么以及何时使用。强烈建议提供;没有它,模型对于何时调用该工具就没有指引。returnDirect—— 把结果直接返回给调用方,而不是喂回给模型。参见 直接返回。resultConverter—— 要使用的ToolCallResultConverter。参见 结果转换。
NOTE
对于 AOT 编译(GraalVM 原生镜像),包含 @Tool 方法的类必须是一个 Spring bean(例如 @Component)。否则,用 @RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_METHODS) 注解它。
添加 @ToolParam 来描述各个参数:
java
class WeatherTools {
@Tool(description = "Get the weather for a city at a specific time")
public String getWeather(
@ToolParam(description = "City name") String city,
@ToolParam(description = "Time in ISO-8601 format", required = false) String at) {
return weatherService.fetch(city, at);
}
}默认所有参数都是必需的。@ToolParam(required = false) 或 @Nullable 使一个参数变为可选。完整模式定制选项参见 JSON Schema。
程序化:MethodToolCallback
对于动态工具注册——当你不控制源类,或想在运行时构建工具定义时——使用 MethodToolCallback:
java
Method method = ReflectionUtils.findMethod(WeatherTools.class, "getWeather", String.class);
MethodToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinitions.builder(method)
.description("Get the current weather for a given city")
.build())
.toolMethod(method)
.toolObject(new WeatherTools())
.build();toolObject 对实例方法是必需的,对静态方法可选。完整的构建器 API 参见 工具规格参考。
程序化:FunctionToolCallback
对于由 Function、Supplier、Consumer 或 BiFunction 支撑的工具——包括 lambda 和方法引用——使用 FunctionToolCallback:
java
FunctionToolCallback callback = FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();该工厂接收一个名称和函数引用。构建器设置描述和输入类型(用于 JSON 模式生成)。
ToolCallback Bean
Spring AI 会自动发现应用上下文中的 ToolCallback bean,并通过 ToolCallbackResolver 暴露它们以供按名查找。把一个工具定义一次为 @Bean,在需要处注入它:
java
@Configuration(proxyBeanMethods = false)
class WeatherToolsConfig {
@Bean
ToolCallback currentWeather(WeatherService weatherService) {
return FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
}
}注入该 bean 并把它传给 ChatClient:
java
@Autowired ToolCallback currentWeather;
ChatClient.create(chatModel)
.prompt("What's the weather in Copenhagen?")
.tools(currentWeather)
.call()
.content();NOTE
Spring AI 2.0 移除了 SpringBeanToolCallbackResolver 模式——在该模式中,裸 Function bean 会通过 toolNames(...) 按名解析。工具现在必须注册为显式的 ToolCallback bean。参见将工具调用从 1.x 升级到 2.0。
向 ChatClient 传递工具
ChatClient 暴露了两个用于提供工具的方法:
java
// Per call — tools available only for this single request
chatClient.prompt(...)
.tools(new WeatherTools(), currentWeather)
.call();
// As defaults — tools available on every request built from this client
ChatClient client = ChatClient.builder(chatModel)
.defaultTools(new WeatherTools(), currentWeather)
.build();两个方法都是异构的:它们接受 @Tool 注解的 POJO 实例、ToolCallback 实例、ToolCallbackProvider 实例,以及以上任意类型的数组或集合。你可以在单次调用中混用风格。
每次调用的 .tools(...) 会追加到客户端的默认工具——而非替换它们。最终发给模型的工具列表是 .defaultTools(...) 与调用点添加的任意 .tools(...) 的并集。
DANGER
默认工具会共享给由同一个 ChatClient.Builder 构建的每个请求。对于应始终可用的工具它们很有用,但若粗心使用也可能危险——风险级和破坏性工具通常应按调用添加,而非作为默认。
NOTE
通过底层 ChatModel 上的 ToolCallingChatOptions.toolCallbacks(...) 注册的工具,会被请求级工具列表覆盖。优先级规则参见 ChatModel 工具调用。
工具调用循环
当你在一个 ChatClient 请求上调用 .call() 或 .stream() 时,请求会流经顾问链。ToolCallingAdvisor——由 DefaultChatClient 自动注册——拥有工具执行的生命周期:
- 该顾问把请求连同所有已注册的工具定义一起发送给模型。
- 模型决定是否调用任何工具并返回一个响应。
- 如果响应包含工具调用,该顾问: a. 把它们交给
ToolCallingManager,由它找到匹配的ToolCallback并执行。 b. 把工具结果追加到对话历史。 c. 把更新后的历史发回给模型。 - 步骤 2–3 重复进行,直到模型产出一个不含工具调用的响应;该响应被返回给调用方。
阻塞式(.call())与流式(.stream())两种模式都被完全支持。
ToolCallingAdvisor 是一个递归顾问——它会在循环的每次迭代重新进入下游顾问链。同一机制也驱动结构化输出校验重试及其他任何循环式顾问模式。底层模式参见递归顾问。
DefaultChatClient 强制链中恰好存在一个 ToolAdvisor。尝试注册第二个会以明确错误失败。
关于完整的 ToolCallingAdvisor 构建器 API、钩子方法与配置选项,参见 ToolCallingAdvisor。
记忆与工具循环
你把 MessageChatMemoryAdvisor 相对于 ToolCallingAdvisor(默认顺序 HIGHEST_PRECEDENCE + 300)放在哪里,决定了记忆存储捕获多少对话上下文。
循环之外(默认)
MessageChatMemoryAdvisor 的默认顺序是 HIGHEST_PRECEDENCE + 200——低于 ToolCallingAdvisor,这把它置于循环之外。记忆顾问:
- 在循环开始前加载一次历史。
- 在循环完成后仅持久化最终的用户消息与最终的助手消息。
- 永远不会看到工具调用请求或工具响应消息。
这是安全的默认,且适用于每个 ChatMemoryRepository 实现。它也匹配 Spring AI 1.x 的行为——在那里工具循环内在于每个 ChatModel,记忆无法观察工具消息。
java
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) // outside the loop by default
.build();循环之内
要让模型在后续回合看到完整的工具转录——以便它就已尝试过什么、调用了哪些工具、它们返回了什么进行推理——请通过给记忆顾问一个大于 ToolCallingAdvisor.DEFAULT_ORDER 的顺序,把它置于循环之内:
java
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
.order(BaseAdvisor.HIGHEST_PRECEDENCE + 400) // inside (after) ToolCallingAdvisor
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(chatMemoryAdvisor)
.build();为避免重复写入,当记忆顾问位于循环之内时必须禁用 ToolCallingAdvisor 的内部对话历史。对于自动注册的 ToolCallingAdvisor,这是自动的——DefaultChatClient 会检测循环内的任意 MemoryAdvisor 并禁用内部历史,无需额外配置。若你手动构造一个 ToolCallingAdvisor,请自己在构建器上调用 .disableInternalConversationHistory()。
NOTE
禁用内部历史只改变每轮转发给链中其余部分的内容——无论工具调用限制针对当前回合都会看到完整历史,因此把记忆顾问嵌套在循环之内时限制仍能正确工作。
后端兼容性
并非每个 ChatMemoryRepository 都能持久化工具消息。该仓库必须知道如何序列化 ToolResponseMessage 和工具调用请求消息,连同普通的用户与助手回合——而当前多数实现只对后者建模。
截至 2.0,支持完整消息集的内置仓库有:
其中任何一个置于循环之内都是安全的。
TIP
关于带完整工具消息支持的 JDBC 持久化——以及事件溯源历史、回合感知的压缩和多智能体分支隔离——参见 spring-ai-session 社区项目。它正是为此场景设计,并计划纳入 Spring AI 2.1。
扩展到数百个工具
默认的 ToolCallingAdvisor 会在每个请求把所有已注册的工具定义发给模型。工具库很小时这没问题。但在 30+ 工具时——或在一次会话可能聚合数百个工具定义的多服务端 MCP 场景下——它会造成上下文膨胀、准确性下降和不必要的令牌成本。
ToolSearchToolCallingAdvisor 是一个直接替换件,实现了渐进式工具披露模式:它每会话把完整工具集索引一次,只注入一个内建的 toolSearchTool,模型用它按自然语言查询检索相关工具。后续请求中只包含被发现的工具。
用一个属性即可启用它:
properties
spring.ai.chat.client.tool-search-advisor.enabled=true完整配置、索引策略、会话 ID 语义和调优选项参见 工具搜索工具。
MCP 工具
模型上下文协议(MCP)是 AI 应用从远端服务端消费工具、资源和提示词的一种标准化方式。Spring AI 从两个方向集成 MCP:你的应用可以消费来自 MCP 服务端的工具,也可以把它自己的工具暴露给 MCP 客户端。
消费 MCP 服务端的工具
添加 MCP 客户端 starter 并配置到一个或多个 MCP 服务端的连接:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>properties
spring.ai.mcp.client.stdio.connections.my-server.command=npx
spring.ai.mcp.client.stdio.connections.my-server.args=-y,@modelcontextprotocol/server-everything自动配置会连接到所有已配置的服务端,发现它们的工具,并把它们作为一个 SyncMcpToolCallbackProvider bean(异步客户端类型为 AsyncMcpToolCallbackProvider)暴露出来。
NOTE
MCP 提供者被刻意地不自动注册到 ChatClient——它们实现 ToolCallbackProvider,但急切地列出工具会在启动时对每个已连接的 MCP 服务端强制一次网络往返。请注入该提供者并显式装配它:
java
@Autowired SyncMcpToolCallbackProvider mcpTools;
// Once, as default tools for every request
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(mcpTools)
.build();
// Or per call
chatClient.prompt()
.user("Search the web for the latest Spring AI release notes")
.tools(mcpTools)
.call()
.content();工具回调的自动配置默认启用,可用 spring.ai.mcp.client.toolcallback.enabled=false 关闭。完整配置面——传输、安全、过滤、名称前缀生成——参见 MCP 客户端 Boot Starter。
把 Spring 工具暴露为 MCP 服务端
反方向——把你的 Spring bean 暴露为 MCP 工具——只需使用 @McpTool(而非 @Tool)并添加 MCP 服务端 starter:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>java
@Component
public class WeatherTools {
@McpTool(description = "Get the current weather for a given city")
public String getWeather(
@McpToolParam(description = "City name") String city) {
return weatherService.fetch(city);
}
}自动配置会扫描 @McpTool 注解的 bean,为其参数生成 JSON 模式,并把它们注册到 MCP 服务端。传输、安全和可观测性选项参见 MCP 服务端 Boot Starter。
组合本地与 MCP 工具
本地 @Tool 方法与远端 MCP 工具共享同一个 ToolCallback 接口——模型和 ToolCallingAdvisor 不区分二者。.tools(...) 和 .defaultTools(...) 在单次调用中接受两种类型,因此你可以自由混合:
java
chatClient.prompt()
.tools(new LocalTools(), mcpTools)
.call()
.content();在混合部署中有几件事需要留意:
- 名称冲突仅在 MCP 一侧处理。
DefaultMcpToolNamePrefixGenerator会为跨 MCP 服务端的重复项加前缀,但它不知道本地@Tool方法。若一个本地工具与一个远端 MCP 工具同名,你需要自己重命名其一,或用McpToolFilter丢弃远端那个。 - 限制暴露范围。 MCP 工具来自你无法完全控制的外部源。一个
McpToolFilterbean 让你依据服务端身份、工具名或描述来选择哪些工具进入命名空间——用于限制一个多嘴或不受信 MCP 服务端的爆炸半径很有用。
DANGER
两个工具源之间的名称冲突只有在合并后的列表经过 ChatClient 时才会被捕获。 .tools(...) 和 .defaultTools(...) 会校验完整的合并列表,若任意两个回调同名则抛 IllegalStateException,因此这里的冲突会使请求失败而非运行错误的工具。 如果你直接用手工组装的 ToolCallingChatOptions 调用 ChatModel.call()/.stream(),或自己构建工具调用循环,该校验不会运行。 在那条路径上,工具执行仅按名解析:合并列表中排在最前的回调会悄无声息地胜出,无错误或日志。 因此,一个恰好与受信本地工具同名的远端 MCP 工具可能取而代之被执行。 若在 ChatClient 之外组合本地与 MCP 工具,要么把各源做命名空间隔离使名称不会冲突(参见 工具名称前缀生成),要么在使用前对合并列表自行调用 ToolCallingChatOptions.validateToolCallbacks(...)。
工具参数增强
Spring AI 提供一个用于对工具输入模式进行动态增强的工具类,为其追加额外参数。这让你能从模型捕获额外信息——例如推理、置信度或元数据——而无需修改底层工具实现。模型看到的是增强后的模式并填入额外字段;你的代码通过一个消费者接收它们;原始工具只收到它期望的参数,保持不变。
常见用例:
- 内部思考/推理 —— 在执行工具前捕获模型的分步推理。
- 记忆增强 —— 提取洞见以存入长期记忆。
- 分析与跟踪 —— 收集元数据、用户意图或使用模式。
- 多智能体协调 —— 传递智能体标识或协调信号。
快速上手
把增强的参数定义为一个 Java record:
java
public record AgentThinking(
@ToolParam(description = "Your reasoning for calling this tool", required = true)
String innerThought,
@ToolParam(description = "Confidence level (low, medium, high)", required = false)
String confidence
) {}用 AugmentedToolCallbackProvider 包装你的工具:
java
AugmentedToolCallbackProvider<AgentThinking> provider = AugmentedToolCallbackProvider
.<AgentThinking>builder()
.toolObject(new WeatherTools()) // wrap the original tools
.argumentType(AgentThinking.class) // augmentation schema type
.argumentConsumer(event -> { // optional consumer of augmented content
AgentThinking thinking = event.arguments();
log.info("Tool: {} | Reasoning: {}", event.toolDefinition().name(), thinking.innerThought());
})
.removeExtraArgumentsAfterProcessing(true)
.build();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(provider)
.build();LLM 看到带你有额外字段的增强模式。你的消费者接收 AgentThinking record;原始工具只收到它期望的参数。
核心组件
AugmentedToolCallbackProvider<T>—— 包装工具对象或提供者,用指定的 record 类型增强所有工具。AugmentedToolCallback<T>—— 包装单个ToolCallback实例。AugmentedArgumentEvent<T>—— 为消费者包含toolDefinition()、rawInput()和arguments()。ToolInputSchemaAugmenter—— 用于模式操作的底层工具类。
removeExtraArgumentsAfterProcessing 选项(默认 true)控制是否把增强的参数传给原始工具。若工具能忽略额外字段且你想在输入中保留它们,就把它设为 false。
用户控制的工具执行
自动注册的循环覆盖了大多数情况,但有些场景确实需要你自己掌控每次迭代:
- 在外部审批步骤上对工具执行做门控。
- 把中间进度转发到 SSE 或 WebSocket 端点。
- 在回合之间施加条件逻辑。
- 依据一个旁路信号停止循环。
通过在请求上设置 AdvisorParams.toolCallingAdvisorAutoRegister(false) 来逐次调用退出。你需要负责在 ChatResponse 中检测工具调用并通过 ToolCallingManager 执行它们:
java
ChatClient chatClient = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder().toolCallbacks(tools).build();
String question = "What is the weather in Amsterdam and Paris?";
// ToolCallingAdvisor disabled — no tool loop runs automatically
ChatClientResponse response = chatClient.prompt()
.user(question)
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
Prompt prompt = new Prompt(List.of(new UserMessage(question)), chatOptions);
// Drive the loop yourself — each iteration is observable and interruptible
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();
}同样的模式适用于流式 API,其中每次迭代的块 Flux 可在被 ChatClientMessageAggregator 聚合的同时转发给订阅者。完整流式变体参见用户控制的流式。
要为自动配置的 ChatClient 全局禁用每次调用的自动注册,设置:
properties
spring.ai.chat.client.tool-calling.enabled=falseNOTE
在框架之外自行驱动循环也会绕过可观测性、顾问组合和单个 ToolAdvisor 的不变式。对大多数需求——包括把流式中间进度转发到 UI——把一个自定义顾问放在循环之内(参见扩展循环)比完全退出更简单、更可组合。
扩展循环:自定义 ToolAdvisor
工具循环不是黑箱。ToolCallingAdvisor 在定义良好的点暴露钩子方法,而 DefaultChatClient 的自动配置透明地接受自定义实现。这正是 ToolSearchToolCallingAdvisor 用来实现渐进式工具披露的同一扩展点。
ToolAdvisor 是一个标记接口:任何自定义的工具调用顾问都必须实现它,以便 DefaultChatClient 识别它、强制单顾问约束,并把它注册在默认顾问的位置。
钩子方法
基础的 ToolCallingAdvisor 暴露四对受保护的钩子方法(call 与 stream 路径各一个):
| 钩子 | 何时触发 |
|---|---|
doInitializeLoop / doInitializeLoopStream | 一次,在首次迭代之前 |
doBeforeCall / doBeforeStream | 每次迭代之前 |
doAfterCall / doAfterStream | 每次迭代之后 |
doFinalizeLoop / doFinalizeLoopStream | 一次,在循环结束之后 |
ToolSearchToolCallingAdvisor 用 doInitializeLoop 索引工具集并增强系统消息,用 doBeforeCall 只注入迄今发现的工具。任何自定义子类都遵循同样的模式。
自定义循环的常见用例:
- 审批门 —— 在执行破坏性工具前暂停并等待人工确认。
- 可观测性 —— 为每次工具调用向旁路发出结构化事件。
- 预算强制 —— 统计每会话的令牌或 LLM 调用数,超限则中止。
- 自定义工具解析 —— 从启动时不可用的动态源解析工具。
自动配置集成
自定义 ToolAdvisor 实现可插入自动配置系统,无需任何手动的 ChatClient 装配。扩展点是 ToolCallingAdvisor.Builder<?> bean。
ChatClientAutoConfiguration 声明一个默认 ToolCallingAdvisor.Builder<?> bean,由 @ConditionalOnMissingBean 守护。要替换它,在一个先于 ChatClientAutoConfiguration 运行的自动配置中,注册你自己的 ToolCallingAdvisor.Builder<?> bean——类型为基础 ToolCallingAdvisor.Builder<?>——:
java
@AutoConfiguration(beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
@ConditionalOnProperty(prefix = "my.advisor", name = "enabled", havingValue = "true")
public class MyToolAdvisorAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ToolCallingAdvisor.Builder<?> toolCallingAdvisorBuilder(ToolCallingManager toolCallingManager) {
return MyCustomToolCallingAdvisor.builder()
.toolCallingManager(toolCallingManager);
}
}随后 ChatClient.Builder 会用你的自定义构建器透明地自动注册你的顾问。ToolSearchToolCallingAdvisor 的自动配置正是用这一机制。
关于完整的 ToolCallingAdvisor 构建器 API 与钩子签名,参见 ToolCallingAdvisor。
工具规格参考
上面的各节覆盖了定义与使用工具的常见路径。本参考覆盖底层接口与定制点。
ToolCallback
ToolCallback 接口为一个工具建模:它携带模型所见的定义,以及模型调用时被执行的执行逻辑。
java
public interface ToolCallback {
/** Definition used by the AI model to decide when and how to call the tool. */
ToolDefinition getToolDefinition();
/** Metadata controlling how the tool is handled (e.g. return-direct). */
ToolMetadata getToolMetadata();
/** Execute with the given JSON input and return the result. */
String call(String toolInput);
/** Execute with the given input and tool context. */
String call(String toolInput, ToolContext toolContext);
}Spring AI 提供 MethodToolCallback 和 FunctionToolCallback 作为内置实现。当你需要完全控制时直接实现 ToolCallback——例如代理一个远端工具源(如 MCP 集成所做的那样)。
ToolDefinition
ToolDefinition 是模型所见的契约:名称、描述和输入模式。
java
public interface ToolDefinition {
/** Unique tool name within the tool set provided to the model. */
String name();
/** Description used by the model to decide when to call the tool. */
String description();
/** JSON schema of the tool's input parameters. */
String inputSchema();
}用 ToolDefinition.builder() 构建一个:
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();对于基于方法的工具,ToolDefinitions.from(method) 会自动生成一个定义。该类是否用 @Tool 注解?该注解的名称和描述会覆盖以方法名命名的默认值。
JSON Schema
输入模式告诉模型如何调用该工具。Spring AI 的 JsonSchemaGenerator 从方法或函数的参数列表生成模式,并支持若干定制注解。
参数描述
使用以下任意一种(Spring AI 的注解优先级最高):
@ToolParam(description = "...")—— Spring AI@JsonClassDescription(description = "...")—— Jackson@JsonPropertyDescription(description = "...")—— Jackson@Schema(description = "...")—— Swagger
java
class CustomerTools {
@Tool(description = "Update customer information")
void updateCustomerInfo(
Long id,
String name,
@ToolParam(description = "Email address, RFC 5322 format", required = false) String email) {
// ...
}
}必需 vs 可选
默认每个参数都是必需的。按(优先级从高到低)以下方式使一个参数变为可选:
@ToolParam(required = false)—— Spring AI@JsonProperty(required = false)—— Jackson@Schema(required = false)—— Swagger@Nullable—— Spring Framework / JSpecify
DANGER
定义正确的必需状态对于减少幻觉至关重要。被标记为必需的参数会强制模型提供一个值;可选的则允许其省略。若你把一个参数标记为必需,而模型无从确定它的值,模型就会编造一个。
工具上下文
ToolContext 让你把非模型数据(租户 ID、用户 ID、请求作用域)传给工具方法。这些数据不会被发给模型——它纯粹供工具内部使用。
聊天请求携带工具加工具上下文,而 Spring AI 只把工具定义发送给模型 (1)。 模型决定调用一个工具并把工具调用请求返回给 Spring AI (2),Spring AI 把它们分派给匹配的工具 (3)。 工具上下文在调用时直接交给工具 (3"),因此它无需经过模型即可抵达工具。 工具把结果返回给 Spring AI (4),Spring AI 把该结果发回给模型 (5),模型的最终答复作为聊天响应返回 (6)。
在下面的示例中,传给 .toolContext() 的 Map.of("tenantId", "acme") 是步骤 (1) 中的工具上下文,而 getCustomerInfo 在步骤 (3") 通过它的 ToolContext 参数读取它——tenantId 无需暴露给模型即可抵达工具。
java
class CustomerTools {
@Tool(description = "Retrieve customer information")
Customer getCustomerInfo(Long id, ToolContext toolContext) {
return customerRepository.findById(id, toolContext.getContext().get("tenantId"));
}
}
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 既作为默认又在调用点被设置,两个 map 会被合并;运行时值优先于默认值。
直接返回
默认情况下,工具调用的结果会被发回给模型,以便它继续对话。当 returnDirect = true 时,结果绕过模型并直接返回给调用方——当工具的输出就是最终答案(例如一次 RAG 检索)而额外往返只会徒增延迟而无价值时,这很有用。
对于声明式工具:
java
@Tool(description = "Retrieve customer information", returnDirect = true)
Customer getCustomerInfo(Long id) { ... }对于程序化工具,通过 ToolMetadata 设置 returnDirect:
java
ToolMetadata toolMetadata = ToolMetadata.builder()
.returnDirect(true)
.build();NOTE
如果模型在单轮中请求多个工具调用,只有当所有被调用的工具都有 returnDirect = true 时,returnDirect 才会被采纳。否则结果会被发回给模型。
结果转换
工具结果由一个 ToolCallResultConverter 转换为 String(模型期望的格式):
java
@FunctionalInterface
public interface ToolCallResultConverter {
String convert(Object result, Type returnType);
}默认的 DefaultToolCallResultConverter 使用 Jackson 序列化结果。在 @Tool 注解(resultConverter)上或通过 ToolMetadata 配置一个自定义转换器。
方法工具的限制
使用 MethodToolCallback 时,某些类型不支持作为方法参数或返回值:
Optional—— 改用@Nullable或@ToolParam(required = false)。- 异步类型(
CompletableFuture、Future)。 - 响应式类型(
Flow、Mono、Flux)。 - 函数式类型(
Function、Supplier、Consumer)。
这些类型对模型不可序列化,也没有模型可解读的模式表示。
异常处理
当某个工具抛异常时,该异常被包装进 ToolExecutionException 并交给 ToolExecutionExceptionProcessor:
java
@FunctionalInterface
public interface ToolExecutionExceptionProcessor {
String process(ToolExecutionException exception);
}该处理器有两个选择:产出一条由框架喂回给模型的错误消息(以便模型恢复或致歉),或重新抛出异常以便调用方处理。
默认的 DefaultToolExecutionExceptionProcessor:
- 把任意
RuntimeException的消息发回给模型。 - 总是重新抛出受检异常和
Error。
通过属性配置:
| 属性 | 描述 | 默认值 |
|---|---|---|
spring.ai.tools.throw-exception-on-error | 若为 true,所有工具错误都被抛出交由调用方处理。若为 false,错误作为消息发回给模型。 | false |
或通过替换 bean:
java
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
return new DefaultToolExecutionExceptionProcessor(true);
}NOTE
如果你实现自己的 ToolCallback,当执行失败时从 call() 抛出 ToolExecutionException——那正是处理器所期望的。
工具调用限制
DefaultToolCallingManager 限制一个工具在一回合内可被调用的次数,以防失控循环:
java
ToolCallingManager toolCallingManager = ToolCallingManager.builder()
.maxCallsPerTool(40) // per-tool default
.maxCallsPerTool("search", 10) // override for one tool
.excludeToolFromLimit("getCurrentWeather") // no limit for this tool
.maxTotalToolCalls(150) // cap across all tools combined
.onLimitExceeded(ToolCallLimitBehavior.THROW) // or RETURN_ERROR_RESPONSE
.build();这些默认值(DefaultToolCallingManager.DEFAULT_MAX_CALLS_PER_TOOL / DEFAULT_MAX_TOTAL_TOOL_CALLS)即便未调用这些方法也适用。用 unlimitedCallsPerTool() / unlimitedTotalToolCalls() 可完全禁用某个上限。
计数
计数仅以当前回合为作用域:只有从最后一个 UserMessage 起的 ToolResponseMessage 条目会被统计,因此由位于循环之外的 ChatMemory 顾问重放的历史绝不会为新回合膨胀计数。管理器自身不缓存任何东西——计数每次都从 prompt.getInstructions() 重新计算,因此单个 ToolCallingManager 实例在并发请求间共享仍是安全的。这也意味着禁用 ToolCallingAdvisor 的内部对话历史(参见循环之内)对限制计数没有影响——无论为链中其余部分裁剪了什么,它总会看到完整的回合历史。
超过限制时的行为
DefaultToolCallingManager 会抛出 ToolCallLimitExceededException,携带工具名(总限制违规时为 null)、所触及的限制,以及当前批次中已执行的部分 ToolExecutionResult,因此没有已完成的工作被丢弃。ToolCallingAdvisor 捕获它并把违规作为最终响应返回——单个 Generation(经 ex.buildGeneration()),绝非每个工具调用一个 generation,因此并行批次中早先成功的调用不会向只读取首个结果的调用方掩盖违规。它的结束原因是 ToolCallLimitExceededException.FINISH_REASON,而批次中早先确实成功的任何工具调用都在 METADATA_PARTIAL_TOOL_RESPONSES 元数据键下被保留,而非被丢弃。
借助 onLimitExceeded(ToolCallLimitBehavior.RETURN_ERROR_RESPONSE),管理器会转而跳过该调用并合成一个错误 ToolResponse——与 ToolExecutionException 所用的同一机制——于是模型被告知已达限制,对话继续而非终止。如果你直接调用 ToolCallingManager.executeToolCalls(...) 而非经由 ToolCallingAdvisor,这也是应选择的选项。
通过属性配置:
| 属性 | 描述 | 默认值 |
|---|---|---|
spring.ai.tools.limits.max-calls-per-tool-default | 每回合对单个工具的最大调用数。-1 禁用。 | 40 |
spring.ai.tools.limits.max-calls-per-tool.<name> | 单工具覆盖;-1 豁免该工具。 | — |
spring.ai.tools.limits.excluded-tools | 免于单工具限制的工具名。 | — |
spring.ai.tools.limits.max-total-tool-calls | 每回合跨所有工具的最大调用数。-1 禁用。 | 150 |
spring.ai.tools.limits.on-limit-exceeded | THROW 或 RETURN_ERROR_RESPONSE。 | THROW |
工具解析
多数时候,工具通过 .tools(...) 和 .defaultTools(...) 显式传递。对于更动态的场景——运行时按名解析——Spring AI 使用 ToolCallbackResolver:
java
public interface ToolCallbackResolver {
@Nullable
ToolCallback resolve(String toolName);
}默认情况下,StaticToolCallbackResolver 会用应用上下文中所有 ToolCallback bean,加上由 ToolCallbackProvider bean 产出的工具来自动配置(MCP 提供者除外,它们被排除以避免急切列出——参见消费 MCP 服务端的工具)。
如果你需要不同的策略(数据库支撑、classpath 扫描、远端),用一个自定义 bean 替换该解析器:
java
@Bean
ToolCallbackResolver toolCallbackResolver(List<ToolCallback> toolCallbacks) {
return new StaticToolCallbackResolver(toolCallbacks);
}该解析器被 ToolCallingManager 内部使用,以同时支持顾问控制的执行和用户控制的执行。
要启用这种行为——即一个为解析器所知、但未附加到请求上的工具可按名被解析并执行——请启用解析回退:
properties
spring.ai.tools.resolution.fallback.enabled=true| 属性 | 描述 | 默认值 |
|---|---|---|
spring.ai.tools.resolution.fallback.enabled | 若为 true,一个请求的、但不存在于请求工具回调中的工具可通过 ToolCallbackResolver 按名解析并执行。若为 false,只有附加到请求的工具才能被执行。 | false |
当直接构造一个 ToolCallingManager 时,同一开关可在构建器上通过 .resolutionFallbackEnabled(true)(默认 false)提供。
DANGER
启用解析回退会使每个可经解析器触达的工具,只要模型点名它就能被执行,无论某次请求附加了什么——包括风险级和破坏性工具。只有当你完全控制解析器暴露哪些工具时才启用它。
可观测性
Spring AI 在 spring.ai.tool 名下为工具调用发出 Micrometer 观测。每个观测捕获:
- 工具名和定义元数据。
- 执行时长。
- 追踪上下文(当有
Tracer可用时)。
完整属性参考参见工具调用可观测性。
工具调用的参数与结果可选择性地作为 span 属性导出;出于敏感性原因默认禁用。启用方式参见工具调用参数与结果数据。
日志
所有主要操作都在 DEBUG 级别记录日志。用以下方式启用:
properties
logging.level.org.springframework.ai=DEBUGChatModel 工具调用
对于想直接驱动 ChatModel——不经 ChatClient、也不经 ToolCallingAdvisor——的用户,Spring AI 支持一条底层工具调用路径。当你需要完全掌控请求/响应周期、且不需要顾问链的组合特性时,这是正确的选择。
API、默认工具与运行时工具之间的优先级规则,以及成表示例,参见 ChatModel 工具调用。
NOTE
Spring AI 1.x 中每个 ChatModel 的内部工具执行循环已被移除。携带工具调用 ChatModel 会把工具定义发送给模型并返回模型的响应——响应中的工具调用不会被自动执行。你自行驱动该循环,或使用 ChatClient(它会为你自动注册 ToolCallingAdvisor)。