小崔撸起袖子:"理论讲完了,上代码吧。"白歌在旁边补充:"别忘了选择性启用和 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人参会。"
注册方式对比表
| 方式 | 代码 | 生效范围 | 适用场景 |
|---|---|---|---|
| defaultTools | builder.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 能精确查询数据