第6天 - Function Calling / Tool Calling 原理与实现

系列: Spring AI Alibaba 技术博客系列
日期: 2026-04-30
难度: ⭐⭐⭐⭐⭐
前置知识: ChatModel 基础使用、Prompt 工程、系统提示词最佳实践


目录

  1. 什么是 Function Calling?为什么它如此重要
  2. Function Calling 的核心工作原理
  3. Spring AI Alibaba 中的 Function Calling 体系
  4. 实战一:定义并注册一个简单工具函数
  5. 实战二:多工具函数与自动路由
  6. 实战三:带参数的复杂工具函数
  7. 实战四:外部 API 调用工具
  8. Tool Calling vs Function Calling:概念辨析
  9. 底层机制:消息协议与往返流程
  10. 错误处理与容错机制
  11. 最佳实践:工具函数设计原则
  12. 常见问题与排查指南
  13. 总结

1. 什么是 Function Calling?为什么它如此重要

1.1 从”只会聊天”到”会做事”

在 Function Calling 出现之前,大型语言模型本质上是一个”语言生成器”——它只能根据输入生成文本回复。这意味着:

  • 你问”今天天气怎么样?”——它会编造一个回答(因为它没有实时数据)
  • 你问”帮我查一下北京的天气”——它仍然会编造
  • 你想让它操作数据库、调用 API、执行计算——它做不到

Function Calling 彻底改变了这一点。 它让模型具备了”决定何时以及如何调用外部函数”的能力,从而将语言模型从”只会说话”升级为”会做事”。

1.2 一个直观的例子

假设你构建了一个智能助手,用户问:”明天北京需要带伞吗?”

没有 Function Calling:

助手:根据我的知识,北京的天气变化较大...(编造的回答)

有 Function Calling:

模型分析用户问题 → 决定需要调用天气查询工具
→ 返回工具调用请求:getWeather(city="北京", date="明天")
→ 系统执行函数,获取真实数据
→ 模型基于真实数据生成回答
助手:明天北京多云转晴,降水概率15%,不需要带伞。

这就是 Function Calling 的魔力——让模型拥有感知和行动的能力

1.3 为什么 Spring AI Alibaba 需要 Function Calling?

在 Spring AI Alibaba 的生态中,Function Calling 是构建以下能力的基石:

能力说明
RAG 检索增强调用检索工具获取知识库内容
Agent 智能体自主选择并调用工具完成任务
工作流编排串联多个外部系统操作
智能客服查询订单、修改信息等真实业务操作

可以说,没有 Function Calling,就没有真正的 AI 应用,只有聊天玩具。


2. Function Calling 的核心工作原理

2.1 整体架构图

┌─────────────────────────────────────────────────────────┐
│                      用户请求                              │
│                   "明天北京天气怎么样?"                     │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│ Step 1: 发送请求 + 工具定义 → LLM                         │
│                                                         │
│  messages: [UserMessage("明天北京天气怎么样?")]              │
│  tools: [                                              │
│    { type: "function", function: {                     │
│        name: "getWeather",                              │
│        description: "查询指定城市的天气",                   │
│        parameters: { city, date }                       │
│    }}                                                  │
│  ]                                                      │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│ Step 2: LLM 决定调用工具,返回 Tool Call                    │
│                                                         │
│  AssistantMessage: [                                     │
│    ToolCall(id="call_abc123",                             │
│             name="getWeather",                            │
│             arguments='{"city":"北京","date":"明天"}')       │
│  ]                                                        │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│ Step 3: 框架自动执行函数,获取结果                           │
│                                                         │
│  执行 getWeather(city="北京", date="明天")                    │
│  → 返回: { "temperature": 22, "condition": "多云",          │
│            "rain_probability": 15 }                       │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│ Step 4: 将工具结果追加到对话,再次发送给 LLM                  │
│                                                         │
│  messages: [                                             │
│    UserMessage("明天北京天气怎么样?"),                        │
│    AssistantMessage(ToolCall(...)),                       │
│    ToolResultMessage("温度22°C,多云,降水概率15%")            │
│  ]                                                        │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│ Step 5: LLM 基于真实数据生成最终回答                         │
│                                                         │
│  "明天北京多云,气温22°C,降水概率只有15%,不需要带伞。"         │
└─────────────────────────────────────────────────────────┘

