让AI学会调用你的Java方法:Spring AI Tool Calling源码全解

0 阅读12分钟

让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无限循环调用⭐⭐⭐

关键类速查

类 / 接口所在包职责
ToolCallbackspring-ai-model工具的核心接口定义
@Tool / @ToolParamspring-ai-model声明式工具注解
FunctionCallbackWrapperspring-ai-model注解→接口的适配器
ToolCallspring-ai-modelAI返回的工具调用请求
ToolResponsespring-ai-model工具执行后的结果封装
ToolResponseMessagespring-ai-common回传给AI的工具结果消息

📚 系列导航

本篇文章属于「Spring AI 系列」源码篇,建议按顺序阅读:

主题关键字
1入门:环境搭建与第一个 AI 对话Quick Start
2一次对话的完整生命线prompt→call→response
3Message 体系:User/System/Assistant消息格式
4RAG 基础:检索增强生成入门VectorStore
5Advisor 拦截器链责任链 / 自定义扩展
★ 6Tool Calling 工具调用Function Callback
→ 7VectorStore与RAG Pipeline检索增强生成

👋 我是亦暖筑序,专注 Java 后端开发者的 AI 应用落地指南。

如果这篇文章对你有帮助,欢迎 点赞 + 收藏, 我们下期见!🚀