Spring AI 1.x 系列【23】:工具配置详解(全局默认+运行时动态)

文章目录

  • [1. 概述](#1. 概述)
  • [2. 全局默认工具](#2. 全局默认工具)
    • [2.1 defaultTools()](#2.1 defaultTools())
    • [2.2 defaultToolCallbacks()](#2.2 defaultToolCallbacks())
      • [2.2.1 ToolCallback 参数](#2.2.1 ToolCallback 参数)
      • [2.2.2 ToolCallbackProvider 参数](#2.2.2 ToolCallbackProvider 参数)
    • [2.3 defaultToolNames()](#2.3 defaultToolNames())
  • [3. 运行时动态工具](#3. 运行时动态工具)
    • [3.1 tools()](#3.1 tools())
    • [3.2 其他方法](#3.2 其他方法)
  • [4. 总结](#4. 总结)

1. 概述

从工具的生命周期来说,Spring AI 支持两种类型的工具,二者在创建时机、优先级、线程安全等方面存在显著差异,核心设计要点对比如下:

特性 全局默认工具 运行时动态工具
创建时机 应用启动 请求时
合并优先级 低(被覆盖) 高(覆盖默认)
线程安全 需考虑(共享) 无需考虑(请求隔离)
内存管理 常驻 请求结束回收
适用场景 通用能力 用户特定资源、会话状态

全局默认工具 :应用级生命周期,是在构建 ChatClient 时使用 defaultTool 相关方法传入的工具,应用关闭时销毁;

运行时动态工具 :请求/会话级生命周期,是在使用 call/stream 时使用 tool 相关方法传入的工具,请求结束后由GC回收。

2. 全局默认工具

ChatClient 提供了一组 defaultTool 相关方法,用于配置全局默认工具:

简单示例如下:

java 复制代码
ChatClient chatClient = ChatClient.builder(chatModel)
    // 默认工具配置
    .defaultToolNames("searchBooks", "getWeather")  // Bean名称
    .defaultTools(new MyTools())                    // @Tool对象
    .defaultToolCallbacks(callback1, callback2)     // 直接注册
    .defaultToolCallbacks(provider)                 // Provider
    .build();

2.1 defaultTools()

defaultTools() 方法将对象中标注了 @Tool 的方法自动解析为工具,无需手动构建 ToolCallback,适合快速将业务服务类转换为工具。

工具定义示例:

java 复制代码
@Service
public class MyTools {
    
    @Tool(description = "搜索书籍")
    public Book searchBooks(@ToolParam(description = "书名关键词") String keyword) {
        return bookService.search(keyword);
    }
    
    @Tool(description = "获取天气")
    public Weather getWeather(@ToolParam(description = "城市") String city) {
        return weatherService.get(city);
    }

defaultTools() 方法本质是调用 defaultRequesttools 方法,最终通过反射扫描 @Tool 注解方法并转换为 ToolCallback

java 复制代码
// defaultTools() 方法入口
@Override
public Builder defaultTools(Object... toolObjects) {
    this.defaultRequest.tools(toolObjects);
    return this;
}

// 底层调用 ToolCallbacks.from() 解析工具对象
@Override
public ChatClientRequestSpec tools(Object... toolObjects) {
    Assert.notNull(toolObjects, "toolObjects cannot be null");
    Assert.noNullElements(toolObjects, "toolObjects cannot contain null elements");
    this.toolCallbacks.addAll(Arrays.asList(ToolCallbacks.from(toolObjects)));
    return this;
}

// ToolCallbacks.from() 核心逻辑:通过 MethodToolCallbackProvider 提取 @Tool 方法
public static ToolCallback[] from(Object... sources) {
    return MethodToolCallbackProvider.builder().toolObjects(sources).build().getToolCallbacks();
}

MethodToolCallbackProvidergetToolCallbacks() 方法会通过反射完成以下流程,最终生成 ToolCallback 实例:

  1. 遍历所有工具对象,若为 AOP 代理对象则获取目标类,否则直接获取当前类;

  2. 过滤出标注了 @Tool 注解的方法,排除函数式接口方法、合成方法和桥接方法;

  3. 为每个有效方法构建 MethodToolCallback,配置工具定义、元数据、结果转换器等;

  4. 校验工具回调合法性(如避免重名),最终返回 ToolCallback 数组。

最终所有工具会存储在 DefaultChatClientRequestSpectoolCallbacks 列表中,请求时通过反射调用 toolMethod.invoke(toolObject, args) 执行工具。

适用场景

  • 快速将业务服务类转换为工具,无需手动构建 ToolCallback

  • 适合方法级别的工具定义,追求开发效率与代码可读性。

2.2 defaultToolCallbacks()

defaultToolCallbacks() 是重载方法,支持直接传入 ToolCallback 对象或 ToolCallbackProvider,灵活适配不同工具定义场景,重载形式如下:

  • defaultToolCallbacks(ToolCallback... toolCallbacks):直接传入单个或多个 ToolCallback 对象;

  • defaultToolCallbacks(List<ToolCallback> toolCallbacks):传入 ToolCallback 列表;

  • defaultToolCallbacks(ToolCallbackProvider... toolCallbackProviders):传入 ToolCallbackProvider,可以自定义提供工具。

2.2.1 ToolCallback 参数

直接传入 ToolCallback 对象,适合定义函数型工具,可完全控制工具逻辑、输入类型和元数据,示例如下:

java 复制代码
// 构建 ToolCallback(函数型工具)
ToolCallback callback1 = FunctionToolCallback.builder("searchBooks", (request, context) -> {
            return bookService.search(request.keyword());
        })
        .description("搜索书籍")
        .inputType(BookSearchRequest.class)
        .build();

ToolCallback callback2 = FunctionToolCallback.builder("getWeather", (request, context) -> {
            return weatherService.get(request.city());
        })
        .description("获取天气")
        .inputType(WeatherRequest.class)
        .build();

方法入口与执行流程:

java 复制代码
// 方法入口
@Override
public Builder defaultToolCallbacks(ToolCallback... toolCallbacks) {
    this.defaultRequest.toolCallbacks(toolCallbacks);
    return this;
}

@Override
public Builder defaultToolCallbacks(List<ToolCallback> toolCallbacks) {
    this.defaultRequest.toolCallbacks(toolCallbacks);
    return this;
}

// 底层存储逻辑:与 defaultTools() 结果存入同一列表
@Override
public ChatClientRequestSpec toolCallbacks(List<ToolCallback> toolCallbacks) {
    Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
    Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
    this.toolCallbacks.addAll(toolCallbacks);
    return this;
}

执行流程

  1. 通过 FunctionToolCallback.builder() 构建工具对象
  2. 直接存入 defaultRequest.toolCallbacks 列表
  3. 请求时无需额外解析,直接使用

适用场景

  • 完全控制工具的定义和行为,需自定义输入类型、元数据或结果转换;
  • 工具逻辑复杂,需要访问 ToolContext
  • 偏好函数式风格定义工具。

2.2.2 ToolCallbackProvider 参数

通过 ToolCallbackProvider 批量提供工具,适合工具按业务领域分组、动态提供工具集的场景,示例如下:

java 复制代码
// ToolCallbackProvider 定义
public class MyToolProvider implements ToolCallbackProvider {
    
    @Override
    public ToolCallback[] getToolCallbacks() {
        return new ToolCallback[] {
            FunctionToolCallback.builder("tool1", ...).build(),
            FunctionToolCallback.builder("tool2", ...).build(),
            FunctionToolCallback.builder("tool3", ...).build()
        };
    }
}

// 使用方式
ToolCallbackProvider provider = new MyToolProvider();
chatClient.builder().defaultToolCallbacks(provider).build();

执行流程

  1. 创建 ToolCallbackProvider 实例并传入 defaultToolCallbacks()
  2. Provider 存入 defaultRequest.toolCallbackProviders 列表;
  3. 请求时,调用 provider.getToolCallbacks() 懒加载获取 ToolCallback 数组;
  4. 将获取到的工具合并到最终工具列表中执行。

适用场景

  • 批量提供相关工具(如数据库操作工具集、电商订单工具集);

  • 工具按业务领域分组管理;

  • 支持动态工具集(Provider 可按条件返回不同工具)。

2.3 defaultToolNames()

通过工具名称引用已定义的工具(如 Spring Bean 工具、ToolCallbackProvider 提供的工具),无需硬编码依赖工具实例,方法入口如下:

java 复制代码
public Builder defaultToolNames(String... toolNames) {
    this.defaultRequest.toolNames(toolNames);
    return this;
}

应用启动时,defaultToolNames 传入的工具名称会存入 DefaultChatClientRequestSpectoolNames 列表;当 ChatClient 发送请求时,会经历以下解析流程:

  1. 请求构建阶段,DefaultChatClientUtils.toChatClientRequest() 会合并 defaultToolNames 与请求级 toolNames

  2. ChatModel#call() 方法执行时,ToolCallingManager 会调用 resolveToolDefinitions() 解析工具名称;

  3. 通过 DelegatingToolCallbackResolver 依次从 StaticToolCallbackResolver(存储 Provider 工具)和 Spring 容器(存储 Bean 工具)中获取对应 ToolCallback

  4. 若未找到对应工具,会抛出 IllegalStateException 异常。

异常示例

java 复制代码
2026-04-03T11:37:00.868+08:00  WARN 16552 --- [ai-chat-demo] [           main] o.s.a.m.tool.DefaultToolCallingManager   : LLM may have adapted the tool name 'query', especially if the name was truncated due to length limits. If this is the case, you can customize the prefixing and processing logic using McpToolNamePrefixGenerator
Exception in thread "main" java.lang.IllegalStateException: No ToolCallback found for tool name: query
	at org.springframework.ai.model.tool.DefaultToolCallingManager.resolveToolDefinitions(DefaultToolCallingManager.java:117)
	at org.springframework.ai.zhipuai.ZhiPuAiChatModel.createRequest(ZhiPuAiChatModel.java:564)
	at org.springframework.ai.zhipuai.ZhiPuAiChatModel.call(ZhiPuAiChatModel.java:251)
	at org.springframework.ai.chat.client.advisor.ChatModelCallAdvisor.adviseCall(ChatModelCallAdvisor.java:56)

适用场景

  • 工具已在其他地方定义为 Spring Bean

  • 按名称动态引用工具,避免硬编码依赖;

  • 支持工具热替换(Bean 可动态更新)。

3. 运行时动态工具

运行时动态工具是在发起请求时配置的工具,属于请求/会话级生命周期,会继承全局默认工具的配置并可覆盖默认工具,ChatClient 提供的相关方法如下:

简单示例:

java 复制代码
chatClient.prompt()
    .user("查询信息")
    // 运行时工具配置(与默认工具合并)
    .toolNames("extraTool")          // 额外Bean名称
    .tools(new ExtraTools())         // 额外@Tool对象
    .toolCallbacks(extraCallback)    // 额外ToolCallback
    .toolContext(Map.of("requestId", "req456"))  // 上下文
    .call().content();

3.1 tools()

tools() 方法用于在运行时传入 @Tool 注解对象,核心执行步骤如下:

java 复制代码
// 第一步:创建请求规格,继承默认配置(默认工具、系统提示等)
ChatClient.ChatClientRequestSpec chatClientRequestSpec = deepSeekChatClient.prompt("几点了");

// 第二步:配置运行时动态工具,覆盖或补充默认工具
ChatClient.ChatClientRequestSpec clientRequestSpec = chatClientRequestSpec.tools(new DateTimeTools());

// 第三步:执行调用,发送请求到 AI 模型
ChatClient.CallResponseSpec callResponseSpec = clientRequestSpec.call();

// 第四步:提取响应内容
String content = callResponseSpec.content();

核心逻辑

  1. 调用 prompt() 方法时,会创建新的 DefaultChatClientRequestSpec 实例,复制 defaultChatClientRequest 中的默认配置(包括全局默认工具);
  2. 后续调用 tools() 方法时,会将运行时工具添加到新的请求规格中,与默认工具合并。
  3. 请求结束后,该请求规格及其中的工具实例会被 GC 回收,下次请求需重新配置运行时工具。

3.2 其他方法

运行时动态工具的其他方法(toolNames()toolCallbacks()toolContext())与全局默认工具的对应方法逻辑一致,均用于配置运行时工具或上下文,差异仅在于生命周期(请求级 vs 应用级),此处不再赘述。

4. 总结

核心结论:全局默认工具适合通用能力,运行时动态工具适合上下文依赖;方法型工具适合快速开发,函数型工具适合精细控制;生产级设计需要兼顾权限、监控、审计与安全。

4.1 是否需要配置全局默认工具?

取决于应用场景,核心判断标准为"是否依赖用户上下文、是否可复用",具体推荐如下:

场景 推荐 原因
通用能力工具(天气查询、搜索、计算) ✅ 全局默认 所有用户/请求共享,无需每次配置
业务领域工具(订单查询、库存管理) ✅ 全局默认 业务系统核心能力,稳定复用
用户私有资源工具(个人文件、私有数据) ❌ 运行时动态 需要用户上下文,请求隔离
会话状态工具(购物车、对话历史) ❌ 运行时动态 状态随会话变化,不能共享
临时/一次性工具(特定任务处理) ❌ 运行时动态 用完即弃,避免内存占用

4.2 函数型还是方法型工具?

Spring AI 支持两种工具创建方式,分别对应方法型和函数型工具,核心区别与推荐原则如下:

方法型工具(一个方法对应一个工具):

  • 声明式:@Tool + @ToolParam 定义工具,框架自动扫描生成;

  • 编程式:普通 Java 对象方法,手动获取方法对象、定义 ToolDefinition 生成工具。

函数型工具(一个函数对象对应一个工具):

  • 声明式:通过 @Bean 注解声明,框架自动解析注册;

  • 编程式:普通 Java 函数对象,手动获取函数对象、定义 ToolDefinition 生成工具。

推荐原则

优先选择 场景
方法型 业务服务类、团队协作、代码可读性优先
函数型 工具逻辑复杂、需要 ToolContext、自定义元数据
混合使用 简单工具用方法型,复杂工具用函数型

4.3 生产级别的工具设计

4.3.1 工具注册策略对比

生产环境中,工具注册推荐使用 ToolCallbackProvider,相比 Bean 名称注册更具灵活性和可扩展性,具体对比如下:

特性 方案一:Bean 名称 方案二:ToolCallbackProvider
动态 CRUD ❌ 不支持 ✅ 支持
热更生效 ❌ 需重启 ✅ 实时生效
业务侵入性 ❌ 需注册 Bean ✅ 无侵入
分层解耦 ❌ 耦合容器 ✅ 完全解耦
生产级可用性 ⭐⭐ ⭐⭐⭐⭐⭐

4.3.2 生产级工具管理架构

生产级工具管理需支持动态扩展、权限管控、实时监控和生命周期管理,推荐采用四层架构,具体如下:

text 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    生产级工具管理架构                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │                      工具注册中心                               │  │
│  ├───────────────────────────────────────────────────────────────┤  │
│  │  ToolCallbackRegistry                                          │  │
│  │  ├─ register(toolName, ToolCallback)     // 注册               │  │
│  │  ├─ unregister(toolName)                 // 注销               │  │
│  │  ├─ resolve(toolName)                    // 解析               │  │
│  │  ├─ list()                               // 列表               │  │
│  │  └─ update(toolName, ToolCallback)       // 更新               │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│                              ▼                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │                      工具权限控制                               │  │
│  ├───────────────────────────────────────────────────────────────┤  │
│  │  ToolAccessManager                                             │  │
│  │  ├─ checkPermission(userId, toolName)   // 权限检查             │  │
│  │  ├─ getAccessibleTools(userId)          // 获取可用工具         │  │
│  │  └─ audit(userId, toolName, action)     // 审计日志             │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│                              ▼                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │                      工具执行监控                               │  │
│  ├───────────────────────────────────────────────────────────────┤  │
│  │  ToolExecutionMonitor                                          │  │
│  │  ├─ recordCall(toolName, duration, result)  // 调用记录         │  │
│  │  ├─ getStatistics(toolName)                 // 统计数据         │  │
│  │  ├─ alertOnFailure(toolName, threshold)     // 失败告警         │  │
│  │  └─ getSlowCalls(threshold)                 // 慢调用分析       │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                              │                                      │
│                              ▼                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │                      工具生命周期管理                           │  │
│  ├───────────────────────────────────────────────────────────────┤  │
│  │  ToolLifecycleManager                                          │  │
│  │  ├─ init()              // 初始化                              │  │
│  │  ├─ healthCheck()       // 健康检查                             │  │
│  │  ├─ reload()            // 热重载                              │  │
│  │  └─ destroy()           // 销毁                                │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

4.3.3 生产级工具设计原则

原则一:工具单一职责

每个工具只处理一件事,避免LLM调用混淆,降低故障风险:

java 复制代码
// ❌ 错误:一个工具做多件事
@Tool(description = "订单操作")
public String orderOperation(String orderId, String action) {
    if ("query".equals(action)) return query(orderId);
    if ("cancel".equals(action)) return cancel(orderId);
    if ("refund".equals(action)) return refund(orderId);
    // LLM 容易混淆,调用出错
}

// ✅ 正确:每个工具单一职责
@Tool(description = "查询订单")
public Order queryOrder(@ToolParam(desc = "订单ID") String orderId) { ... }

@Tool(description = "取消订单")
public boolean cancelOrder(@ToolParam(desc = "订单ID") String orderId) { ... }

@Tool(description = "退款订单")
public boolean refundOrder(@ToolParam(desc = "订单ID") String orderId) { ... }
原则二:输入参数明确

工具参数需清晰描述,明确格式和要求,避免LLM无法正确传参:

java 复制代码
// ❌ 错误:参数模糊
@Tool(description = "查询")
public Object query(String input) { ... }  // LLM 不知道传什么

// ✅ 正确:参数清晰
@Tool(description = "根据订单ID查询订单详情")
public Order queryOrderById(
    @ToolParam(description = "订单ID,格式:ORD-XXXXX", required = true) 
    String orderId
) { ... }
原则三:结果可解析

工具返回结果需精简、结构化,避免返回复杂对象,便于LLM理解与业务系统对接:

java 复制代码
// ❌ 错误:返回复杂对象,LLM 难解析
@Tool(description = "查询用户")
public User queryUser(String userId) {
    return userRepository.findById(userId);  // 包含几十个字段
}

// ✅ 正确:返回结构化、精简结果
@Tool(description = "查询用户基本信息")
public UserInfo queryUserInfo(String userId) {
    User user = userRepository.findById(userId);
    return new UserInfo(user.getId(), user.getName(), user.getEmail());  // 只返回关键信息
}
原则四:幂等性设计

查询类工具需保证幂等(重复调用结果一致);非幂等工具需添加防重机制,避免重复操作导致异常:

java 复制代码
// ✅ 正确:幂等工具,重复调用结果一致
@Tool(description = "查询订单状态")
public OrderStatus getOrderStatus(String orderId) {
    return orderRepository.findStatus(orderId);  // 只读,幂等
}

// ❌ 非幂等工具需谨慎(需添加防重机制)
@Tool(description = "发送通知")
public boolean sendNotification(String userId, String message) {
    // 每次调用都会发送,需要防重机制
    if (notificationCache.exists(userId, message)) {
        return true;  // 已发送,跳过
    }
    notificationService.send(userId, message);
    notificationCache.mark(userId, message);
    return true;
}
原则五:异常友好处理

工具异常需转换为LLM与用户可理解的文本,避免直接抛出技术异常中断业务流程:

java 复制代码
// ✅ 正确:异常转换为友好消息
@Tool(description = "查询订单")
public String queryOrder(String orderId) {
    try {
        Order order = orderService.findById(orderId);
        return formatOrder(order);
    } catch (OrderNotFoundException e) {
        return "订单不存在:" + orderId;  // LLM 可理解的错误
    } catch (Exception e) {
        return "查询失败,请稍后重试";
    }
}

// ❌ 错误:直接抛异常
@Tool(description = "查询订单")
public Order queryOrder(String orderId) {
    return orderService.findById(orderId);  // 异常会中断对话
}
原则六:敏感数据保护

用户敏感信息(手机号、邮箱、银行卡号等)需脱敏处理,避免数据泄露:

java 复制代码
// ✅ 正确:敏感数据脱敏
@Tool(description = "查询用户信息")
public UserInfo queryUserInfo(String userId) {
    User user = userService.findById(userId);
    return new UserInfo(
        user.getId(),
        user.getName(),
        maskEmail(user.getEmail()),      // 邮箱脱敏:a***@gmail.com
        maskPhone(user.getPhone())       // 电话脱敏:138****1234
    );
}

4.3.4 工具最佳实践总结

最佳实践 说明
单一职责 每个工具只做一件事
参数清晰 使用 @ToolParam 详细描述参数
结果精简 只返回必要信息,避免复杂对象
幂等设计 重复调用结果一致,非幂等需防重
异常友好 异常转换为 LLM 可理解的文本
数据脱敏 保护敏感信息,避免泄露
权限控制 按用户/角色限制工具访问
调用监控 记录调用次数、耗时、成功率
审计日志 关键操作留痕,便于追溯
文档完善 工具描述准确,包含示例
相关推荐
CesareCheung2 小时前
Python+Vue +K6接口性能压测平台搭建
开发语言·vue.js·python
m0_462605222 小时前
R4Pytorch实现:LSTM-火灾温度预测
人工智能·rnn·lstm
AI-小柒2 小时前
大模型API中转推荐:Dataeyes API 600+模型统一网关与负载均衡部署,claude编程、香蕉生图、视频大模型聚合平台
大数据·运维·开发语言·人工智能·算法·机器学习·负载均衡
学技术的大胜嗷2 小时前
详细讲解YOLO 里的 P、R、F1、PR 曲线、AP 和 mAP
人工智能·计算机视觉·目标跟踪
遇见你...2 小时前
A02 Spring-IOC和DI注解开发
数据库·spring·sqlserver
人工干智能2 小时前
科普:Python / Numpy / PyTorch 的数据拼接方法
pytorch·python·numpy
lulu12165440782 小时前
大模型API中转平台weelinking技术深度解析:架构、性能与部署实践
运维·人工智能·架构·ai编程
智能运维指南2 小时前
嘉为蓝鲸 DevOps 平台与 AI 技术结合:推动数字化转型的行业标杆
运维·人工智能·devops
DeepModel2 小时前
机器学习降维:因子分析(Factor Analysis)通俗完整版
人工智能·机器学习