2.2 关键要点

  1. 模型不执行函数——模型只负责”决定调用哪个函数”和”参数是什么”
  2. 框架负责执行——Spring AI Alibaba 负责在本地执行函数并返回结果
  3. 多轮往返——一次 Function Calling 通常涉及 2 次模型调用
  4. 工具定义即文档——函数名、描述、参数描述直接影响模型的理解

3. Spring AI Alibaba 中的 Function Calling 体系

3.1 核心组件

Spring AI Alibaba 基于 Spring AI 的抽象,提供了完善的 Function Calling 支持体系:

组件作用说明
@Tool 注解标记工具方法最便捷的定义方式
ToolCallback 接口工具回调抽象底层回调接口
ToolCallingManager工具调用管理器协调工具发现、注册、调用
ChatClient聊天客户端高层 API,内置工具调用支持
ChatModel聊天模型底层 API,需手动处理工具调用
FunctionCallback函数回调(旧版)兼容旧版本的 API

3.2 两种 API 层次

高层 API(ChatClient)——推荐日常使用:

ChatClient.builder(chatModel)
    .defaultTools("weatherTool", "searchTool")
    .build();
// 工具调用完全自动,无需手动干预

底层 API(ChatModel)——适合精细控制:

ChatResponse response = chatModel.call(prompt);
// 需要手动检查是否需要调用工具,手动执行,手动回传结果

下面我们将从简单到复杂,通过多个实战案例深入理解。


4. 实战一:定义并注册一个简单工具函数

4.1 使用 @Tool 注解定义工具

这是 Spring AI Alibaba 最推荐的工具定义方式:

package com.example.ai.tools;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

@Service
public class WeatherTool {

    @Tool(description = "查询指定城市的当前天气情况,包括温度、天气状况和降水概率")
    public String getWeather(
            @ToolParam(description = "城市名称,例如:北京、上海、广州") String city,
            @ToolParam(description = "日期,例如:今天、明天、后天") String date) {
        
        // 模拟天气数据(实际项目中应调用天气 API)
        String weatherData = switch (city) {
            case "北京" -> """
                {
                    "city": "北京",
                    "date": "%s",
                    "temperature": 22,
                    "condition": "多云",
                    "humidity": 45,
                    "windSpeed": 12,
                    "rainProbability": 15
                }
                """.formatted(date);
            case "上海" -> """
                {
                    "city": "上海",
                    "date": "%s",
                    "temperature": 26,
                    "condition": "晴",
                    "humidity": 60,
                    "windSpeed": 8,
                    "rainProbability": 5
                }
                """.formatted(date);
            case "广州" -> """
                {
                    "city": "广州",
                    "date": "%s",
                    "temperature": 30,
                    "condition": "雷阵雨",
                    "humidity": 85,
                    "windSpeed": 15,
                    "rainProbability": 80
                }
                """.formatted(date);
            default -> """
                {
                    "city": "%s",
                    "date": "%s",
                    "temperature": -1,
                    "condition": "未知",
                    "message": "暂无该城市的天气数据"
                }
                """.formatted(city, date);
        };
        
        return weatherData;
    }
}

4.2 注册工具到 ChatClient

package com.example.ai.config;

import com.example.ai.tools.WeatherTool;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient chatClient(ChatModel chatModel, WeatherTool weatherTool) {
        return ChatClient.builder(chatModel)
                .defaultTools(weatherTool)  // 自动扫描 @Tool 注解的方法
                .build();
    }
}

4.3 使用工具增强型 ChatClient

