Spring AI 的函数调用(Function Calling)让大语言模型能够请求执行应用中预先定义的函数,用于查询数据、调用接口或执行业务逻辑。模型负责理解问题和生成调用参数,Java 程序负责执行函数,模型再根据返回结果组织回答。
本文先介绍函数调用的执行流程,再通过两个案例说明实现方式:用加法与乘法理解基本调用,用订单、物流和库存查询演示多个方法的协作。
1. 函数调用的工作原理
1.1 什么是函数与工具
这里的"函数"就是应用提供的方法,例如计算两个数的和、查询某个订单。把方法的名称、用途和参数结构提供给模型后,模型便可以请求调用它。在 Spring AI 中,这类可调用能力也称为 Tool(工具),对应机制称为 Tool Calling。
模型不会直接运行 Java 代码,也不需要读取方法源码。实际执行发生在应用侧。
1.2 一次调用如何完成
- 定义函数:声明方法及其名称、描述和参数。
- 注册并请求模型:应用将函数定义和用户问题一起发送给模型。
- 执行函数:模型返回函数名与参数,Spring AI 调用对应方法。
- 回传结果:Spring AI 将执行结果发送给模型,模型生成回答,或继续请求其他函数。
一次用户提问可能触发多次函数调用,也可能无需调用函数。一次 .call() 因此不一定只对应一次模型请求。
1.3 System Prompt 的作用
System Prompt 用来说明角色与调用规则,例如"查询订单时必须依据函数返回的数据,缺少订单号时先询问"。函数则负责实际获取数据。
提示词和函数注册需要配合:提示词说明什么时候调用、如何使用结果;注册函数让模型知道有哪些能力可用。
2. 工程结构
示例使用 Java 17、Spring Boot 3.5.15 和 Spring AI 1.1.8,包名为 org.xingyue.ai。应用需已配置可用的 ChatClient Bean,底层模型支持工具调用。
| 组件 | 职责 |
|---|---|
| ToolChatController | 接收 HTTP 请求,检查输入 |
| ToolChatService | 设置提示词、注册工具、调用模型 |
| CalculatorService | 定义加法与乘法 Function Bean |
| StoreTools | 暴露订单与库存查询方法 |
| StoreService | 组合订单、物流和库存查询 |
| StoreRepository | 提供数据访问 |
业务查询沿用 Controller、Service、Repository 分层;Tool 是供模型调用的业务入口。
3. 入门案例:调用加法与乘法函数
以"10 加 20,再乘以 4"为例,演示函数定义、注册和结果回传。本例将加法与乘法逻辑封装为 Function Bean,供模型请求调用。
3.1 定义函数
使用 @Bean 注册 Function 对象,以 record 定义输入参数,通过 @Description 描述函数用途。
CalculatorService.java
java
package org.xingyue.ai.config;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
@Configuration(proxyBeanMethods = false)
public class CalculatorService {
private static final Logger log = LoggerFactory.getLogger(CalculatorService.class);
public record AddOperation(int a, int b) {}
public record MulOperation(int m, int n) {}
@Bean
@Description("计算两个整数的和,参数 a 和 b 为两个加数")
public Function<AddOperation, Integer> addOperation() {
return request -> {
int result = Math.addExact(request.a(), request.b());
log.info("执行加法函数:{} + {} = {}", request.a(), request.b(), result);
return result;
};
}
@Bean
@Description("计算两个整数的乘积,参数 m 和 n 为两个乘数")
public Function<MulOperation, Integer> mulOperation() {
return request -> {
int result = Math.multiplyExact(request.m(), request.n());
log.info("执行乘法函数:{} × {} = {}", request.m(), request.n(), result);
return result;
};
}
}
Function<AddOperation, Integer> 表示接收一个 AddOperation 参数对象,返回一个整数。addOperation、mulOperation 默认也是两个 Bean 的名称。
创建 Bean 时,配置方法只是返回 Function 对象,并不执行计算。真正的计算发生在 Lambda 的函数体中:Spring AI 收到模型的调用请求后,将参数转换为对应的 record,再执行 Function 的 apply(request)。
例如,模型请求 addOperation 并传入 {"a":10,"b":20},实际执行相当于:
java
addFunction.apply(new CalculatorService.AddOperation(10, 20));
这里的 addFunction 指 Spring 容器中的加法 Function Bean。以上仅说明执行过程,业务调用代码无需手动调用 apply()。
Math.addExact 和 Math.multiplyExact 在整数溢出时抛出异常;正常范围内与 +、* 的结果相同。日志记录实际计算过程。
3.2 注册函数并发起调用
在 Service 中,通过 .toolNames(...) 指定本次请求可以使用的 Function Bean 名称。核心调用如下,完整 Service 和 Controller 见第 5 节。
java
return chatClient.prompt()
.system("你是计算助手。加法、乘法请调用对应函数,根据返回结果用中文回答。")
.user(message)
.toolNames("addOperation", "mulOperation")
.call()
.content();
Bean 名称必须与定义一致。仅定义 @Bean 不会自动向所有对话开放函数,也不需要在 Service 中注入 CalculatorService。
本文使用 Spring AI 1.1.8 的 .toolNames(...) 按名称解析 Function Bean。实现方式可参考 Spring AI 函数注册文档。
3.3 测试函数调用
完成第 5 节的接口配置并启动应用后,发送以下请求,设置 Content-Type: application/json。示例端口为 9999。
接口:POST http://localhost:9999/ai/tools/calculate
json
{"message": "请使用工具计算:10 加 20,再乘以 4。"}
期望结果为 120。正常执行两次工具时,可观察到类似日志:
text
执行加法函数:10 + 20 = 30
执行乘法函数:30 × 4 = 120
单看答案 120 不能证明函数执行过,需要检查上述日志或在 Lambda 内设置断点。
4. 进阶案例:组合查询订单、物流与库存
加法与乘法展示了函数调用过程。实际开发中,更常见的需求是查询数据库或调用业务接口,例如订单状态、物流和库存。这些信息不能仅凭模型已有知识判断。这里使用固定的内存数据,便于运行和核对;更换数据源时,只需调整 Repository。
用户可以一次提出:"ORD1001 发货了吗?这个订单里的商品还有多少库存?"应用需要先查询订单和物流,再用订单返回的商品编号查询库存。

