SpringAI-03-Tools与FunctionCall

一、Function Call 是什么

​ Function Call 是让 AI 调用外部工具的功能。简单说,就是 AI 可以调用你写的 Java 方法,执行实际的业务逻辑。

举个例子:

  • 用户问:”查询我的航班信息,预订号是 101,乘客姓名是徐庶”
  • AI 分析后,调用 queryFlightInfo("101", "徐庶") 工具
  • 工具返回航班信息
  • AI 根据返回结果生成回复:”您的航班信息如下:…”

Function Call 的优势:

  • 扩展能力:AI 可以调用外部工具,不再局限于文本生成
  • 实时数据:可以获取实时数据,如天气、股票、航班信息等
  • 业务集成:可以调用业务系统,实现真正的业务逻辑

什么时候需要 Function Call:

  • 需要查询数据库
  • 需要调用外部 API
  • 需要执行业务操作(增删改查)
  • 需要获取实时信息

二、工具定义

2.1 @Tool 注解

​ 使用 @Tool 注解定义工具方法,AI 就能识别并调用这个方法。

最简单的例子:

1
2
3
4
5
6
7
8
9
@Service
public class ToolService {

@Tool(description = "查询航班信息")
public String queryFlightInfo(String bookingNumber, String passengerName) {
// 查询逻辑
return "航班信息:...";
}
}

关键点:

  • @Tool 注解标记这是一个工具方法
  • description 参数很重要,AI 根据这个描述判断是否需要调用
  • 方法必须是 public 的
  • 返回值通常是 String,AI 会读取这个返回值

2.2 description 的重要性

description 是 AI 判断是否调用工具的唯一依据,所以描述要准确、清晰。

好的描述:

1
2
3
4
@Tool(description = "查询航班信息,需要提供预订号和乘客姓名")
public String queryFlightInfo(String bookingNumber, String passengerName) {
// ...
}

不好的描述:

1
2
3
4
@Tool(description = "查询")  // 太模糊,AI 不知道什么时候调用
public String queryFlightInfo(String bookingNumber, String passengerName) {
// ...
}

描述要点:

  • 清晰说明工具的功能
  • 说明需要什么参数
  • 说明返回什么结果
  • 避免歧义

2.3 参数设计

​ 工具方法的参数设计很重要,影响 AI 是否能正确调用。

实际案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service
public class ToolService {

@Resource
private FlightBookingService flightBookingService;

@Tool(description = "查询航班信息,需要提供预订号和乘客姓名")
public String queryFlightInfo(
@ToolParam String bookingNumber,
@ToolParam String passengerName) {
FlightBooking booking = flightBookingService
.findBookingByBookingNumberAndName(bookingNumber, passengerName);
if (booking != null) {
return "航班信息:" + booking;
}
return "未找到航班信息";
}

@Tool(description = "退票/取消预定,需要提供预订号和乘客姓名")
public String cancelBooking(
String bookingNumber,
String passengerName) {
flightBookingService.cancelBookingByBookingNumberAndName(
bookingNumber, passengerName);
return "退票成功";
}
}

参数设计要点:

  • 类型明确:使用明确的类型(String、int、double 等),避免 Object
  • 数量适中:建议不超过 5 个参数
  • 命名清晰:参数名要能表达含义
  • @ToolParam:虽然不强制,但建议使用,提高可读性

参数类型支持:

  • 基本类型:String、int、double、boolean 等
  • 包装类型:Integer、Double、Boolean 等
  • 日期类型:LocalDate、LocalDateTime 等
  • 复杂对象:需要 AI 能理解的结构

2.4 返回值设计

​ 返回值通常是 String,AI 会读取这个返回值并生成回复。

好的返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Tool(description = "查询航班信息")
public String queryFlightInfo(String bookingNumber, String passengerName) {
FlightBooking booking = flightBookingService
.findBookingByBookingNumberAndName(bookingNumber, passengerName);
if (booking != null) {
return String.format(
"预订号:%s,乘客:%s,出发地:%s,目的地:%s,出发日期:%s,状态:%s",
booking.getId(),
booking.getPassengerName(),
booking.getFrom(),
booking.getTo(),
booking.getDepartureDate(),
booking.getStatus()
);
}
return "未找到航班信息,请检查预订号和乘客姓名是否正确";
}

返回值设计要点:

  • 信息完整:包含 AI 需要的信息
  • 格式清晰:便于 AI 理解和处理
  • 错误提示:失败时返回有意义的错误信息

三、工具集成

3.1 ToolCallbackProvider 配置