package com.example.ai.service;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class WeatherService {

    private final ChatClient chatClient;

    public WeatherService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    /**
     * 用户询问天气时,ChatClient 会自动:
     * 1. 识别需要调用 getWeather 工具
     * 2. 提取城市和日期参数
     * 3. 执行工具获取数据
     * 4. 基于数据生成自然语言回答
     */
    public String askWeather(String userQuestion) {
        return chatClient.prompt()
                .user(userQuestion)
                .call()
                .content();
    }
}

4.4 测试效果

@Test
void testWeatherQuery() {
    String question = "明天北京天气怎么样?我需要带伞吗?";
    String answer = weatherService.askWeather(question);
    
    System.out.println("用户:" + question);
    System.out.println("助手:" + answer);
    
    // 预期输出(示意):
    // 用户:明天北京天气怎么样?我需要带伞吗?
    // 助手:明天北京多云,气温22°C,降水概率只有15%。
    //       天气还不错,不需要带伞。建议穿薄外套,注意防晒。
}

5. 实战二:多工具函数与自动路由

5.1 定义多个工具

真实场景中,AI 助手通常需要多个工具。让我们定义一个更完整的工具集:

package com.example.ai.tools;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class AssistantTools {

    // ========== 天气工具 ==========

    @Tool(description = "查询指定城市的天气信息")
    public String getWeather(
            @ToolParam(description = "城市名称") String city,
            @ToolParam(description = "查询日期(今天/明天/后天)") String date) {
        // 同上一节的实现
        return "{ \"city\": \"" + city + "\", \"temperature\": 22, \"condition\": \"多云\" }";
    }

    // ========== 时间工具 ==========

    @Tool(description = "获取当前日期和时间信息")
    public String getCurrentTime(
            @ToolParam(description = "时区,例如:Asia/Shanghai、UTC", required = false) String timezone) {
        String tz = (timezone != null) ? timezone : "Asia/Shanghai";
        String now = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
        String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
        return String.format("当前时间:%s %s,时区:%s", today, now, tz);
    }

    // ========== 计算器工具 ==========

    @Tool(description = "执行数学计算,支持加减乘除和简单函数")
    public String calculate(
            @ToolParam(description = "数学表达式,例如:(128 + 256) * 3") String expression) {
        try {
            // 简化版:使用 Nashorn 或第三方表达式引擎
            // 实际项目中建议使用 exp4j、mXparser 等安全表达式引擎
            javax.script.ScriptEngine engine = 
                new javax.script.ScriptEngineManager()
                    .getEngineByName("JavaScript");
            Object result = engine.eval(expression);
            return "计算结果:" + result;
        } catch (Exception e) {
            return "计算错误:无法解析表达式 '" + expression + "'";
        }
    }

    // ========== 知识查询工具 ==========

    @Tool(description = "查询内部知识库,获取特定主题的信息")
    public String searchKnowledgeBase(
            @ToolParam(description = "搜索关键词") String keyword,
            @ToolParam(description = "搜索类别(可选)", required = false) String category) {
        Map<String, String> knowledge = Map.of(
            "Spring AI", "Spring AI 是 Spring 生态中用于构建 AI 应用的框架...",
            "RAG", "RAG(检索增强生成)是一种将外部知识注入 LLM 的技术...",
            "Function Calling", "Function Calling 允许 LLM 调用外部函数..."
        );
        
        return knowledge.getOrDefault(keyword, 
            "未找到关于 '" + keyword + "' 的知识条目");
    }
}

5.2 注册多个工具

@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient chatClient(ChatModel chatModel, AssistantTools assistantTools) {
        return ChatClient.builder(chatModel)
                .defaultTools(assistantTools)  // 一次注册所有 @Tool 方法
                .build();
    }
}

5.3 多工具自动路由效果

// 用户问时间 → 自动调用 getCurrentTime
chatClient.prompt()
    .user("现在几点了?")
    .call().content();
// → 自动调用 getCurrentTime()

// 用户问计算 → 自动调用 calculate
chatClient.prompt()
    .user("128乘以256等于多少?")
    .call().content();
// → 自动调用 calculate("128*256")

// 用户问知识 → 自动调用 searchKnowledgeBase
chatClient.prompt()
    .user("什么是 RAG?")
    .call().content();