4.1 数据访问
订单 ORD1001 对应商品 SKU1001;SKU1001 库存为 17,SKU1002 库存为 0。
StoreRepository.java
java
package org.xingyue.ai.repository;
import java.util.Map;
import org.springframework.stereotype.Repository;
@Repository
public class StoreRepository {
public record Order(String orderNo, String sku, String status) {}
public record Stock(String sku, int available) {}
private final Map<String, Order> orders = Map.of(
"ORD1001", new Order("ORD1001", "SKU1001", "已发货"));
private final Map<String, String> logistics = Map.of(
"ORD1001", "包裹已到达杭州转运中心");
private final Map<String, Stock> stocks = Map.of(
"SKU1001", new Stock("SKU1001", 17),
"SKU1002", new Stock("SKU1002", 0));
public Order findOrder(String orderNo) {
return orders.get(orderNo);
}
public String findLogistics(String orderNo) {
return logistics.getOrDefault(orderNo, "暂无物流信息");
}
public Stock findStock(String sku) {
return stocks.get(sku);
}
}
4.2 业务逻辑
订单查询内部依次查询订单与物流;找不到订单时直接返回。库存为零与商品不存在分别处理。
StoreService.java
java
package org.xingyue.ai.service;
import org.springframework.stereotype.Service;
import org.xingyue.ai.repository.StoreRepository;
@Service
public class StoreService {
private final StoreRepository repository;
public StoreService(StoreRepository repository) {
this.repository = repository;
}
public String queryOrder(String orderNo) {
var order = repository.findOrder(orderNo);
if (order == null) {
return "未找到订单:" + orderNo;
}
String logistics = repository.findLogistics(orderNo);
return "订单号:" + order.orderNo() + ",商品:" + order.sku()
+ ",状态:" + order.status() + ",物流:" + logistics;
}
public String queryStock(String sku) {
var stock = repository.findStock(sku);
return stock == null ? "未找到商品:" + sku
: "商品:" + stock.sku() + ",可售库存:" + stock.available();
}
}
4.3 暴露工具
本例使用另一种定义方式:用 @Tool 标记普通方法,用 @ToolParam 描述参数,再通过 .tools(storeTools) 注册。这与 Function Bean 写法都属于函数调用,区别在于定义与注册方式。
工具层校验参数后委托 Service,并记录执行结果。业务方法无需了解模型或提示词。
StoreTools.java
java
package org.xingyue.ai.tool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import org.xingyue.ai.service.StoreService;
@Component
public class StoreTools {
private static final Logger log = LoggerFactory.getLogger(StoreTools.class);
private final StoreService storeService;
public StoreTools(StoreService storeService) {
this.storeService = storeService;
}
@Tool(description = "根据用户提供的订单号查询订单状态、商品编号和物流")
public String queryOrder(@ToolParam(description = "订单号,例如 ORD1001") String orderNo) {
if (orderNo == null || !orderNo.matches("ORD[0-9]{4}")) {
return "订单号格式错误,请提供 ORD 加四位数字的订单号";
}
String result = storeService.queryOrder(orderNo);
log.info("tool=queryOrder orderNo={} result={}", orderNo, result);
return result;
}
@Tool(description = "根据商品编号查询当前可售库存")
public String queryStock(@ToolParam(description = "商品编号,例如 SKU1001") String sku) {
if (sku == null || !sku.matches("SKU[0-9]{4}")) {
return "商品编号格式错误,请提供 SKU 加四位数字的编号";
}
String result = storeService.queryStock(sku);
log.info("tool=queryStock sku={} result={}", sku, result);
return result;
}
}
这里有两种不同的调用关系:
- 内部方法调用 :
queryOrder中查询订单、查询物流,是一次工具执行中的普通 Java 调用。 - 多次工具调用 :模型先请求
queryOrder,再根据返回的商品编号请求queryStock,是两个独立的工具调用。
4.4 注册业务函数
业务请求使用 .tools(storeTools),使模型可以调用 queryOrder 和 queryStock。系统提示词要求数据来自查询结果,并明确"先查订单,再根据商品编号查库存"的顺序。完整实现见第 5 节的 askStore 方法。
4.5 联合查询测试
接口:POST http://localhost:9999/ai/tools/store
json
{"message": "帮我查一下 ORD1001 发货了吗?这个订单里的商品还有多少库存?"}
预期调用顺序:
text
queryOrder("ORD1001")
→ 返回订单状态、物流、商品编号 SKU1001
queryStock("SKU1001")
→ 返回可售库存 17
正常调用时的日志示例:
text
tool=queryOrder orderNo=ORD1001 result=订单号:ORD1001,商品:SKU1001,状态:已发货,物流:包裹已到达杭州转运中心
tool=queryStock sku=SKU1001 result=商品:SKU1001,可售库存:17
回答应包含"已发货""杭州转运中心""可售库存 17",具体措辞由模型决定。
5. 完整的 Service 与 HTTP 接口
计算接口通过 .toolNames(...) 启用 Function Bean;业务查询接口通过 .tools(...) 注册带有 @Tool 注解的方法。两者都只为相应请求提供调用能力。
ToolChatService.java
java
package org.xingyue.ai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import org.xingyue.ai.tool.StoreTools;
@Service
public class ToolChatService {
private final ChatClient chatClient;
private final StoreTools storeTools;
public ToolChatService(ChatClient chatClient, StoreTools storeTools) {
this.chatClient = chatClient;
this.storeTools = storeTools;
}
public String calculate(String message) {
return chatClient.prompt()
.system("""
你是计算助手,支持整数加法和乘法。
用户需要计算时,请调用对应的函数。
缺少操作数或运算类型时,先向用户询问。
多步计算时,使用上一步的结果继续计算。
不支持的运算请明确说明。
根据函数返回的结果,用中文回答。
""")
.user(message)
.toolNames("addOperation", "mulOperation")
.call()
.content();
}
public String askStore(String message) {
return chatClient.prompt()
.system("""
你是订单与库存查询助手,使用中文回答。
订单、物流和库存信息必须依据工具查询结果,不得猜测。
缺少订单号或商品编号时,先向用户询问。
查询订单商品的库存时,先查订单,再使用返回的商品编号查库存。
未查到记录时如实说明;库存为零表示缺货,不表示商品不存在。
只支持查询,不执行下单、退款或取消订单。
""")
.user(message)
.tools(storeTools)
.call()
.content();
}
}
.system(...) 为本次请求指定系统提示词,覆盖构建 ChatClient 时的默认系统文本。订单助手的规则有三个重点:数据必须来自工具、缺少编号先询问、查询库存时使用订单返回的商品编号。
在默认工具执行流程下,Spring AI 负责执行工具并回传结果。因此,一次 .call() 可能包含多轮模型请求。提示词可以引导调用,但不能代替程序校验或强制执行策略。
ToolChatController.java
java
package org.xingyue.ai.controller;
import org.springframework.http.MediaType;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import org.xingyue.ai.service.ToolChatService;
@RestController
@RequestMapping("/ai/tools")
public class ToolChatController {
private final ToolChatService service;
public ToolChatController(ToolChatService service) {
this.service = service;
}
public record AskRequest(String message) {}
@PostMapping(value = "/calculate", produces = MediaType.TEXT_PLAIN_VALUE)
public String calculate(@RequestBody AskRequest request) {
return service.calculate(requireMessage(request));
}
@PostMapping(value = "/store", produces = MediaType.TEXT_PLAIN_VALUE)
public String store(@RequestBody AskRequest request) {
return service.askStore(requireMessage(request));
}
private String requireMessage(AskRequest request) {
if (request == null || request.message() == null || request.message().isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "message 不能为空");
}
return request.message().trim();
}
}
两个 HTTP 入口分别处理计算和业务咨询;业务入口可根据问题选择订单查询、库存查询,或组合使用。
6. 边界测试与调用核验
6.1 业务边界
以下问题均发送到 /ai/tools/store。
| message | 验证点 |
|---|---|
| 查询 SKU1001 的库存 | 调用 queryStock,返回 17 |
| SKU1002 还有货吗? | 库存为 0,回答缺货 |
| 查一下 ORD9999 | 返回未找到订单,不编造物流 |
| SKU9999 还有多少库存? | 返回未找到商品 |
| 帮我查订单 | 先询问订单号,不猜测编号 |
| 帮我取消 ORD1001 | 说明不支持取消,不执行写操作 |
6.2 确认内部方法确实执行
先检查工具日志或在工具方法中设置断点,再核对参数与返回值。进一步验证时,可以将 SKU1001 的库存从 17 改为 23,重启应用后重新查询:工具日志与回答应一起变化。
工具本身的边界测试可以直接使用 JUnit,无需连接模型。例如:
java
var tools = new StoreTools(new StoreService(new StoreRepository()));
assertEquals("商品:SKU1001,可售库存:17", tools.queryStock("SKU1001"));
assertEquals("商品:SKU1002,可售库存:0", tools.queryStock("SKU1002"));
assertEquals("未找到订单:ORD9999", tools.queryOrder("ORD9999"));
assertTrue(tools.queryStock(null).contains("格式错误"));
直接调用方法验证的是业务逻辑;通过 HTTP 请求并观察日志,验证的是模型到工具的完整链路。
7. 开发时注意什么
- 工具描述写清用途与参数。 名称、描述和参数语义共同影响模型的选择。
- 缺少业务数据时不要补写答案。 未找到订单、商品不存在、库存为零,应有明确区分。
- 权限检查放在应用侧。 真实订单接口需从登录上下文校验订单归属,不能只靠提示词限制访问。
- 写操作单独设计。 退款、取消订单等操作需要授权、确认和幂等控制,不应直接照搬只读查询。
System Prompt 规定如何回答,Tool Calling 提供可执行能力。将工具委托给业务 Service 后,订单、库存等现有接口就能成为 AI 助手的数据来源,而调用日志让结果有据可查。