工具调用
Tool calling(工具调用)—— 即 AI 模型调用应用程序定义的函数并基于其结果执行操作的能力 —— 是构建代理式 AI 系统的基础构件。一个只能生成文本的模型只是一个聊天机器人;而一个能够发现信息、采取行动并持续迭代直至达成目标的模型,则是一个 Agent(代理)。

工具用于两大类目的:
-
信息检索(Information retrieval) —— 利用来自数据库、Web 服务、文件系统或搜索引擎的数据来增强模型的知识。示例:获取当前天气、查询客户记录、查找最新新闻。
-
采取行动(Taking action) —— 在你的系统中执行操作。示例:发送邮件、预订航班、更新记录、触发工作流。
尽管我们常将“工具调用”视为模型的能力,但实际上拥有工具调用逻辑的是应用程序。模型可以请求工具调用并提供输入参数;而负责执行工具并返回结果的是应用程序。模型永远不会直接访问工具背后的 API——这是一个关键的安全考量。
架构概览
Spring AI 2.0 将工具调用循环作为一等公民和可组合组件纳入 ChatClient 的 advisor chain。流程概览如下:
此架构取代了 Spring AI 1.x 中基于每个 ChatModel 的工具执行循环。对于底层使用场景,仍然支持直接调用 ChatModel —— 请参阅 ChatModel Tool Calling。
请参阅 聊天模型对比,了解哪些模型支持工具调用(tool calling)。
快速入门
通过为方法添加 @Tool 注解来定义工具:
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,然后提出需要它们的问题:
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 提供了三种定义工具的方式,从最高层到最显式:
这三种方式都会生成 ToolCallback 实例,并且它们遵循相同的循环流程。您可以自由地混合使用不同的风格。
声明式:@Tool
使用 @Tool 注释任意方法 —— 公共、包私有、私有;静态或实例方法:
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— 直接将结果返回给调用者,而不将其反馈给模型。参见 Return Direct。 -
resultConverter— 要使用的ToolCallResultConverter。参见 Result Conversion。
对于 AOT 编译(GraalVM 原生镜像),包含 @Tool 方法的类必须是一个 Spring bean(例如 @Component)。否则,请使用 @RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_METHODS) 对其进行注解。
添加 @ToolParam 以描述各个参数:
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 可将参数设为可选。完整的 Schema 自定义选项请参见 JSON Schema。
编程方式:MethodToolCallback
对于动态工具注册——当你无法控制源类,或希望在运行时构建工具定义时——请使用 MethodToolCallback:
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 请参阅 Tool Specification Reference。
编程方式:FunctionToolCallback
对于由 Function、Supplier、Consumer 或 BiFunction 支持的工具(包括 lambda 和方法引用),请使用 FunctionToolCallback:
FunctionToolCallback callback = FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
工厂接收名称和函数引用。构建器设置描述和输入类型(用于 JSON Schema 生成)。
ToolCallback Beans
Spring AI 会自动发现应用程序上下文中的 ToolCallback Bean,并通过 ToolCallbackResolver 暴露它们以支持基于名称的查找。只需将工具定义一次为 @Bean,然后在需要的地方注入即可:
@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:
@Autowired ToolCallback currentWeather;
ChatClient.create(chatModel)
.prompt("What's the weather in Copenhagen?")
.tools(currentWeather)
.call()
.content();
Spring AI 2.0 移除了 SpringBeanToolCallbackResolver 模式,该模式曾允许通过 toolNames(…) 按名称解析裸的 Function bean。现在,工具必须注册为显式的 ToolCallback bean。请参阅 Upgrading Tool Calling from 1.x to 2.0。
向 ChatClient 传递工具
ChatClient 提供了两个用于提供工具的方法:
// 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(…) 的并集。
默认工具在所有由同一个 ChatClient.Builder 构建的请求中共享。它们对于应始终可用的工具非常有用,但如果使用不当也可能存在风险——风险等级较高或具有破坏性的工具通常应按调用添加,而不是作为默认工具。
底层 ChatModel 上通过 ToolCallingChatOptions.toolCallbacks(…) 注册的工具会被请求级别的工具列表所覆盖。有关优先级规则,请参阅 ChatModel 工具调用。
工具调用循环
当你在 ChatClient 请求上调用 .call() 或 .stream() 时,请求会流经顾问链。ToolCallingAdvisor——由 DefaultChatClient 自动注册——负责工具执行的生命周期管理:

-
顾问(advisor)将请求发送给模型,并包含所有已注册的工具定义。
-
模型决定是否需要调用任何工具,并返回响应。
-
如果响应中包含工具调用,顾问将:
-
将它们交给
ToolCallingManager,由其查找匹配的ToolCallback并执行。 -
将工具执行结果追加到对话历史中。
-
将更新后的历史发送回模型。
-
-
重复步骤 2–3,直到模型生成一个不包含工具调用的响应;该响应将返回给调用者。
阻塞模式(.call())和流式模式(.stream())均得到完全支持。
ToolCallingAdvisor 是一个递归 Advisor——它在循环的每次迭代中重新进入下游 Advisor 链。相同的机制驱动了结构化输出验证的重试以及其他任何循环 Advisor 模式。有关底层模式,请参阅 Recursive Advisors。
DefaultChatClient 强制要求链中恰好存在一个 ToolAdvisor。尝试注册第二个 ToolAdvisor 时,会因显式错误而失败。
完整的 ToolCallingAdvisor 构建器 API、钩子方法及配置选项,请参阅 ToolCallingAdvisor。
内存与工具循环
将 MessageChatMemoryAdvisor 放置在 ToolCallingAdvisor(默认顺序为 HIGHEST_PRECEDENCE + 300)的相对位置,决定了记忆库捕获多少会话上下文。
循环外(默认)
MessageChatMemoryAdvisor 的默认顺序为 HIGHEST_PRECEDENCE + 200——低于 ToolCallingAdvisor,这使其位于循环的外部。该记忆顾问:
-
在循环开始前加载一次历史记录。
-
仅在循环完成后持久化最终的用户消息和最终的助手消息。
-
不会接收到工具调用请求或工具响应消息。
这是安全的默认设置,适用于所有 ChatMemoryRepository 实现。这也与 Spring AI 1.x 的行为一致,在 1.x 版本中,工具循环是各个 ChatModel 的内部行为,因此内存无法观察到工具消息。
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) // outside the loop by default
.build();
循环内部
要在后续轮次中为模型提供完整的工具交互记录——以便它能推理已尝试的内容、调用了哪些工具以及这些工具返回了什么——请将 memory advisor 置于循环内部,方法是赋予其一个大于 ToolCallingAdvisor.DEFAULT_ORDER 的 order 值:
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
.order(BaseAdvisor.HIGHEST_PRECEDENCE + 400) // inside (after) ToolCallingAdvisor
.build();
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(chatMemoryAdvisor)
.build();
为避免重复写入,当循环内部存在内存顾问(memory advisor)时,必须禁用 ToolCallingAdvisor 的内部对话历史。对于自动注册的 ToolCallingAdvisor,这是自动完成的——DefaultChatClient 会检测循环内部的任何 MemoryAdvisor,并自动禁用内部对话历史,无需额外配置。如果手动构建 ToolCallingAdvisor,则需要自行在 builder 上调用 .disableInternalConversationHistory()。
禁用内部历史记录仅会更改每轮转发到链中其余部分的内容 —— 工具调用限制 仍然会看到当前轮次的完整历史记录,因此即使 memory advisor 嵌套在循环内部,限制也能正确工作。

