← 返回上一页

第7章: Function Calling 讲透:让AI稳定调用业务工具

让 AI 真正调用工具函数!

📋 本章目标

本章通过几个核心示例,带你掌握 Spring AI 的 Function Calling 功能。 你将学会如何使用 @Tool 注解定义工具函数,让 AI 能够自动调用这些函数获取实时数据。

✨ 你将学到什么?

  • ✅ 理解 Function Calling 的原理
  • ✅ 学会使用 @Tool@ToolParam 定义工具函数
  • ✅ 掌握 ChatClient 配置工具的方法
  • ✅ 实现 AI 自动调用工具函数
  • ✅ 理解 ChatModel 和 ChatClient 的区别
  • ✅ 测试和验证所有工具函数

📚 测试用例索引

  • 测试 7.0:使用 ChatClient 配置工具函数
  • 测试 7.1:工具函数小结

⚠️ 重要提示

本章内容是 Spring AI 的核心功能,Function Calling 是 AI 应用开发的关键技术,请仔细学习!

🎯 前置准备

1. 确认已完成前面的章节

2. 理解 ChatModel vs ChatClient

❌ ChatModel(基础对话)

java
// 只能进行简单对话
String answer = chatModel.call("现在几点?");
// ❌ AI 无法自动调用工具函数

✅ ChatClient(高级功能)

java
// 配置工具后,AI 可以自动调用
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(toolFunctionConfig)  // ⭐ 关键
    .build();

String answer = chatClient.prompt()
    .user("现在几点?")
    .call()
    .content();
// ✅ AI 会自动调用 getCurrentTime() 工具

💡 关键区别

  • ChatModel:无法配置工具,AI 不会自动调用函数
  • ChatClient:可以配置工具,AI 会自动调用函数
  • 这是本章最重要的知识点!

📝 Step 1:创建工具函数配置类

1.1 理解 @Tool 注解

Spring AI 提供了 @Tool 注解来定义工具函数:

1.2 创建 ToolFunctionConfig 类

创建文件:src/main/java/com/example/chatbot/config/ToolFunctionConfig.java

java
package com.example.chatbot.config;

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

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 工具函数配置
 * 
 * 使用 @Tool 注解定义 AI 可以调用的工具函数
 */
@Component
public class ToolFunctionConfig {

    /**
     * 获取当前时间
     */
    @Tool(description = "获取当前的日期和时间,返回格式化的时间字符串")
    public String getCurrentTime() {
        LocalDateTime now = LocalDateTime.now();
        String formatted = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        System.out.println("🔧 工具调用: getCurrentTime() -> " + formatted);
        return formatted;
    }

    /**
     * 查询数据库
     */
    @Tool(description = "查询数据库,获取用户、订单等数据的统计信息")
    public String queryDatabase(
            @ToolParam(description = "查询的内容,如'用户数量'、'今日订单'、'活跃用户'等") String query) {
        System.out.println("🔧 工具调用: queryDatabase(\"" + query + "\")");
        
        String result = switch (query.toLowerCase()) {
            case "用户数量", "有多少用户", "用户总数" -> "系统目前有 10,523 个注册用户";
            case "今日订单", "今天的订单", "订单数量" -> "今天共有 156 个订单";
            case "活跃用户", "在线用户" -> "当前有 342 个活跃用户在线";
            default -> "未找到相关数据";
        };
        
        System.out.println("✅ 返回: " + result);
        return result;
    }

    /**
     * 文件操作
     */
    @Tool(description = "执行文件操作,如查看文件大小、检查文件是否存在等")
    public String fileOperation(
            @ToolParam(description = "操作类型,如'查看大小'、'检查存在'、'最后修改时间'") String operation,
            @ToolParam(description = "文件名") String filename) {
        System.out.println("🔧 工具调用: fileOperation(" + operation + ", " + filename + ")");
        
        String result = switch (operation.toLowerCase()) {
            case "查看大小", "文件大小", "size" -> "文件 " + filename + " 的大小为 2.5 MB";
            case "检查存在", "是否存在", "exists" -> "文件 " + filename + " 存在";
            case "最后修改时间", "修改时间", "modified" -> "文件 " + filename + " 最后修改时间: 2024-11-21 10:30:00";
            default -> "不支持的操作: " + operation;
        };
        
        System.out.println("✅ 返回: " + result);
        return result;
    }