​ 工具定义好后,需要配置 ToolCallbackProvider 来注册工具。

配置方式:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class ToolsApplication {

@Bean
public ToolCallbackProvider toolCallbackProvider(ToolService toolService) {
return MethodToolCallbackProvider.builder()
.toolObjects(toolService) // 传入工具对象
.build();
}
}

关键点:

  • MethodToolCallbackProvider:基于方法反射的工具提供者
  • toolObjects:传入包含 @Tool 注解的对象
  • 可以传入多个对象:.toolObjects(service1, service2, service3)

3.2 ChatClient 集成

​ 在 ChatClient 中配置工具:

1
2
3
4
5
6
7
8
9
@Autowired
private ToolService toolService;
@Autowired
private ToolCallbackProvider toolCallbackProvider;

ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(toolService) // 注册工具
.defaultToolCallbacks(toolCallbackProvider) // 注册工具回调
.build();

注意:

  • defaultToolsdefaultToolCallbacks 都要配置
  • defaultTools 告诉 AI 有哪些工具可用
  • defaultToolCallbacks 提供工具的实际执行逻辑

3.3 完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@RestController
@RequestMapping("/api/chat")
@CrossOrigin(origins = "*")
public class ChatController {

@Autowired
private ChatModel chatModel;

@Autowired
private ChatMemory chatMemory;

@Autowired
private ToolService toolService;

@Autowired
private ToolCallbackProvider toolCallbackProvider;

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
if (message == null || message.trim().isEmpty()) {
return Flux.error(new IllegalArgumentException("message参数不能为空"));
}

ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("""
#角色
你是一个专业的航空客服助手,名为'南方国际航空'的客服代表
你的职责是帮助客户处理航班预订相关的问题,包括查询航班信息、取消预订等。请用友好、专业的态度回复客户。
#要求
1、在涉及增删改(除了查询)function-call前,必须等用户回复"确认"后再调用tools
2、请讲中文

今天的日期是 {currentDate}
""")
.defaultAdvisors(
PromptChatMemoryAdvisor.builder(chatMemory).build(),
new SimpleLoggerAdvisor()
)
.defaultTools(toolService)
.defaultToolCallbacks(toolCallbackProvider)
.build();

return chatClient.prompt()
.user(message)
.system(p -> p.param("currentDate", LocalDate.now()))
.stream()
.content()
.map(content -> content == null ? "" : content);
}
}

四、实际案例:航空客服助手

4.1 业务场景

​ 假设我们要做一个航空客服助手,需要支持:

  • 查询航班信息
  • 取消航班预订

数据模型:

1
2
3
4
5
6
7
8
9
public class FlightBooking {
private String id; // 预订编号
private String passengerName; // 乘客姓名
private LocalDate departureDate; // 出发日期
private String from; // 出发地
private String to; // 目的地
private BookingStatus status; // 状态
private BookingClass bookingClass; // 舱位等级
}

4.2 工具实现

查询工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class ToolService {

@Resource
private FlightBookingService flightBookingService;

@Tool(description = "查询航班信息,需要提供预订号和乘客姓名")
public String queryFlightInfo(
@ToolParam String bookingNumber,
@ToolParam String passengerName) {
FlightBooking booking = flightBookingService
.findBookingByBookingNumberAndName(bookingNumber, passengerName);
if (booking != null) {
return String.format(
"预订号:%s,乘客:%s,出发地:%s,目的地:%s,出发日期:%s,状态:%s",
booking.getId(),
booking.getPassengerName(),
booking.getFrom(),
booking.getTo(),
booking.getDepartureDate(),
booking.getStatus()
);
}
return "未找到航班信息,请检查预订号和乘客姓名是否正确";
}
}

取消工具:

1
2
3
4
5
6
7
8
9
10
11
@Tool(description = "退票/取消预定,需要提供预订号和乘客姓名")
public String cancelBooking(
String bookingNumber,
String passengerName) {
boolean success = flightBookingService
.cancelBookingByBookingNumberAndName(bookingNumber, passengerName);
if (success) {
return "退票成功";
}
return "退票失败,请检查预订号和乘客姓名是否正确";
}

4.3 系统提示词设计

​ 系统提示词要明确告诉 AI 如何使用工具:

1
2
3
4
5
6
7
8
9
10
11
12
.defaultSystem("""
#角色
你是一个专业的航空客服助手,名为'南方国际航空'的客服代表
你的职责是帮助客户处理航班预订相关的问题,包括查询航班信息、取消预订等。请用友好、专业的态度回复客户。

#要求
1、在涉及增删改(除了查询)function-call前,必须等用户回复"确认"后再调用tools
2、请讲中文
3、如果用户的问题无法通过工具解决,请引导用户联系人工客服

今天的日期是 {currentDate}
""")

