乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • Spring AI 学习路径
  • 第1章 Spring AI 概述与核心理念

    • 章节导读
    • Spring AI 概述与可移植 API
    • 核心模块与依赖关系
  • 第2章 快速入门与第一个AI应用

    • 章节导读
    • 环境准备与配置
    • 第一个 AI 对话应用
  • 第3章 聊天模型与ChatClient

    • 章节导读
    • ChatClient 详解
    • ChatModel 底层抽象
    • 多轮对话与 ChatMemory
  • 第4章 提示词管理与模板

    • 章节导读
    • Prompt 与 Message 体系
    • 提示词模板与动态构建
  • 第5章 输出解析与结构化响应

    • 章节导读
    • 输出解析器与 BeanOutputConverter
  • 第6章 嵌入模型与向量存储

    • 章节导读
    • EmbeddingModel 与文本向量化
    • ETL 数据注入流水线
    • 向量存储抽象与配置
  • 第7章 检索增强生成(RAG)

    • 章节导读
    • RAG 核心机制与流程
    • QuestionAnswerAdvisor 详解
    • RAG 实战案例
  • 第8章 函数调用(Function Calling)

    • 章节导读
    • Tool 注解与函数声明
    • 函数调用实战
  • 第9章 多模态

    • 章节导读
    • 图像输入与视觉模型
    • 图像生成
  • 第10章 Advisor拦截器链

    • 章节导读
    • Advisor 链与内置拦截器
  • 第11章 MCP 协议与跨语言工具集成

    • 章节导读
    • MCP 协议深入
  • 第12章 可观测性测试与最佳实践

    • 章节导读
    • 可观测性与指标监控
    • 测试策略与 Mock 实践
    • 最佳实践与面试考点汇总

小崔撸起袖子:"理论讲完了,上代码吧。"白歌在旁边补充:"别忘了选择性启用和 FunctionCallback 两种高级用法。"

函数调用实战

定义与作用

本章通过两个完整案例,展示 Tool Calling 在生产级项目中的落地方式:①课程查询(简单参数多工具);②会议室预订(复杂 Record 参数 + 选择性启用)。

核心原理

图释:ChatClient 将 Prompt + 工具列表一同发送给模型,模型的决策(调用/不调用、调用哪个工具)完全由其内部推理决定,Spring AI 只负责解析和执行。

案例一:飞翔科技课程查询工具

场景说明

孔蓝提需求:"学生管理系统里,用户问'有哪些算法课'或'白歌教什么课'时,AI 要能直接查课程表。"

依赖引入与配置

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o
          temperature: 0.1   # 工具调用建议低温度

关键代码

@Component
public class StudentServiceTools {

    private static final Map<String, String> COURSE_DB = Map.of(
        "CS101", "计算机组成原理 | 白歌 | 4学分 | 周一 8:00-10:00",
        "CS201", "数据结构与算法 | 小崔 | 3学分 | 周三 10:00-12:00",
        "CS301", "操作系统 | 白歌 | 4学分 | 周四 14:00-16:00",
        "MATH101", "线性代数 | 李眉 | 4学分 | 周二 8:00-10:00",
        "ALGO101", "算法分析与设计 | 小崔 | 3学分 | 周五 14:00-16:00"
    );

    @Tool(description = "按课程编号精确查询课程,如 CS101")
    public String getCourseByCode(String courseCode) {
        return COURSE_DB.getOrDefault(
            courseCode.toUpperCase(), 
            "未找到课程编号 " + courseCode
        );
    }

    @Tool(description = "查询指定教师的所有课程,输入教师姓名")
    public String getCoursesByTeacher(String teacherName) {
        List<String> courses = COURSE_DB.entrySet().stream()
            .filter(e -> e.getValue().contains(teacherName))
            .map(e -> "  - " + e.getKey() + " " + e.getValue())
            .toList();
        
        if (courses.isEmpty()) {
            return teacherName + " 没有找到相关课程";
        }
        return teacherName + " 的课程:\n" + String.join("\n", courses);
    }

    @Tool(description = "按关键字搜索课程名称,如'算法''数学'")
    public String searchCourses(String keyword) {
        List<String> matches = COURSE_DB.entrySet().stream()
            .filter(e -> e.getValue().contains(keyword))
            .map(e -> "  - " + e.getKey() + " " + e.getValue())
            .toList();
        
        if (matches.isEmpty()) {
            return "没有找到包含'" + keyword + "'的课程";
        }
        return "搜索'" + keyword + "'的结果:\n" + String.join("\n", matches);
    }
}

运行结果对比

// ========== 操作前(无 Tool Calling)==========
GET /course/ask?message=白歌老师教什么课

AI: "我没有关于白歌老师的具体课程信息。
     建议您查看教务系统或联系教务处获取最新课表。"
// → AI 不知道这些信息,只能泛泛回答

// ========== 操作后(有 Tool Calling)==========
GET /course/ask?message=白歌老师教什么课

AI: "根据查询结果,白歌老师本学期教授以下课程:
     - CS101 计算机组成原理(周一 8:00-10:00,4学分)
     - CS301 操作系统(周四 14:00-16:00,4学分)
     这些课程都是计算机专业的核心课程。"
// → AI 精确回答了课程信息