    /**
     * 发送邮件
     */
    @Tool(description = "发送邮件给指定收件人,返回发送结果")
    public String sendEmail(
            @ToolParam(description = "收件人邮箱") String to,
            @ToolParam(description = "邮件主题") String subject,
            @ToolParam(description = "邮件正文") String body) {
        System.out.println("🔧 工具调用: sendEmail(" + to + ", \"" + subject + "\")");
        
        String mailId = "MAIL-" + System.currentTimeMillis();
        String result = "邮件已发送给 " + to + ",邮件ID: " + mailId;
        
        System.out.println("✅ 返回: " + result);
        return result;
    }
}

1.3 编译验证

bash
# 编译项目
mvn compile

预期输出:

text
[INFO] BUILD SUCCESS

✅ 关键点

  • 使用 @Component 注册为 Spring Bean
  • 使用 @Tool 标记工具函数
  • 使用 @ToolParam 描述参数
  • 添加日志输出,方便观察调用

📝 Step 2:创建测试类

测试类基本结构

创建文件:src/test/java/com/example/chatbot/knowledge/Test07_ToolAnnotation.java

java
package com.example.chatbot.knowledge;

import com.example.chatbot.config.ToolFunctionConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @Tool 注解测试
 * 
 * 测试使用 @Tool 注解定义的工具函数
 */
@SpringBootTest(classes = com.example.chatbot.AiChatbotApplication.class)
@DisplayName("知识点7:@Tool 注解")
public class Test07_ToolAnnotation {

    @Autowired
    private ChatModel chatModel;
    
    @Autowired
    private ToolFunctionConfig toolFunctionConfig;
    
    // 测试方法将在这里添加
}

编译测试类

bash
# 编译测试类
mvn test-compile

预期输出:

text
[INFO] BUILD SUCCESS

🧪 测试 7.0:使用 ChatClient 配置工具函数

学习如何使用 ChatClient 配置工具函数,让 AI 能够自动调用。

测试代码

java
@Test
@DisplayName("7.0 使用 ChatClient 配置工具函数")
public void test00_ChatClientWithTools() {
    System.out.println("\n========== 测试 7.0:使用 ChatClient 配置工具函数 ==========");
    
    System.out.println("\n📝 创建配置了工具的 ChatClient");
    
    // 创建配置了工具的 ChatClient
    org.springframework.ai.chat.client.ChatClient chatClient = 
        org.springframework.ai.chat.client.ChatClient.builder(chatModel)
            .defaultTools(toolFunctionConfig)
            .build();
    
    System.out.println("✅ ChatClient 创建成功,已配置工具函数");
    
    // 测试1:获取时间
    System.out.println("\n🧪 测试1:让 AI 获取当前时间");
    String question1 = "现在几点了?请告诉我准确的时间";
    System.out.println("👤 问题: " + question1);
    System.out.println("⏰ 观察是否有 '🔧 工具调用' 日志...\n");
    
    String answer1 = chatClient.prompt()
        .user(question1)
        .call()
        .content();
    
    System.out.println("🤖 回答: " + answer1);
    
    boolean hasTime = answer1.matches(".*\\d{1,2}[点:]\\d{1,2}.*") || 
                     answer1.matches(".*\\d{4}-\\d{2}-\\d{2}.*");
    System.out.println(hasTime ? "✅ 包含时间信息 - AI 可能调用了工具!" : "❌ 没有时间信息");
    
    // 测试2:查询数据库
    System.out.println("\n🧪 测试2:让 AI 查询数据库");
    String question2 = "系统有多少用户?";
    System.out.println("👤 问题: " + question2);
    System.out.println("⏰ 观察是否有 '🔧 工具调用' 日志...\n");
    
    String answer2 = chatClient.prompt()
        .user(question2)
        .call()
        .content();
    
    System.out.println("🤖 回答: " + answer2);
    
    boolean hasUserCount = answer2.contains("10,523") || answer2.contains("10523");
    System.out.println(hasUserCount ? "✅ 包含用户数量 - AI 可能调用了工具!" : "❌ 没有用户数量");

    // 测试3:发送邮件
    System.out.println("\n🧪 测试3:让 AI 发送邮件");
    String question3 = "请给 zhangsan@example.com 发送一封邮件,主题是\"会议提醒\",内容是\"明天下午3点开会\"";
    System.out.println("👤 问题: " + question3);
    System.out.println("⏰ 观察是否有 '🔧 工具调用' 日志...\n");

    String answer3 = chatClient.prompt()
        .user(question3)
        .call()
        .content();

    System.out.println("🤖 回答: " + answer3);
    System.out.println(answer3.contains("邮件") ? "✅ 包含邮件发送结果 - AI 可能调用了工具!" : "❌ 没有邮件发送结果");
    
    System.out.println("\n💡 说明:");
    System.out.println("- 使用 ChatClient.builder(chatModel).defaultTools(tools).build()");
    System.out.println("- 这样配置后,AI 就能自动调用工具函数了");
    System.out.println("- 如果看到 '🔧 工具调用' 日志,说明成功!");
    
    System.out.println("\n✅ 测试通过");
}

