当前位置: 首页 > news >正文

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

文章目录

  • 1. 前言
  • 2. 加载流程
    • 2.1 初始化工具解析器
    • 2.2 全局默认
    • 3.3 运行时配置
  • 3. 执行流程
    • 3.1 构建模型请求
    • 3.2 工具执行
  • 4. 总结
    • 4.1 流程图
    • 4.2 一定要注册 ToolCallbackProvider 为 Bean 吗
    • 4.2 ToolCallbackResolver 再思考

1. 前言

在上篇文档中,我们通过自定义的ToolCallbackProvider实现了从文件动态加载工具,有必要深入了解一下Provider的加载和执行流程,便于我们在实际项目中更好地扩展和定制工具注册机制。

2. 加载流程

我们使用的是@ComponentToolCallbackProvider注册到了Spring容器中,对象本身的实例化、销毁生命周期由Spring负责。

2.1 初始化工具解析器

在程序启动过程中,因为我们将ToolCallbackProvider注册为了Bean,在ToolCallingAutoConfiguration自动配置类注册ToolCallbackResolver时,会使用ObjectProvider机制获取到该Bean实例,并调用自定义的ToolCallbackProvider获取到工具实例,合并到总工具列表。

首先会将ToolCallbackProvider合并到一个集合中:

// Merge ToolCallbackProviders from both ObjectProviders.List<ToolCallbackProvider>totalToolCallbackProviders=newArrayList<>(tcbProviderList.stream().flatMap(List::stream).toList());totalToolCallbackProviders.addAll(tcbProviders.stream().toList());


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

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


最终Provider中的工具会加载到StaticToolCallbackResolver,并统一封装到DelegatingToolCallbackResolver中:

// 1. 静态解析器:持有所有合并后的工具(固定+动态)varstaticToolCallbackResolver=newStaticToolCallbackResolver(allFunctionAndToolCallbacks);// 2. Spring Bean 解析器:从 Spring 容器中查找工具(@Tool / @Bean)varspringBeanToolCallbackResolver=SpringBeanToolCallbackResolver.builder().applicationContext(applicationContext).build();// 3. 委托解析器:依次使用两个解析器查找工具(优先静态,再查Bean)returnnewDelegatingToolCallbackResolver(List.of(staticToolCallbackResolver,springBeanToolCallbackResolver));

2.2 全局默认

接着进入到@Configuration中进行ChatClient初始化,这里配置了全局默认的ToolCallbackProvider

@ConfigurationpublicclassChatClientConfig{@Bean("zhiPuAiChatClient")publicChatClientzhiPuAiChatClient(ZhiPuAiChatModelzhiPuAiChatModel,FileToolCallbackProviderfileToolCallbackProvider){ChatClientclient=ChatClient.builder(zhiPuAiChatModel).defaultToolCallbacks(fileToolCallbackProvider).build();returnclient;}}

defaultToolCallbacks方法会在默认的请求对象中设置ToolCallbackProvider对象实例:

@OverridepublicBuilderdefaultToolCallbacks(ToolCallbackProvider...toolCallbackProviders){this.defaultRequest.toolCallbacks(toolCallbackProviders);returnthis;}

继续调用DefaultChatClientBuilder#toolCallbacks()

@OverridepublicChatClientRequestSpectoolCallbacks(ToolCallbackProvider...toolCallbackProviders){Assert.notNull(toolCallbackProviders,"toolCallbackProviders cannot be null");Assert.noNullElements(toolCallbackProviders,"toolCallbackProviders cannot contain null elements");this.toolCallbackProviders.addAll(List.of(toolCallbackProviders));returnthis;}

最后ToolCallbackProvider会被添加到DefaultChatClientRequestSpec的属性中:

publicstaticclassDefaultChatClientRequestSpecimplementsChatClientRequestSpec{privatefinalList<ToolCallbackProvider>toolCallbackProviders=newArrayList<>();//.............}

ChatClient对象构建完成后,这里只存储了Provider实例,toolCallbacks工具实例对象为空:

3.3 运行时配置

如果在ChatClient调用过程中配置ToolCallbackProvider

Stringcontent=deepSeekChatClient.prompt("几点了").toolCallbacks(fileToolCallbackProvider).call().content();

和全局默认一样,也只保存Provider实例,区别是保存在ChatClientRequestSpec请求对象中,请求结束时对象就被销毁了,下一次调用call()/stream()时,又是新的请求对象了:

@OverridepublicChatClientRequestSpectoolCallbacks(ToolCallbackProvider...toolCallbackProviders){Assert.notNull(toolCallbackProviders,"toolCallbackProviders cannot be null");Assert.noNullElements(toolCallbackProviders,"toolCallbackProviders cannot contain null elements");this.toolCallbackProviders.addAll(List.of(toolCallbackProviders));returnthis;}

3. 执行流程

调用call()时的方法入口:

@OverridepublicCallResponseSpeccall(){BaseAdvisorChainadvisorChain=buildAdvisorChain();returnnewDefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this),...);}

