
Spring AI Function Calling:让AI调用你的Java方法
前言
在大语言模型的应用中,有一个核心问题:AI虽然聪明,但它无法感知外部世界,也无法执行实际操作。
比如你问"今天北京的天气怎么样",AI只能根据训练数据猜测,而无法获取实时天气。
Function Calling(函数调用)解决了这个问题:让AI能够调用你提供的Java方法,获取实时数据,然后基于这些数据回答问题。
本文分享我在实际项目中使用Spring AI Function Calling的经验,包括踩过的坑和最佳实践。
一、Function Calling原理
1.1 问题场景
假设我们要实现一个智能助手,用户会问:
javascript
用户:帮我查一下北京今天的天气
AI(无Function Calling):根据我的知识,北京今天可能是...
AI(有Function Calling):调用getWeather("北京") → 获取实时数据 → 北京今天晴,25°C
1.2 工作流程
markdown
用户提问
↓
AI分析:需要调用工具吗?
↓ 是
AI决定调用哪个方法 + 参数
↓
Spring AI执行对应的Java方法
↓
将方法返回值返回给AI
↓
AI基于返回数据生成回答
↓
返回给用户
1.3 支持的方法类型
Spring AI支持以下几种方式定义工具方法:
| 方式 | 说明 | 推荐度 |
|---|---|---|
@Tool注解 |
最简洁,Spring AI 1.1+ | ⭐⭐⭐⭐⭐ |
Function接口 |
传统方式,兼容性好 | ⭐⭐⭐ |
ToolCallback |
更灵活,支持动态工具 | ⭐⭐⭐⭐ |
二、基础实战:天气查询助手
2.1 环境准备
xml
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>function-calling-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-spring-boot-starter</artifactId>
<version>1.0.0-M3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
2.2 配置
yaml
# application.yml
server:
port: 8080
spring:
ai:
alibaba:
api-key: ${ALI_API_KEY}
chat:
options:
model: qwen-plus
temperature: 0.1 # Function Calling 场景温度要低
2.3 定义工具方法
java
package com.example.demo.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.Map;
import java.util.Random;
/**
* 天气查询工具
* 实际项目中应调用真实的天气API(如和风天气、OpenWeather等)
*/
@Component
public class WeatherTool {
private static final Map<String, String> CITY_WEATHER = Map.of(
"北京", "晴,25°C,空气质量良好",
"上海", "多云,22°C,空气质量优",
"广州", "雷阵雨,28°C,空气质量良",
"深圳", "多云转晴,27°C,空气质量优",
"杭州", "小雨,20°C,空气质量良"
);
private final Random random = new Random();
@Tool(description = "获取指定城市的天气信息。支持北京、上海、广州、深圳、杭州等城市。")
public String getWeather(
@ToolParam(description = "城市名称,如:北京、上海、广州等")
String city) {
System.out.println("[Tool Called] getWeather(city=" + city + ")");
// 模拟网络延迟
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return CITY_WEATHER.getOrDefault(city,
"抱歉," + city + "的天气数据暂不可用");
}
@Tool(description = "获取多个城市的天气对比信息")
public String getWeatherCompare(
@ToolParam(description = "城市名称列表,用逗号分隔")
String cities) {
System.out.println("[Tool Called] getWeatherCompare(cities=" + cities + ")");
StringBuilder result = new StringBuilder();
String[] cityArray = cities.split("[,,]");
for (String city : cityArray) {
city = city.trim();
String weather = CITY_WEATHER.getOrDefault(city, "数据暂不可用");
result.append(city).append(":").append(weather).append("\n");
}
return result.toString();
}
@Tool(description = "获取今天的日期")
public String getTodayDate() {
return LocalDate.now().toString();
}
}
2.4 注册工具并调用
java
package com.example.demo.service;
import com.example.demo.tool.WeatherTool;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class WeatherAssistantService {
private final ChatClient chatClient;
private final WeatherTool weatherTool;
public WeatherAssistantService(
ChatClient.Builder chatClientBuilder,
WeatherTool weatherTool) {
this.chatClient = chatClientBuilder
.defaultTools(weatherTool) // 注册工具
.build();
this.weatherTool = weatherTool;
log.info("WeatherAssistantService initialized with tools");
}
/**
* 智能天气查询
*/
public String queryWeather(String userMessage) {
log.info("User message: {}", userMessage);
String response = chatClient.prompt()
.user(userMessage)
.call()
.content();
log.info("AI response generated");
return response;
}
}
2.5 Controller
java
package com.example.demo.controller;
import com.example.demo.service.WeatherAssistantService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/weather")
@RequiredArgsConstructor
public class WeatherController {
private final WeatherAssistantService weatherService;
@PostMapping("/query")
public String query(@RequestBody QueryRequest request) {
if (request.message() == null || request.message().isBlank()) {
throw new IllegalArgumentException("消息不能为空");
}
return weatherService.queryWeather(request.message());
}
/**
* 测试场景
*/
@GetMapping("/test")
public String test() {
return weatherService.queryWeather("北京今天天气怎么样?和上海比怎么样?");
}
}
record QueryRequest(String message) {}
三、进阶实战:企业知识库助手
3.1 场景说明
在企业内部,我们可能需要一个助手,能够:
- 查询员工信息
- 查询项目进度
- 查询系统状态
- 执行简单的运维操作
3.2 定义多个工具
java
package com.example.demo.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* 企业系统工具集
*/
@Component
public class EnterpriseTools {
@Tool(description = "查询员工信息,包括姓名、部门、职位等")
public String getEmployeeInfo(
@ToolParam(description = "员工姓名或工号")
String employeeId) {
System.out.println("[Tool Called] getEmployeeInfo(employeeId=" + employeeId + ")");
// 模拟数据库查询
Map<String, String> employees = Map.of(
"张三", "工号:E001,部门:技术部,职位:高级工程师",
"李四", "工号:E002,部门:产品部,职位:产品经理",
"王五", "工号:E003,部门:运维部,职位:SRE工程师"
);
return employees.getOrDefault(employeeId, "未找到员工:" + employeeId);
}
@Tool(description = "查询项目进度,返回项目当前状态和完成百分比")
public String getProjectStatus(
@ToolParam(description = "项目编号或项目名称")
String projectId) {
System.out.println("[Tool Called] getProjectStatus(projectId=" + projectId + ")");
Map<String, String> projects = Map.of(
"项目A", "状态:进行中,完成度:65%,预计完成时间:2026-06-30",
"项目B", "状态:已上线,完成度:100%,上线时间:2026-03-15",
"项目C", "状态:需求评审中,完成度:10%,预计完成时间:2026-08-15"
);
return projects.getOrDefault(projectId, "未找到项目:" + projectId);
}
@Tool(description = "查询系统运行状态,包括CPU、内存、磁盘使用率")
public String getSystemStatus() {
System.out.println("[Tool Called] getSystemStatus()");
// 实际项目中应调用监控系统API
return """
系统运行状态:
- CPU使用率:45%
- 内存使用率:62%
- 磁盘使用率:78%
- 网络状态:正常
- 活跃连接数:342
""";
}
@Tool(description = "重启指定服务。注意:此操作会中断服务,请谨慎使用!")
public String restartService(
@ToolParam(description = "服务名称,如:user-service、order-service")
String serviceName) {
System.out.println("[Tool Called] restartService(serviceName=" + serviceName + ")");
// 实际项目中应调用运维系统API
return "服务 " + serviceName + " 重启指令已发送,预计30秒后恢复";
}
}
3.3 多工具注册
java
package com.example.demo.service;
import com.example.demo.tool.EnterpriseTools;
import com.example.demo.tool.WeatherTool;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class EnterpriseAssistantService {
private final ChatClient chatClient;
public EnterpriseAssistantService(
ChatClient.Builder chatClientBuilder,
WeatherTool weatherTool,
EnterpriseTools enterpriseTools) {
// 注册多个工具类
this.chatClient = chatClientBuilder
.defaultTools(weatherTool, enterpriseTools)
.build();
log.info("EnterpriseAssistantService initialized with multiple tool classes");
}
public String chat(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
四、动态工具:根据上下文加载
4.1 场景说明
在某些场景下,工具需要根据用户权限、当前会话等动态加载。
4.2 实现动态工具
java
package com.example.demo.service;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class DynamicToolService {
private final ChatClient.Builder chatClientBuilder;
public DynamicToolService(ChatClient.Builder chatClientBuilder) {
this.chatClientBuilder = chatClientBuilder;
}
/**
* 根据用户角色动态注册工具
*/
public String chatWithDynamicTools(String message, UserRole role) {
List<ToolCallback> tools = new ArrayList<>();
// 基础工具:所有用户可用
tools.add(FunctionToolCallback.builder("getWeather", this::getWeather)
.description("获取天气信息")
.inputType(String.class)
.build());
// 管理员专属工具
if (role == UserRole.ADMIN) {
tools.add(FunctionToolCallback.builder("restartService", this::restartService)
.description("重启服务(管理员专属)")
.inputType(String.class)
.build());
}
// 构建ChatClient
ChatClient client = chatClientBuilder
.defaultTools(tools.toArray(new ToolCallback[0]))
.build();
return client.prompt()
.user(message)
.call()
.content();
}
// 工具方法
private String getWeather(String city) {
return city + "今天天气晴,25°C";
}
private String restartService(String serviceName) {
return "服务 " + serviceName + " 正在重启...";
}
public enum UserRole {
USER, ADMIN
}
}
五、踩坑记录与解决方案
5.1 工具未被调用
问题现象: 即使用户问题明显需要调用工具,AI也没有调用,而是直接回答。
原因分析:
- 工具描述不够清晰,AI不理解何时调用
- Temperature参数过高,AI过于"创造性"
- 模型不支持Function Calling(检查模型版本)
解决方案:
java
// 1. 改进工具描述
@Tool(description = """
获取指定城市的实时天气信息。
当用户询问天气、气温、是否下雨等问题时,必须调用此工具。
支持的城市:北京、上海、广州、深圳、杭州。
""")
public String getWeather(String city) { ... }
// 2. 降低temperature
spring.ai.alibaba.chat.options.temperature=0.1
// 3. 使用支持Function Calling的模型
// qwen-plus以上版本支持
5.2 参数解析错误
问题现象:
AI尝试调用工具,但参数类型错误,导致调用失败
原因分析: AI生成的参数与Java方法签名不匹配。
解决方案:
java
// 1. 使用@ToolParam明确参数描述
@Tool(description = "查询员工信息")
public String getEmployeeInfo(
@ToolParam(description = "员工姓名,如:张三、李四")
String name) { ... }
// 2. 参数类型使用String(最灵活)
// 避免用int、boolean等,让AI生成String再由你转换
// 3. 添加参数校验
@Tool(description = "根据年龄查询用户")
public String getUsersByAge(
@ToolParam(description = "年龄,整数")
String ageStr) {
int age;
try {
age = Integer.parseInt(ageStr);
} catch (NumberFormatException e) {
return "年龄参数错误,请提供数字";
}
// 查询逻辑...
return "找到 " + age + " 岁的用户共10人";
}
5.3 工具执行超时
问题现象: 工具方法执行时间过长,导致AI调用超时。
解决方案:
java
@Tool(description = "执行长时间任务")
public String longRunningTask(String param) {
// 方案1:异步执行,立即返回任务ID
String taskId = submitAsyncTask(param);
return "任务已提交,任务ID:" + taskId + ",请稍后查询结果";
// 方案2:设置超时
// 在application.yml中配置
// spring.ai.alibaba.chat.options.timeout=60000
}
5.4 工具返回数据过大
问题现象: 工具返回了大量数据,导致Token超限。
解决方案:
java
@Tool(description = "查询用户列表(分页)")
public String getUsers(
@ToolParam(description = "页码,从1开始")
String pageStr) {
int page = Integer.parseInt(pageStr);
// 限制返回数量
List<User> users = queryUsersByPage(page, 10); // 每页10条
// 只返回摘要,不要完整对象
return users.stream()
.map(u -> u.getName() + "(" + u.getAge() + "岁)")
.collect(Collectors.joining("\n"));
}
5.5 多个工具冲突
问题现象: 注册了多个功能相似的工具,AI不知道调用哪个。
解决方案:
java
// 明确区分工具的职责
@Tool(description = """
获取单个城市的天气(用于查询一个城市的天气)
""")
public String getWeather(String city) { ... }
@Tool(description = """
对比多个城市的天气(用于对比两三个城市的天气差异)
当用户问"北京和上海天气怎么样"时调用此工具。
""")
public String compareWeather(String cities) { ... }
六、生产级最佳实践
6.1 工具设计原则
markdown
1. 单一职责:每个工具只做一件事
2. 明确描述:description要详细,说明何时调用
3. 参数简单:优先用String,避免复杂对象
4. 快速返回:工具执行时间控制在3秒内
5. 安全校验:所有参数都要校验
6.2 安全防护
java
@Component
public class SecureTools {
@Tool(description = "执行系统命令(仅管理员)")
public String executeCommand(
@ToolParam(description = "命令内容")
String command,
UserContext userContext) { // 从上下文获取用户信息
// 权限校验
if (!userContext.hasRole("ADMIN")) {
return "权限不足,需要管理员权限";
}
// 命令白名单
if (!isCommandAllowed(command)) {
return "此命令不在允许列表中";
}
// 执行命令
return executeSystemCommand(command);
}
private boolean isCommandAllowed(String command) {
String[] allowed = {"ls", "pwd", "df -h", "top -b -n 1"};
for (String allowedCmd : allowed) {
if (command.startsWith(allowedCmd)) {
return true;
}
}
return false;
}
}
6.3 监控与日志
java
@Component
@Slf4j
public class MonitoredTools {
private final MeterRegistry meterRegistry;
public MonitoredTools(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Tool(description = "查询用户订单")
public String getUserOrders(String userId) {
long startTime = System.currentTimeMillis();
try {
// 执行查询
String result = queryOrdersFromDb(userId);
// 记录成功
meterRegistry.counter("tools.user_orders.calls.success").increment();
return result;
} catch (Exception e) {
// 记录失败
meterRegistry.counter("tools.user_orders.calls.failure").increment();
log.error("查询用户订单失败", e);
return "查询失败:" + e.getMessage();
} finally {
// 记录耗时
long duration = System.currentTimeMillis() - startTime;
meterRegistry.timer("tools.user_orders.duration").record(duration, java.util.concurrent.TimeUnit.MILLISECONDS);
log.info("getUserOrders executed in {} ms", duration);
}
}
}
6.4 优雅降级
java
@Tool(description = "查询实时库存")
public String getInventory(String productId) {
try {
// 主数据源
return queryFromMainDatabase(productId);
} catch (Exception e) {
log.warn("主数据源查询失败,尝试备用方案", e);
try {
// 降级:读缓存
return queryFromCache(productId);
} catch (Exception e2) {
log.error("备用方案也失败", e2);
// 返回友好提示
return "库存查询暂时不可用,请稍后重试";
}
}
}
七、完整项目示例
7.1 项目结构
css
function-calling-demo/
├── src/main/java/com/example/demo/
│ ├── DemoApplication.java
│ ├── config/
│ │ └── AiConfig.java
│ ├── controller/
│ │ ├── WeatherController.java
│ │ └── AssistantController.java
│ ├── service/
│ │ ├── WeatherAssistantService.java
│ │ ├── EnterpriseAssistantService.java
│ │ └── DynamicToolService.java
│ ├── tool/
│ │ ├── WeatherTool.java
│ │ └── EnterpriseTools.java
│ └── model/
│ └── QueryRequest.java
├── src/main/resources/
│ └── application.yml
└── pom.xml
7.2 启动类
java
package com.example.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Slf4j
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
log.info("Function Calling Demo started!");
log.info("Test URL: http://localhost:8080/api/weather/test");
}
}
7.3 测试用例
java
package com.example.demo;
import com.example.demo.service.WeatherAssistantService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class FunctionCallingTest {
@Autowired
private WeatherAssistantService weatherService;
@Test
void testWeatherQuery() {
String response = weatherService.queryWeather("北京今天天气怎么样?");
assertNotNull(response);
assertTrue(response.contains("北京") || response.contains("天气") || response.contains("°C"));
System.out.println("AI回答:" + response);
}
@Test
void testMultiCityComparison() {
String response = weatherService.queryWeather("北京和上海天气对比");
assertNotNull(response);
assertTrue(response.length() > 10);
System.out.println("AI回答:" + response);
}
}
八、性能优化
8.1 工具结果缓存
java
@Component
public class CachedTools {
@Cacheable(value = "weather", key = "#city", unless = "#result == null")
@Tool(description = "获取天气(带缓存)")
public String getWeather(String city) {
System.out.println("【执行查询】" + city);
// 实际调用天气API
return callWeatherApi(city);
}
}
8.2 并发工具调用
java
@Tool(description = "批量查询天气")
public String getMultipleWeather(String cities) {
List<CompletableFuture<String>> futures = Arrays.stream(cities.split(","))
.map(city -> CompletableFuture.supplyAsync(() -> queryWeather(city.trim())))
.toList();
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.joining("\n"));
}
九、总结
9.1 核心要点
- Function Calling让AI能够调用Java方法,获取实时数据
- 工具定义 使用
@Tool注解,描述要清晰明确 - 参数设计优先使用String类型,避免复杂对象
- 安全防护必须做权限校验和命令白名单
- 监控日志记录工具调用情况,便于排查问题
9.2 适用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 实时数据查询 | 天气、股票、新闻等 | 天气查询助手 |
| 数据库操作 | 根据用户意图查询数据库 | 智能客服 |
| 系统运维 | 执行运维命令、查询状态 | 运维助手 |
| 业务操作 | 下单、退款、审批等 | 企业助手 |
9.3 参考资源
- Spring AI官方文档:docs.spring.io/spring-ai/r...
- 通义千问Function Calling文档:help.aliyun.com/zh/model-st...
- OpenAI Function Calling指南:platform.openai.com/docs/guides...
如果有帮助,欢迎点赞、收藏、关注!如有问题,欢迎在评论区交流。