运行测试

bash
mvn test -Dtest=Test07_ToolAnnotation#test00_ChatClientWithTools

预期输出

text
========== 测试 7.0:使用 ChatClient 配置工具函数 ==========

📝 创建配置了工具的 ChatClient
✅ ChatClient 创建成功,已配置工具函数

🧪 测试1:让 AI 获取当前时间
👤 问题: 现在几点了?请告诉我准确的时间
⏰ 观察是否有 '🔧 工具调用' 日志...

🔧 工具调用: getCurrentTime() -> 2024-11-29 14:30:45
🤖 回答: 现在是 2024-11-29 14:30:45
✅ 包含时间信息 - AI 可能调用了工具!

🧪 测试2:让 AI 查询数据库
👤 问题: 系统有多少用户?
⏰ 观察是否有 '🔧 工具调用' 日志...

🔧 工具调用: queryDatabase("用户数量")
✅ 返回: 系统目前有 10,523 个注册用户
🤖 回答: 系统目前有 10,523 个注册用户
✅ 包含用户数量 - AI 可能调用了工具!

🧪 测试3:让 AI 发送邮件
👤 问题: 请给 zhangsan@example.com 发送一封邮件,主题是"会议提醒",内容是"明天下午3点开会"
⏰ 观察是否有 '🔧 工具调用' 日志...

🔧 工具调用: sendEmail(zhangsan@example.com, "会议提醒")
✅ 返回: 邮件已发送给 zhangsan@example.com,邮件ID: MAIL-1700000000000
🤖 回答: 邮件已发送给 zhangsan@example.com,邮件ID: MAIL-1700000000000
✅ 包含邮件发送结果 - AI 可能调用了工具!

✅ 测试通过

✅ 关键发现

  • 使用 ChatClient 配置工具后,AI 能自动调用函数
  • 看到 🔧 工具调用 日志说明函数被成功调用
  • AI 的回答包含了工具函数返回的数据

🧪 测试 7.1:工具函数小结

前面的示例已经分别演示了时间查询、数据库查询、文件操作、发送通知等工具函数的定义方式。 在真实项目中,更推荐直接通过 ChatClient.defaultTools(...) 暴露这些工具, 由大模型在对话中按需选择调用,而不是再写一个额外的"汇总测试"去逐个打印结果。

因此,本节不再提供额外单元测试代码,只作为一个小结: 只要单个工具函数已经通过各自的测试验证,且成功注册到 ChatClient, 就可以认为整个工具集是可被大模型正常调用的。

🚀 运行所有测试

运行完整测试类,验证所有工具函数功能:

bash
mvn test -Dtest=Test07_ToolAnnotation

预期输出

text
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

✅ 所有测试通过!

测试用例总结

测试编号 测试名称 核心知识点
7.0 使用 ChatClient 配置工具函数 ChatClient.defaultTools()
7.1 工具函数小结 最佳实践

