让AI学会调用你的Java方法:Spring AI Tool Calling源码全解
从"黑盒聊天"到"能查天气、能调API、能操作数据库"——一文搞懂Spring AI如何把你的方法变成AI的工具
你有没有遇到过这些问题?
❶ 写了个天气查询接口,想让AI自动调用,但不知道怎么把Java方法和LLM串起来?
❷ 看到GPT的Function Calling很香,想在自己的Spring Boot项目里用,但官方文档只给了个Hello World?
❷ 工具注册了,AI就是不调用(或者乱调用),排查半天找不到问题出在哪?
如果你对其中任何一个点了头——这篇就是为你写的。
Tool Calling是Spring AI里最核心、也最实用的功能。它让AI从"只会聊天的黑盒"变成"可以调用你任何代码的能力中心"。今天我们从源码层面彻底拆解这套机制。
一张图看懂Tool Calling在干什么
┌──────────────────────────────────────────────────────────────┐
│ Spring AI Tool Calling 全景 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ 注册工具 ┌──────────────────────────┐ │
│ │ Java方法 │ ─────────────→ │ ToolCallback 接口 │ │
│ │ @Tool │ │ getName / getDescription │ │
│ └─────────┘ │ getInputTypeSchema / call│ │
│ 或 └──────────┬───────────────┘ │
│ ┌─────────┐ │ │
│ │ 手动实现 │ ←── FunctionCallbackWrapper适配器 │
│ │ToolCallback│ │ │
│ └─────────┘ ↓ │
│ ┌──────────────────────────┐ │
│ │ ChatModel / API层 │ │
│ │ 将工具定义发给 LLM │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ LLM (GPT/Ollama) │ │
│ │ "我需要调用get_weather" │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌────────────────────┼────────────────┐ │
│ ↓ ↓ │ │
│ ┌─────────────┐ ┌──────────────┐ │ │
│ │ 执行工具方法 │ │ 返回结果给LLM │ │ │
│ │ getWeather() │────→│ 生成最终回复 │ │ │
│ └─────────────┘ └──────────────┘ │ │
│ │
└──────────────────────────────────────────────────────────────┘
核心思想:你的Java方法 → 变成工具定义 → 发给AI → AI决定是否调用 → 执行后返回结果
简单说就三步:定义工具 → 告诉AI → AI按需调用。但每一步背后都有不少源码细节值得深挖。
一、Tool Calling 的完整交互流程
1.1 四轮交互:一个真实请求的生命周期
用一个"北京今天天气怎么样?"的问题走一遍完整流程:
第 1 轮:用户发起请求
┌─────────────────────────────────────────┐
│ 用户: "北京今天天气怎么样?" │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ AI 模型分析:需要调用 get_weather 工具 │
│ 返回: { │
│ "tool_calls": [{ │
│ "id": "call_123", │
│ "function": "get_weather", │
│ "arguments": {"city": "北京"} │
│ }] │
│ } │
└─────────────────────────────────────────┘
第 2 轮:Spring AI 执行工具
┌─────────────────────────────────────────┐
│ Spring AI 解析 tool_calls: │
│ → 查找名为 get_weather 的 ToolCallback │
│ → 调用 tool.call('{"city":"北京"}') │
│ → 你的 Java 方法被执行! │
│ → 返回: "北京今天晴天,温度 25°C" │
└─────────────────────────────────────────┘
第 3 轮:将工具结果回传给 AI
┌─────────────────────────────────────────┐
│ 完整消息列表(全部上下文): │
│ [ │
│ SystemMessage("你是一个助手"), │
│ UserMessage("北京今天天气怎么样?"), │
│ AssistantMessage(tool_calls=[...]), │ ← AI说"我要调工具"
│ ToolResponseMessage("北京今天晴天...") │ ← 工具执行结果
│ ] │
└─────────────────────────────────────────┘
第 4 轮:AI 基于工具结果生成最终回复
┌─────────────────────────────────────────┐
│ AI: "北京今天晴天,温度 25°C, │
│ 适合出游,记得带防晒哦~" │
└─────────────────────────────────────────┘
关键洞察:这不是一次简单的请求-响应,而是一个多轮对话循环。Spring AI在幕后帮你完成了"检测tool_calls → 执行工具 → 回传结果 → 再次请求"的全部流程。
二、两种方式定义你的工具
方式一:手动实现 ToolCallback 接口(完全控制)
// org.springframework.ai.model.function.ToolCallback
public interface ToolCallback {
// 工具名称 — AI通过这个名称来识别和调用你的工具
String getName();
// 工具描述 — 这是最重要的字段!AI靠描述决定何时调用
String getDescription();
// 参数 Schema(JSON Schema 格式)— 告诉AI这个工具接受什么参数
String getInputTypeSchema();
// 执行逻辑 — 工具被调用时实际运行的代码
String call(String toolInput);
}
来看一个完整的可运行示例:
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": "城市名称"
},
"date": {
"type": "string",
"description": "日期,格式 YYYY-MM-DD"
}
},
"required": ["city"]
}
""";
}
@Override
public String call(String toolInput) {
// 1. 解析AI传来的JSON参数
Map<String, String> params = parseJson(toolInput);
String city = params.get("city");
// 2. 调用你的业务逻辑(数据库、HTTP API、任意代码)
String weather = getWeatherFromAPI(city);
// 3. 返回字符串结果给AI
return weather;
}
}
方式二:@Tool 注解(零样板代码)
如果觉得手写Schema太麻烦,Spring AI提供了更优雅的方式:
@Component
public class WeatherTools {
@Tool(
name = "get_weather",
description = "获取指定城市的天气信息"
)
public String getWeather(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "日期,格式 YYYY-MM-DD") String date) {
return "北京今天晴天,温度 25°C";
}
@Tool(description = "获取指定城市的空气质量")
public String getAirQuality(
@ToolParam(description = "城市名称") String city) {
return "北京空气质量优秀,AQI 50";
}
}
幕后的魔法:FunctionCallbackWrapper 适配器
@Tool注解的方法是怎么变成ToolCallback的?答案就是这个适配器:
// Spring AI 自动将 @Tool 方法转换为 ToolCallback
public class FunctionCallbackWrapper implements ToolCallback {
private final Method method; // 被注解的方法引用
private final Object bean; // 方法所在的 Bean 实例
private final Tool toolAnnotation; // @Tool 注解元数据
@Override
public String getName() {
String name = toolAnnotation.name();
return name.isEmpty() ? method.getName() : name;
// 没指定name就用方法名,很贴心
}
@Override
public String getDescription() {
return toolAnnotation.description();
}
@Override
public String getInputTypeSchema() {
// 🎯 核心能力:根据方法的参数+@ToolParam自动生成JSON 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返回给AI
return String.valueOf(result);
}
}
这就是为什么推荐用@Tool注解——Schema生成、参数映射、反射调用全帮你做了。
两种方式怎么选?
| 维度 | 手动实现 ToolCallback | @Tool 注解 |
|---|---|---|
| 代码量 | 较多(需手写接口实现) | 极少(只需加注解) |
| 灵活性 | ⭐⭐⭐ 完全可控 | ⭐⭐ 受注解约束 |
| Schema 控制 | 手动精确控制 | 自动生成,也可覆盖 |
| 适用场景 | 复杂工具、动态工具、需要运行时确定行为的场景 | 绝大多数常规场景 |
| 维护成本 | 高(改方法签名要同步改多处) | 低(改一处即可) |
| 推荐指数 | 特殊需求时使用 | 首选 ✅ |
💡 实战建议:90%的场景用
@Tool注解就够了。只有当工具的行为需要在运行时动态决定(比如从数据库读取工具列表),才考虑手动实现ToolCallback。
三、ChatModel 里的 Tool Calling 实现
工具注册好了之后,Spring AI 是怎么把它们集成到模型调用流程中的?我们来看看 ChatModel 层面的处理:
// OllamaChatModel 中的 Tool Calling 处理逻辑(简化)
public class OllamaChatModel implements StreamingChatModel {
private final List<ToolCallback> tools; // 已注册的工具列表
@Override
public ChatResponse call(Prompt prompt) {
// 1. 构建HTTP请求(关键:把工具 definitions 一并发给模型)
ChatRequest request = buildRequest(prompt);
// request 里会包含 tools 数组,告诉模型"你有这些工具可以用"
// 2. 发送请求到 LLM
ChatResponse response = ollamaApi.chat(request);
// 3. 🔑 判断模型是否要调用工具
if (hasToolCalls(response)) {
// 模型返回了 tool_calls → 进入工具执行分支
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
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: 🔁 递归调用自己,把结果再发给AI
Prompt newPrompt = new Prompt(messages, prompt.getOptions());
return this.call(newPrompt); // 可能再次触发工具调用...
}
}
消息流的详细结构
为了让你看清每次请求到底发了什么,这里是一个完整的消息流快照:
第 1 次调用(用户提问)
═══════════════════════════════════════
Prompt {
messages: [
SystemMessage("你是一个助手"),
UserMessage("北京今天天气怎么样?")
],
tools: [get_weather, get_air_quality] ← 告诉AI有哪些工具可用
}
↓
ChatResponse {
content: "", ← AI不直接回答
toolCalls: [{ ← 而是要求调用工具
id: "call_123",
functionName: "get_weather",
functionArguments: "{\"city\": \"北京\"}"
}]
}
═══ 执行工具 ═══
get_weather(city="北京") → "北京今天晴天,温度 25°C"
第 2 次调用(携带工具结果)
═══════════════════════════════════════
Prompt {
messages: [
SystemMessage("你是一个助手"),
UserMessage("北京今天天气怎么样?"),
AssistantMessage("", toolCalls=[...]), ← 第1轮AI的决策
ToolResponseMessage([{ ← 工具的实际输出
toolCallId: "call_123",
result: "北京今天晴天,温度 25°C"
}])
]
}
↓
ChatResponse {
content: "北京今天晴天,温度 25°C,适合出游" ← 最终自然语言回复
}
注意那个递归调用——handleToolCalls 最后会再次调用 this.call()。这意味着如果AI拿到工具结果后还想再调别的工具,整个流程会继续循环(当然有深度限制,后面会说)。
四、多工具并发与错误处理
4.1 当AI一次要求调用多个工具
有些场景下AI可能会同时请求调用多个独立工具(比如同时查天气+空气质量)。Spring AI支持并发执行:
private List<ToolResponse> executeToolsConcurrently(List<ToolCall> toolCalls) {
// 创建线程池,最多5个并发
ExecutorService executor = Executors.newFixedThreadPool(
Math.min(toolCalls.size(), 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();
// 收集结果(每个工具最多等30秒)
List<ToolResponse> responses = futures.stream()
.map(future -> {
try {
return future.get(30, TimeUnit.SECONDS);
} catch (TimeoutException e) {
return new ToolResponse(null, "工具执行超时");
}
})
.toList();
executor.shutdown();
return responses;
}
4.2 工具执行的"安全网"
工具是从AI那边传入参数调用的——参数可能畸形、工具可能不存在、网络可能超时。一套健壮的错误处理必不可少:
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(),
"错误:工具参数格式错误 - " + e.getMessage()
);
// 防御3:工具内部抛异常
} catch (Exception e) {
return new ToolResponse(
toolCall.getId(),
"错误:工具执行失败 - " + e.getMessage()
);
}
}
设计要点:注意这里没有把异常向上抛出,而是把错误信息包装成 ToolResponse 返回给AI。这样AI看到错误信息后可以决定要不要重试或换一种方式回答——这比直接报500友好多了。
五、ChatClient 中的一行搞定
说了这么多底层原理,实际用起来有多简单?
5.1 注册工具
@Configuration
public class ToolsConfiguration {
@Bean
public ChatClient chatClient(
ChatModel chatModel,
WeatherTools weatherTools) { // 你的@Tool类
return ChatClient.builder(chatModel)
.defaultTools( // 一行注册所有工具
weatherTools.getWeather,
weatherTools.getAirQuality
)
.build();
}
}
5.2 使用——跟普通对话一模一样
// 用户代码:看起来就是一个普通的ChatClient调用
String response = chatClient.prompt()
.user("北京今天天气怎么样?")
.call()
.content();
// 但幕后发生了这些事:
// ① 发送请求给AI(附带工具定义)
// ② AI分析后决定:"我需要调用get_weather"
// ③ Spring AI执行你的 getWeather("北京") 方法
// ④ 拿到结果:"北京今天晴天,温度 25°C"
// ⑤ 把结果塞进消息历史,再发给AI
// ⑥ AI基于真实数据生成最终回复
// ⑦ 返回给用户 —— 对用户来说全程无感
这就是Spring AI的设计哲学:底层复杂度封装好,上层API保持简洁。
六、踩坑指南:限制与注意事项
6.1 不是所有模型都支持Tool Calling
✅ 支持 Tool Calling 的主流模型:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• OpenAI GPT-4 / GPT-4o / GPT-3.5-turbo
• Ollama(部分模型,如Llama 3.1+)
• 智谱 AI GLM-4 系列
• 阿里云通义千问
• Azure OpenAI
• Google Gemini
❌ 不支持 / 支持有限:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
• 某些早期开源模型(7B以下的小模型)
• 较旧版本的模型API
• 纯文本补全类模型(非chat模型)
选型建议:如果Tool Calling是你的核心功能,优先选 GPT-4o 或 GLM-4,这两家的工具调用质量最高。
6.2 工具描述写得好不好,直接影响AI的调用准确率
这是新手最容易忽略的点——description字段的质量决定了AI会不会正确调用你的工具:
// ✅ 好的描述:清晰、具体、有边界
@Tool(description = "查询指定城市的实时天气状况,包括温度、湿度、风力")
public String getWeather(
@ToolParam(description = "城市中文名称,如'北京'、'上海'") String city) {
return weatherAPI.getWeather(city);
}
// ❌ 差的描述:模糊、笼统
@Tool(description = "获取天气") // 太短,AI不知道什么时候该调用
public String getWeather(String city) { // 没参数描述,AI可能传错格式
return weatherAPI.getWeather(city);
}
经验法则:
- 描述要说明 做什么 + 什么时候该用
- 参数描述要给出 示例值
- 如果工具有副作用(写库、发消息),一定要在描述里注明
6.3 防止无限递归:深度限制
前面看到了handleToolCalls里的递归调用——万一AI陷入"调用工具→拿到结果→又想调另一个工具→..."的死循环怎么办?Spring AI内置了深度限制:
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("工具调用深度超过限制(最大" + MAX_TOOL_CALL_DEPTH + "轮)");
}
toolCallDepth++;
try {
return executeTools(response, prompt);
} finally {
toolCallDepth--; // 确保递归返回时计数器正确回退
}
}
默认最多 5轮工具调用,对绝大多数场景足够了。如果你的工作流确实需要更多轮次(比如复杂的多步骤Agent),可以通过配置调整。
本篇小结
| 主题 | 一句话总结 | 重要度 |
|---|---|---|
| 核心流程 | 用户提问 → AI决策调用工具 → 执行Java方法 → 结果回传 → AI生成最终回复 | ⭐⭐⭐⭐⭐ |
| ToolCallback | 手动实现的工具接口,4个方法定义完整工具契约 | ⭐⭐⭐⭐ |
| @Tool注解 | 加在方法上自动变工具,FunctionCallbackWrapper负责适配 | ⭐⭐⭐⭐⭐ |
| ChatModel处理 | 检测toolCalls→执行工具→组装消息→递归调用,全自动 | ⭐⭐⭐⭐ |
| 并发执行 | 多工具并行跑,线程池+超时控制 | ⭐⭐⭐ |
| 错误处理 | 错误包装成ToolResponse而不是抛异常,给AI自我修正的机会 | ⭐⭐⭐⭐ |
| 递归防护 | 默认5轮深度上限,防止AI无限循环调用 | ⭐⭐⭐ |
关键类速查
| 类 / 接口 | 所在包 | 职责 |
|---|---|---|
ToolCallback | spring-ai-model | 工具的核心接口定义 |
@Tool / @ToolParam | spring-ai-model | 声明式工具注解 |
FunctionCallbackWrapper | spring-ai-model | 注解→接口的适配器 |
ToolCall | spring-ai-model | AI返回的工具调用请求 |
ToolResponse | spring-ai-model | 工具执行后的结果封装 |
ToolResponseMessage | spring-ai-common | 回传给AI的工具结果消息 |
📚 系列导航
本篇文章属于「Spring AI 系列」源码篇,建议按顺序阅读:
| 篇 | 主题 | 关键字 |
|---|---|---|
| 入门:环境搭建与第一个 AI 对话 | Quick Start | |
| 一次对话的完整生命线 | prompt→call→response | |
| Message 体系:User/System/Assistant | 消息格式 | |
| RAG 基础:检索增强生成入门 | VectorStore | |
| Advisor 拦截器链 | 责任链 / 自定义扩展 | |
| ★ 6 | Tool Calling 工具调用 | Function Callback |
| → 7 | VectorStore与RAG Pipeline | 检索增强生成 |
👋 我是亦暖筑序,专注 Java 后端开发者的 AI 应用落地指南。
如果这篇文章对你有帮助,欢迎 点赞 + 收藏, 我们下期见!🚀