后端兼容性
并非所有 ChatMemoryRepository 都能持久化工具消息。该仓储类必须知道如何将 ToolResponseMessage 以及工具调用请求消息与普通用户和助手轮次一起进行序列化——而大多数现有实现仅对后者建模。
从 2.0 版本开始,支持完整消息集的内建仓库如下:
其中任何一个都可以安全地用在循环内部。
如果您需要基于 JDBC 的持久化方案,且具备完整的工具消息支持、事件溯源历史、基于轮次的压缩(compaction)以及多智能体分支隔离功能,请参阅 spring-ai-session 社区项目。该项目正是为此场景设计,并计划在 Spring AI 2.1 中纳入支持。
扩展到数百个工具
默认的 ToolCallingAdvisor 会在每次请求时向模型发送所有已注册的工具定义。对于规模较小的工具库而言,这完全没有问题。但当工具数量达到 30 个以上,或者在可能聚合数百个工具定义的多服务器 MCP 环境中,这会导致上下文膨胀、准确性下降以及不必要的 token 成本。
ToolSearchToolCallingAdvisor 是一个可直接替换的组件,实现了渐进式工具披露模式:它在每个会话中对完整的工具集进行一次索引,并仅注入一个内置的 toolSearchTool,模型通过该工具使用自然语言查询来检索相关工具。只有被发现的工具才会包含在后续请求中。
通过单个属性启用:
spring.ai.chat.client.tool-search-advisor.enabled=true
请参阅 Tool Search Tool 以获取完整的配置、索引策略、会话 ID 语义及调优选项。
MCP 工具
Model Context Protocol(MCP)是一种标准化方式,允许 AI 应用从远程服务器获取工具、资源和提示词。Spring AI 支持双向集成 MCP:你的应用可以消费来自 MCP 服务器的工具,也可以将其自身的工具暴露给 MCP 客户端。
使用 MCP 服务器工具
添加 MCP 客户端的起始器,并配置到一个或多个 MCP 服务器的连接:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
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)。
MCP 提供者被有意地未自动注册到 ChatClient 中——它们实现了 ToolCallbackProvider,但如果急切地列出工具,将会强制在启动时与每个已连接的 MCP 服务器进行一次网络往返。请注入该提供者并显式地进行配置:
@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 Client Boot Starter。
将 Spring 工具暴露为 MCP 服务器
反过来的方向——将你的 Spring 组件暴露为 MCP 工具——只需使用 @McpTool(而不是 @Tool)并添加 MCP 服务器 starter:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
@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 Schema,并将其注册到 MCP 服务器中。有关传输、安全性和可观测性选项,请参阅 MCP Server Boot Starter。
结合本地工具和 MCP 工具
本地 @Tool 方法 和远程 MCP 工具共享同一个 ToolCallback 接口 —— 模型和 ToolCallingAdvisor 不会区分它们。.tools(…) 和 .defaultTools(…) 接受这两种类型并在同一次调用中处理,因此可以自由混合使用:
chatClient.prompt()
.tools(new LocalTools(), mcpTools)
.call()
.content();
在混合环境中需注意以下几点:
-
名称冲突仅在 MCP 侧进行处理。
DefaultMcpToolNamePrefixGenerator会为跨越不同 MCP 服务器的重名工具添加前缀,但它不感知本地的@Tool方法。如果本地工具与远程 MCP 工具共享同一个名称,您需要自行重命名其中一个,或使用McpToolFilter来丢弃远程工具。 -
限制暴露的内容。 MCP 工具来自外部源,其接口面并不完全在您控制之下。
McpToolFilterBean 允许您根据服务器标识、工具名称或描述来选择哪些工具进入命名空间 —— 这对于限制健谈或不可信 MCP 服务器的影响范围非常有用。
工具参数增强
Spring AI 提供了一项工具,用于动态增强工具输入模式(schema),以添加额外参数。这允许你从模型中获取额外信息——例如推理过程、置信度或元数据——而无需修改底层工具的实现。模型会看到增强后的模式并填充这些额外字段;你的代码通过消费者(consumer)接收这些信息;原始工具则仅接收其预期的参数,且保持不变。
常见用例:
-
内部思考 / 推理 —— 在执行工具之前,捕捉模型逐步推理的过程。
-
记忆增强 —— 提取洞见并存储到长期记忆中。
-
分析与追踪 —— 收集元数据、用户意图或使用模式。
-
多智能体协调 —— 传递智能体标识符或协调信号。