// → 自动调用 searchKnowledgeBase("RAG")

// 用户同时问多个问题 → 可能调用多个工具
chatClient.prompt()
    .user("帮我算一下 99*99 等于多少?另外今天北京天气怎么样?")
    .call().content();
// → 可能先后调用 calculate() 和 getWeather()

5.4 多工具路由的原理

模型在收到请求时,会基于以下信息自动决策:

  1. 工具名称——直观的名称帮助模型理解工具用途
  2. 工具描述——详细的描述是模型决策的关键依据
  3. 参数描述——帮助模型正确提取和映射参数
  4. 用户问题——模型分析问题意图,匹配最合适的工具
用户问题:"帮我查一下明天上海的天气"
    │
    ├── 意图分析:查询天气
    ├── 实体提取:城市=上海,时间=明天
    ├── 工具匹配:getWeather(描述包含"查询城市天气")
    └── 参数填充:city="上海", date="明天"

6. 实战三:带参数的复杂工具函数

6.1 复杂参数定义

有些工具需要更复杂的参数结构,Spring AI Alibaba 支持多种参数类型:

package com.example.ai.tools;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class OrderManagementTool {

    @Tool(description = "查询订单信息,支持按订单号、用户ID或状态筛选")
    public String queryOrders(
            @ToolParam(description = "订单号(精确匹配)", required = false) String orderId,
            @ToolParam(description = "用户ID", required = false) String userId,
            @ToolParam(description = "订单状态:PENDING/PAID/SHIPPED/COMPLETED/CANCELLED", 
                       required = false) String status,
            @ToolParam(description = "页码,从1开始", required = false) Integer page,
            @ToolParam(description = "每页条数,默认10", required = false) Integer pageSize) {
        
        // 模拟订单数据
        List<Order> orders = mockOrders();
        
        // 应用过滤条件
        if (orderId != null) {
            orders = orders.stream()
                .filter(o -> o.id().equals(orderId))
                .toList();
        }
        if (userId != null) {
            orders = orders.stream()
                .filter(o -> o.userId().equals(userId))
                .toList();
        }
        if (status != null) {
            orders = orders.stream()
                .filter(o -> o.status().equals(status))
                .toList();
        }
        
        // 分页
        int p = (page != null) ? page : 1;
        int ps = (pageSize != null) ? pageSize : 10;
        int fromIndex = (p - 1) * ps;
        int toIndex = Math.min(fromIndex + ps, orders.size());
        
        List<Order> pageOrders = orders.subList(
            Math.max(0, fromIndex), Math.max(0, toIndex));
        
        return """
            {
                "total": %d,
                "page": %d,
                "pageSize": %d,
                "orders": %s
            }
            """.formatted(orders.size(), p, ps, pageOrders);
    }

    @Tool(description = "修改订单状态")
    public String updateOrderStatus(
            @ToolParam(description = "订单号") String orderId,
            @ToolParam(description = "目标状态:PAID/SHIPPED/COMPLETED/CANCELLED") String newStatus,
            @ToolParam(description = "修改原因", required = false) String reason) {
        
        return String.format(
            "订单 %s 状态已更新为 %s。原因:%s",
            orderId, newStatus, reason != null ? reason : "无");
    }

    record Order(String id, String userId, String status, double amount) {}
    
    private List<Order> mockOrders() {
        return List.of(
            new Order("ORD-001", "user_1001", "COMPLETED", 299.00),
            new Order("ORD-002", "user_1001", "SHIPPED", 158.50),
            new Order("ORD-003", "user_1002", "PENDING", 89.90),
            new Order("ORD-004", "user_1003", "PAID", 1299.00),
            new Order("ORD-005", "user_1001", "CANCELLED", 45.00)
        );
    }
}

6.2 使用复杂工具

// 查询特定用户的订单
chatClient.prompt()
    .user("帮我查一下用户 user_1001 的所有订单")
    .call().content();