📚 课后习题与常见面试题

请完成下面的练习,巩固本章的“Function Calling / 工具调用”核心机制与工程化落地方式

⚠️ 提示:高频点集中在 ChatClient.defaultTools()、@Tool 描述质量、参数设计、以及“模型如何选择工具”的可观测性(日志/trace)。

🎯 课后习题(动手为主)

1) 新增一个工具:validateEmail(to) 校验邮箱格式,并让 AI 在发送前先校验
要求:新增 @Tool 方法 validateEmail(String to);当邮箱不合法时返回明确的错误信息。 然后在对话提示词中引导模型“先校验邮箱,再发送邮件”。观察日志是否出现两次工具调用。
参考答案(展开/收起)
工具实现:
java
@Tool(description = "校验邮箱格式是否正确,返回校验结果")
public String validateEmail(@ToolParam(description = "收件人邮箱") String to) {
    boolean ok = to != null && to.matches("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
    return ok ? "OK" : "INVALID_EMAIL";
}
提示词引导(让模型先校验再发送):
text
你要发送邮件前,必须先调用 validateEmail(to)。
如果返回 INVALID_EMAIL,就不要发送,直接提示用户更换邮箱。
如果返回 OK,再调用 sendEmail(to, subject, body)。
验证点:控制台应出现两次工具日志(validateEmail -> sendEmail),且无效邮箱时不会触发 sendEmail。
2) 让工具更可观测:为每次工具调用打印 toolName + 参数 + 返回值摘要
要求:统一封装日志输出(例如输出 🔧 工具调用 以及参数),并避免打印敏感信息(如邮件正文可截断)。 目标:当模型调用工具“失败/没调用”时,你能从日志快速定位原因。
参考答案(展开/收起)
一个可行的做法:在每个工具函数内统一打印三段:入参、核心返回、耗时(可选)。
java
private String preview(String s) {
    if (s == null) return "null";
    return s.length() <= 50 ? s : s.substring(0, 50) + "...";
}

@Tool(description = "发送邮件给指定收件人,返回发送结果")
public String sendEmail(String to, String subject, String body) {
    System.out.println("🔧 tool=sendEmail to=" + to + " subject=" + preview(subject) + " body=" + preview(body));
    String result = "邮件已发送给 " + to;
    System.out.println("✅ tool=sendEmail result=" + preview(result));
    return result;
}
收益:你能判断“工具是否被调用、参数是否正确、返回是否合理”,排查成本会大幅下降。
3) 增加一个“非确定性”工具:getRandomDiscount(userId),并写测试验证 AI 会按需调用
要求:工具返回折扣(如 95 折/9 折);提示词引导 AI 在回答里包含折扣结果。 验证点:回答里出现折扣,并且控制台出现工具调用日志。
参考答案(展开/收起)
工具实现:
java
@Tool(description = "根据用户ID返回一个随机折扣(例如 9 折/95 折)")
public String getRandomDiscount(@ToolParam(description = "用户ID") String userId) {
    int[] discounts = {90, 95};
    int d = discounts[(int) (System.currentTimeMillis() % discounts.length)];
    String result = d + "折";
    System.out.println("🔧 tool=getRandomDiscount userId=" + userId + " -> " + result);
    return result;
}
验证方法:让模型必须返回折扣,例如:
text
请给用户 u001 生成一个折扣,并把折扣结果原样写在回答里(必须调用工具)。
4) 做一次“工具选择失败”的复盘:故意把 description 写得很差,观察模型会不会不调用
要求:把工具描述改成模糊描述(比如“工具1”),观察模型是否仍会选择它。 然后再把描述改回清晰版本,对比两次结果,总结 description 对工具选择的影响。
参考答案(展开/收起)
对比实验:
java
// ❌ 差描述:模型几乎无法判断用途
@Tool(description = "工具1")
public String sendEmail(String to, String subject, String body) { ... }

// ✅ 好描述:告诉模型什么时候该用、需要什么参数、返回什么
@Tool(description = "发送邮件:输入收件人邮箱、主题和正文,返回发送结果")
public String sendEmail(String to, String subject, String body) { ... }
现象:差描述时模型更可能“编造一个答案”而不是调用工具;好描述时更容易触发工具调用。
结论:description 是模型选择工具的重要依据,越清晰越可被稳定调用。