案例二:会议室预订(选择性启用工具)

场景说明

飞翔科技的会议室预订系统需要两个工具:查询和预订。但大翔要求"预订操作需要确认才执行"——所以默认只启用查询功能。

关键代码

public record BookingConfirmation(
    String building,
    String room,
    LocalDate date,
    int hours,
    int participants,
    String bookingId
) {}

@Component
public class MeetingTools {

    @Tool(description = "查询指定办公楼所有可用会议室,返回房间号和容量")
    public String checkRooms(String building) {
        return switch (building) {
            case "A座" -> "A座:A-201(6人)、A-305(12人)、A-101(30人)";
            case "B座" -> "B座:B-102(4人)、B-301(8人)、B-501(20人)";
            default -> building + " 无可用会议室";
        };
    }

    @Tool(description = "正式预订会议室,请确认日期和人数后再调用")
    public BookingConfirmation bookRoom(
            String building,
            int participants,
            LocalDate date,
            int hours) {
        
        String room = participants <= 6 ? "小会议室" : 
                      participants <= 20 ? "中型会议室" : "大型会议室";
        String bookingId = UUID.randomUUID().toString().substring(0, 8);
        
        return new BookingConfirmation(
            building, room, date, hours, participants, bookingId
        );
    }
}
@RestController
public class SmartBookingController {

    private final ChatClient chatClient;

    public SmartBookingController(ChatClient.Builder builder,
                                   MeetingTools tools) {
        // 默认只注册查询工具,预订工具需手动确认
        this.chatClient = builder
                .defaultTools(tools)          // 注册所有工具
                .build();
    }

    @GetMapping("/booking/check")
    public String check(@RequestParam String message) {
        // 仅启用查询工具
        return chatClient.prompt()
                .user(message)
                .functions("checkRooms")  // 仅本次启用 checkRooms
                .call()
                .content();
    }

    @GetMapping("/booking/book")
    public String book(@RequestParam String message) {
        // 启用所有工具(含 bookRoom)
        return chatClient.prompt()
                .user(message)
                .functions("checkRooms", "bookRoom")  // 明确启用
                .call()
                .content();
    }
}

运行结果对比

// ========== 场景 1:查询会议室(仅 checkRooms 可用)==========
GET /booking/check?message=A座有哪些会议室

AI: "A座目前可用的会议室有:
     - A-201(6人)
     - A-305(12人)
     - A-101(30人大会议室)
     请问您需要预订哪一间?"
// → 不会调用 bookRoom,因为未启用

// ========== 场景 2:预订会议室(两个工具都可用)==========
GET /booking/book?message=帮我订明天 A 座 8 人 2 小时

AI:
  [调用 checkRooms("A座")]
  [调用 bookRoom("A座", 8, "2026-06-14", 2)]
  → "已在 A 座预订中型会议室,预订号 BK-8a3f2c,
     明天(6月14日)2小时,8人参会。"

注册方式对比表

方式代码生效范围适用场景
defaultToolsbuilder.defaultTools(tools)全局常用工具
functions().functions("getWeather")单次请求按场景启用
FunctionCallback.builder()FunctionCallback.builder().function("name", fn).build()手动控制需要自定义 Schema

易错场景与面试考点

易错场景一:默认注册所有工具导致权限泄露

// ❌ 错误:普通用户查询也能触发删除操作
@Configuration
public class Config {
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder,
                                  AllTools tools) {  // 含 delete 工具
        return builder.defaultTools(tools).build();
    }
}

问题分析

全局注册所有工具意味着任何请求都可能触发敏感操作。

// ✅ 正确:敏感工具不加入 defaultTools,仅在需要时通过 functions() 启用
@Configuration
public class Config {
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder,
                                  SafeTools safeTools) {
        return builder.defaultTools(safeTools).build();  // 仅安全工具
    }
}

// 敏感操作单独控制器,明确启用
@GetMapping("/admin/delete")
public String delete(@RequestParam String message, AdminTools adminTools) {
    return chatClient.prompt()
            .user(message)
            .functions("deleteRecord")  // 显式授权
            .call()
            .content();
}

面试高频题

Q1:AI 会调用多次工具吗?

会。当 AI 判断需要多个工具配合时,会依次调用。ChatClient 自动处理多轮 Tool Calling,直到 AI 不再请求工具为止。但需注意:每次工具调用都算一轮 API 调用(消耗 Token),复杂任务可能触发 3-5 轮。

Q2:工具返回结果太长怎么办?

工具结果会作为 ToolResponseMessage 追加到对话中,如果过长可能超出上下文窗口。建议:①工具方法只返回关键信息,不做过度详细描述;②设置 ChatOptions 中的 maxTokens 限制总 Token 数;③在 @Tool 描述中说明"返回简要信息"。

本章小结

  • 简单参数工具用 String/int,复杂参数用 Record/POJO,Spring AI 自动生成 JSON Schema
  • 选择性启用(.functions())是生产环境的必要安全机制
  • defaultTools 适合通用工具,functions() 适合敏感工具
  • 操作前后对比:无 Tool Calling 时 AI 只能泛泛回答;有 Tool Calling 时 AI 能精确查询数据
上一页
Tool 注解与函数声明