// 修改订单状态
chatClient.prompt()
    .user("把订单 ORD-003 的状态改为已发货,原因是仓库已出库")
    .call().content();

7. 实战四:外部 API 调用工具

7.1 集成第三方 API

Function Calling 最常见的场景就是调用外部 API。下面演示如何集成一个真实的天气 API:

package com.example.ai.tools;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class RealWeatherTool {

    private final RestClient restClient;

    public RealWeatherTool() {
        this.restClient = RestClient.builder()
                .baseUrl("https://api.openweathermap.org/data/2.5")
                .build();
    }

    @Tool(description = "查询全球任意城市的实时天气,返回温度、湿度、风速和天气描述")
    public String getRealTimeWeather(
            @ToolParam(description = "城市名称(支持中文城市名,如:北京、上海、东京)") String city) {
        
        try {
            // 调用 OpenWeatherMap API
            var response = restClient.get()
                .uri(uriBuilder -> uriBuilder
                    .path("/weather")
                    .queryParam("q", city)
                    .queryParam("appid", "${weather.api.key}")
                    .queryParam("units", "metric")
                    .queryParam("lang", "zh_cn")
                    .build())
                .retrieve()
                .body(WeatherResponse.class);
            
            if (response == null) {
                return "无法获取 " + city + " 的天气信息";
            }
            
            return String.format(
                """
                【%s 实时天气】
                🌡️ 温度:%.1f°C(体感 %.1f°C)
                🌤️ 天气:%s
                💧 湿度:%d%%
                💨 风速:%.1m/s
                👁️ 能见度:%d米
                """,
                response.name(),
                response.main().temp(),
                response.main().feelsLike(),
                response.weather().get(0).description(),
                response.main().humidity(),
                response.wind().speed(),
                response.visibility()
            );
            
        } catch (Exception e) {
            return "查询 " + city + " 天气时出错:" + e.getMessage();
        }
    }

    // DTO 定义
    public record WeatherResponse(
            String name,
            Main main,
            java.util.List<WeatherItem> weather,
            Wind wind,
            int visibility
    ) {}

    public record Main(double temp, double feelsLike, int humidity, int pressure) {}
    
    public record WeatherItem(String main, String description, String icon) {}
    
    public record Wind(double speed, double deg) {}
}

7.2 配置外部服务

# application.yml
spring:
  ai:
    openai:
      api-key: ${DASHSCOPE_API_KEY}
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1

# 天气 API 配置
weather:
  api:
    key: ${OPENWEATHER_API_KEY:your-api-key-here}

8. Tool Calling vs Function Calling:概念辨析

8.1 术语说明

在社区中,你经常会看到这两个术语交替使用。它们的本质相同,但略有区别:

维度Function CallingTool Calling
起源OpenAI 最早提出OpenAI 后期演进 + 其他厂商跟进
范围专注于函数调用更广义,可包含代码解释器、文件搜索等
语义“调用一个函数”“使用一个工具”
Spring AI两者都支持推荐使用 @Tool 注解

在 Spring AI Alibaba 中,推荐使用 @Tool 注解,这是框架统一支持的 API。

8.2 OpenAI 协议中的表现

// 工具定义(Tool Calling 格式)
{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "getWeather",
        "description": "查询天气",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string", "description": "城市名称" }
          },
          "required": ["city"]
        }
      }
    }
  ]
}

注意:即使使用 Tool Calling 格式,type 仍然是 "function"


9. 底层机制:消息协议与往返流程

9.1 消息结构详解

理解底层消息结构对于调试和优化至关重要。让我们看看整个流程中消息的变化:

第一轮:发送请求 + 工具定义

Prompt prompt = new Prompt(List.of(
    new UserMessage("明天北京天气怎么样?")
));

// 添加工具定义
prompt = new Prompt(
    prompt.getInstructions(),
    ChatOptions.builder()
        .tools(List.of(new FunctionTool("getWeather", 
            "查询天气", weatherParamSchema)))
        .build()
);

底层发送给模型的请求体:

{
  "model": "qwen-plus",
  "messages": [
    {
      "role": "user",
      "content": "明天北京天气怎么样?"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "getWeather",
        "description": "查询指定城市的天气情况",
        "parameters": {
          "type": "object",
          "properties": {
            "city": {
              "type": "string",
              "description": "城市名称"
            },
            "date": {
              "type": "string",
              "description": "日期"
            }
          },
          "required": ["city"]
        }
      }
    }
  ]
}

模型返回工具调用

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "getWeather",
              "arguments": "{\"city\":\"北京\",\"date\":\"明天\"}"
            }
          }
        ]
      }
    }
  ]
}

第二轮:发送工具结果

{
  "messages": [
    { "role": "user", "content": "明天北京天气怎么样?" },
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "call_abc123",
          "type": "function",
          "function": {
            "name": "getWeather",
            "arguments": "{\"city\":\"北京\",\"date\":\"明天\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_abc123",
      "content": "{ \"temperature\": 22, \"condition\": \"多云\", \"rainProbability\": 15 }"
    }
  ]
}

模型生成最终回答

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "明天北京多云,气温22°C,降水概率15%,不需要带伞。"
      }
    }
  ]
}

9.2 ChatClient 自动化的秘密

使用 ChatClient 时,上述往返过程完全自动化:

// 这一行代码背后发生了:
// 1. 组装消息 + 工具定义 → 发送请求
// 2. 接收响应,检测到 tool_calls
// 3. 解析工具名和参数,在本地执行对应方法
// 4. 将工具结果封装为 ToolResultMessage
// 5. 追加到消息列表,再次发送请求
// 6. 接收最终回答,返回给用户
String result = chatClient.prompt()
    .user("明天北京天气怎么样?")
    .call()
    .content();

这就是高层 API 的价值——隐藏复杂性,暴露简洁性


10. 错误处理与容错机制

10.1 工具执行异常处理

工具函数在执行过程中可能抛出异常,需要妥善处理:

@Service
public class RobustWeatherTool {

    private static final Logger log = LoggerFactory.getLogger(RobustWeatherTool.class);

    @Tool(description = "查询城市天气,如果查询失败会返回错误信息")
    public String getWeatherSafe(
            @ToolParam(description = "城市名称") String city) {
        
        try {
            // 模拟可能失败的操作
            String result = callWeatherApi(city);
            return result;
        } catch (HttpClientErrorException e) {
            log.error("天气 API 调用失败:{}", e.getMessage());
            return String.format(
                "⚠️ 天气服务暂时不可用(HTTP %d),请稍后重试",
                e.getStatusCode().value());
        } catch (ResourceAccessException e) {
            log.error("网络异常:{}", e.getMessage());
            return "⚠️ 网络连接失败,无法获取天气信息";
        } catch (Exception e) {
            log.error("未知错误:{}", e.getMessage(), e);
            return "⚠️ 查询天气时发生错误,请稍后重试";
        }
    }

    private String callWeatherApi(String city) {
        // 实际 API 调用
        throw new RuntimeException("模拟异常");
    }
}

10.2 模型调用工具失败的处理

当模型调用了一个不存在的工具,或者参数格式错误时:

@Configuration
public class ToolCallingErrorHandler {

    @Bean
    public ToolCallingManager toolCallingManager(
            List<ToolCallbackProvider> providers) {
        
        return new ToolCallingManager() {
            @Override
            public ToolExecutionResult executeToolCalls(
                    ToolCallingRequest request, 
                    ToolCallingResponse response) {
                
                try {
                    // 调用默认执行逻辑
                    return ToolCallingManager.super
                        .executeToolCalls(request, response);
                } catch (ToolExecutionException e) {
                    // 自定义错误处理
                    log.error("工具执行失败:{}", e.getMessage());
                    
                    // 返回错误信息给模型,让其重新决策
                    String errorMessage = "工具执行失败:" + e.getMessage() 
                        + ",请尝试其他方式回答用户问题";
                    
                    return ToolExecutionResult.builder()
                        .toolResponseMessage(
                            new ToolResponseMessage(
                                List.of(new ToolResponse(
                                    e.getToolCallId(),
                                    e.getToolName(),
                                    errorMessage))))
                        .build();
                }
            }
        };
    }
}