💼 常见面试题(概念 + 场景)

1) 什么是 Function Calling?它解决了大模型的什么问题?
关键点:把“自然语言意图”转换成“结构化工具调用”,让模型可以访问外部系统能力(时间/数据库/邮件/文件),并让结果可验证、可审计。
参考答案(展开/收起)
定义:让模型把“要做什么”转成可执行的函数调用(带结构化参数),由程序执行并把结果回填给模型。
解决的问题:
1) 实时性:模型能查时间/DB/文件等外部事实。 2) 可控性:用白名单工具限制能力边界。 3) 可验证:结果来自工具返回,便于审计与回放。
注意点:工具要做权限与参数校验,避免模型“越权/注入/误调用”。
2) ChatModel 和 ChatClient 有什么区别?为什么工具调用通常用 ChatClient?
关键点:ChatClient 提供“高阶能力编排”(默认系统提示、工具注册、对话模板等);ChatModel 更偏底层对话接口。
参考答案(展开/收起)
区别:
ChatModel:更底层,完成一次对话调用; ChatClient:更高层,提供 prompt 构建、默认配置、工具注册等“编排能力”。
为什么工具调用常用 ChatClient:
因为工具要注册(defaultTools)、提示模板要统一、且更容易做跨用例的复用与治理。
3) 你如何设计 @Tool 的 description 和参数?有哪些常见坑?
关键点:description 要清晰可判定;参数要语义化且限制范围;返回值要稳定;避免“一个工具做太多事”。常见坑:描述模糊、参数名无意义、返回不可预测。
参考答案(展开/收起)
设计原则:
1) description 说明何时该用输入是什么输出是什么。 2) 参数名/说明要语义化(例如 to/subject/body),并给出取值约束(格式/枚举/长度)。 3) 返回值要稳定、可读、可用于后续拼接(避免随机长文本)。
常见坑:
描述太泛导致不调用;参数名无意义导致填参错;一个工具做多件事导致模型不确定何时用;返回不稳定导致回答不可复现。
4) 如何保证工具调用安全?(权限、注入、审计、幂等)
关键点:权限校验、参数校验、敏感信息脱敏、执行审计日志、幂等与重试策略、工具白名单。
参考答案(展开/收起)
安全治理清单:
1) 权限:工具执行前做身份/权限校验(谁能发邮件、能发给谁)。 2) 参数校验:格式/长度/白名单(邮箱、主题长度、附件大小等)。 3) 幂等:同一请求重复触发要可控(requestId/幂等键)。 4) 审计:记录 toolName、参数摘要、结果、耗时、调用人。 5) 限流与熔断:避免模型误触发造成外部系统压力。
5) 如果模型没有调用工具,你怎么排查?
排查思路:确认工具是否注册到 ChatClient;检查 description 是否足够明确;检查提示词是否引导“必须调用”;查看工具调用日志/trace;用更可控的问题复现。
参考答案(展开/收起)
排查顺序(从确定性到不确定性):
1) 注册:确认 ChatClient.defaultTools(...) 真的注册了工具 Bean。 2) 模型能力:确认所用模型/配置支持工具调用。 3) 描述与参数:description 是否清晰,参数是否语义化。 4) 提示词:用“必须调用工具才能回答”的问题做最小复现。 5) 日志/trace:是否出现工具调用日志;没有就回到 1-4。

📝 本章小结

🎓 你已经掌握:

  • @Tool 注解的使用:标记工具函数和描述参数
  • ChatClient 配置工具:使用 defaultTools() 配置
  • 工具函数设计:无参数、单参数、多参数函数
  • Function Calling:AI 自动调用工具函数
  • ChatModel vs ChatClient:理解两者的区别
  • 测试和验证:直接调用和通过 AI 调用

🎉 恭喜!

你已经掌握了 @Tool 注解和 Function Calling 的核心技术! 这是 AI 应用开发的关键技术,建议多练习!

← 上一章 下一章 →