文章目录
- [1. 前言](#1. 前言)
- [2. 加载流程](#2. 加载流程)
-
- [2.1 初始化工具解析器](#2.1 初始化工具解析器)
- [2.2 全局默认](#2.2 全局默认)
- [3.3 运行时配置](#3.3 运行时配置)
- [3. 执行流程](#3. 执行流程)
-
- [3.1 构建模型请求](#3.1 构建模型请求)
- [3.2 工具执行](#3.2 工具执行)
- [4. 总结](#4. 总结)
-
- [4.1 流程图](#4.1 流程图)
- [4.2 一定要注册 ToolCallbackProvider 为 Bean 吗](#4.2 一定要注册 ToolCallbackProvider 为 Bean 吗)
- [4.2 ToolCallbackResolver 再思考](#4.2 ToolCallbackResolver 再思考)
1. 前言
在上篇文档中,我们通过自定义的 ToolCallbackProvider 实现了从文件动态加载工具,有必要深入了解一下 Provider 的加载和执行流程,便于我们在实际项目中更好地扩展和定制工具注册机制。
2. 加载流程
我们使用的是 @Component 将 ToolCallbackProvider 注册到了 Spring 容器中,对象本身的实例化、销毁生命周期由 Spring 负责。
2.1 初始化工具解析器
在程序启动过程中,因为我们将 ToolCallbackProvider 注册为了 Bean ,在 ToolCallingAutoConfiguration 自动配置类注册 ToolCallbackResolver 时,会使用 ObjectProvider 机制获取到该 Bean 实例,并调用自定义的 ToolCallbackProvider 获取到工具实例,合并到总工具列表。
首先会将 ToolCallbackProvider 合并到一个集合中:
java
// Merge ToolCallbackProviders from both ObjectProviders.
List<ToolCallbackProvider> totalToolCallbackProviders = new ArrayList<>(
tcbProviderList.stream().flatMap(List::stream).toList());
totalToolCallbackProviders.addAll(tcbProviders.stream().toList());

然后调用所有 Providers 的 getToolCallbacks() 方法,解析出所有工具:
java
// 过滤掉 MCP 特殊工具(无关,跳过)
.filter(pr -> !isMcpToolCallbackProvider(ResolvableType.forInstance(pr)))
// 从每个 Provider 中获取所有 ToolCallback
.map(pr -> List.of(pr.getToolCallbacks()))
// 合并到总工具列表
.forEach(allFunctionAndToolCallbacks::addAll);

最终 Provider 中的工具会加载到 StaticToolCallbackResolver ,并统一封装到 DelegatingToolCallbackResolver 中:
java
// 1. 静态解析器:持有所有合并后的工具(固定+动态)
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks);
// 2. Spring Bean 解析器:从 Spring 容器中查找工具(@Tool / @Bean)
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
.applicationContext(applicationContext)
.build();
// 3. 委托解析器:依次使用两个解析器查找工具(优先静态,再查Bean)
return new DelegatingToolCallbackResolver(
List.of(staticToolCallbackResolver, springBeanToolCallbackResolver)
);

2.2 全局默认
接着进入到 @Configuration 中进行 ChatClient 初始化,这里配置了全局默认的 ToolCallbackProvider :
java
@Configuration
public class ChatClientConfig {
@Bean("zhiPuAiChatClient")
public ChatClient zhiPuAiChatClient(ZhiPuAiChatModel zhiPuAiChatModel, FileToolCallbackProvider fileToolCallbackProvider) {
ChatClient client = ChatClient.builder(zhiPuAiChatModel)
.defaultToolCallbacks(fileToolCallbackProvider)
.build();
return client;
}
}
defaultToolCallbacks 方法会在默认的请求对象中设置 ToolCallbackProvider 对象实例:
java
@Override
public Builder defaultToolCallbacks(ToolCallbackProvider... toolCallbackProviders) {
this.defaultRequest.toolCallbacks(toolCallbackProviders);
return this;
}
继续调用 DefaultChatClientBuilder#toolCallbacks() :
java
@Override
public ChatClientRequestSpec toolCallbacks(ToolCallbackProvider... toolCallbackProviders) {
Assert.notNull(toolCallbackProviders, "toolCallbackProviders cannot be null");
Assert.noNullElements(toolCallbackProviders, "toolCallbackProviders cannot contain null elements");
this.toolCallbackProviders.addAll(List.of(toolCallbackProviders));
return this;
}
最后 ToolCallbackProvider 会被添加到 DefaultChatClientRequestSpec 的属性中:
java
public static class DefaultChatClientRequestSpec implements ChatClientRequestSpec {
private final List<ToolCallbackProvider> toolCallbackProviders = new ArrayList<>();
//.............
}
在 ChatClient 对象构建完成后,这里只存储了 Provider 实例,toolCallbacks 工具实例对象为空:

3.3 运行时配置
如果在 ChatClient 调用过程中配置 ToolCallbackProvider :
java
String content = deepSeekChatClient.prompt("几点了")
.toolCallbacks(fileToolCallbackProvider)
.call()
.content();
和全局默认一样,也只保存 Provider 实例,区别是保存在 ChatClientRequestSpec 请求对象中,请求结束时对象就被销毁了,下一次调用 call()/stream() 时,又是新的请求对象了:
java
@Override
public ChatClientRequestSpec toolCallbacks(ToolCallbackProvider... toolCallbackProviders) {
Assert.notNull(toolCallbackProviders, "toolCallbackProviders cannot be null");
Assert.noNullElements(toolCallbackProviders, "toolCallbackProviders cannot contain null elements");
this.toolCallbackProviders.addAll(List.of(toolCallbackProviders));
return this;
}
3. 执行流程
调用 call() 时的方法入口:
java
@Override
public CallResponseSpec call() {
BaseAdvisorChain advisorChain = buildAdvisorChain();
return new DefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), ...);
}
3.1 构建模型请求
在 DefaultChatClientUtils#toChatClientRequest 方法中,构建 AI 模型可执行的 ChatClientRequest 时,会判断 toolCallbackProviders 是否为空,不为空则构建工具相关的对话配置信息 DefaultToolCallingChatOptions :
java
ChatOptions processedChatOptions = inputRequest.getChatOptions();
// ========== 关键判断:是否包含【工具调用配置】 ==========
if (!inputRequest.getToolNames().isEmpty()
|| !inputRequest.getToolCallbacks().isEmpty() // 函数/方法型工具封装对象
|| !inputRequest.getToolCallbackProviders().isEmpty()
|| !CollectionUtils.isEmpty(inputRequest.getToolContext())) {
// 无配置 → 新建工具调用配置
if (processedChatOptions == null) {
processedChatOptions = new DefaultToolCallingChatOptions();
}
// 有普通配置 → 转换为【工具调用专用配置】
else if (processedChatOptions instanceof DefaultChatOptions defaultChatOptions) {
processedChatOptions = ModelOptionsUtils.copyToTarget(...);
}
}
如果有工具,然后才开始调用 ToolCallbackProvider#getToolCallbacks 方法获取工具实例对象(懒加载):
java
if (processedChatOptions instanceof ToolCallingChatOptions toolCallingChatOptions) {
// 1. 合并工具名称
Set<String> toolNames = ToolCallingChatOptions.mergeToolNames(...);
toolCallingChatOptions.setToolNames(toolNames);
// 2. 懒加载工具提供者 → 生成 ToolCallback(核心!)
List<ToolCallback> allToolCallbacks = new ArrayList<>(inputRequest.getToolCallbacks());
for (var provider : inputRequest.getToolCallbackProviders()) {
allToolCallbacks.addAll(provider.getToolCallbacks());
}
// 3. 合并、校验所有工具回调
List<ToolCallback> toolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(...);
ToolCallingChatOptions.validateToolCallbacks(toolCallbacks);
toolCallingChatOptions.setToolCallbacks(toolCallbacks);
// 4. 合并工具上下文
Map<String, Object> toolContext = ToolCallingChatOptions.mergeToolContext(...);
toolCallingChatOptions.setToolContext(toolContext);
}
ToolCallbackProvider 中获取到的工具对象会封装到模型请求( ChatClient 层面的请求对象):

ChatClient 在调用 ChatModel 时,还会创建请求对象( ChatModel 层面),例如 ZhiPuAiChatModel#createRequest() 中还会调用 ToolCallingManager 方法通过解析器获取可用工具对象:
java
// Add the tool definitions to the request's tools parameter.
List<ToolDefinition> toolDefinitions = this.toolCallingManager.resolveToolDefinitions(requestOptions);
if (!CollectionUtils.isEmpty(toolDefinitions)) {
request = ModelOptionsUtils.merge(
ZhiPuAiChatOptions.builder().tools(this.getFunctionTools(toolDefinitions)).build(), request,
ChatCompletionRequest.class);
}
这里的解析器是自动配置提供的 DelegatingToolCallbackResolver ,在之前说过它在程序启动时,会加载 Provider 中的工具实例,这里会根据 ToolCallingChatOptions 中传递的工具名称,在解析器中查找工具实例:
java
public List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions) {
Assert.notNull(chatOptions, "chatOptions cannot be null");
List<ToolCallback> toolCallbacks = new ArrayList<>(chatOptions.getToolCallbacks());
for (String toolName : chatOptions.getToolNames()) {
// Skip the tool if it is already present in the request toolCallbacks.
// That might happen if a tool is defined in the options
// both as a ToolCallback and as a tool name.
if (chatOptions.getToolCallbacks()
.stream()
.anyMatch(tool -> tool.getToolDefinition().name().equals(toolName))) {
continue;
}
ToolCallback toolCallback = this.toolCallbackResolver.resolve(toolName);
if (toolCallback == null) {
logger.warn(POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING, toolName);
throw new IllegalStateException("No ToolCallback found for tool name: " + toolName);
}
toolCallbacks.add(toolCallback);
}
return toolCallbacks.stream().map(ToolCallback::getToolDefinition).toList();
}
最终,所有的默认工具实例对象,会拼接到对话上下文传递给大模型,让大模型判断是否调用哪个工具。
3.2 工具执行
当需要调用工具时,进入到 ToolCallingManager 工具执行器生命周期。 ChatModel 执行请求后,如果要调用工具,模型会返回 toolCalls 消息:

toolCalls 中只包含了工具名称、参数等信息:

在 ZhiPuAiChatModel#call() 方法中, 会先判断是否需要执行工具,然后调用 DefaultToolCallingManager#executeToolCalls() 方法:
java
if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(requestPrompt.getOptions(), response)) {
var toolExecutionResult = this.toolCallingManager.executeToolCalls(requestPrompt, response);
if (toolExecutionResult.returnDirect()) {
// Return tool execution result directly to the client.
return ChatResponse.builder()
.from(response)
.generations(ToolExecutionResult.buildGenerations(toolExecutionResult))
.build();
}
else {
// Send the tool execution result back to the model.
return this.call(new Prompt(toolExecutionResult.conversationHistory(), requestPrompt.getOptions()));
}
}
executeToolCall() 只转入了工具名称,会先从 toolCallbacks 中查找(构建时直接传入的 ToolCallback),没找到再调用解析器获取工具实例:
java
// 从 当前请求携带的所有工具实例 中查找
ToolCallback toolCallback = toolCallbacks.stream()
// 过滤:只保留 工具名称 = AI要求调用的工具名 的工具
.filter(tool -> toolName.equals(tool.getToolDefinition().name()))
// 取第一个匹配的工具(工具名全局唯一,只会有一个)
.findFirst()
// 如果当前请求里没找到,就调用【全局工具解析器】兜底查找
.orElseGet(() -> this.toolCallbackResolver.resolve(toolName));
最终调用 ToolCallback#call() 方法返回工具执行结果,进行下一步处理。
4. 总结
先明确 4 个核心组件,是理解流程的基础:
ToolCallbackProvider:工具提供者(你自定义的动态文件加载工具就是它),负责产出具体工具;ToolCallback:具体工具实例(真正的工具方法 / 逻辑);ToolCallbackResolver:工具解析器,负责根据名称查找 / 获取工具;ToolCallingManager:工具执行器,负责调用工具并返回结果。
4.1 流程图
启动加载流程:

ChatClient 工具配置流程:

运行时执行流程(核心链路):

4.2 一定要注册 ToolCallbackProvider 为 Bean 吗
回答:不需要!
直接通过类也是可以的:
java
@Bean("zhiPuAiChatClient")
public ChatClient zhiPuAiChatClient(ZhiPuAiChatModel zhiPuAiChatModel) {
ChatClient client = ChatClient.builder(zhiPuAiChatModel)
.defaultToolCallbacks(new FileToolCallbackProvider( SchemaType.JSON_SCHEMA))
.build();
return client;
}
只是默认的 ToolCallbackResolver 无法通过 ObjectProvider 机制获取到工具实例,在创建模型请求时,会懒加载工具提供者中的所有实例,在执行时也会从先从 ToolCallingChatOptions 中查找。
4.2 ToolCallbackResolver 再思考
在之前工具解析器的篇章中,我们只了解了 ToolCallbackResolver 是一个通过名称找到工具的解析器。实现子类中,不仅提供解析,还提供了工具注册表,内存中维护了所有的工具实例 ,例如 SpringBeanToolCallbackResolver :
java
private static final Map<String, ToolCallback> toolCallbacksCache = new HashMap<>();
ToolCallbackResolver 解析器可以「工具查找器 / 注册表」,负责「存工具、找工具、匹配工具」,默认会加载 Provider 中的所有工具实例。
ToolCallbackResolver 并非保存了所有的工具实例,默认 Bean 中只有:
- 从
ToolCallbackProvider中加载,且加载后放入了StaticToolCallbackResolver中,是不可变的 - 从
SpringBeanToolCallbackResolver加载,内部封装了Spring容器,可以配置工具Bean名称,最终在执行时,可以从容器中查找
在上一篇的动态工具中,需要注意禁用了某个工具,在创建请求时,该工具信息不会给我大模型,但是 StaticToolCallbackResolver 中,还是保存了当前工具实例,这是需要注意的。