10.3 重试机制

对于可能失败的外部调用,可以加入重试:

@Service
public class RetryableWeatherTool {

    private final RestClient restClient;

    @Tool(description = "查询天气(带自动重试)")
    @Retryable(
        value = {ResourceAccessException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2))
    public String getWeatherWithRetry(
            @ToolParam(description = "城市名称") String city) {
        
        return restClient.get()
            .uri("/weather?q={city}&appid={key}&units=metric&lang=zh_cn", 
                 city, apiKey)
            .retrieve()
            .body(String.class);
    }
}

11. 最佳实践:工具函数设计原则

11.1 命名原则

✅ 推荐❌ 避免
getWeatherfunc1
queryOrderByIddoSomething
sendEmailNotificationprocess
calculateCompoundInterestcalc

原则:使用动词开头,清晰表达功能意图。

11.2 描述原则

// ❌ 描述太简单
@Tool(description = "查询天气")
public String getWeather(String city) { ... }

// ✅ 描述详细,包含输入输出说明
@Tool(description = """
    查询指定城市的实时天气信息。
    返回内容包括:温度(摄氏度)、天气状况描述、湿度、风速、降水概率。
    适用于:回答用户关于天气的提问、出行建议等场景。
    """)
public String getWeather(
    @ToolParam(description = "城市名称,如:北京、上海、纽约") String city) { ... }

11.3 参数设计原则

  1. 参数数量不宜过多——建议不超过 5 个,过多会增加模型理解负担
  2. 参数名要有语义——用 city 而不是 c,用 startDate 而不是 s
  3. 必填和可选要明确——使用 required = false 标注可选参数
  4. 参数描述是核心——模型靠描述理解参数含义,描述越准确效果越好

11.4 返回值原则

// ❌ 返回难以解析的格式
@Tool(description = "查询用户")
public String getUser(String id) {
    return id + "|" + "张三" + "|" + "北京";  // 自定义分隔符
}

// ✅ 返回结构化 JSON
@Tool(description = "查询用户信息")
public String getUser(@ToolParam(description = "用户ID") String id) {
    User user = userRepository.findById(id);
    return objectMapper.writeValueAsString(Map.of(
        "id", user.getId(),
        "name", user.getName(),
        "city", user.getCity(),
        "email", user.getEmail()
    ));
}

11.5 工具粒度原则

策略说明示例
细粒度每个工具做一件事getWeathergetHumiditygetWindSpeed
粗粒度一个工具返回所有相关数据getWeatherDetail 返回温度+湿度+风速

推荐:粗粒度优先。 减少工具数量可以降低模型的决策复杂度。但如果返回数据过大(超过上下文窗口限制),则需要拆分。

11.6 安全原则

@Service
public class SecureDatabaseTool {

    @Tool(description = "查询数据库记录(只读)")
    public String queryDatabase(
            @ToolParam(description = "表名") String table,
            @ToolParam(description = "查询条件(WHERE 子句)") String whereClause) {
        
        // ❌ 危险:直接拼接 SQL
        // String sql = "SELECT * FROM " + table + " WHERE " + whereClause;
        
        // ✅ 安全:白名单校验
        Set<String> allowedTables = Set.of("users", "orders", "products");
        if (!allowedTables.contains(table)) {
            return "错误:不允许访问表 '" + table + "'";
        }
        
        // ✅ 安全:参数化查询
        String sql = "SELECT * FROM " + table + " WHERE id = ?";
        // 使用 PreparedStatement 执行
    }
}

12. 常见问题与排查指南

12.1 模型不调用工具

症状: 模型直接回答,不触发工具调用。

排查步骤:

  1. 检查工具是否注册
// 打印已注册的工具
chatClient.getToolCallingManager()
    .getAvailableToolCallbacks()
    .forEach(tc -> System.out.println(tc.getName()));
  1. 检查工具描述是否清晰
