在前几篇文章中,我们学习了如何使用 Spring AI 进行基础的对话、流式响应、会话记忆等功能。但在实际应用中,AI 模型往往需要与外部系统交互,获取实时信息或执行特定操作。这就是 Tool Calling(工具调用) 的核心价值所在。
本文将深入探讨 Spring AI 的工具调用机制,从基础概念到高级特性,帮助您构建功能强大的 AI 应用。
一、什么是 Tool Calling?
1.1 核心概念
Tool Calling 是指 AI 模型在执行任务时,可以调用预定义的工具(函数/方法)来获取信息或执行操作。这是一种让 AI "突破"自身知识限制的重要机制。
典型场景:
• 📅 获取当前时间、日期等实时信息 • 🌤️ 查询天气、股票等动态数据 • 💾 执行数据库增删改查操作 • 🔔 设置闹钟、发送通知等系统操作 • 📊 检索企业内部文档(RAG)
1.2 工作原理
用户提问 → AI 分析 → 决定调用工具 → Spring AI 执行工具 → 返回结果给 AI → AI 生成最终响应关键组件:
1. Tool Definition:工具定义(名称、描述、参数 Schema) 2. Tool Callback:工具回调(实际执行的 Java 方法) 3. ToolCallingManager:工具执行管理器 4. ToolCallAdvisor:工具调用拦截器(自动模式)
二、快速开始:最简单的工具调用
2.1 使用 @Tool 注解
Spring AI 提供了最简洁的方式——通过 @Tool 注解将普通方法转换为 AI 可调用的工具。
@Slf4j
publicclassDateTimeTools {
/**
* 获取当前日期时间
*/
@Tool(description = "获取用户时区的当前日期和时间")
String getCurrentDateTime() {
log.info("工具调用: getCurrentDateTime");
LocalDateTimenow= LocalDateTime.now();
return now.atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
/**
* 设置闹钟
*/
@Tool(description = "为用户设置闹钟")
voidsetAlarm(@ToolParam(description = "ISO-8601 格式的时间") String time) {
log.info("设置闹钟: {}", time);
LocalDateTimealarmTime= LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("闹钟已设置为 " + alarmTime);
}
}2.2 在 ChatClient 中使用
@RestController
@RequestMapping("/tool")
publicclassToolCallingController {
@Resource
private ZhiPuAiChatModel zhiPuAiChatModel;
@RequestMapping("/datetime-now")
public String getCurrentDateTime() {
ChatClientchatClient= ChatClient.create(zhiPuAiChatModel);
Stringresponse= chatClient.prompt("What day is tomorrow?")
.tools(newDateTimeTools()) // 注册工具
.call()
.content();
return response;
}
}执行流程:
1. 用户问:"明天是星期几?" 2. AI 分析后决定调用 getCurrentDateTime工具3. Spring AI 执行该方法,获取当前时间 4. 将结果返回给 AI 模型 5. AI 根据当前时间计算明天是星期几,生成最终响应
三、工具注册的三种方式
3.1 方式一:@Tool 注解(推荐用于简单工具)
publicclassMyTools {
@Tool(description = "计算数学表达式")
public String calculate(String expression) {
// 实现逻辑
}
}
// 使用时
chatClient.prompt("计算 6 * 8")
.tools(newMyTools())
.call()
.content();优点:
• ✅ 简单直观,代码可读性好 • ✅ 支持任意可见性(public、private 均可) • ✅ 自动生成 JSON Schema
缺点:
• ❌ 不支持依赖注入 • ❌ 工具必须在运行时显式传递
3.2 方式二:Function Bean(推荐用于复杂工具)
这是 Spring AI 的高级特性,通过 Spring Bean 注册工具,支持依赖注入和全局共享。
步骤 1:定义 Function Bean
@Configuration
publicclassToolCallingConfig {
publicstaticfinalStringCURRENT_WEATHER_TOOL="currentWeather";
@Bean(CURRENT_WEATHER_TOOL)
@Description("Get the current weather for a given location")
public Function<WeatherRequest, WeatherResponse> currentWeather() {
returnnewWeatherService();
}
}步骤 2:实现 Function 接口
@Slf4j
publicclassWeatherServiceimplementsFunction<WeatherRequest, WeatherResponse> {
@Override
public WeatherResponse apply(WeatherRequest request) {
log.info("查询天气: location={}, unit={}", request.location(), request.unit());
// 模拟天气数据(实际应调用真实 API)
doubletemperature= getTemperatureFromApi(request.location(), request.unit());
returnnewWeatherResponse(temperature, request.unit());
}
publicrecordWeatherRequest(
@ToolParam(description = "城市名称") String location,
@ToolParam(description = "温度单位") Unit unit
) {}
publicrecordWeatherResponse(double temp, Unit unit) {}
}步骤 3:通过工具名称引用
@RequestMapping("/weather-bean")
public String getWeatherByBean(String location) {
ChatClientchatClient= ChatClient.create(zhiPuAiChatModel);
Stringresponse= chatClient.prompt("What's the weather in " + location + "?")
.toolNames(ToolCallingConfig.CURRENT_WEATHER_TOOL) // 通过名称引用
.call()
.content();
return response;
}底层原理:
• Spring AI 的 SpringBeanToolCallbackResolver会扫描所有 Function 类型的 Bean• 自动将其转换为 ToolCallback• 使用 DelegatingToolCallbackResolver在运行时动态解析工具名称
优点:
• ✅ 支持依赖注入(可注入 Service、Repository) • ✅ 工具可被多个 ChatClient 共享 • ✅ 工具名称集中管理
缺点:
• ❌ 缺少编译时类型安全(工具名称是字符串) • ❌ 配置相对复杂
3.3 方式三:Lambda 表达式(适合简单计算)
@Bean("mathCalculator")
@Description("Perform basic mathematical calculations")
public Function<MathRequest, MathResponse> mathCalculator() {
return request -> {
double result;
switch (request.operation()) {
case"+" -> result = request.a() + request.b();
case"-" -> result = request.a() - request.b();
case"*" -> result = request.a() * request.b();
case"/" -> result = request.a() / request.b();
default -> thrownewIllegalArgumentException("Unsupported operation");
}
returnnewMathResponse(result);
};
}
// 使用
chatClient.prompt("What is 6 * 8?")
.toolNames("mathCalculator")
.call()
.content();四、高级特性
4.1 ToolContext:传递上下文信息
在某些场景下,我们需要向工具传递额外的上下文信息(如租户 ID、用户权限),但这些信息不应该发送给 AI 模型。这时可以使用 ToolContext。
@Slf4j
publicclassCustomerTools {
@Tool(description = "Retrieve customer information by ID")
Customer getCustomerInfo(
@ToolParam(description = "客户 ID") Long id,
ToolContext toolContext // 最后一个参数,由 Spring AI 自动注入
) {
// 从上下文中获取租户 ID
StringtenantId= (String) toolContext.getContext().get("tenantId");
log.info("Tenant ID: {}", tenantId);
// 根据租户 ID 和客户 ID 查询数据(多租户隔离)
Customercustomer= customerRepository.findByIdAndTenant(id, tenantId);
return customer;
}
}使用时传递上下文:
Map<String, Object> context = Map.of(
"tenantId", "acme-corp",
"requestId", UUID.randomUUID().toString()
);
Stringresponse= chatClient.prompt("Tell me about customer 42")
.tools(newCustomerTools())
.toolContext(context) // 传递上下文
.call()
.content();应用场景:
• 🔐 多租户系统:传递 tenantId 实现数据隔离 • 👤 权限控制:传递 userId、roles 进行权限校验 • 📝 审计日志:传递 requestId、sessionId 用于追踪
4.2 Return Direct:直接返回工具结果
默认情况下,工具执行结果会发送回 AI 模型,让模型生成自然语言响应。但有些场景需要直接返回原始数据,这时可以使用 returnDirect=true。
@Tool(
description = "Retrieve customer information by ID",
returnDirect = true // 直接返回,不经过 AI 处理
)
Customer getCustomerInfo(Long id, ToolContext toolContext) {
return customerRepository.findById(id);
}对比:
重要注意事项:
• ⚠️ 如果一次请求调用了多个工具,所有工具的 returnDirect 都必须为 true 才能直接返回 • ⚠️ 直接返回的是原始工具返回值,不是自然语言
适用场景:
• 📚 RAG 检索:检索到的文档片段直接返回 • 💾 数据库查询:精确的结构化数据直接返回 • 🔒 敏感信息:保证数据完整性和准确性
4.3 可选参数:防止 AI 幻觉
某些参数不是必需的,应该标记为 required=false,避免 AI 编造不存在的值。
@Tool(description = "Update customer information")
voidupdateCustomerInfo(
@ToolParam(description = "客户 ID") Long id,
@ToolParam(description = "客户姓名") String name,
@ToolParam(description = "客户邮箱(可选)", required = false) String email
) {
// 更新逻辑
}为什么需要可选参数?
如果参数标记为 required=true,但用户没有提供该值,AI 模型可能会产生幻觉(hallucination),编造一个看似合理的值。将非必需参数标记为 optional 可以避免这个问题。
五、用户控制的工具执行
5.1 框架控制 vs 用户控制
Spring AI 提供了两种工具执行模式:
框架控制(Framework-Controlled)
// 默认行为:Spring AI 自动处理工具调用循环
Stringresponse= chatClient.prompt("What day is tomorrow?")
.tools(newDateTimeTools())
.call()
.content();优点:
• ✅ 简单易用,一行代码搞定 • ✅ 自动处理工具调用循环 • ✅ 适合大多数场景
用户控制(User-Controlled)
// 禁用自动工具执行
ZhiPuAiChatOptionschatOptions= ZhiPuAiChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(newDateTimeTools()))
.internalToolExecutionEnabled(false) // 关键配置
.build();
// 手动执行工具调用循环
Promptprompt=newPrompt(
List.of(newSystemMessage("You are helpful."), newUserMessage("What day is tomorrow?")),
chatOptions
);
ChatResponsechatResponse= zhiPuAiChatModel.call(prompt);
// 循环执行工具调用
while (chatResponse.hasToolCalls()) {
ToolExecutionResultresult= toolCallingManager.executeToolCalls(prompt, chatResponse);
prompt = newPrompt(result.conversationHistory(), chatOptions);
chatResponse = zhiPuAiChatModel.call(prompt);
}
StringfinalResponse= chatResponse.getResult().getOutput().getText();适用场景:
• 🔧 需要在工具执行前后添加自定义逻辑(日志、监控、权限检查) • 📊 需要访问工具执行的中间结果 • ⚙️ 需要自定义错误处理策略 • 🔗 需要与外部系统集成(消息队列、事件总线)
5.2 结合 ChatMemory 的用户控制执行
将用户控制的工具执行与 ChatMemory 结合,可以实现有状态的多轮工具调用对话。
@RequestMapping("/memory-tool-execution")
public String memoryWithToolExecution() {
ToolCallingManagertoolCallingManager= ToolCallingManager.builder().build();
StringconversationId= UUID.randomUUID().toString();
// 配置选项
ZhiPuAiChatOptionschatOptions= ZhiPuAiChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(newDateTimeTools()))
.internalToolExecutionEnabled(false)
.build();
// 创建初始提示词
Promptprompt=newPrompt(
List.of(
newSystemMessage("You are a helpful assistant."),
newUserMessage("What day is tomorrow?")
),
chatOptions
);
// 将初始消息添加到 ChatMemory
chatMemory.add(conversationId, prompt.getInstructions());
// 从 ChatMemory 获取完整对话历史
PromptpromptWithMemory=newPrompt(chatMemory.get(conversationId), chatOptions);
// 第一次调用模型
ChatResponsechatResponse= zhiPuAiChatModel.call(promptWithMemory);
// 循环执行工具调用
intmaxIterations=5;
intiteration=0;
while (chatResponse.hasToolCalls() && iteration < maxIterations) {
iteration++;
// 如果有工具调用,将 AI 的响应添加到记忆
if (chatResponse.hasToolCalls()) {
chatMemory.add(conversationId, chatResponse.getResult().getOutput());
}
// 执行工具调用
ToolExecutionResulttoolExecutionResult=
toolCallingManager.executeToolCalls(promptWithMemory, chatResponse);
// 将工具执行结果添加到 ChatMemory
chatMemory.add(conversationId,
toolExecutionResult.conversationHistory()
.get(toolExecutionResult.conversationHistory().size() - 1));
// 更新提示词
promptWithMemory = newPrompt(chatMemory.get(conversationId), chatOptions);
// 再次调用模型
chatResponse = zhiPuAiChatModel.call(promptWithMemory);
}
// 继续对话:询问之前的问题
chatMemory.add(conversationId, newUserMessage("What did I ask you earlier?"));
ChatResponsenewResponse= zhiPuAiChatModel.call(
newPrompt(chatMemory.get(conversationId))
);
return newResponse.getResult().getOutput().getText();
}优势:
• ✅ 支持复杂的多轮工具调用场景 • ✅ 保持完整的对话上下文 • ✅ 可以查询之前的对话内容 • ✅ 适合构建智能 Agent 系统
六、组合多种工具
在实际应用中,我们经常需要同时使用多种类型的工具。
@RequestMapping("/combined-tools")
public String combinedTools() {
ChatClientchatClient= ChatClient.create(zhiPuAiChatModel);
Stringresponse= chatClient.prompt(
"What's the current date and time, and what's the weather in Beijing? " +
"Also, can you calculate 15 * 7 for me?"
)
.tools(newDateTimeTools()) // @Tool 注解工具
.toolNames( // Bean 注册工具
ToolCallingConfig.CURRENT_WEATHER_TOOL,
ToolCallingConfig.MATH_CALCULATOR_TOOL
)
.call()
.content();
return response;
}关键点:
• 🔄 可以同时使用 @Tool注解工具和 Bean 注册工具• 🎯 AI 模型会自动选择需要的工具并编排调用顺序 • ⚡ 多个工具可以协同完成复杂任务
七、最佳实践
7.1 工具设计原则
1. 单一职责:每个工具只做一件事 // ✅ 好:单一职责
@Tool(description = "获取当前时间")
String getCurrentTime() { ... }
@Tool(description = "获取当前日期")
String getCurrentDate() { ... }
// ❌ 不好:职责混乱
@Tool(description = "获取时间或日期")
String getTimeOrDate(String type) { ... }2. 详细描述:为工具和参数提供清晰的描述 @Tool(description = "获取指定城市的当前天气,返回温度和湿度信息")
WeatherResponse getWeather(
@ToolParam(description = "城市名称,如:北京、上海、New York") String city,
@ToolParam(description = "温度单位:C=摄氏度,F=华氏度") Unit unit
) { ... }3. 使用常量定义工具名称 publicclassToolCallingConfig {
publicstaticfinalStringCURRENT_WEATHER_TOOL="currentWeather";
@Bean(CURRENT_WEATHER_TOOL)
public Function<...> currentWeather() { ... }
}
// 使用时
.toolNames(ToolCallingConfig.CURRENT_WEATHER_TOOL)4. 合理设置可选参数 // ✅ 好:email 是可选的
@ToolParam(description = "邮箱(可选)", required = false) String email
// ❌ 不好:可能导致 AI 幻觉
@ToolParam(description = "邮箱") String email
7.2 安全性考虑
1. 工具执行边界:AI 模型只能请求调用工具,不能直接访问工具 API 2. 权限校验:在工具内部进行权限检查 @Tool(description = "删除用户")
voiddeleteUser(Long userId, ToolContext context) {
Stringrole= (String) context.getContext().get("userRole");
if (!"ADMIN".equals(role)) {
thrownewSecurityException("Permission denied");
}
// 执行删除
}3. 输入验证:始终验证工具参数 @Tool(description = "执行命令")
String executeCommand(String command) {
// 白名单验证
if (!ALLOWED_COMMANDS.contains(command)) {
thrownewIllegalArgumentException("Command not allowed");
}
// 执行
}
7.3 性能优化
1. 懒加载工具:只在需要时注册工具 2. 缓存工具结果:对于频繁调用的工具,考虑缓存结果 3. 异步工具:对于耗时操作,考虑使用异步执行
八、常见问题
Q1: 工具没有被调用怎么办?
可能原因:
1. 工具描述不够清晰,AI 不知道何时调用 2. 提示词中没有明确暗示需要调用工具 3. 工具参数 Schema 不正确
解决方案:
• 提供更详细的工具描述 • 在提示词中暗示可能需要工具 • 检查 @ToolParam的描述是否准确
Q2: 如何调试工具调用?
方法:
1. 启用日志:查看工具调用日志 logging.level.cn.dianyu.ai.myspringai.toolcalling=DEBUG2. 检查 JSON Schema:确保生成的 Schema 符合预期 3. 使用用户控制模式:逐步跟踪工具执行过程
Q3: 如何处理工具执行异常?
Spring AI 提供了 ToolExecutionExceptionProcessor 来处理异常:
@Tool(description = "查询数据")
String queryData(String key) {
try {
return dataRepository.findByKey(key);
} catch (Exception e) {
log.error("Query failed", e);
thrownewRuntimeException("Failed to query data: " + e.getMessage());
}
}异常会被捕获并返回给 AI 模型,AI 可以根据错误信息调整策略。
九、总结
本文详细介绍了 Spring AI 的工具调用机制,包括:
✅ 基础概念:什么是 Tool Calling,工作原理是什么
✅ 三种注册方式:@Tool 注解、Function Bean、Lambda 表达式
✅ 高级特性:ToolContext、Return Direct、可选参数
✅ 执行模式:框架控制 vs 用户控制
✅ 整合 ChatMemory:实现有状态的多轮工具调用
✅ 最佳实践:工具设计、安全性、性能优化
核心要点:
1. 工具调用让 AI 能够突破自身限制,与外部系统交互 2. 根据场景选择合适的工具注册方式 3. 合理使用 ToolContext 和 Return Direct 提升灵活性 4. 用户控制模式适合需要精细控制的复杂场景 5. 始终遵循最佳实践,确保工具的安全性和可维护性
夜雨聆风