3.1 构建模型请求

DefaultChatClientUtils#toChatClientRequest方法中,构建AI模型可执行的ChatClientRequest时,会判断toolCallbackProviders是否为空,不为空则构建工具相关的对话配置信息DefaultToolCallingChatOptions

ChatOptionsprocessedChatOptions=inputRequest.getChatOptions();// ========== 关键判断:是否包含【工具调用配置】 ==========if(!inputRequest.getToolNames().isEmpty()||!inputRequest.getToolCallbacks().isEmpty()// 函数/方法型工具封装对象||!inputRequest.getToolCallbackProviders().isEmpty()||!CollectionUtils.isEmpty(inputRequest.getToolContext())){// 无配置 → 新建工具调用配置if(processedChatOptions==null){processedChatOptions=newDefaultToolCallingChatOptions();}// 有普通配置 → 转换为【工具调用专用配置】elseif(processedChatOptionsinstanceofDefaultChatOptionsdefaultChatOptions){processedChatOptions=ModelOptionsUtils.copyToTarget(...);}}

如果有工具,然后才开始调用ToolCallbackProvider#getToolCallbacks方法获取工具实例对象(懒加载):

if(processedChatOptionsinstanceofToolCallingChatOptionstoolCallingChatOptions){// 1. 合并工具名称Set<String>toolNames=ToolCallingChatOptions.mergeToolNames(...);toolCallingChatOptions.setToolNames(toolNames);// 2. 懒加载工具提供者 → 生成 ToolCallback(核心!)List<ToolCallback>allToolCallbacks=newArrayList<>(inputRequest.getToolCallbacks());for(varprovider: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方法通过解析器获取可用工具对象:

// 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中传递的工具名称,在解析器中查找工具实例:

publicList<ToolDefinition>resolveToolDefinitions(ToolCallingChatOptionschatOptions){Assert.notNull(chatOptions,"chatOptions cannot be null");List<ToolCallback>toolCallbacks=newArrayList<>(chatOptions.getToolCallbacks());for(StringtoolName: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;}ToolCallbacktoolCallback=this.toolCallbackResolver.resolve(toolName);if(toolCallback==null){logger.warn(POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING,toolName);thrownewIllegalStateException("No ToolCallback found for tool name: "+toolName);}toolCallbacks.add(toolCallback);}returntoolCallbacks.stream().map(ToolCallback::getToolDefinition).toList();}

最终,所有的默认工具实例对象,会拼接到对话上下文传递给大模型,让大模型判断是否调用哪个工具。

3.2 工具执行

当需要调用工具时,进入到ToolCallingManager工具执行器生命周期。ChatModel执行请求后,如果要调用工具,模型会返回toolCalls消息:


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

ZhiPuAiChatModel#call()方法中, 会先判断是否需要执行工具,然后调用DefaultToolCallingManager#executeToolCalls()方法:

if(this.toolExecutionEligibilityPredicate.isToolExecutionRequired(requestPrompt.getOptions(),response)){vartoolExecutionResult=this.toolCallingManager.executeToolCalls(requestPrompt,response);if(toolExecutionResult.returnDirect()){// Return tool execution result directly to the client.returnChatResponse.builder().from(response).generations(ToolExecutionResult.buildGenerations(toolExecutionResult)).build();}else{// Send the tool execution result back to the model.returnthis.call(newPrompt(toolExecutionResult.conversationHistory(),requestPrompt.getOptions()));}}

executeToolCall()只转入了工具名称,会先从toolCallbacks中查找(构建时直接传入的ToolCallback),没找到再调用解析器获取工具实例:

// 从 当前请求携带的所有工具实例 中查找ToolCallbacktoolCallback=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 吗

回答:不需要!

直接通过类也是可以的:

@Bean("zhiPuAiChatClient")publicChatClientzhiPuAiChatClient(ZhiPuAiChatModelzhiPuAiChatModel){ChatClientclient=ChatClient.builder(zhiPuAiChatModel).defaultToolCallbacks(newFileToolCallbackProvider(SchemaType.JSON_SCHEMA)).build();returnclient;}

只是默认的ToolCallbackResolver无法通过ObjectProvider机制获取到工具实例,在创建模型请求时,会懒加载工具提供者中的所有实例,在执行时也会从先从ToolCallingChatOptions中查找。

4.2 ToolCallbackResolver 再思考

在之前工具解析器的篇章中,我们只了解了ToolCallbackResolver是一个通过名称找到工具的解析器。实现子类中,不仅提供解析,还提供了工具注册表,内存中维护了所有的工具实例 ,例如SpringBeanToolCallbackResolver

privatestaticfinalMap<String,ToolCallback>toolCallbacksCache=newHashMap<>();

ToolCallbackResolver解析器可以「工具查找器 / 注册表」,负责「存工具、找工具、匹配工具」,默认会加载Provider中的所有工具实例。

ToolCallbackResolver并非保存了所有的工具实例,默认Bean中只有:

  • ToolCallbackProvider中加载,且加载后放入了StaticToolCallbackResolver中,是不可变的
  • SpringBeanToolCallbackResolver加载,内部封装了Spring容器,可以配置工具Bean名称,最终在执行时,可以从容器中查找

在上一篇的动态工具中,需要注意禁用了某个工具,在创建请求时,该工具信息不会给我大模型,但是StaticToolCallbackResolver中,还是保存了当前工具实例,这是需要注意的。

http://www.jsqmd.com/news/578259/

相关文章:

  • 2026年上海保洁服务推荐榜单:日常/精细/定点/厂房/开荒/装修后/别墅/展会/深度/商场保洁,专业高效的全场景洁净解决方案 - 品牌企业推荐师(官方)
  • 计算机毕业设计springboot在线运营工单处理系统 基于SpringBoot的客户服务工单流转与协同处理平台 SpringBoot框架下的智能运维服务请求跟踪管理系统
  • 2026年格兰富水泵厂家推荐排行榜:成套供水机组/无负压供水机组/供暖循环泵/空调循环泵/污水泵/污水提升泵/循环泵/不锈钢水泵/密封泵/螺杆泵,专业流体解决方案实力之选 - 品牌企业推荐师(官方)
  • 2026年AI风口已至!月薪3万+岗位盘点+零基础转行指南,速收藏!
  • 告别ArcGIS依赖!用QGIS 3.28把SHP属性表一键导出Excel,附赠3个数据清洗小技巧
  • 2026年 胶带厂家推荐排行榜:双面胶带/PET胶带/绝缘胶带/玛拉胶带/高温胶带/线圈胶带/保温胶带/透明胶带/警示胶带/布基胶带/美纹路胶带,精选粘接解决方案实力品牌! - 品牌企业推荐师(官方)
  • 3个AI视频总结功能让B站信息处理效率提升300%
  • 给我找一个能用的 typora 序列号 正版买了 爽 淘宝便宜 5 块
  • 3步搞定小红书无水印下载:XHS-Downloader开源神器实战全解析
  • 新闻科技简报 (2026-04-02)
  • 利用快马平台快速构建b站a8直播观看页面原型
  • 提示词合集【自用】
  • 超自动化运维的终极目标:让系统自治运行
  • 告别手动复制粘贴!用Python脚本一键搞定Labelme标注转YOLOv8训练集(附自动划分数据集)
  • Comsol 实现水岩耦合作用下围岩数值模拟
  • 如何用Python快速开发Android应用:Python for Android完整指南
  • 13-40K!AI大模型应用工程师,非常详细收藏我这一篇就够了
  • Video-subtitle-remover:让视频创作者实现硬字幕无痕去除的AI解决方案
  • 2026年 四氟防腐储罐厂家推荐榜单:四氟喷涂储罐/四氟防腐塔器/PFA喷涂储罐/衬氟管道,专注高温防腐的匠心工艺之选 - 品牌企业推荐师(官方)
  • 2026届最火的降重复率平台解析与推荐
  • ios企业签名证书创建从零到一教程最新
  • 广州PMP培训机构怎么选?才聚是标准答案
  • 拯救受损二维码:用QRazyBox实现高效恢复的4个实战策略
  • 火山方舟管理运维手册
  • CSS动画实战:5分钟搞定微信语音发送震动效果(附完整代码)
  • 今日心理学知识2026.4.2
  • Claude Code Windows 常用快捷/命令
  • 天地图三维地名服务集成指南:从Token申请到避坑配置(Cesium 1.80+适用)
  • 保姆级教程:在Windows下用VSCode和STM32CubeProgrammer给Pixhawk4飞控烧写Bootloader
  • 从85分到95+:复盘我在科大奥锐虚拟仿真实验平台踩过的那些‘坑’