// 模型需要清晰的描述才能匹配意图
// "查询天气" → 可能不够具体
// "查询指定城市的实时天气,包括温度、湿度、降水概率" → 更好
  1. 检查 prompt 是否引导使用工具
chatClient.prompt()
    .system("你可以使用以下工具来帮助用户:天气查询、时间查询、计算。请根据用户问题选择合适的工具。")
    .user("明天北京天气?")
    .call().content();

12.2 工具参数提取错误

症状: 模型调用了正确的工具,但参数值错误。

排查步骤:

  1. 增强参数描述
// ❌ 模糊
@ToolParam(description = "日期") String date

// ✅ 明确
@ToolParam(description = "日期,格式为:今天、明天、后天,或具体日期如 2026-04-30") String date
  1. 提供示例值
@ToolParam(description = "城市名称,例如:北京、上海、广州、深圳") String city

12.3 工具调用循环

症状: 模型反复调用同一个工具,陷入死循环。

解决方案:

@Configuration
public class LoopPreventionConfig {

    @Bean
    public ToolCallingManager toolCallingManager(...) {
        DefaultToolCallingManager manager = new DefaultToolCallingManager(...);
        
        // 设置最大工具调用次数
        manager.setMaxToolCalls(5);  // 超过5次自动终止
        
        return manager;
    }
}

12.4 调试工具调用过程

// 开启 Spring AI 的调试日志
logging:
  level:
    org.springframework.ai: DEBUG
    org.springframework.ai.chat.client: DEBUG
    org.springframework.ai.tool: DEBUG

// 在代码中添加观察
ChatClient chatClient = ...;

String response = chatClient.prompt()
    .user("明天北京天气怎么样?")
    .advisors(advisor -> advisor.param("observation_enabled", true))
    .call()
    .content();

// 观察完整的工具调用过程

13. 总结

13.1 核心知识点回顾

知识点关键内容
什么是 Function Calling让 LLM 具备调用外部函数的能力
工作原理模型决定调用 → 框架执行 → 结果回传 → 模型生成回答
Spring AI Alibaba 支持@Tool 注解 + ChatClient 自动调用
工具定义名称、描述、参数描述决定模型的理解效果
多工具路由模型根据问题意图自动匹配最合适的工具
错误处理工具内部异常不应暴露给模型,应转化为友好提示

13.2 知识地图

Function Calling
├── 定义工具
│   ├── @Tool 注解(推荐)
│   ├── ToolCallback 接口
│   └── FunctionCallback(旧版)
├── 注册工具
│   ├── ChatClient.defaultTools()
│   └── ChatModel 手动配置
├── 工具执行
│   ├── 自动执行(ChatClient)
│   └── 手动执行(ChatModel)
├── 最佳实践
│   ├── 命名:动词开头,语义清晰
│   ├── 描述:详细、包含示例
│   ├── 参数:不超过5个,描述明确
│   └── 安全:白名单、参数化查询
└── 进阶
    ├── 多工具协同
    ├── 外部 API 集成
    ├── 错误处理与重试
    └── Agent 编排(后续文章)

13.3 下一步

Function Calling 是构建智能应用的基石。在后续文章中,我们将继续深入:

  • 第7天:多轮对话与 Memory 管理——让 AI 记住上下文
  • 第8天:Streaming 流式响应处理——实时输出体验
  • 第11天:RAG 检索增强生成架构——Function Calling 的超级应用场景
  • 第18天:Agent 编排与多 Agent 协作——Function Calling 的终极形态

掌握了 Function Calling,你就掌握了让 AI 从”聊天机器人”进化为”智能助手”的关键钥匙 🔑。


📝 作业: 尝试为你的 Spring Boot 项目添加一个自定义工具函数,比如查询数据库、发送邮件或调用第三方 API,然后让 AI 助手通过自然语言触发它。

💡 思考题: 如果工具函数执行需要很长时间(比如 30 秒的 API 调用),应该如何处理?(提示:异步执行 + 状态查询模式)