Spring AI Alibaba Function Calling:外部工具集成与业务函数注册
导读:Function Calling(工具调用)是让 AI 从"纸上谈兵"走向"真正干活"的关键能力。本文深入讲解如何用 Spring AI Alibaba 1.1 版让大模型调用你的 Java 方法、查询数据库、访问第三方 API,并覆盖并行调用、参数校验、权限控制等生产级细节。
一、Function Calling 的本质
Function Calling 解决的问题很具体:大模型的知识有截止日期,也无法直接访问你的系统。但通过 Function Calling,你可以告诉模型"有哪些工具可以用",当它判断需要某个工具时,会输出一个结构化的函数调用请求,你的代码执行后把结果返回给模型,模型再基于结果给出最终答案。
用户:现在上海的天气怎么样?
[无 Function Calling]:
模型:"我无法获取实时天气信息,我的训练数据截止到..."
[有 Function Calling]:
模型:"需要调用天气查询工具"
↓
你的代码:调用天气 API,得到"上海 25°C,晴"
↓
模型:"目前上海天气晴朗,气温 25°C,适合户外活动..."
这个能力让 AI 真正成为能执行任务的智能体,而不仅仅是"知识检索器"。
二、依赖与配置
Function Calling 能力包含在 spring-ai-alibaba-starter-dashscope 中,无需额外依赖:
xml
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<!-- 参数校验(JSR-303) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
三、三种函数注册方式
3.1 方式一:@Bean + Function 接口(推荐)
这是最标准的注册方式,函数实现与 Spring 容器解耦:
java
package com.example.ai.tools;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Function;
@Slf4j
@Configuration
public class ToolsConfiguration {
/**
* 工具一:获取当前时间
* 模型会在需要知道当前时间时调用此工具
*/
@Bean
@Description("获取当前的日期和时间,当用户询问时间相关问题时调用")
public Function<CurrentTimeRequest, CurrentTimeResponse> getCurrentTime() {
return request -> {
log.info("工具调用:getCurrentTime,时区:{}", request.timezone());
String now = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new CurrentTimeResponse(now, "Asia/Shanghai");
};
}
/**
* 工具二:天气查询(模拟实现)
*/
@Bean
@Description("查询指定城市的实时天气信息。当用户询问某城市天气时调用此工具。")
public Function<WeatherRequest, WeatherResponse> getWeather() {
return request -> {
log.info("工具调用:getWeather,城市:{}", request.city());
// 实际项目:调用天气 API(如 OpenWeatherMap、和风天气等)
return simulateWeather(request.city());
};
}
private WeatherResponse simulateWeather(String city) {
// 模拟数据,实际需调用真实 API
return new WeatherResponse(city, "晴", 22.5, 65, "适宜户外活动");
}
// ---- 请求/响应 DTO(JSON 描述是 Prompt 工程的关键!)----
public record CurrentTimeRequest(
@JsonProperty("timezone")
@JsonPropertyDescription("时区,如 Asia/Shanghai")
String timezone
) {}
public record CurrentTimeResponse(
@JsonProperty("current_time")
@JsonPropertyDescription("当前时间,格式:yyyy-MM-dd HH:mm:ss")
String currentTime,
@JsonProperty("timezone")
String timezone
) {}
public record WeatherRequest(
@JsonProperty("city")
@JsonPropertyDescription("城市名称,如:上海、北京、广州")
String city
) {}
public record WeatherResponse(
String city,
String condition,
double temperature,
int humidity,
String suggestion
) {}
}
3.2 方式二:@Tool 注解(1.1版推荐的简化写法)
Spring AI 1.1 版引入了 @Tool 注解,可以直接标注方法,更简洁:
java
package com.example.ai.tools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
/**
* 数据库查询工具类
* 使用 @Tool 注解,无需额外的 Function Bean 注册
*/
@Component
public class DatabaseTools {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
public DatabaseTools(OrderRepository orderRepository,
ProductRepository productRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
}
/**
* 查询订单状态
* 模型描述决定了模型何时会调用此工具
*/
@Tool(description = "根据订单号查询订单的状态、金额、收货地址等详细信息")
public String queryOrderStatus(
@JsonPropertyDescription("订单号,格式如:ORD20250320001")
String orderId) {
log.info("[Tool] 查询订单:{}", orderId);
Order order = orderRepository.findById(orderId)
.orElse(null);
if (order == null) {
return String.format("未找到订单号为 %s 的订单,请确认订单号是否正确", orderId);
}
return String.format("订单 %s 状态:%s,金额:%.2f 元,下单时间:%s,收货地址:%s",
orderId, order.getStatus(), order.getAmount(),
order.getCreatedAt(), order.getShippingAddress());
}
/**
* 查询商品库存
*/
@Tool(description = "查询指定商品的当前库存数量,用于回答用户关于商品是否有货的问题")
public String queryProductStock(
@JsonPropertyDescription("商品 SKU 编码或商品名称")
String productIdentifier) {
log.info("[Tool] 查询库存:{}", productIdentifier);
// 先尝试按 SKU 查询,再按名称模糊查询
Product product = productRepository.findBySku(productIdentifier)
.orElseGet(() -> productRepository.findByNameContaining(productIdentifier)
.stream().findFirst().orElse(null));
if (product == null) {
return "未找到商品:" + productIdentifier;
}
return String.format("商品 [%s] 当前库存:%d 件,状态:%s",
product.getName(),
product.getStock(),
product.getStock() > 0 ? "有货" : "缺货");
}
}
3.3 在 ChatClient 中注册工具
java
@Service
@RequiredArgsConstructor
@Slf4j
public class ToolChatService {
private final ChatClient chatClient;
// 注入 @Tool 注解的工具类
private final DatabaseTools databaseTools;
// 通过 @Bean 注册的工具
@Qualifier("getCurrentTime")
private final Function<ToolsConfiguration.CurrentTimeRequest,
ToolsConfiguration.CurrentTimeResponse> getCurrentTime;
@Qualifier("getWeather")
private final Function<ToolsConfiguration.WeatherRequest,
ToolsConfiguration.WeatherResponse> getWeather;
/**
* 带工具的对话:模型自主决定是否调用工具
*/
public String chatWithTools(String userMessage) {
log.info("用户消息:{}", userMessage);
return chatClient.prompt()
.user(userMessage)
// 方式一:注册 @Bean Function 工具
.tools(
FunctionToolCallback.builder("getCurrentTime", getCurrentTime)
.description("获取当前时间")
.inputType(ToolsConfiguration.CurrentTimeRequest.class)
.build(),
FunctionToolCallback.builder("getWeather", getWeather)
.description("查询城市天气")
.inputType(ToolsConfiguration.WeatherRequest.class)
.build()
)
// 方式二:注册 @Tool 注解的工具类(Spring AI 1.1)
// .tools(databaseTools) // 自动发现类中所有 @Tool 方法
.call()
.content();
}
/**
* 智能客服场景:使用数据库查询工具
*/
public String customerService(String question) {
return chatClient.prompt()
.system("""
你是一个智能客服助手,可以帮助用户查询订单状态和商品信息。
当用户询问订单时,调用查询工具获取实时信息。
回答要友好、简洁,并在适当时主动提供帮助。
""")
.user(question)
.tools(databaseTools) // 注入数据库查询工具
.call()
.content();
}
}
四、完整工具调用链分析
理解调用链有助于排查问题:
用户请求 → Spring Controller
|
v
[ChatClient]
|
(携带工具描述的 Prompt)
|
v
[DashScope API]
模型判断:需要调用工具
|
v
模型输出:
{
"tool_calls": [{
"function": {
"name": "getWeather",
"arguments": {"city": "上海"}
}
}]
}
|
v
[Spring AI 内部处理]
自动解析工具调用
|
v
[你的 Java 函数]
WeatherResponse {city="上海", condition="晴", ...}
|
v
[结果回传模型]
模型收到工具结果
|
v
[模型生成最终回答]
"上海今天天气晴朗,气温约 22.5°C..."
|
v
[返回给用户]
五、MyBatis 数据库查询集成
工具调用最常见的场景之一是让 AI 查询数据库。以下是结合 MyBatis 的完整示例:
java
@Component
@Slf4j
public class MyBatisQueryTools {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 查询用户积分余额
*/
@Tool(description = "查询用户的积分余额,当用户询问自己有多少积分时调用")
public String getUserPoints(
@JsonPropertyDescription("用户 ID 或用户名")
String userIdentifier) {
try {
User user = userMapper.findByIdOrName(userIdentifier);
if (user == null) {
return "未找到用户:" + userIdentifier;
}
return String.format("用户 %s 当前积分:%d 分,等级:%s",
user.getName(), user.getPoints(), user.getLevel());
} catch (Exception e) {
log.error("查询用户积分失败", e);
return "查询失败,请稍后重试";
}
}
/**
* 查询历史订单列表
*/
@Tool(description = "查询用户最近的历史订单列表(最近10条),当用户询问历史购买记录时调用")
public String getRecentOrders(
@JsonPropertyDescription("用户 ID")
String userId) {
List<Order> orders = orderMapper.findRecentByUserId(userId, 10);
if (orders.isEmpty()) {
return "该用户暂无订单记录";
}
StringBuilder sb = new StringBuilder("最近订单:\n");
orders.forEach(o -> sb.append(String.format(
"- 订单号:%s,商品:%s,金额:%.2f元,状态:%s\n",
o.getId(), o.getProductName(), o.getAmount(), o.getStatus())));
return sb.toString();
}
}
六、并行工具调用
当一次对话需要调用多个工具时,模型可以并行发起多个调用,Spring AI 会并行执行并聚合结果:
java
/**
* 并行调用示例:同时查询多个数据源
* 用户:"帮我看看北京和上海的天气,还有告诉我现在几点了"
*/
public String parallelToolsDemo(String message) {
return chatClient.prompt()
.user(message)
// 注册多个工具,模型可以并行调用
.tools(
FunctionToolCallback.builder("getWeather", getWeather)
.description("查询城市天气,支持并行调用多个城市")
.inputType(WeatherRequest.class)
.build(),
FunctionToolCallback.builder("getCurrentTime", getCurrentTime)
.description("获取当前时间")
.inputType(CurrentTimeRequest.class)
.build()
)
.call()
.content();
// 模型会一次性输出:
// tool_calls: [
// {name: "getWeather", args: {city: "北京"}},
// {name: "getWeather", args: {city: "上海"}},
// {name: "getCurrentTime", args: {timezone: "Asia/Shanghai"}}
// ]
// Spring AI 并行执行三个工具调用,再将结果聚合返回给模型
}
七、安全控制:权限白名单与参数校验
工具调用涉及真实的业务操作,必须有安全防护:
7.1 参数校验(JSR-303)
java
@Component
public class SecureQueryTools {
@Tool(description = "查询指定日期范围内的销售报表")
public String getSalesReport(
@NotBlank(message = "开始日期不能为空")
@Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}", message = "日期格式必须为 yyyy-MM-dd")
@JsonPropertyDescription("开始日期,格式:yyyy-MM-dd")
String startDate,
@NotBlank(message = "结束日期不能为空")
@JsonPropertyDescription("结束日期,格式:yyyy-MM-dd")
String endDate) {
// 业务逻辑校验
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
if (end.isBefore(start)) {
throw new IllegalArgumentException("结束日期不能早于开始日期");
}
if (ChronoUnit.DAYS.between(start, end) > 90) {
throw new IllegalArgumentException("查询范围不能超过 90 天");
}
// 执行查询...
return "查询成功,日期范围:" + startDate + " 至 " + endDate;
}
}
7.2 工具执行超时控制
java
@Component
public class TimeoutSafeTools {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
@Tool(description = "调用外部 API 获取数据(含超时保护)")
public String callExternalApi(String apiName, String params) {
Future<String> future = executor.submit(() -> {
// 实际的 API 调用逻辑
return externalApiService.call(apiName, params);
});
try {
// 工具执行超时:5秒
return future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
log.warn("工具 [{}] 执行超时", apiName);
return "查询超时,请稍后重试";
} catch (Exception e) {
log.error("工具执行失败:{}", e.getMessage());
return "执行失败:" + e.getMessage();
}
}
}
八、工具描述的 Prompt 工程
工具描述(Description)的质量直接决定模型何时调用工具,这是被很多人忽视的"Prompt 工程":
java
// ❌ 差的描述:模糊不清
@Description("查询信息")
public Function<Request, Response> queryInfo() { ... }
// ✅ 好的描述:明确触发条件 + 输入输出说明
@Description("查询指定城市的实时天气信息,包括温度、天气状况、湿度等。" +
"当用户询问某城市天气、是否需要带雨伞、户外活动是否合适等问题时调用此工具。" +
"输入城市名称(支持中文城市名),返回当前天气信息。")
public Function<WeatherRequest, WeatherResponse> getWeather() { ... }
好的工具描述需要包含:
- 工具做什么(功能说明);
- 什么时候调用(触发条件);
- 输入什么(参数说明);
- 返回什么(输出说明)。
九、总结
Function Calling 是 AI 应用从"对话机器人"进化为"智能助手"的关键能力:
- 三种注册方式 :
@Bean + Function接口(标准)、@Tool注解(1.1版简化)、手动FunctionToolCallback; - 调用链理解:模型 → 工具调用请求 → Spring AI 执行 → 结果回传模型 → 最终回答;
- 数据库集成:结合 MyBatis,让 AI 直接查询业务数据库;
- 并行调用:多工具并行执行,结果自动聚合;
- 安全防护:JSR-303 参数校验 + 超时控制 + 权限白名单。
下一篇将深入 AI 应用的可观测性:Token 消耗监控、链路追踪、Prometheus 指标接入,打造生产级运维能力。
参考资料