一、单个Function Calling
大模型本质上是一个"文字接龙"引擎。它知道很多,但有三个致命缺陷:
-
知识有截止日期:不知道今天天气、最新股价、你的数据库里有什么。
-
无法执行动作:不能帮你发邮件、下单、修改数据库记录。
-
无法访问私有数据:不知道你公司内部的订单系统里有哪些数据。
Function Calling 就是解决这三个问题的。
核心思路是:模型只负责"决定调用哪个函数、传什么参数",真正的执行由你的 Java 代码来完成。
这里有一个非常重要的安全边界------模型永远无法直接访问你的 API。
它只能输出一个结构化的"调用请求",Spring AI 拦截这个请求,执行你注册的 Java 方法,然后把结果返回给模型,模型再基于结果生成自然语言回答。
整个流程大致是这样的:
用户提问
→ Spring AI 把提示 + 工具定义(名称、描述、参数 schema)发给模型
→ 模型判断需要调用工具,返回调用请求 {name: "getWeather", arguments: {city: "北京"}}
→ Spring AI 拦截请求,执行你写的 Java 方法
→ 把执行结果作为上下文注入,再次发给模型
→ 模型基于真实数据生成最终回答
两种实现方式
Spring AI 提供了两种定义工具的路径,新手建议从 @Tool 注解方式入手,它最直观、代码量最少。
方式一:@Tool 注解(推荐)
直接在方法上加注解。
Spring AI 会自动扫描方法签名,生成对应的 JSON Schema 给模型。
第一步:定义一个工具类
java
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
public class WeatherTools {
@Tool(description = "查询指定城市的当前天气情况")
public String getWeather(
@ToolParam(description = "城市名称,例如:北京、上海") String city) {
// 这里写真实的业务逻辑,比如调用第三方天气 API
if ("北京".equals(city)) {
return "北京当前天气:晴,25°C,湿度 40%";
}
return city + "当前天气:多云,22°C";
}
@Tool(description = "根据城市和日期预订航班")
public String bookFlight(
String origin,
String destination,
@ToolParam(description = "日期,格式 YYYY-MM-DD") String date) {
return "已为您预订 " + date + " 从" + origin + "到" + destination + "的航班";
}
}
关键点:
-
description 必须写清楚。模型靠它来判断"什么时候该调用这个工具"。描述越准确,模型的决策越靠谱。
-
@ToolParam用来给每个参数加描述,帮助模型理解参数含义。 -
方法参数和返回值类型没有限制,Spring AI 会自动处理序列化。
第二步:在 ChatClient 中注册并使用
java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/chat")
public String chat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.tools(new WeatherTools()) // 注册工具实例
.call()
.content();
}
}
测试一下,发一个请求 GET /chat?message=北京今天天气怎么样?
Spring AI 会自动完成上面流程图中的所有步骤。模型看到你注册了一个叫 getWeather 的工具,描述是"查询指定城市的当前天气情况",于是决定调用它,参数 city="北京",你的 Java 方法被执行,返回结果,模型最终生成一句自然语言回答。
方式二:@Bean 定义 Function(传统方式)
如果你需要把一个已有的 java.util.function.Function 注册为工具,可以用 @Bean 的方式。
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import java.util.function.Function;
@Configuration
public class FunctionConfig {
public record WeatherRequest(String city) {}
public record WeatherResponse(String condition, int tempCelsius) {}
@Bean
@Description("根据城市名称查询当前天气")
public Function<WeatherRequest, WeatherResponse> weatherFunction() {
return request -> {
// 真实逻辑:调用天气 API
return new WeatherResponse("晴", 25);
};
}
}
然后在调用时通过 bean 名称注册:
java
chatClient.prompt()
.user(message)
.functions("weatherFunction") // 注意是 functions(),不是 tools()
.call()
.content();
⚠️ 注意 :
FunctionCallback和functions()API 在 Spring AI 新版本中已标记为废弃(Deprecated),官方推荐迁移到ToolCallback和@Tool注解方式。新项目直接用 @Tool 即可。
补充一个关键细节:工具循环
你可能注意到,模型调用工具后,结果被送回模型,模型可能再次决定调用另一个工具,然后再一次执行、再一次送回。
Spring AI 内部通过 ToolCallingAdvisor 管理这个循环,直到模型返回一个不包含工具调用的响应为止。
这意味着你可以定义多个工具,模型会根据用户的问题自动组合调用。
比如用户说"北京天气怎么样?如果晴就帮我订一张明天去上海的机票",模型可能先调用 getWeather,拿到结果后再调用 bookFlight。
小结一下
| 要点 | 说明 |
|---|---|
| 核心思想 | 模型只决策,执行在你的 Java 代码 |
| 推荐方式 | @Tool 注解 + .tools() 注册 |
| 关键注解 | @Tool(description=...) 告诉模型什么时候调用 |
| 参数描述 | @ToolParam 帮助模型正确生成参数 |
| 底层机制 | Spring AI 自动生成 JSON Schema,处理调用循环 |
| 安全边界 | 模型无法直接访问你的 API,只能发出调用请求 |
二、spring AI 一般的项目结构
java
src/main/java/com/example/
├── controller/
│ └── ChatController.java
├── service/
│ ├── ChatService.java
│ └── WeatherService.java
├── ai/
│ ├── config/
│ │ └── ChatClientConfig.java
│ └── tool/
│ ├── WeatherTools.java
│ └── OrderTools.java
├── domain/
│ └── ...
└── infrastructure/
└── ...
三、多个Function Calling
模型是"决策者",不是"执行者"。
多个 function 的调用顺序,要么由模型自主决定,要么由你的代码强制编排。
下面分三种模式讲清楚。
模式一:模型自主编排(最常见)
你注册多个工具,模型根据用户问题,自己决定调不调、调哪个、调几个、什么顺序。
机制:ToolCallingAdvisor 循环
Spring AI 内部有一个循环,我画给你看:
java
用户问题
→ 发给模型(附带所有工具定义)
→ 模型返回:我要调 getWeather(city="北京")
→ Spring AI 执行 getWeather,拿到结果
→ 把结果连同对话历史再次发给模型
→ 模型返回:我要调 bookFlight(origin="北京", dest="上海", date="2026-09-15")
→ Spring AI 执行 bookFlight,拿到结果
→ 再次发给模型
→ 模型返回:最终自然语言回答(不再要求调用工具)
→ 循环结束
关键点:这个循环会一直持续到模型不再要求调用工具为止。 模型可以连续调用 1 个、2 个、甚至 10 个工具。
代码示例
java
@Service
@RequiredArgsConstructor
public class ChatService {
private final ChatClient chatClient;
private final WeatherTools weatherTools;
private final FlightTools flightTools;
public String chat(String message) {
return chatClient.prompt()
.user(message)
.tools(weatherTools, flightTools) // 注册多个工具
.call()
.content();
}
}
工具类:
java
@Component
@RequiredArgsConstructor
public class WeatherTools {
private final WeatherService weatherService;
@Tool(description = "查询指定城市的当前天气情况")
public String getWeather(
@ToolParam(description = "城市名称,例如:北京、上海") String city) {
return weatherService.getCurrentWeather(city);
}
}
@Component
@RequiredArgsConstructor
public class FlightTools {
private final FlightService flightService;
@Tool(description = "根据出发地、目的地和日期预订航班")
public String bookFlight(
@ToolParam(description = "出发城市") String origin,
@ToolParam(description = "目的城市") String destination,
@ToolParam(description = "日期,格式 YYYY-MM-DD") String date) {
return flightService.book(origin, destination, date);
}
}
测试
用户:北京天气怎么样?如果晴就帮我订一张明天去上海的机票
模型可能的行为:
-
先调
getWeather("北京")→ 得到"晴,25°C" -
判断"晴",再调
bookFlight("北京", "上海", "2026-09-15") -
生成最终回答:"北京今天晴,25°C,已为您预订明天北京到上海的航班。"
注意:这个顺序不是我写的代码,是模型自己推理出来的。 这就是 Function Calling 最强大的地方。
模式二:代码强制编排(业务流程固定时)
有时候业务逻辑是固定的,不允许模型自由发挥。比如"先查订单,再根据订单状态决定是否退款"。这时候有两种做法。
做法 A:把编排藏在工具内部
模型只看到一个工具,但工具内部调用了多个 Service:
java
@Component
@RequiredArgsConstructor
public class OrderTools {
private final OrderService orderService;
private final RefundService refundService;
@Tool(description = "处理订单退款:查询订单状态,如果符合条件则发起退款")
public String processRefund(
@ToolParam(description = "订单号") String orderId) {
// 代码强制顺序:先查,再判断,再退款
Order order = orderService.getOrder(orderId);
if (order == null) {
return "订单不存在";
}
if (!"PAID".equals(order.getStatus())) {
return "订单状态为 " + order.getStatus() + ",不可退款";
}
refundService.refund(orderId);
return "退款成功,订单号:" + orderId;
}
}
优点 :模型只负责"决定要不要退款",具体流程由 Java 代码保证。
适用:流程固定、有严格业务规则、不允许模型自由发挥的场景。
做法 B:不用 Function Calling,直接代码编排
如果流程完全固定,模型根本不需要"决策",那就不该用 Function Calling。直接:
java
public String processRefund(String orderId) {
// 纯 Java 代码编排,模型只负责最后的文案生成
Order order = orderService.getOrder(orderId);
if (order == null) return "订单不存在";
if (!"PAID".equals(order.getStatus())) return "不可退款";
refundService.refund(orderId);
return "退款成功";
}
什么时候用这个? 当"调用哪个函数"根本不需要智能决策时,Function Calling 就是过度设计。
模式三:多工具选择(模型选一个)
这是最简单的场景:注册多个工具,模型根据用户问题选一个。
java
chatClient.prompt()
.user(message)
.tools(weatherTools, orderTools, emailTools)
.call()
.content();
- 用户问"北京天气" → 调
getWeather - 用户问"订单 12345 状态" → 调
getOrder - 用户问"给张三发邮件" → 调
sendEmail
模型根据每个工具的 description 来判断该用哪个。
所以 description 写得好不好,直接决定了模型选得对不对。
工程实践:怎么让模型选对、调对?
1. 工具描述要"互斥"
❌ 反例:
java
@Tool(description = "查询信息")
public String getWeather(String city) { ... }
@Tool(description = "查询数据")
public String getOrder(String orderId) { ... }
两个描述都太模糊,模型会懵。
✅ 正例:
java
@Tool(description = "查询指定城市的当前天气情况,包括温度、湿度、天气状况")
public String getWeather(String city) { ... }
@Tool(description = "根据订单号查询订单的详细信息和当前状态")
public String getOrder(String orderId) { ... }
2. 单次注册的工具不要太多
一般建议 5~10 个以内。工具太多,模型选择成本高,容易选错。
如果工具超过 15 个,建议按场景拆分多个 ChatClient:
java
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient weatherChatClient(ChatClient.Builder builder, WeatherTools tools) {
return builder.defaultTools(tools).build();
}
@Bean
public ChatClient orderChatClient(ChatClient.Builder builder, OrderTools tools) {
return builder.defaultTools(tools).build();
}
}
不同业务场景注入不同的 ChatClient。
3. 用系统提示词引导顺序
如果你希望模型优先调用某个工具,可以在 system prompt 里写:
java
chatClient.prompt()
.system("""
你是订票助手。当用户询问订票时,必须先调用 getWeather 查询目的地天气,
如果天气恶劣(暴雨、大雪),先提醒用户再询问是否继续。
""")
.user(message)
.tools(weatherTools, flightTools)
.call()
.content();
4. 参数校验放在工具方法内部
模型生成的参数不一定可靠,永远不要相信模型的参数。工具方法内部必须做校验:
java
@Tool(description = "根据订单号查询订单")
public String getOrder(@ToolParam(description = "订单号,纯数字") String orderId) {
if (orderId == null || !orderId.matches("\\d+")) {
return "订单号格式不正确";
}
// ...
}
总结成一张表
| 模式 | 谁决定顺序 | 适用场景 | 实现方式 |
|---|---|---|---|
| 模型自主编排 | 模型 | 复杂推理、多步骤任务 | 注册多个工具,让模型自由调用 |
| 代码强制编排 | 你的 Java 代码 | 流程固定、有严格规则 | 工具内部调用多个 Service |
| 直接代码编排 | 你的 Java 代码 | 无需智能决策 | 不用 Function Calling |
| 多工具选择 | 模型 | 多功能入口 | 注册多个工具,模型选一个 |
最后一句忠告
Function Calling 不是"让模型帮你写业务逻辑",而是"让模型帮你决定调用哪个业务逻辑"。
业务规则、顺序约束、参数校验,永远应该由你的 Java 代码保证。模型只负责"理解用户意图"和"决定调用哪个工具"。