提示词要点:

  • 明确角色:告诉 AI 你是谁
  • 明确规则:什么时候可以调用工具,什么时候需要确认
  • 错误处理:无法解决时如何处理

4.4 完整代码

ToolService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package org.study.ai.service;

import jakarta.annotation.Resource;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.study.ai.model.FlightBooking;

@Service
public class ToolService {

@Resource
private FlightBookingService flightBookingService;

@Tool(description = "查询航班信息,需要提供预订号和乘客姓名")
public String queryFlightInfo(
@ToolParam String bookingNumber,
@ToolParam String passengerName) {
FlightBooking booking = flightBookingService
.findBookingByBookingNumberAndName(bookingNumber, passengerName);
if (booking != null) {
return String.format(
"预订号:%s,乘客:%s,出发地:%s,目的地:%s,出发日期:%s,状态:%s",
booking.getId(),
booking.getPassengerName(),
booking.getFrom(),
booking.getTo(),
booking.getDepartureDate(),
booking.getStatus()
);
}
return "未找到航班信息,请检查预订号和乘客姓名是否正确";
}

@Tool(description = "退票/取消预定,需要提供预订号和乘客姓名")
public String cancelBooking(
String bookingNumber,
String passengerName) {
boolean success = flightBookingService
.cancelBookingByBookingNumberAndName(bookingNumber, passengerName);
if (success) {
return "退票成功";
}
return "退票失败,请检查预订号和乘客姓名是否正确";
}
}

FlightBookingService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Service
public class FlightBookingService {

private final ConcurrentHashMap<String, FlightBooking> bookings = new ConcurrentHashMap<>();

@PostConstruct
public void init() {
// 初始化示例数据
addBooking(new FlightBooking("101", "徐庶", LocalDate.of(2025, 7, 15),
"重庆", "成都", BookingStatus.CONFIRMED, BookingClass.ECONOMY));
addBooking(new FlightBooking("102", "诸葛", LocalDate.of(2025, 7, 16),
"武汉", "南京", BookingStatus.CONFIRMED, BookingClass.BUSINESS));
// ... 更多数据
}

public FlightBooking findBookingByBookingNumberAndName(String bookingNumber, String passengerName) {
FlightBooking booking = bookings.get(bookingNumber);
if (booking != null && booking.getPassengerName().equals(passengerName)) {
return booking;
}
return null;
}

public boolean cancelBookingByBookingNumberAndName(String bookingNumber, String passengerName) {
FlightBooking booking = findBookingByBookingNumberAndName(bookingNumber, passengerName);
if (booking != null) {
booking.setStatus(BookingStatus.CANCELLED);
return true;
}
return false;
}
}

五、工具调用流程

​ 完整的工具调用流程:

1. 用户提问

1
用户:查询我的航班信息,预订号是 101,乘客姓名是徐庶

2. AI 分析
​ AI 分析问题,判断需要调用 queryFlightInfo 工具。

3. 工具调用
​ AI 生成工具调用请求(内部处理):

1
2
3
4
5
6
7
{
"function": "queryFlightInfo",
"arguments": {
"bookingNumber": "101",
"passengerName": "徐庶"
}
}

4. 执行工具
​ Spring AI 执行工具方法:

1
queryFlightInfo("101", "徐庶")

5. 返回结果
​ 工具执行结果返回给 AI:

1
预订号:101,乘客:徐庶,出发地:重庆,目的地:成都,出发日期:2025-07-15,状态:CONFIRMED

6. 生成回复
​ AI 根据工具执行结果生成最终回复:

1
2
3
4
5
6
7
根据您提供的信息,我查询到您的航班信息如下:
- 预订号:101
- 乘客姓名:徐庶
- 出发地:重庆
- 目的地:成都
- 出发日期:2025年7月15日
- 状态:已确认

实际对话示例:

1
2
3
4
5
6
7
8
9
用户:查询我的航班信息,预订号是 101,乘客姓名是徐庶
AI:[调用 queryFlightInfo("101", "徐庶")]
AI:根据您提供的信息,我查询到您的航班信息如下:
预订号:101
乘客:徐庶
出发地:重庆
目的地:成都
出发日期:2025715
状态:已确认

六、常见问题

6.1 AI 不调用工具

问题: AI 不调用工具,直接回复。

