Spring AI Function Calling 完全指南:让 AI 查数据库、调 API、发通知

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 永远不直接执行代码,它只负责"判断"和"请求"。 真正的执行权在你的系统手里。

这种设计有几个好处:

  1. 安全性:AI 不能直接操作你的数据库、文件系统、网络------你可以在执行前做权限校验
  2. 可控性:你可以拦截、记录、审计每一次工具调用
  3. 灵活性:工具可以是任何 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 时,会经历以下判断过程:

  1. 读取你注册的所有工具的名称描述
  2. 分析用户的问题,判断"我是否需要工具来回答?"
  3. 如果需要,选择最合适的工具,并从用户问题中提取参数值
  4. 返回工具调用请求

这意味着:工具的描述(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.BuilderdefaultTools() 方法:

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();
    }
}

注册后的效果:

  1. 每次调用 ChatClient 时,大模型都会收到所有注册工具的 JSON Schema
  2. 大模型自动判断用户问题是否需要调用工具
  3. 如果需要,大模型返回工具调用请求(函数名 + 参数 JSON)
  4. Spring AI 自动执行对应方法,将结果发回大模型
  5. 大模型基于结果生成最终回答

你不需要手动处理工具调用的循环 ------Spring AI 在 ChatClient.call() 内部已经实现了。


六、工具调用链

6.1 一次对话中多次调用不同工具

用户的问题有时候需要 AI 连续调用多个工具才能回答。比如:

"帮我查一下订单 ORD-2024-0088 的物流,然后发个通知给 138****8888。"

AI 会依次:

  1. 调用 queryOrder("ORD-2024-0088") 查到物流信息
  2. 调用 sendLogisticsNotification("138****8888", "您的订单...") 发送通知
  3. 综合两次工具的结果,生成最终回答

这个过程称为工具调用链,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 中工具调用的完整实现方案:

  1. 原理:AI 不执行代码,而是"请求"你的系统执行------安全性由你掌控
  2. @Tool 注解:用注解定义工具,description 是灵魂------写好 description,AI 就能准确调用
  3. 参数映射:支持基本类型和 record 复杂对象,自动生成 JSON Schema
  4. 工具调用链:一次对话中 AI 可自动调用多个工具,框架全自动处理
  5. 错误处理:工具方法内 try-catch 返回错误字符串,不要抛异常
  6. 实战:完整的订单查询系统,从实体到 Service 到 Tools 到 Controller

下一章我们将学习 MCP 协议------Function Calling 的升级版,让工具像 USB 一样"即插即用"。


Spring AI 实战 -- 第9章:完整内容与源码

相关推荐
沫儿笙7 小时前
弧焊机器人氩气节气装置
人工智能·机器人
中微极客7 小时前
AI Agent与TinyML边缘部署:OAuth 2.0集成实战
人工智能
是上好佳佳佳呀7 小时前
【机器学习|DAY01】机器学习概述
人工智能·机器学习
风起洛阳@不良使7 小时前
spring中xml和注解开发的对比
xml·java·spring
沉默王二7 小时前
又一个顶级 AI 终端诞生了!Kaku 开源,实测安装配置+分屏对决
人工智能·openai·claude
xcLeigh7 小时前
Doubao-Seed-Evolving大模型接入教程|搭建全品类提示词+AI工具导航网页
前端·人工智能·python·ai·html·ai开发·豆包
不如语冰7 小时前
AI大模型入门-模块导入import
数据结构·人工智能·pytorch·python
海兰8 小时前
【高速缓存】RedisVL 指南:从向量搜索到 AI 应用落地
人工智能·redis
weixin_727535628 小时前
双Token认证体系深度拆解:Spring Security + JWT + Redis
redis·spring·wpf