Spring AI Tool Calling源码解析:@Tool注解、ToolCallback接口与ChatModel工具调用机制完整指南
Spring AI Tool Calling源码解析:@Tool注解、ToolCallback接口与ChatModel工具调用机制完整指南
摘要:本文深入分析Spring AI中Tool Calling(工具调用)机制的完整实现原理,包括ToolCallback接口设计、@Tool注解自动适配、FunctionCallbackWrapper适配器源码、ChatModel中的多轮交互处理流程、多工具并发执行策略及错误处理机制,并提供大量可运行的代码示例和最佳实践建议。
文章目录
- 一、什么是Tool Calling?为什么需要它?
- 二、Tool Calling完整交互流程图解
- 三、ToolCallback接口详解与手动实现
- 四、@Tool注解与FunctionCallbackWrapper适配器
- 五、两种工具定义方式对比
- 六、ChatModel中的Tool Calling实现源码
- 七、多工具并发调用与错误处理
- 八、ChatClient中使用Tool Calling
- 九、模型支持情况与最佳实践
- 十、常见问题FAQ
- 十一、总结与系列索引
一、什么是Tool Calling?为什么需要它?
Tool Calling(工具调用) 是让AI大语言模型(LLM)能够调用外部函数/方法的关键技术。在Spring AI框架中,Tool Calling机制允许开发者将普通的Java方法注册为AI可调用的"工具",使AI从单纯的"对话黑盒"升级为具备实际操作能力的智能体。
1.1 Tool Calling解决的核心问题
| 问题 | 场景 | 解决方案 |
|---|---|---|
| AI无法获取实时数据 | 用户问"今天北京天气如何?" | 注册天气查询工具,AI按需调用 |
| AI无法操作业务系统 | 需要AI查询订单、创建工单 | 将Service方法暴露为工具 |
| AI无法执行计算 | 复杂的数据计算、格式转换 | 提供专用计算工具 |
| AI无法访问外部API | 调用第三方HTTP服务 | 封装API为ToolCallback |
1.2 Spring AI Tool Calling的核心价值
- 零协议复杂度:开发者无需关注LLM的工具调用协议细节
- 类型安全:通过Java类型系统保证参数正确性
- 统一抽象:一套代码适配多种AI模型(OpenAI、Ollama、通义千问等)
- 自动Schema生成:基于注解自动生成JSON Schema
二、Tool Calling完整交互流程图解
Tool Calling的本质是一个多轮请求-响应循环。以下以用户询问天气为例,展示完整的4轮交互过程:
第 1 轮:用户发起请求
┌─────────────────────────────────────────┐
│ 用户: "北京今天天气怎么样?" │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ AI 模型分析:需要调用 get_weather 工具 │
│ 返回: { │
│ "tool_calls": [{ │
│ "id": "call_123", │
│ "function": "get_weather", │
│ "arguments": {"city": "北京"} │
│ }] │
│ } │
└─────────────────────────────────────────┘
第 2 轮:Spring AI 执行工具
┌─────────────────────────────────────────┐
│ Spring AI 执行工具: │
│ getWeather(city="北京") │
│ 返回: "北京今天晴天,温度 25°C" │
└─────────────────────────────────────────┘
第 3 轮:二次请求(携带工具结果)
┌─────────────────────────────────────────┐
│ 完整消息上下文: │
│ [ │
│ SystemMessage("你是一个助手"), │
│ UserMessage("北京今天天气怎么样?"), │
│ AssistantMessage(tool_calls=[...]), │
│ ToolResponseMessage("北京今天晴天...") │
│ ] │
└─────────────────────────────────────────┘
第 4 轮:最终回复
┌─────────────────────────────────────────┐
│ AI 基于工具结果: │
│ "北京今天晴天,温度 25°C,适合出游" │
└─────────────────────────────────────────┘
关键要点:Spring AI在幕后自动完成了工具调用检测、执行、结果回传和再次请求的全部流程,对上层调用者完全透明。
三、ToolCallback接口详解与手动实现
3.1 接口定义
ToolCallback是Spring AI中工具的核心接口,定义了工具的完整契约:
// org.springframework.ai.model.function.ToolCallback
public interface ToolCallback {
// 获取工具名称 — AI通过此名称识别和调用工具
String getName();
// 获取工具描述 — AI根据描述决定何时调用该工具
String getDescription();
// 获取输入参数的JSON Schema — 定义工具接受的参数结构
String getInputTypeSchema();
// 执行工具逻辑 — 接收JSON格式的参数,返回字符串结果
String call(String toolInput);
}
3.2 输入参数的JSON Schema格式
getInputTypeSchema()返回的是标准的JSON Schema格式,用于向AI描述工具的参数结构:
{
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
},
"date": {
"type": "string",
"description": "日期,格式 YYYY-MM-DD"
}
},
"required": ["city"]
}
3.3 完整实现示例
以下是一个查询天气的ToolCallback完整实现:
public class GetWeatherToolCallback implements ToolCallback {
@Override
public String getName() {
return "get_weather";
}
@Override
public String getDescription() {
return "获取指定城市的实时天气信息";
}
@Override
public String getInputTypeSchema() {
return """
{
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
}
},
"required": ["city"]
}
""";
}
@Override
public String call(String toolInput) {
// Step 1: 解析AI传入的JSON参数
Map<String, String> params = parseJson(toolInput);
String city = params.get("city");
// Step 2: 调用实际的业务逻辑(HTTP API / 数据库等)
String weather = getWeatherFromAPI(city);
// Step 3: 返回结果给AI
return weather;
}
}
四、@Tool注解与FunctionCallbackWrapper适配器
4.1 @Tool注解定义
为了简化工具的定义,Spring AI提供了声明式的@Tool注解:
// org.springframework.ai.model.function.FunctionCallback
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tool {
// 工具名称,默认使用方法名
String name() default "";
// 工具描述(必填)— 决定AI是否以及何时调用
String description();
// 自定义JSON Schema,留空则自动生成
String inputTypeSchema() default "";
}
4.2 @ToolParam参数注解
用于标注方法参数的语义信息:
public @interface ToolParam {
// 参数描述
String description();
// 是否必需(可选)
boolean required() default true;
}
4.3 使用示例
@Component
public class WeatherTools {
@Tool(
name = "get_weather",
description = "获取指定城市的天气信息"
)
public String getWeather(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "日期,格式 YYYY-MM-DD") String date) {
// 调用天气API
return "北京今天晴天,温度 25°C";
}
@Tool(description = "获取指定城市的空气质量指数")
public String getAirQuality(
@ToolParam(description = "城市名称") String city) {
return "北京空气质量优秀,AQI 50";
}
}
4.4 FunctionCallbackWrapper适配器源码
这是将@Tool注解方法转换为ToolCallback接口实现的核心适配器:
/**
* FunctionCallbackWrapper - Spring AI的工具适配器
* 职责:将带有@Tool注解的普通Java方法适配为ToolCallback接口
*/
public class FunctionCallbackWrapper implements ToolCallback {
private final Method method; // 目标方法的反射引用
private final Object bean; // 方法所在的Bean实例
private final Tool toolAnnotation; // @Tool注解元数据
public FunctionCallbackWrapper(Method method, Object bean, Tool annotation) {
this.method = method;
this.bean = bean;
this.toolAnnotation = annotation;
}
@Override
public String getName() {
String name = toolAnnotation.name();
// 如果未指定name,则使用方法名作为默认值
return name.isEmpty() ? method.getName() : name;
}
@Override
public String getDescription() {
return toolAnnotation.description();
}
@Override
public String getInputTypeSchema() {
// 核心能力:从方法签名 + @ToolParam注解 自动生成JSON Schema
String customSchema = toolAnnotation.inputTypeSchema();
if (!customSchema.isEmpty()) {
return customSchema; // 使用自定义Schema
}
return generateSchemaFromMethod(method); // 自动生成
}
@Override
public String call(String toolInput) {
// 1. 解析AI传入的JSON字符串为Map
Map<String, Object> params = parseJson(toolInput);
// 2. 将Map中的参数值映射到方法参数的位置顺序
Object[] args = mapParamsToMethodArgs(method, params);
3. 通过反射调用目标方法
Object result = method.invoke(bean, args);
4. 将返回值序列化为String
return String.valueOf(result);
}
}
五、两种工具定义方式对比
| 对比维度 | 手动实现 ToolCallback | 使用 @Tool 注解 |
|---|---|---|
| 实现方式 | 实现ToolCallback接口的4个方法 |
在方法上添加@Tool注解 |
| 代码量 | 较多(需手写Schema、解析逻辑等) | 极少(仅需注解+参数描述) |
| 灵活性 | ⭐⭐⭐⭐⭐ 完全可控 | ⭐⭐⭐ 受注解规范约束 |
| Schema控制 | 可精确控制每个字段 | 自动生成,也可覆盖 |
| 适用场景 | 动态工具、运行时确定的工具 | 绝大多数静态工具场景 |
| 维护成本 | 高(修改签名需同步更新多处) | 低(改一处即生效) |
| 学习成本 | 需理解接口契约 | 几乎为零 |
| 推荐程度 | 特殊场景备选方案 | 日常开发首选 ✅ |
选型建议:对于90%以上的常规场景,强烈推荐使用
@Tool注解。仅在需要运行时动态决定工具行为时(如从数据库加载工具列表),才考虑手动实现ToolCallback接口。
六、ChatModel中的Tool Calling实现源码
6.1 核心处理流程
ChatModel是Spring AI中与LLM交互的核心抽象。以下是Tool Calling在其中的集成方式:
/**
* ChatModel中的Tool Calling处理(以OllamaChatModel为例)
*
* 核心职责:
* 1. 将已注册的工具定义发送给LLM
* 2. 检测LLM响应中是否包含tool_calls
* 3. 如有,执行对应工具并将结果回传给LLM
*/
public class OllamaChatModel implements StreamingChatModel {
/** 已注册的工具列表 */
private final List<ToolCallback> tools;
/**
* 主入口方法 - 处理用户的Prompt请求
*/
@Override
public ChatResponse call(Prompt prompt) {
// 1. 构建请求对象(关键:包含tools定义)
ChatRequest request = buildRequest(prompt);
// request中将包含类似如下的tools数组:
// [{"type":"function","function":{"name":"get_weather","description":"...",...}}]
// 2. 发送HTTP请求到LLM API
ChatResponse response = ollamaApi.chat(request);
// 3. 判断LLM是否要求调用工具
if (hasToolCalls(response)) {
// 进入工具执行分支
return handleToolCalls(response, prompt);
}
// 无工具调用,直接返回文本回复
return response;
}
/**
★★★ 核心方法:处理工具调用并完成多轮交互循环 ★★★
*/
private ChatResponse handleToolCalls(ChatResponse response, Prompt prompt) {
// Step 1: 从响应中提取所有工具调用请求
List<ToolCall> toolCalls = extractToolCalls(response);
// Step 2: 逐一执行每个工具调用
List<ToolResponse> toolResponses = new ArrayList<>();
for (ToolCall toolCall : toolCalls) {
String toolName = toolCall.getFunctionName();
String toolInput = toolCall.getFunctionArguments();
// 在已注册的工具列表中查找匹配项
ToolCallback tool = findTool(toolName);
if (tool != null) {
String result = tool.call(toolInput);
toolResponses.add(new ToolResponse(toolCall.getId(), result));
}
}
// Step 3: 组装完整的消息历史记录
List<Message> messages = new ArrayList<>(prompt.getInstructions());
messages.add(new AssistantMessage(response.getContent(), toolCalls)); // AI的决策记录
messages.add(new ToolResponseMessage(toolResponses)); // 工具执行结果
// Step 4: 🔁 递归调用 - 将完整上下文再次发送给LLM
Prompt newPrompt = new Prompt(messages, prompt.getOptions());
return this.call(newPrompt); // 可能触发下一轮工具调用...
}
}
6.2 消息流详细结构
以下是每次请求/响应的具体数据结构:
第 1 次调用
═════════════════════════════════════
发送 (Prompt):
{
messages: [
SystemMessage("你是一个助手"),
UserMessage("北京今天天气怎么样?")
],
tools: [get_weather, get_air_quality] ← 工具定义随请求一起发给LLM
}
接收 (ChatResponse):
{
content: "", ← LLM不直接回答问题
toolCalls: [{
id: "call_123",
functionName: "get_weather",
functionArguments: "{\"city\": \"北京\"}" ← 要求调用工具
}]
}
═════════════════════════════════════
中间步骤: Spring AI执行 getWeather("北京")
返回: "北京今天晴天,温度 25°C"
第 2 次调用(递归)
═════════════════════════════════════
发送 (Prompt):
{
messages: [
SystemMessage("你是一个助手"),
UserMessage("北京今天天气怎么样?"),
AssistantMessage("", tool_calls=[...]), ← 第1轮的决策
ToolResponseMessage([{ ← 工具的实际输出
toolCallId: "call_123",
result: "北京今天晴天,温度 25°C"
}])
]
}
接收 (ChatResponse):
{
content: "北京今天晴天,温度 25°C,适合出游" ← 最终自然语言回复
}
6.3 递归调用机制的设计考量
注意到handleToolCalls方法末尾的this.call(newPrompt)——这是一个递归调用。这种设计的优点:
- 支持多步骤工具链:AI可以连续调用多个工具(先查天气→再查空气质量→综合给出建议)
- 代码简洁:无需显式管理状态机或循环
- 自动终止:当AI不再返回
tool_calls时,递归自然结束
七、多工具并发调用与错误处理
7.1 并发执行多个工具
当AI在一次响应中同时请求调用多个独立工具时,Spring AI支持并发执行以提高效率:
/**
* 并发执行多个工具调用
* 设计要点:
* - 使用固定大小线程池(最多5个并发)
* - 每个工具调用有独立的超时控制
* - 一个工具失败不影响其他工具
*/
private List<ToolResponse> executeToolsConcurrently(List<ToolCall> toolCalls) {
// 创建线程池,限制最大并发数
ExecutorService executor = Executors.newFixedThreadPool(
Math.min(toolCalls.size(), 5) // 上限5个并发线程
);
// 提交所有工具执行任务到线程池
List<Future<ToolResponse>> futures = toolCalls.stream()
.map(toolCall -> executor.submit(() -> {
ToolCallback tool = findTool(toolCall.getFunctionName());
String result = tool.call(toolCall.getFunctionArguments());
return new ToolResponse(toolCall.getId(), result);
}))
.toList();
// 收集所有执行结果(带超时控制)
List<ToolResponse> responses = futures.stream()
.map(future -> {
try {
return future.get(30, TimeUnit.SECONDS); // 单个工具超时30秒
} catch (TimeoutException e) {
return new ToolResponse(null, "工具执行超时");
} catch (Exception e) {
return new ToolResponse(null, "工具执行异常: " + e.getMessage());
}
})
.toList();
executor.shutdown();
return responses;
}
7.2 分层错误处理策略
工具调用涉及外部输入(AI生成的参数),必须做好防御性编程:
/**
* 安全执行单个工具调用
*
* 设计原则:
* - 不向上抛异常 → 将错误包装为ToolResponse回传给AI
* - 让AI有机会自我修正 → 而不是直接返回500给用户
*/
private ToolResponse executeToolSafely(ToolCall toolCall) {
try {
// 查找工具
ToolCallback tool = findTool(toolCall.getFunctionName());
// 防御1: 工具不存在
if (tool == null) {
return new ToolResponse(
toolCall.getId(),
"错误:工具 '" + toolCall.getFunctionName() + "' 不存在,请检查工具名称"
);
}
// 正常执行
String result = tool.call(toolCall.getFunctionArguments());
return new ToolResponse(toolCall.getId(), result);
// 防御2: 参数解析失败(AI传来非法JSON)
} catch (JsonParseException e) {
return new ToolResponse(
toolCall.getId(),
"错误:工具参数JSON格式无效 - " + e.getMessage()
);
// 防御3: 工具内部执行异常
} catch (Exception e) {
return new ToolResponse(
toolCall.getId(),
"错误:工具执行过程中发生异常 - " + e.getMessage()
);
}
}
为什么要把错误返回给AI而不是抛出异常?
这是因为AI具有自我纠错能力。当它收到"参数格式错误"的反馈后,可能会:
- 以修正后的参数重新调用工具
- 改为调用其他可用工具
- 直接告知用户无法完成该操作
这比直接抛出500错误给用户体验好得多。
7.3 递归深度限制
由于handleToolCalls存在递归调用,必须有防止无限循环的保护措施:
/** 当前工具调用嵌套深度 */
private int toolCallDepth = 0;
/** 最大允许的工具调用深度 */
private static final int MAX_TOOL_CALL_DEPTH = 5;
/**
* 带深度限制的工具调用处理
*/
private ChatResponse handleToolCalls(ChatResponse response, Prompt prompt) {
// 安全阀检查
if (toolCallDepth >= MAX_TOOL_CALL_DEPTH) {
throw new RuntimeException(
"工具调用深度超过上限(当前: " + toolCallDepth +
", 最大允许: " + MAX_TOOL_CALL_DEPTH + ")"
);
}
toolCallDepth++;
try {
return executeTools(response, prompt);
} finally {
toolCallDepth--; // 确保无论成功与否都正确回退计数器
}
}
默认限制为5轮,适用于绝大多数场景。对于复杂的Agent工作流,可通过配置适当调高此值。
八、ChatClient中使用Tool Calling
8.1 注册工具到ChatClient
@Configuration
public class ToolsConfiguration {
@Bean
public ChatClient chatClient(
ChatModel chatModel,
WeatherTools weatherTools) { // 注入包含@Tool方法的组件
return ChatClient.builder(chatModel)
.defaultTools( // 注册工具列表
weatherTools.getWeather,
weatherTools.getAirQuality
)
.build();
}
}
8.2 调用方式(对外透明)
// 开发者视角:就是一个普通的ChatClient调用
String response = chatClient.prompt()
.user("北京今天天气怎么样?")
.call()
.content();
/*
* 幕后自动完成的流程(开发者无需关心):
*
* ① 构建请求,附带已注册的工具定义
* ② 发送给LLM,LLM决定调用 get_weather 工具
* ③ Spring AI 反射执行 getWeather("北京")
* ④ 得到结果:"北京今天晴天,温度 25°C"
* ⑤ 将结果追加到消息历史
* ⑥ 再次发送给LLM,生成最终自然语言回复
* ⑦ 返回给调用者
*/
这就是Spring AI的核心设计理念:底层复杂度完全封装,上层API保持极致简洁。
九、模型支持情况与最佳实践
9.1 各大模型的Tool Calling支持情况
✅ 完整支持的模型/平台:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• OpenAI GPT-4o / GPT-4 Turbo / GPT-3.5-turbo
• Azure OpenAI(与OpenAI同源)
• Ollama(Llama 3.1及以上版本)
• 智谱AI GLM-4 系列
• 阿里云通义千问(Qwen-Max/Turbo Plus)
• Google Gemini Pro/Flash
• Anthropic Claude 3.x 系列
⚠️ 部分支持或实验性支持:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 某些7B以下的开源小模型
• Mistral系列(部分版本)
• 国内部分二线模型API
❌ 不支持:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 纯文本补全模型(text-davinci等非chat模型)
• 过于陈旧的模型版本
9.2 工具描述编写最佳实践
工具的description字段质量直接影响AI的调用准确率:
// ✅ 推荐:清晰、具体、有明确边界
@Tool(description = "查询指定城市的实时天气状况,包括温度、湿度和风力等级")
public String getWeather(
@ToolParam(description = "城市中文名称,例如'北京'、'上海'、'广州'") String city) {
return weatherService.query(city);
}
// ❌ 不推荐:过于模糊
@Tool(description = "获取天气") // 太短,AI不知道何时应该调用
public String getWeather(String city) { // 缺少参数描述
return weatherService.query(city);
}
描述编写的黄金法则:
| 法则 | 说明 | 示例 |
|---|---|---|
| 说明功能 + 触发条件 | 让AI知道什么时候该用这个工具 | “当用户询问天气时调用…” |
| 给出参数示例 | 减少AI传参错误 | "如'北京'、'上海'" |
| 标注副作用 | 如果工具会修改数据,务必说明 | “注意:此操作会创建订单记录” |
| 保持简洁 | 避免冗长描述,控制在50字以内 | — |
十、常见问题FAQ
Q1:Tool Calling和Function Calling有什么区别?
A:本质上是一回事。“Function Calling"是OpenAI最初提出的术语,而"Tool Calling"是更通用的叫法。Spring AI统一采用"Tool Calling”,因为工具不一定是"函数"——它可以是对接HTTP API、数据库查询或任何外部操作。
Q2:AI不调用我的工具怎么办?
A:按以下顺序排查:
- 检查description——是否足够清晰地说明了何时该调用?
- 检查模型兼容度——当前使用的模型是否支持Tool Calling?
- 检查参数Schema——
getInputTypeSchema()是否返回了合法的JSON Schema? - 检查日志——查看Spring AI的DEBUG日志,确认工具是否正确注册
Q3:工具执行的性能开销大吗?
A:每次工具调用意味着一次额外的LLM API往返(因为需要把结果回传给AI)。优化建议:
- 合并相关工具减少调用次数
- 对于简单查询,考虑放在System Message而非注册为工具
- 使用流式响应减少首字延迟感知
Q4:可以动态注册/注销工具吗?
A:可以。ChatClient.builder()的.defaultTools()接受可变的工具列表。你也可以在运行时通过重新构建ChatClient来更新工具集合。
Q5:工具调用的安全性如何保障?
A:Spring AI本身提供:
- 递归深度限制(防止无限循环)
- 单次超时控制(防止长时间阻塞)
- 错误封装(防止异常泄露)
但你需要自行关注:
- 工具内部的权限校验(谁可以调用什么工具)
- 敏感数据的脱敏(不要把密码、token返回给AI)
- 操作的幂等性(防止重复调用导致副作用)
十一、总结与系列索引
本篇核心知识点回顾
| 知识点 | 重要度 | 关键要点 |
|---|---|---|
| Tool Calling交互流程 | ⭐⭐⭐⭐⭐ | 4轮循环:请求→工具决策→执行→结果回传 |
| ToolCallback接口 | ⭐⭐⭐⭐ | 4方法契约:name/description/schema/call |
| @Tool注解 | ⭐⭐⭐⭐⭐ | 声明式工具定义,零样板代码 |
| FunctionCallbackWrapper | ⭐⭐⭐⭐ | 注解→接口的桥梁,核心适配器 |
| ChatModel递归调用 | ⭐⭐⭐⭐⭐ | handleToolCalls的多轮交互机制 |
| 并发执行 | ⭐⭐⭐ | ExecutorService + 超时控制 |
| 错误处理策略 | ⭐⭐⭐⭐ | 包装为ToolResponse而非抛异常 |
| 递归深度防护 | ⭐⭐⭐ | 默认5轮上限 |
关键类速查表
| 类/接口 | 所在模块 | 核心职责 |
|---|---|---|
ToolCallback |
spring-ai-model | 工具接口定义 |
@Tool |
spring-ai-model | 方法级工具声明注解 |
@ToolParam |
spring-ai-model | 参数描述注解 |
FunctionCallbackWrapper |
spring-ai-model | 注解→ToolCallback适配器 |
ToolCall |
spring-ai-model | AI返回的工具调用请求 |
ToolResponse |
spring-ai-model | 工具执行结果封装 |
ToolResponseMessage |
spring-ai-common | 回传给AI的结果消息 |
📖 本文属于「亦暖筑序 · Spring AI 系列」。
🔗 系列文章导航:
👋 如果本文对你有帮助,欢迎点赞、收藏、评论!有问题欢迎评论区交流~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)