快速入门
将增强的参数定义为一个 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 封装你的工具:
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 记录;原始工具仅会收到其预期的参数。
核心组件
-
AugmentedToolCallbackProvider<T>— 封装工具对象或提供者,并使用指定的记录类型增强所有工具。 -
AugmentedToolCallback<T>— 封装单独的ToolCallback实例。 -
AugmentedArgumentEvent<T>— 包含供消费者使用的toolDefinition()、rawInput()和arguments()。 -
ToolInputSchemaAugmenter— 用于模式操作的低层实用工具。
removeExtraArgumentsAfterProcessing 选项(默认值为 true)控制是否将增强后的参数传递给原始工具。如果工具可以忽略额外字段并且您希望这些字段在输入中保留,请将其设置为 false。
用户可控的工具执行
自动注册的循环涵盖了大多数情况,但某些场景确实需要您自行管理每一次迭代:
-
在外部审批步骤上对工具执行进行门控。
-
将中间进度转发到 SSE 或 WebSocket 端点。
-
在各轮次之间应用条件逻辑。
-
基于旁路信号停止循环。
通过在请求中设置 AdvisorParams.toolCallingAdvisorAutoRegister(false) 可按调用禁用自动注册。此时,你需要负责在 ChatResponse 中检测工具调用,并通过 ToolCallingManager 执行它们:
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 发起的每次调用的自动注册,请设置:
spring.ai.chat.client.tool-calling.enabled=false
在框架之外自行驱动循环,同样会绕过可观测性、顾问组合以及单-ToolAdvisor 不变式。对于大多数需求——包括将中间流式进度推送到 UI——将自定义顾问放在循环内部(参见 扩展循环)比完全退出(full opt-out)更简单且更具可组合性。
扩展循环:自定义 ToolAdvisor
工具循环(tool loop)并非黑盒。ToolCallingAdvisor 在明确定义的节点上暴露了钩子方法(hook methods),并且 DefaultChatClient 的自动配置能够透明地接受自定义实现。这正是 ToolSearchToolCallingAdvisor 用于实现渐进式工具披露(progressive tool disclosure)的同一扩展点。
ToolAdvisor 是一个标记接口:任何自定义的工具调用 Advisor 都必须实现它,以便 DefaultChatClient 能够识别该接口,强制单 Advisor 约束,并将其注册以替代默认实现。
Hook 方法
基础 ToolCallingAdvisor 暴露了四对受保护的钩子方法(调用路径和流式路径各一对):
| Hook | 触发时机 |
|---|---|
doInitializeLoop / doInitializeLoopStream | 首次迭代前触发一次 |
doBeforeCall / doBeforeStream | 每次迭代前 |
doAfterCall / doAfterStream | 每次迭代后 |
doFinalizeLoop / doFinalizeLoopStream | 循环结束后触发一次 |
ToolSearchToolCallingAdvisor 使用 doInitializeLoop 来索引工具集并增强系统消息,并使用 doBeforeCall 来仅注入迄今为止发现的工具。任何自定义子类都遵循相同的模式。
自定义循环的常见用例:
-
审批关卡(Approval gate) —— 在执行破坏性工具前暂停,并等待人工确认。
-
可观测性(Observability) —— 通过旁路通道,为每次工具调用发出结构化事件。
-
预算强制执行(Budget enforcement) —— 统计每个会话中的 Token 数量或 LLM 调用次数,若超出限制则中止执行。
-
自定义工具解析(Custom tool resolution) —— 从启动时不可用的动态源中解析工具。
自动配置集成
自定义的 ToolAdvisor 实现可以无缝接入自动配置系统,无需手动配置 ChatClient。其扩展点为 ToolCallingAdvisor.Builder<?> Bean。
ChatClientAutoConfiguration 声明了一个默认的 ToolCallingAdvisor.Builder<?> Bean,并通过 @ConditionalOnMissingBean 进行保护。若要替换它,请在 先于 ChatClientAutoConfiguration 执行的自动配置中注册您自己的 ToolCallingAdvisor.Builder<?> Bean —— 其类型应声明为基类 ToolCallingAdvisor.Builder<?> :
@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 随后会使用您的自定义构建器来透明地自动注册您的 advisor。`ToolSearchToolCallingAdvisor` 的自动配置正是使用了此机制。
完整的 ToolCallingAdvisor 构建器 API 和钩子签名,请参阅 ToolCallingAdvisor。
工具规范参考
上述章节介绍了定义和使用工具的常见方法。本参考文档涵盖了底层的接口和自定义点。
ToolCallback
ToolCallback 接口用于建模工具:它携带模型可见的定义,以及当模型调用该工具时执行的逻辑。
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 是模型所见的契约:名称、描述和输入模式。
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() 构建一个:
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
输入模式(input schema)用于告诉模型如何调用工具。Spring AI 的 JsonSchemaGenerator 可根据方法或函数的参数列表生成模式,并支持多种自定义注解。
参数说明
使用以下任意一种(Spring AI 的注解具有最高优先级):
-
@ToolParam(description = "…")— Spring AI -
@JsonClassDescription(description = "…")— Jackson -
@JsonPropertyDescription(description = "…")— Jackson -
@Schema(description = "…")— Swagger
class CustomerTools {
@Tool(description = "Update customer information")
void updateCustomerInfo(
Long id,
String name,
@ToolParam(description = "Email address, RFC 5322 format", required = false) String email) {
// ...
}
}
必需与可选
默认情况下,每个参数都是必需的。可以通过以下方式将参数设为可选(按优先级排序):
-
@ToolParam(required = false)— Spring AI -
@JsonProperty(required = false)— Jackson -
@Schema(required = false)— Swagger -
@Nullable— Spring Framework / JSpecify
定义正确的必需状态对于减少幻觉至关重要。标记为必填的参数会强制模型提供值;而可选参数则允许其省略。如果您将某个参数标记为必填,但模型无法确定其值,它就会自行编造一个值。
工具上下文
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 无需暴露给模型即可到达工具。
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 既作为默认值设置,又在调用点设置,则这两个映射会被合并;运行时值优先于默认值。
直接返回
默认情况下,工具调用的结果会发送回模型,以便其继续对话。设置 returnDirect = true 时,结果将绕过模型,直接返回给调用者——这在工具的输出即为最终答案(例如 RAG 检索)且额外的往返交互只会增加无价值的延迟时非常有用。

