第6天 - Function Calling / Tool Calling 原理与实现
系列: Spring AI Alibaba 技术博客系列
日期: 2026-04-30
难度: ⭐⭐⭐⭐⭐
前置知识: ChatModel 基础使用、Prompt 工程、系统提示词最佳实践
目录
- 什么是 Function Calling?为什么它如此重要
- Function Calling 的核心工作原理
- Spring AI Alibaba 中的 Function Calling 体系
- 实战一:定义并注册一个简单工具函数
- 实战二:多工具函数与自动路由
- 实战三:带参数的复杂工具函数
- 实战四:外部 API 调用工具
- Tool Calling vs Function Calling:概念辨析
- 底层机制:消息协议与往返流程
- 错误处理与容错机制
- 最佳实践:工具函数设计原则
- 常见问题与排查指南
- 总结
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 关键要点
- 模型不执行函数——模型只负责”决定调用哪个函数”和”参数是什么”
- 框架负责执行——Spring AI Alibaba 负责在本地执行函数并返回结果
- 多轮往返——一次 Function Calling 通常涉及 2 次模型调用
- 工具定义即文档——函数名、描述、参数描述直接影响模型的理解
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 多工具路由的原理
模型在收到请求时,会基于以下信息自动决策:
- 工具名称——直观的名称帮助模型理解工具用途
- 工具描述——详细的描述是模型决策的关键依据
- 参数描述——帮助模型正确提取和映射参数
- 用户问题——模型分析问题意图,匹配最合适的工具
用户问题:"帮我查一下明天上海的天气"
│
├── 意图分析:查询天气
├── 实体提取:城市=上海,时间=明天
├── 工具匹配: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 Calling | Tool 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 命名原则
| ✅ 推荐 | ❌ 避免 |
|---|---|
getWeather | func1 |
queryOrderById | doSomething |
sendEmailNotification | process |
calculateCompoundInterest | calc |
原则:使用动词开头,清晰表达功能意图。
11.2 描述原则
// ❌ 描述太简单
@Tool(description = "查询天气")
public String getWeather(String city) { ... }
// ✅ 描述详细,包含输入输出说明
@Tool(description = """
查询指定城市的实时天气信息。
返回内容包括:温度(摄氏度)、天气状况描述、湿度、风速、降水概率。
适用于:回答用户关于天气的提问、出行建议等场景。
""")
public String getWeather(
@ToolParam(description = "城市名称,如:北京、上海、纽约") String city) { ... }
11.3 参数设计原则
- 参数数量不宜过多——建议不超过 5 个,过多会增加模型理解负担
- 参数名要有语义——用
city而不是c,用startDate而不是s - 必填和可选要明确——使用
required = false标注可选参数 - 参数描述是核心——模型靠描述理解参数含义,描述越准确效果越好
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 工具粒度原则
| 策略 | 说明 | 示例 |
|---|---|---|
| 细粒度 | 每个工具做一件事 | getWeather、getHumidity、getWindSpeed |
| 粗粒度 | 一个工具返回所有相关数据 | 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 模型不调用工具
症状: 模型直接回答,不触发工具调用。
排查步骤:
- 检查工具是否注册
// 打印已注册的工具
chatClient.getToolCallingManager()
.getAvailableToolCallbacks()
.forEach(tc -> System.out.println(tc.getName()));
- 检查工具描述是否清晰
// 模型需要清晰的描述才能匹配意图
// "查询天气" → 可能不够具体
// "查询指定城市的实时天气,包括温度、湿度、降水概率" → 更好
- 检查 prompt 是否引导使用工具
chatClient.prompt()
.system("你可以使用以下工具来帮助用户:天气查询、时间查询、计算。请根据用户问题选择合适的工具。")
.user("明天北京天气?")
.call().content();
12.2 工具参数提取错误
症状: 模型调用了正确的工具,但参数值错误。
排查步骤:
- 增强参数描述
// ❌ 模糊
@ToolParam(description = "日期") String date
// ✅ 明确
@ToolParam(description = "日期,格式为:今天、明天、后天,或具体日期如 2026-04-30") String date
- 提供示例值
@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 调用),应该如何处理?(提示:异步执行 + 状态查询模式)