原因:

  • description 描述不准确
  • 系统提示词没有引导 AI 使用工具
  • 工具未正确注册

解决:

  1. 检查 description 是否清晰准确
  2. 在系统提示词中明确告诉 AI 什么时候使用工具
  3. 确认 defaultToolsdefaultToolCallbacks 都已配置

6.2 AI 调用工具但参数错误

问题: AI 调用了工具,但参数值不对。

原因:

  • 参数类型不匹配
  • 参数名称不清晰
  • AI 理解有误

解决:

  1. 使用明确的参数类型(String、int 等)
  2. 参数名称要清晰(如 bookingNumber 而不是 id
  3. description 中说明参数的含义

6.3 工具执行失败

问题: 工具执行时报错。

原因:

  • 参数验证不足
  • 业务逻辑错误
  • 异常未处理

解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Tool(description = "查询航班信息")
public String queryFlightInfo(String bookingNumber, String passengerName) {
try {
// 参数验证
if (bookingNumber == null || bookingNumber.trim().isEmpty()) {
return "预订号不能为空";
}
if (passengerName == null || passengerName.trim().isEmpty()) {
return "乘客姓名不能为空";
}

// 业务逻辑
FlightBooking booking = flightBookingService
.findBookingByBookingNumberAndName(bookingNumber, passengerName);
if (booking != null) {
return "航班信息:" + booking;
}
return "未找到航班信息";
} catch (Exception e) {
return "查询失败:" + e.getMessage();
}
}

6.4 工具返回值 AI 不理解

问题: 工具返回了结果,但 AI 回复不准确。

原因:

  • 返回值格式不清晰
  • 返回值信息不完整

解决:

  • 返回格式化的字符串,包含完整信息
  • 使用清晰的格式,便于 AI 理解

七、最佳实践

7.1 工具设计原则

单一职责
​ 每个工具方法只做一件事:

1
2
3
4
5
6
7
8
9
10
// 好的设计
@Tool(description = "查询航班信息")
public String queryFlightInfo(...) { }

@Tool(description = "取消航班预订")
public String cancelBooking(...) { }

// 不好的设计
@Tool(description = "查询或取消航班")
public String flightOperation(...) { } // 职责不单一

参数验证
​ 在工具方法中进行参数验证:

1
2
3
4
5
6
7
8
9
10
@Tool(description = "查询航班信息")
public String queryFlightInfo(String bookingNumber, String passengerName) {
if (bookingNumber == null || bookingNumber.trim().isEmpty()) {
return "预订号不能为空";
}
if (passengerName == null || passengerName.trim().isEmpty()) {
return "乘客姓名不能为空";
}
// ... 业务逻辑
}

错误处理
​ 完善的错误处理:

1
2
3
4
5
6
7
8
9
10
@Tool(description = "查询航班信息")
public String queryFlightInfo(String bookingNumber, String passengerName) {
try {
// 业务逻辑
return result;
} catch (Exception e) {
log.error("查询航班信息失败", e);
return "查询失败,请稍后重试";
}
}

7.2 系统提示词设计

明确规则
​ 在系统提示词中明确工具使用规则:

1
2
3
4
#要求
1、查询操作可以直接调用工具
2、增删改操作必须等用户确认后再调用工具
3、如果工具返回错误,要向用户说明原因

角色定义
​ 明确 AI 的角色和职责:

1
2
3
#角色
你是一个专业的航空客服助手
你的职责是帮助客户处理航班预订相关的问题

7.3 日志记录

​ 在工具方法中记录日志,便于排查问题:

1
2
3
4
5
6
7
@Tool(description = "查询航班信息")
public String queryFlightInfo(String bookingNumber, String passengerName) {
log.info("查询航班信息,预订号:{},乘客姓名:{}", bookingNumber, passengerName);
// ... 业务逻辑
log.info("查询结果:{}", result);
return result;
}

八、总结

  1. Function Call 让 AI 可以调用外部工具,扩展 AI 的能力
  2. @Tool 注解:使用 @Tool 定义工具,description 很重要
  3. 工具注册:需要配置 ToolCallbackProvider 并在 ChatClient 中注册
  4. 参数设计:类型明确、数量适中、命名清晰
  5. 返回值设计:信息完整、格式清晰、错误提示明确
  6. 系统提示词:明确角色、规则、错误处理方式
  7. 最佳实践:单一职责、参数验证、错误处理、日志记录

​ 下一篇文章会介绍 MCP 协议,这是 Spring AI 提供的标准化工具调用协议。