对于声明式工具:
@Tool(description = "Retrieve customer information", returnDirect = true)
Customer getCustomerInfo(Long id) { ... }
对于编程工具,请通过 ToolMetadata 设置 returnDirect:
ToolMetadata toolMetadata = ToolMetadata.builder()
.returnDirect(true)
.build();
如果模型在单轮请求中要求多次工具调用,只有当所有被调用的工具都设置了 returnDirect = true 时,returnDirect 才会生效。否则,结果将返回给模型。
结果转换
工具结果由 ToolCallResultConverter 转换为 String(模型所期望的格式):
@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)。
这些类型无法序列化为模型,且没有可供模型解释的 Schema 表示形式。
异常处理
当工具抛出异常时,该异常会被包装在 ToolExecutionException 中,并传递给 ToolExecutionExceptionProcessor:
@FunctionalInterface
public interface ToolExecutionExceptionProcessor {
String process(ToolExecutionException exception);
}
处理器有两个选择:生成一条错误消息,由框架反馈给模型(以便模型能够恢复或致歉),或者重新抛出异常,由调用方进行处理。
默认的 DefaultToolExecutionExceptionProcessor:
- 将任何
RuntimeException的信息发回模型。 - 始终重新抛出受检查异常和 `Error`。
通过属性进行配置:
| 属性 | 描述 | 默认值 |
|---|---|---|
spring.ai.tools.throw-exception-on-error | 如果为 true,所有工具错误都将作为异常抛出,由调用者处理。如果为 false,错误将以消息形式返回给模型。 | false |
或者通过替换该 bean:
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
return new DefaultToolExecutionExceptionProcessor(true);
}
如果你自行实现了 ToolCallback,当执行失败时,应从 call() 抛出 ToolExecutionException —— 这正是处理器所期望的行为。
工具调用限制
DefaultToolCallingManager 会限制工具在一轮对话中可以被调用的次数,以防范失控的循环:
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 的内部对话历史(参见 Inside the Loop)对限制计数没有影响——无论链条中其他部分如何裁剪历史,它始终能看到完整的回合历史。
超出限制时的行为
DefaultToolCallingManager 会抛出 ToolCallLimitExceededException,其中包含工具名称(如果是总限额超限则为 null)、触发的限额,以及当前批次中已执行的部分 ToolExecutionResult,从而确保已完成的处理结果不会被丢弃。ToolCallingAdvisor 捕获该异常并将其作为最终响应返回——这是一个单一的 Generation(通过 ex.buildGeneration() 生成),而不是每个工具调用对应一个 Generation,因此并行批次中较早的成功调用不会让只读取第一个结果的调用方忽略该超限情况。其结束原因为 ToolCallLimitExceededException.FINISH_REASON,且批次中较早成功执行的任何工具调用都会保留在 METADATA_PARTIAL_TOOL_RESPONSES 元数据键下,而不是被丢弃。
使用 onLimitExceeded(ToolCallLimitBehavior.RETURN_ERROR_RESPONSE) 时,管理器会跳过该调用并合成一个错误的 ToolResponse ——这与用于处理 ToolExecutionException 的机制相同——从而告知模型已达到限制,对话继续而不是终止。如果你不通过 ToolCallingAdvisor 而是直接调用 ToolCallingManager.executeToolCalls(…),这也是应该选择的选项。
通过属性进行配置:
| Property | Description | Default |
|---|---|---|
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:
public interface ToolCallbackResolver {
@Nullable
ToolCallback resolve(String toolName);
}
默认情况下,StaticToolCallbackResolver 会自动配置应用程序上下文中的所有 ToolCallback Bean,以及由 ToolCallbackProvider Bean 生成的工具(MCP 提供者除外,为了避免急切列出而被排除——请参阅消费 MCP 服务器工具)。
如果需要采用不同的策略(基于数据库、通过类路径扫描或远程),请用自定义 bean 替换解析器(resolver)。
@Bean
ToolCallbackResolver toolCallbackResolver(List<ToolCallback> toolCallbacks) {
return new StaticToolCallbackResolver(toolCallbacks);
}
要启用该行为——即一个已被 resolver 识别但未附加到请求的工具可通过名称解析并执行——请启用 resolution fallback:
spring.ai.tools.resolution.fallback.enabled=true
| 属性 | 描述 | 默认值 |
|---|---|---|
spring.ai.tools.resolution.fallback.enabled | 如果为 true,请求中不存在的工具回调中缺失的工具,可能会通过 ToolCallbackResolver 按名称进行解析并执行。如果为 false,则只能执行附加到请求上的工具。 | false |
在直接构建 ToolCallingManager 时,可以通过 .resolutionFallbackEnabled(true) 在构建器上设置相同的开关(默认值为 false)。
启用解析回退机制后,只要模型中指定了工具名称,无论请求附带了哪些参数(包括风险等级和破坏性操作工具),所有工具都可通过解析器可执行文件被访问。请仅在完全控制解析器所暴露工具的情况下启用此功能。
可观测性
Spring AI 会在 spring.ai.tool 名称下发出工具调用的 Micrometer 观测。每个观测记录了:
-
工具名称和定义元数据。
-
执行时长。
-
追踪上下文(当有
Tracer可用时)。
参见 Tool Calling Observability 以获取完整的属性参考。
工具调用参数和结果可以选择性地导出为 span 属性;出于敏感性考虑,此功能默认处于禁用状态。请参见 工具调用参数和结果数据 以启用该功能。
日志记录
所有主要操作均在 DEBUG 级别记录日志。启用方法如下:
logging.level.org.springframework.ai=DEBUG
聊天模型工具调用
对于希望直接使用 ChatModel(不通过 ChatClient 或 ToolCallingAdvisor)的用户,Spring AI 提供了一种低级别的工具调用路径。当你需要对请求/响应周期拥有完全控制,且不需要使用顾问链的组合功能时,这是一个合适的选择。
参见ChatModel Tool Calling,了解 API、默认工具与运行时工具之间的优先级规则以及实际用例。
Spring AI 1.x 中移除了每个 ChatModel 的内部工具执行循环。通过 ChatModel 调用工具时,会将工具定义发送给模型并返回模型的响应——响应中的工具调用 不会 自动执行。你需要自行驱动该循环,或者使用 ChatClient(它会自动为你注册 ToolCallingAdvisor)。
章节摘要
ToolCallingAdvisor
ToolCallingAdvisor 是 Spring AI 2.0 中负责工具执行生命周期的递归 advisor。它由 DefaultChatClient 自动注册,并驱动请求/响应循环,直到模型生成不包含工具调用的响应。
工具搜索工具
随着 AI 智能体连接更多的服务 —— Slack、GitHub、Jira、MCP 服务器 —— 工具库迅速增长。一个典型的多服务器配置在对话开始之前,很容易聚合 50+ 个工具并消耗 55,000+ 个 token。当模型面对 30+ 个名称相似的选项时,工具选择的准确性也会下降。
ChatModel 工具调用
ChatModel 是面向聊天提供方的低层级请求/响应接口。它接受工具定义,将其发送给模型,并返回模型的响应。响应中的工具调用不会自动执行——执行的责任在调用方。