Spring AI Function Calling 完全指南:让 AI 查数据库、调 API、发通知
本文是《Spring AI 实战》系列第 9 章。前 8 章我们搭建了完整的 RAG 系统和对话式问答,但 AI 始终只能基于已有信息"说"。本章讲解 Function Calling(工具调用)机制------让 AI 在需要时"请求"你的系统执行代码,实现查数据库、调外部 API、发通知等真实操作。包含 @Tool 注解详解、参数映射、工具调用链、错误处理和完整的订单查询系统实战。
一、开篇:AI 只能"说"不能"做"的局限
前面章节的 RAG 系统有个本质限制:AI 只能基于已有数据回答问题。
用户问"公司年假有几天?"------AI 能从知识库中找到答案。但用户问以下问题,RAG 就无能为力了:
- "我的订单 ORD-2024-0088 到哪了?"(需要查数据库)
- "明天北京天气怎么样?"(需要调外部 API)
- "帮我发一条通知给运维组"(需要执行操作)
根本原因是:大模型是一个语言模型,它的能力边界在"生成文本"。它没有数据库连接、没有 HTTP 客户端、没有权限操作系统资源。它的世界是封闭的------只有训练数据和你塞进 Prompt 的上下文。
Function Calling 打破了这堵墙。 它不是让 AI 直接执行代码(那太危险了),而是让 AI 在需要时返回一个"工具调用请求"------告诉你应该调用哪个函数、传什么参数。你的系统执行完后,把结果返回给 AI,AI 基于结果生成最终的自然语言回答。
本章基于 Spring AI 2.0.0 和 Java 21,带你从原理到实战彻底搞懂 Function Calling。
二、Function Calling 原理详解
2.1 AI 不执行代码,而是"请求"执行
Function Calling 的核心设计理念是:AI 永远不直接执行代码,它只负责"判断"和"请求"。 真正的执行权在你的系统手里。
这种设计有几个好处:
- 安全性:AI 不能直接操作你的数据库、文件系统、网络------你可以在执行前做权限校验
- 可控性:你可以拦截、记录、审计每一次工具调用
- 灵活性:工具可以是任何 Java 方法------查数据库、调 REST API、发邮件,甚至执行 shell 命令
2.2 完整四步流程
用一个具体的对话来说明完整流程:
用户提问: "我的订单 ORD-2024-0088 现在什么状态?"
|
============ 第一步:AI 判断需要工具 ============
AI 分析问题后认为:我需要查询订单信息,
但我没有订单数据,需要调用 queryOrder 工具。
|
============ 第二步:AI 返回工具调用请求 ============
AI 不返回文本,而是返回一个结构化的工具调用:
{
"tool_name": "queryOrder",
"arguments": { "orderId": "ORD-2024-0088" }
}
|
============ 第三步:你的系统执行工具 ============
Spring AI 框架自动找到 queryOrder 方法,
用参数 "ORD-2024-0088" 调用它。
queryOrder 方法查数据库,返回:
{
"orderId": "ORD-2024-0088",
"status": "SHIPPED",
"logistics": "顺丰速运 SF1234567890"
}
|
============ 第四步:AI 基于结果生成回答 ============
框架把工具执行结果发回给 AI。
AI 基于数据生成自然语言回答:
"您的订单 ORD-2024-0088 已发货,物流公司为顺丰速运,
运单号 SF1234567890,预计明天送达。"
关键点:步骤二到步骤三是自动完成的。 Spring AI 框架在底层通过大模型的 Tool Calling API 实现了"请求-执行-返回"的循环。你在代码层面只需要定义工具函数和注册到 ChatClient。
2.3 AI 如何决定调用哪个工具?
大模型在做 Function Calling 时,会经历以下判断过程:
- 读取你注册的所有工具的名称 和描述
- 分析用户的问题,判断"我是否需要工具来回答?"
- 如果需要,选择最合适的工具,并从用户问题中提取参数值
- 返回工具调用请求
这意味着:工具的描述(description)至关重要。 如果描述写得好,AI 就能准确判断何时调用、调用哪个。如果描述写得差,AI 可能会乱调用或漏调用。后面会详细讲解 description 的写法。
三、@Tool 注解详解
3.1 定义工具函数的完整代码
在 Spring AI 2.0.0 中,定义工具函数非常简单------用 @Tool 注解标注方法即可:
java
package com.example.springaidemo.tools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
/**
* 订单工具类
*
* 用 @Tool 注解标注的方法会被自动注册为 AI 可调用的工具。
* AI 根据方法的 description 判断何时调用该方法。
*/
@Component
public class OrderTools {
private final OrderService orderService;
public OrderTools(OrderService orderService) {
this.orderService = orderService;
}
/**
* 根据订单号查询单个订单
*
* @Tool 的 description 是 AI 决定是否调用的关键依据。
* 写法要求:清晰描述"做什么"和"什么时候用"
*/
@Tool(description = "根据订单号查询订单详情,包括状态、物流信息和收货地址。" +
"当用户询问某个具体订单的进度、状态或物流时调用此工具。")
public OrderInfo queryOrder(
@ToolParam(description = "订单号,格式如 ORD-2024-0088")
String orderId) {
return orderService.getOrder(orderId);
}
/**
* 查询用户的所有订单
*
* 当用户问"我的订单有哪些"或"我买过什么"时调用
*/
@Tool(description = "查询指定用户的所有订单列表。" +
"当用户想查看自己的全部订单、查询购买记录时调用。")
public List<OrderSummary> listUserOrders(
@ToolParam(description = "用户ID,通常以 USER- 开头")
String userId) {
return orderService.getOrdersByUserId(userId);
}
/**
* 取消订单------注意:这个工具会修改数据!生产环境应该加权限校验
*/
@Tool(description = "取消指定的订单。仅当用户明确要求取消订单时调用。" +
"取消后订单状态变为 CANCELLED,不可恢复。")
public CancelResult cancelOrder(
@ToolParam(description = "要取消的订单号")
String orderId,
@ToolParam(description = "取消原因,如:不想要了、地址填错了")
String reason) {
return orderService.cancelOrder(orderId, reason);
}
/**
* 发送物流通知短信
*/
@Tool(description = "向指定手机号发送物流通知短信。" +
"当用户要求发送物流通知或催促发货通知时调用。")
public String sendLogisticsNotification(
@ToolParam(description = "收件人手机号")
String phone,
@ToolParam(description = "通知内容")
String message) {
// 调用短信服务发送通知
smsService.send(phone, message);
return "通知已发送至 " + phone;
}
}
3.2 @Tool 和 @ToolParam 属性说明
@Tool 注解属性:
| 属性 | 说明 | 示例 |
|---|---|---|
description |
工具功能描述,AI 根据这个描述决定调用时机 | "查询订单详情" |
name |
工具名称(可选),默认使用方法名 | "queryOrder" |
returnDirect |
是否直接返回工具结果给用户,不经过 AI 处理(默认 false) | true |
@ToolParam 注解属性:
| 属性 | 说明 | 示例 |
|---|---|---|
description |
参数描述,帮助 AI 从用户问题中提取正确的参数值 | "订单号,格式如 ORD-2024-0088" |
name |
参数名称(可选),默认使用参数变量名 | "orderId" |
required |
是否必填(默认 true) | false |
3.3 description 的写法------至关重要
description 是 Function Calling 的灵魂。好的 description 能让 AI 准确判断调用时机;差的 description 会导致乱调用或漏调用。
好的 description 写法:
java
// 好的写法:说明了做什么 + 什么时候用 + 返回什么
@Tool(description = "根据订单号查询订单详情,包括状态、物流和收货信息。" +
"当用户询问某个具体订单的进度、状态或物流时调用。" +
"返回订单的完整信息,包括商品列表、金额和物流轨迹。")
public OrderInfo queryOrder(...) { ... }
差的 description 写法(不要这样写):
java
// 差的写法:太简短,AI 不知道什么时候该用
@Tool(description = "查订单")
public OrderInfo queryOrder(...) { ... }
// 差的写法:太技术化,AI 不理解业务含义
@Tool(description = "执行 select * from orders where id = ?")
public OrderInfo queryOrder(...) { ... }
核心原则: 写给 AI 看的,不是写给程序员看的。要用自然语言描述业务场景。
四、参数类型映射
4.1 基本类型映射
Spring AI 支持以下基本类型作为工具参数:
java
@Component
public class BasicTypeTools {
@Tool(description = "根据用户名查询用户信息")
public UserInfo getUserByName(
@ToolParam(description = "用户名") String username) {
return userService.findByName(username);
}
@Tool(description = "根据订单号查询订单总金额")
public double getOrderAmount(
@ToolParam(description = "订单号") String orderId) {
return orderService.getAmount(orderId);
}
@Tool(description = "设置商品的库存数量")
public String setStock(
@ToolParam(description = "商品ID") String productId,
@ToolParam(description = "库存数量") int quantity) {
productService.updateStock(productId, quantity);
return "库存已更新为 " + quantity;
}
@Tool(description = "判断商品是否在售")
public boolean isProductOnSale(
@ToolParam(description = "商品ID") String productId) {
return productService.isOnSale(productId);
}
}
4.2 复杂对象(record)映射
工具参数也可以是复杂对象。Spring AI 会自动生成 JSON Schema,大模型按 Schema 构造 JSON 参数,框架再反序列化为 Java 对象:
java
/**
* 搜索请求参数
*
* Spring AI 会自动将此 record 转为 JSON Schema 传给大模型。
* 大模型根据 JSON Schema 构造参数,框架反序列化为 ProductSearchRequest 对象。
*/
public record ProductSearchRequest(
@ToolParam(description = "搜索关键词,如'手机'、'运动鞋'")
String keyword,
@ToolParam(description = "最低价格(元),可选,不传则不限")
Integer minPrice,
@ToolParam(description = "最高价格(元),可选,不传则不限")
Integer maxPrice,
@ToolParam(description = "排序方式:price_asc 价格升序,price_desc 价格降序,sales 销量")
String sortBy,
@ToolParam(description = "分类过滤,如'电子产品'、'服装'")
String category
) {}
@Component
public class ProductTools {
@Tool(description = "搜索商品。根据关键词、价格范围和分类搜索商品列表。" +
"当用户想找商品、比价或浏览商品时调用。")
public List<Product> searchProducts(ProductSearchRequest request) {
return productService.search(
request.keyword(),
request.minPrice(),
request.maxPrice(),
request.sortBy(),
request.category()
);
}
}
自动生成的 JSON Schema 大致如下:
json
{
"type": "object",
"properties": {
"keyword": { "type": "string", "description": "搜索关键词" },
"minPrice": { "type": "integer", "description": "最低价格(元)" },
"maxPrice": { "type": "integer", "description": "最高价格(元)" },
"sortBy": { "type": "string", "description": "排序方式" },
"category": { "type": "string", "description": "分类过滤" }
},
"required": []
}
注意:使用 record 作为参数时,所有字段默认都是可选的(required = false)。如果某些字段是必填的,需要在方法体内做校验。
五、注册工具到 ChatClient
注册工具非常简单,通过 ChatClient.Builder 的 defaultTools() 方法:
java
package com.example.springaidemo.config;
import com.example.springaidemo.tools.OrderTools;
import com.example.springaidemo.tools.WeatherTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* ChatClient 配置
*
* 通过 defaultTools() 注册工具类。
* Spring AI 会自动扫描该类中所有 @Tool 标注的方法,
* 生成 JSON Schema 并发送给大模型。
*/
@Configuration
public class ChatConfig {
@Bean
public ChatClient chatClient(
ChatClient.Builder builder,
OrderTools orderTools,
WeatherTools weatherTools) {
return builder
.defaultSystem("""
你是一个智能客服助手。
你可以帮用户查询订单、天气等信息。
回答时要简洁、准确、有礼貌。
如果工具调用失败,请友好地告知用户并建议重试。
""")
// 注册工具类------Spring AI 会扫描其中所有 @Tool 方法
.defaultTools(orderTools, weatherTools)
.build();
}
}
注册后的效果:
- 每次调用 ChatClient 时,大模型都会收到所有注册工具的 JSON Schema
- 大模型自动判断用户问题是否需要调用工具
- 如果需要,大模型返回工具调用请求(函数名 + 参数 JSON)
- Spring AI 自动执行对应方法,将结果发回大模型
- 大模型基于结果生成最终回答
你不需要手动处理工具调用的循环 ------Spring AI 在 ChatClient.call() 内部已经实现了。
六、工具调用链
6.1 一次对话中多次调用不同工具
用户的问题有时候需要 AI 连续调用多个工具才能回答。比如:
"帮我查一下订单 ORD-2024-0088 的物流,然后发个通知给 138****8888。"
AI 会依次:
- 调用
queryOrder("ORD-2024-0088")查到物流信息 - 调用
sendLogisticsNotification("138****8888", "您的订单...")发送通知 - 综合两次工具的结果,生成最终回答
这个过程称为工具调用链,Spring AI 框架自动支持,你不需要写额外代码。
6.2 代码示例
java
@Service
public class AssistantService {
private final ChatClient chatClient;
public AssistantService(ChatClient chatClient) {
this.chatClient = chatClient;
}
/**
* 统一聊天接口
*
* 用户的问题可能需要 0 次、1 次或多次工具调用。
* Spring AI 自动处理工具调用链------你只需要一次 call()。
*
* 框架内部流程:
* 1. 发送用户问题 + 工具列表给大模型
* 2. 如果大模型返回工具调用 -> 执行 -> 结果发回大模型
* 3. 重复步骤 2 直到大模型返回文本回答(不再需要工具)
* 4. 返回最终文本回答
*/
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}
java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final AssistantService assistantService;
public ChatController(AssistantService assistantService) {
this.assistantService = assistantService;
}
@PostMapping
public Map<String, String> chat(@RequestBody Map<String, String> request) {
String userMessage = request.get("message");
String reply = assistantService.chat(userMessage);
return Map.of("reply", reply);
}
}
bash
# 测试工具调用链
curl -X POST "http://localhost:8080/api/chat" \
-H "Content-Type: application/json" \
-d '{"message": "查一下订单 ORD-2024-0088 的物流,然后发短信通知 13812345678"}'
# AI 会自动:
# 1. 调用 queryOrder 查询物流
# 2. 调用 sendLogisticsNotification 发送通知
# 3. 返回:"已查询到您的订单已发货,运单号 SF1234567890,通知已发送。"
七、错误处理
工具调用可能失败------数据库连接异常、外部 API 超时、参数校验不通过。Spring AI 提供两种错误处理策略:
7.1 策略一:工具方法内部 try-catch
在工具方法内部捕获异常,返回错误信息字符串。AI 会根据错误信息生成友好的回答:
java
@Tool(description = "根据订单号查询订单详情,包括状态和物流信息。" +
"当用户询问某个具体订单的进度、状态或物流时调用。")
public String queryOrder(
@ToolParam(description = "订单号,格式如 ORD-2024-0088")
String orderId) {
try {
// 参数校验
if (orderId == null || orderId.isBlank()) {
return "错误:订单号不能为空,请提供有效的订单号。";
}
OrderInfo order = orderService.getOrder(orderId);
if (order == null) {
// 返回明确提示,AI 会把这个信息告知用户
return "未找到订单 " + orderId + ",请检查订单号是否正确。";
}
// 将订单信息转为可读文本返回给 AI
return """
订单号:%s
状态:%s
物流:%s %s
收货人:%s
""".formatted(
order.orderId(), order.status(),
order.carrier(), order.trackingNumber(),
order.receiverName());
} catch (Exception e) {
// 捕获异常,返回错误信息而非抛出异常(抛出异常会导致整个调用失败)
return "查询订单时出错:%s,请稍后重试。".formatted(e.getMessage());
}
}
7.2 策略二:参数预校验 + 日志分离
对于生产环境,建议在工具方法内做完整的参数校验,同时将技术异常和业务错误分开处理:
java
@Tool(description = "安全地查询订单,包含完整的参数校验和权限检查")
public String safeQueryOrder(
@ToolParam(description = "订单号") String orderId) {
// ====== 第一层:参数格式校验 ======
if (orderId == null || orderId.isBlank()) {
return "错误:订单号不能为空";
}
if (!orderId.matches("^ORD-\\d{4}-\\d{4}$")) {
return "错误:订单号格式不正确,正确格式如 ORD-2024-0088";
}
// ====== 第二层:权限校验(生产环境必须加!) ======
// String currentUser = getCurrentUser();
// OrderInfo order = orderService.getOrder(orderId);
// if (!order.userId().equals(currentUser)) {
// return "错误:您无权查看该订单";
// }
// ====== 第三层:业务逻辑 ======
try {
OrderInfo order = orderService.getOrder(orderId);
if (order == null) {
return "未找到订单 " + orderId;
}
return "订单 %s 状态为 %s,物流公司 %s,运单号 %s".formatted(
order.orderId(), order.status(),
order.carrier(), order.trackingNumber());
} catch (Exception e) {
// 记录完整日志,但给 AI 返回友好信息
log.error("查询订单失败: orderId={}", orderId, e);
return "系统繁忙,请稍后重试";
}
}
选择建议: 大多数场景用策略一就够了(方法内部 try-catch 返回错误字符串)。生产环境建议加上参数校验和权限检查。绝对不要在工具方法中抛出未捕获异常,否则 Spring AI 无法将错误信息传回 AI,用户只能得到一个模糊的系统错误。
八、实战:订单查询系统
8.1 Order 实体
java
package com.example.springaidemo.entity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 订单详情
*/
public record OrderInfo(
String orderId, // 订单号
String userId, // 用户ID
String status, // PENDING / PAID / SHIPPED / DELIVERED / CANCELLED
BigDecimal totalAmount, // 订单总金额
List<OrderItem> items, // 商品列表
String receiverName, // 收货人
String receiverPhone, // 收货电话
String address, // 收货地址
String carrier, // 物流公司
String trackingNumber, // 运单号
LocalDateTime createTime, // 下单时间
LocalDateTime updateTime // 更新时间
) {}
/** 订单商品 */
public record OrderItem(
String productId,
String productName,
int quantity,
BigDecimal price
) {}
/** 订单摘要(列表展示用) */
public record OrderSummary(
String orderId,
String status,
BigDecimal totalAmount,
LocalDateTime createTime
) {}
/** 取消结果 */
public record CancelResult(
boolean success,
String message
) {}
8.2 OrderService
java
package com.example.springaidemo.service;
import com.example.springaidemo.entity.*;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 订单服务(模拟实现)
*
* 生产环境中替换为真实的数据库操作(JPA / MyBatis)
*/
@Service
public class OrderService {
// 模拟数据存储(生产环境用数据库)
private final Map<String, OrderInfo> orderDb = new ConcurrentHashMap<>();
public OrderService() {
// 初始化两条模拟订单
orderDb.put("ORD-2024-0088", new OrderInfo(
"ORD-2024-0088", "USER-001", "SHIPPED",
new BigDecimal("299.00"),
List.of(new OrderItem("P001", "无线蓝牙耳机", 1, new BigDecimal("299.00"))),
"张三", "138****8888", "北京市朝阳区xxx",
"顺丰速运", "SF1234567890",
LocalDateTime.of(2024, 3, 10, 14, 30),
LocalDateTime.of(2024, 3, 12, 9, 0)
));
orderDb.put("ORD-2024-0099", new OrderInfo(
"ORD-2024-0099", "USER-001", "DELIVERED",
new BigDecimal("1599.00"),
List.of(new OrderItem("P002", "机械键盘", 1, new BigDecimal("1599.00"))),
"张三", "138****8888", "北京市海淀区xxx",
"中通快递", "ZT9876543210",
LocalDateTime.of(2024, 2, 20, 10, 15),
LocalDateTime.of(2024, 2, 22, 16, 0)
));
}
/** 根据订单号查询 */
public OrderInfo getOrder(String orderId) {
return orderDb.get(orderId);
}
/** 查询用户的所有订单 */
public List<OrderSummary> getOrdersByUserId(String userId) {
return orderDb.values().stream()
.filter(o -> o.userId().equals(userId))
.map(o -> new OrderSummary(o.orderId(), o.status(),
o.totalAmount(), o.createTime()))
.toList();
}
/** 取消订单 */
public CancelResult cancelOrder(String orderId, String reason) {
OrderInfo order = orderDb.get(orderId);
if (order == null) {
return new CancelResult(false, "订单不存在");
}
if ("DELIVERED".equals(order.status()) || "CANCELLED".equals(order.status())) {
return new CancelResult(false,
"订单状态为 " + order.status() + ",无法取消");
}
// 实际场景:更新数据库状态为 CANCELLED
return new CancelResult(true, "订单 " + orderId + " 已取消,原因:" + reason);
}
}
8.3 OrderTools(工具定义)
java
package com.example.springaidemo.tools;
import com.example.springaidemo.entity.*;
import com.example.springaidemo.service.OrderService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 订单工具类
*
* 每个方法用 @Tool 标注,Spring AI 会自动扫描注册。
* AI 根据描述决定调用哪个方法。
*/
@Component
public class OrderTools {
private final OrderService orderService;
public OrderTools(OrderService orderService) {
this.orderService = orderService;
}
@Tool(description = "根据订单号查询订单详情,包括状态、物流、商品和金额。" +
"当用户询问某个具体订单的进度、状态、物流或商品信息时调用。")
public String queryOrder(
@ToolParam(description = "订单号,格式如 ORD-2024-0088")
String orderId) {
try {
OrderInfo order = orderService.getOrder(orderId);
if (order == null) {
return "未找到订单 " + orderId + ",请确认订单号是否正确。";
}
// 格式化返回,方便 AI 理解并转述给用户
StringBuilder sb = new StringBuilder();
sb.append("订单号:").append(order.orderId()).append("\n");
sb.append("状态:").append(order.status()).append("\n");
sb.append("金额:").append(order.totalAmount()).append(" 元\n");
sb.append("收货人:").append(order.receiverName()).append("\n");
if (order.carrier() != null) {
sb.append("物流:").append(order.carrier())
.append(" ").append(order.trackingNumber());
}
return sb.toString();
} catch (Exception e) {
return "查询出错:" + e.getMessage();
}
}
@Tool(description = "查询指定用户的所有订单列表。" +
"当用户想查看自己的全部订单或购买记录时调用。")
public String listUserOrders(
@ToolParam(description = "用户ID,如 USER-001")
String userId) {
try {
List<OrderSummary> orders = orderService.getOrdersByUserId(userId);
if (orders.isEmpty()) {
return "该用户暂无订单记录。";
}
StringBuilder sb = new StringBuilder("共 " + orders.size() + " 条订单:\n");
for (var o : orders) {
sb.append("- ").append(o.orderId())
.append(" | ").append(o.status())
.append(" | ").append(o.totalAmount()).append(" 元")
.append(" | ").append(o.createTime()).append("\n");
}
return sb.toString();
} catch (Exception e) {
return "查询出错:" + e.getMessage();
}
}
@Tool(description = "取消指定的订单。仅当用户明确要求取消时调用。" +
"已发货或已完成的订单无法取消。")
public String cancelOrder(
@ToolParam(description = "要取消的订单号")
String orderId,
@ToolParam(description = "取消原因")
String reason) {
try {
CancelResult result = orderService.cancelOrder(orderId, reason);
return result.success()
? result.message()
: "取消失败:" + result.message();
} catch (Exception e) {
return "取消出错:" + e.getMessage();
}
}
}
8.4 OrderController
java
package com.example.springaidemo.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 订单智能客服 Controller
*
* 用户通过自然语言与客服交互,AI 自动判断是否需要调用工具。
*/
@RestController
@RequestMapping("/api/order-chat")
public class OrderController {
private final ChatClient chatClient;
public OrderController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@PostMapping
public Map<String, String> chat(@RequestBody Map<String, String> request) {
String message = request.get("message");
String reply = chatClient.prompt()
.user(message)
.call()
.content();
return Map.of("reply", reply);
}
}
8.5 测试完整流程
bash
# 测试 1:查询单个订单
curl -X POST "http://localhost:8080/api/order-chat" \
-H "Content-Type: application/json" \
-d '{"message": "帮我查一下订单 ORD-2024-0088 到哪了"}'
# AI 自动调用 queryOrder -> 返回物流信息
# 测试 2:查询所有订单
curl -X POST "http://localhost:8080/api/order-chat" \
-H "Content-Type: application/json" \
-d '{"message": "我用户ID是 USER-001,帮我看看我有哪些订单"}'
# AI 自动调用 listUserOrders -> 返回订单列表
# 测试 3:取消订单
curl -X POST "http://localhost:8080/api/order-chat" \
-H "Content-Type: application/json" \
-d '{"message": "我不想买了,帮我把订单 ORD-2024-0088 取消掉,原因是不想要了"}'
# AI 自动调用 cancelOrder -> 返回取消结果
# 测试 4:不需要工具的问题
curl -X POST "http://localhost:8080/api/order-chat" \
-H "Content-Type: application/json" \
-d '{"message": "你们的退货政策是什么?"}'
# AI 不调用任何工具,直接用自身知识回答
九、踩坑总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| AI 不调用工具,直接瞎回答 | 工具的 description 写得不够清晰,AI 不知道有这个能力 | 优化 description,明确说明"做什么"和"什么时候用" |
| AI 调用了错误的工具 | 多个工具的 description 太相似,AI 分不清 | 给每个工具加场景限定词,比如"查单个订单用A,查全部订单用B" |
| AI 传了错误的参数 | @ToolParam 的 description 不够具体 | 在 description 中给出参数格式示例,如"格式如 ORD-2024-0088" |
| 工具方法抛异常,整个调用失败 | 工具方法没有 try-catch,异常传不到 AI | 工具方法内必须 try-catch,返回错误字符串给 AI |
| 工具执行很慢,用户等太久 | 外部 API 或数据库查询耗时 | 给工具方法加超时控制;考虑异步工具 + 流式返回 |
| AI 对同一个问题反复调用工具 | 工具调用结果不够明确,AI 不确定是否完成 | 返回格式清晰的结果文本,包含明确的"查询成功/失败"标记 |
| 注册了太多工具,AI 选不准 | 工具数量超过 20 个,大模型的决策能力下降 | 按场景拆分多个 ChatClient,每个只注册相关工具 |
小结
本章从 Function Calling 的原理出发,讲解了 Spring AI 2.0.0 中工具调用的完整实现方案:
- 原理:AI 不执行代码,而是"请求"你的系统执行------安全性由你掌控
- @Tool 注解:用注解定义工具,description 是灵魂------写好 description,AI 就能准确调用
- 参数映射:支持基本类型和 record 复杂对象,自动生成 JSON Schema
- 工具调用链:一次对话中 AI 可自动调用多个工具,框架全自动处理
- 错误处理:工具方法内 try-catch 返回错误字符串,不要抛异常
- 实战:完整的订单查询系统,从实体到 Service 到 Tools 到 Controller
下一章我们将学习 MCP 协议------Function Calling 的升级版,让工具像 USB 一样"即插即用"。
