1. Spring AI工具配置概述
在构建AI驱动的应用程序时,工具调用(Tool Calling)是一个关键功能。Spring AI 1.x提供了灵活的机制来配置和管理工具,支持全局默认配置和运行时动态配置两种模式。这种设计允许开发者在不同场景下灵活控制工具的行为。
工具配置的核心价值在于:
- 解耦工具定义与使用:将工具实现与调用逻辑分离
- 动态性:运行时根据需要启用/禁用特定工具
- 复用性:全局工具可被多个对话共享
- 安全性:精确控制工具的执行上下文
2. 全局默认工具配置
2.1 配置方式与生命周期
全局默认工具通过defaultTools()方法配置,其生命周期与创建它们的ChatClient.Builder或ChatModel实例绑定:
java复制// 配置全局默认工具的ChatClient
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new DateTimeTools(), new WeatherTools())
.build();
// 配置全局默认工具的ChatModel
ChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(OllamaApi.builder().build())
.defaultOptions(ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new DateTimeTools()))
.build())
.build();
关键特点:
- 全局工具对所有通过该构建器创建的ChatClient实例可见
- 适用于需要跨多个对话共享的工具(如通用工具)
- 生命周期长,通常伴随应用整个运行周期
2.2 典型使用场景
全局工具特别适合以下场景:
- 常用工具:如时间日期工具、单位转换等基础功能
- 低风险工具:不涉及敏感数据或关键业务逻辑的操作
- 高频使用工具:需要被多个对话频繁调用的功能
java复制// 使用全局工具的对话示例
String response = chatClient.prompt("设置10分钟后的闹钟").call().content();
2.3 配置注意事项
使用全局工具时需注意:
- 线程安全:确保工具实现是线程安全的
- 状态管理:避免在工具中保存对话特定状态
- 资源占用:长期存在的工具要注意资源释放
- 安全边界:全局工具应有严格的权限控制
警告:全局工具配置不当可能导致工具在不该出现的上下文中可用。建议对敏感操作使用运行时工具而非全局工具。
3. 运行时动态工具配置
3.1 动态工具绑定机制
运行时工具通过tools()方法配置,仅对当前对话有效:
java复制// 使用运行时工具的对话示例
String response = ChatClient.create(chatModel)
.prompt("查询纽约天气")
.tools(new WeatherTools())
.call()
.content();
关键特点:
- 工具实例仅对当前Prompt有效
- 每次对话可指定不同的工具集
- 生命周期短暂,随对话结束而销毁
3.2 动态解析工具
Spring AI支持通过ToolCallbackResolver接口动态解析工具:
java复制// 自定义工具解析器示例
public class DynamicToolResolver implements ToolCallbackResolver {
@Override
public ToolCallback resolve(String toolName) {
if("weather".equals(toolName)) {
return new WeatherTools();
}
return null;
}
}
// 使用动态解析
ChatClient.create(chatModel)
.prompt("查询天气")
.tools("weather") // 通过名称动态解析
.call()
.content();
3.3 使用场景对比
| 特性 | 全局工具 | 运行时工具 |
|---|---|---|
| 作用范围 | 所有对话 | 单个对话 |
| 配置方式 | Builder级别 | Prompt级别 |
| 生命周期 | 长期 | 短暂 |
| 适用场景 | 基础服务 | 业务特定逻辑 |
| 性能影响 | 初始加载开销 | 每次对话开销 |
| 内存占用 | 持续占用 | 临时占用 |
4. 混合配置与优先级规则
4.1 配置叠加机制
当同时配置全局工具和运行时工具时:
java复制ChatClient client = ChatClient.builder(chatModel)
.defaultTools(new BasicTools()) // 全局工具
.build();
String response = client.prompt("请求")
.tools(new AdvancedTools()) // 运行时工具
.call()
.content();
4.2 优先级规则
Spring AI处理工具冲突的规则:
- 同名工具:运行时工具完全覆盖全局工具
- 合并策略:不同名工具会合并使用
- 执行顺序:运行时工具优先于全局工具
重要提示:运行时工具不会继承或扩展全局工具,而是完全替代。如果需要保留全局工具同时添加新工具,必须显式包含两者。
4.3 最佳实践建议
- 基础功能:使用全局工具(如单位转换)
- 业务逻辑:使用运行时工具(如订单查询)
- 敏感操作:总是使用运行时工具
- 性能敏感:优先考虑全局工具减少实例化开销
- 调试阶段:使用运行时工具便于隔离问题
5. 高级配置技巧
5.1 工具上下文传递
通过ToolContext传递额外上下文信息:
java复制ChatClient.create(chatModel)
.prompt("查询客户信息")
.tools(new CustomerTools())
.toolContext(Map.of("tenantId", "acme", "userId", "123"))
.call()
.content();
// 工具内获取上下文
@Tool
public Customer getCustomer(Long id, ToolContext context) {
String tenant = (String) context.get("tenantId");
// ...
}
5.2 返回结果控制
控制工具结果返回方式:
java复制// 直接返回给调用者(不经过模型)
@Tool(returnDirect = true)
public String sensitiveOperation() {
return "直接结果";
}
// 默认返回给模型继续处理
@Tool
public String normalOperation() {
return "模型将处理此结果";
}
5.3 工具组合策略
有效组合全局和运行时工具:
java复制// 保留全局工具并添加运行时工具
List<ToolCallback> allTools = new ArrayList<>();
allTools.addAll(globalTools); // 添加全局工具
allTools.addAll(runtimeTools); // 添加运行时工具
ChatClient.create(chatModel)
.prompt("复杂请求")
.tools(allTools)
.call()
.content();
6. 性能优化与问题排查
6.1 工具加载优化
- 延迟加载:对资源密集型工具实现懒加载
- 缓存机制:为工具结果添加合理缓存
- 连接池:工具涉及远程调用时使用连接池
java复制// 懒加载工具示例
public class LazyTool implements ToolCallback {
private ExpensiveService service;
@Override
public String call(String input) {
if(service == null) {
service = initService(); // 首次调用时初始化
}
return service.process(input);
}
}
6.2 常见问题排查
问题1:工具未被调用
- 检查工具名称是否匹配
- 验证工具描述是否清晰
- 确认输入参数格式正确
问题2:意外工具覆盖
- 检查全局和运行时工具命名冲突
- 使用唯一、描述性的工具名称
- 添加日志输出当前可用工具列表
问题3:性能下降
- 分析工具初始化时间
- 检查工具执行时间
- 评估网络调用开销
6.3 调试技巧
- 启用Spring AI调试日志:
properties复制logging.level.org.springframework.ai=DEBUG
- 工具调用跟踪:
java复制ChatClient.create(chatModel)
.prompt("请求")
.tools(new LoggingProxy(new ActualTools())) // 代理模式添加日志
.call()
.content();
- 上下文检查:
java复制@Tool
public String debugTool(ToolContext context) {
System.out.println("当前上下文: " + context);
return "调试信息";
}
