Spring AI Tool Calling源码解析:@Tool注解、ToolCallback接口与ChatModel工具调用机制完整指南

摘要:本文深入分析Spring AI中Tool Calling(工具调用)机制的完整实现原理,包括ToolCallback接口设计、@Tool注解自动适配、FunctionCallbackWrapper适配器源码、ChatModel中的多轮交互处理流程、多工具并发执行策略及错误处理机制,并提供大量可运行的代码示例和最佳实践建议。


文章目录


一、什么是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具有自我纠错能力。当它收到"参数格式错误"的反馈后,可能会:

  1. 以修正后的参数重新调用工具
  2. 改为调用其他可用工具
  3. 直接告知用户无法完成该操作

这比直接抛出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:按以下顺序排查:

  1. 检查description——是否足够清晰地说明了何时该调用?
  2. 检查模型兼容度——当前使用的模型是否支持Tool Calling?
  3. 检查参数Schema——getInputTypeSchema()是否返回了合法的JSON Schema?
  4. 检查日志——查看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 系列」

🔗 系列文章导航

👋 如果本文对你有帮助,欢迎点赞、收藏、评论!有问题欢迎评论区交流~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