Spring AI 1.x 系列【22】深度拆解 ToolCallbackProvider 生命周期与调用链路

文章目录

  • [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. 加载流程

我们使用的是 @ComponentToolCallbackProvider 注册到了 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());

然后调用所有 ProvidersgetToolCallbacks() 方法,解析出所有工具:

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 中,还是保存了当前工具实例,这是需要注意的。

相关推荐
李元豪2 小时前
3分分类计算差值
人工智能·分类·数据挖掘
萌>__<新2 小时前
AI聊天助手-测试报告
人工智能·python
KC2702 小时前
OpenAkita 深度解析:开源多Agent协作框架的实战指南
人工智能·aigc·ai编程
RNEA ESIO2 小时前
Spring Boot应用关闭分析
java·spring boot·后端
元拓数智2 小时前
基于数据关系映射的企业AI系统权限最小化落地方法
人工智能
柠萌f2 小时前
从“尝鲜”到“落地”:易元AI真实商家案例拆解(美妆/服饰/3C)
人工智能
Ashore11_2 小时前
用户中心项目—需求分析
java
人工智能AI技术2 小时前
阿里云发布Qwen3.5-Omni,全模态大战开启
人工智能
用户446594547872 小时前
用 React 写 CLI 是什么体验?—— Ink 框架深度解析与实战
人工智能