第2053篇:Spring AI的Function Calling——让AI调用Spring Bean
2026/4/30大约 5 分钟
第2053篇:Spring AI的Function Calling——让AI调用Spring Bean
适读人群:使用Spring AI构建Agent应用的工程师 | 阅读时长:约19分钟 | 核心价值:掌握Spring AI的Function Calling完整实现,包括函数注册、动态启用和错误处理
Spring AI的Function Calling和LangChain4j的@Tool从效果上是一样的,但实现方式差别很大。Spring AI的方式更符合Spring开发者的习惯——用@Bean注册Function,用.functions()在调用时启用。
Function Calling的基本实现
/**
* Spring AI的Function定义
* 实现java.util.function.Function接口
*/
public class StockPriceFunction implements Function<StockPriceFunction.Request,
StockPriceFunction.Response> {
private final StockPriceService priceService;
public StockPriceFunction(StockPriceService priceService) {
this.priceService = priceService;
}
@Override
public Response apply(Request request) {
StockPrice price = priceService.getLatestPrice(request.symbol());
return new Response(
request.symbol(),
price.getPrice(),
price.getCurrency(),
price.getTimestamp()
);
}
// 请求和响应作为record定义(Jackson序列化友好)
public record Request(
@JsonProperty(required = true, value = "symbol")
@JsonPropertyDescription("股票代码,如 AAPL、MSFT、600519.SS")
String symbol
) {}
public record Response(
String symbol,
BigDecimal price,
String currency,
String timestamp
) {}
}/**
* 注册Function为Spring Bean
*/
@Configuration
@RequiredArgsConstructor
public class FunctionConfig {
private final StockPriceService stockPriceService;
private final WeatherService weatherService;
private final CalendarService calendarService;
/**
* @Description注解告诉LLM这个Function做什么
* Bean名称就是Function的标识符
*/
@Bean
@Description("查询指定股票代码的最新价格,支持A股(如600519.SS)和美股(如AAPL)")
public Function<StockPriceFunction.Request, StockPriceFunction.Response> getStockPrice() {
return new StockPriceFunction(stockPriceService);
}
@Bean
@Description("查询指定城市的当前天气情况,包括温度、湿度和天气状况")
public Function<WeatherFunction.Request, WeatherFunction.Response> getWeather() {
return new WeatherFunction(weatherService);
}
@Bean
@Description("创建日历事件,将事件添加到用户的日程中")
public Function<CalendarFunction.CreateRequest, CalendarFunction.CreateResponse>
createCalendarEvent() {
return new CalendarFunction(calendarService);
}
}/**
* 使用Function Calling
*/
@Service
@RequiredArgsConstructor
public class IntelligentAssistant {
private final ChatClient chatClient;
/**
* 启用所有Functions(简单场景)
*/
public String askWithAllFunctions(String question) {
return chatClient.prompt()
.system("你是一个智能助手,可以查询股价、天气和管理日历")
.user(question)
.functions("getStockPrice", "getWeather", "createCalendarEvent")
.call()
.content();
}
/**
* 按场景启用不同Functions(生产推荐)
* 避免LLM误调用不相关的Function
*/
public String askForFinance(String question) {
return chatClient.prompt()
.system("你是金融助手,专注于股票信息查询")
.user(question)
.functions("getStockPrice") // 只启用股价查询
.call()
.content();
}
/**
* 动态决定启用哪些Functions
*/
public String askDynamic(String question, UserContext userContext) {
List<String> enabledFunctions = new ArrayList<>();
// 根据用户权限决定启用哪些Functions
if (userContext.hasFinancePermission()) {
enabledFunctions.add("getStockPrice");
}
if (userContext.hasCalendarAccess()) {
enabledFunctions.add("createCalendarEvent");
}
enabledFunctions.add("getWeather"); // 天气查询所有人都可以用
return chatClient.prompt()
.user(question)
.functions(enabledFunctions.toArray(String[]::new))
.call()
.content();
}
}更复杂的Function:支持多步骤操作
/**
* 订单管理Function(复杂示例)
* 支持查询、取消等多个操作
*/
@Configuration
@RequiredArgsConstructor
public class OrderFunctionConfig {
private final OrderService orderService;
/**
* 设计原则:每个Function单一职责
* 不要把所有订单操作放进一个Function
*/
@Bean
@Description("根据订单ID查询订单详情,包括状态、金额、商品列表和物流信息")
public Function<QueryOrderFunction.Request, QueryOrderFunction.Response> queryOrder() {
return request -> {
try {
Order order = orderService.findById(request.orderId());
if (order == null) {
return new QueryOrderFunction.Response(null, "订单不存在: " + request.orderId());
}
return new QueryOrderFunction.Response(
OrderDTO.from(order), null);
} catch (Exception e) {
return new QueryOrderFunction.Response(null, "查询失败: " + e.getMessage());
}
};
}
@Bean
@Description("取消订单,只有状态为'待付款'或'待发货'的订单才能取消")
public Function<CancelOrderFunction.Request, CancelOrderFunction.Response> cancelOrder() {
return request -> {
try {
OrderCancelResult result = orderService.cancel(
request.orderId(),
request.reason()
);
return new CancelOrderFunction.Response(
result.isSuccess(),
result.getMessage()
);
} catch (Exception e) {
return new CancelOrderFunction.Response(false, "取消失败: " + e.getMessage());
}
};
}
@Bean
@Description("申请退款,已支付的订单可以申请退款,退款金额将在3-5个工作日退回")
public Function<RefundFunction.Request, RefundFunction.Response> applyRefund() {
return request -> {
try {
String refundId = orderService.applyRefund(
request.orderId(),
request.amount(),
request.reason()
);
return new RefundFunction.Response(true, refundId,
"退款申请成功,预计3-5个工作日到账");
} catch (OrderException e) {
return new RefundFunction.Response(false, null, e.getMessage());
}
};
}
}
public class QueryOrderFunction {
public record Request(
@JsonPropertyDescription("订单ID,纯数字格式") String orderId
) {}
public record Response(OrderDTO order, String error) {}
}
public class CancelOrderFunction {
public record Request(
@JsonPropertyDescription("订单ID") String orderId,
@JsonPropertyDescription("取消原因,如:不想要了、下错了") String reason
) {}
public record Response(boolean success, String message) {}
}
public class RefundFunction {
public record Request(
@JsonPropertyDescription("订单ID") String orderId,
@JsonPropertyDescription("退款金额(元),填0表示全额退款") BigDecimal amount,
@JsonPropertyDescription("退款原因") String reason
) {}
public record Response(boolean success, String refundId, String message) {}
}处理Function执行失败
Spring AI的Function异常处理有个细节:如果Function抛异常,Spring AI会把异常信息发给LLM,让LLM尝试继续处理。这个行为有时候不是我们想要的:
/**
* Function异常处理的最佳实践
* 不要在Function里抛异常,而是返回包含错误信息的Response
*/
@Configuration
public class SafeFunctionConfig {
@Bean
@Description("查询用户账户余额")
public Function<BalanceRequest, BalanceResponse> getAccountBalance(
AccountService accountService) {
return request -> {
try {
// 参数校验
if (request.accountId() == null || request.accountId().isBlank()) {
return BalanceResponse.error("账户ID不能为空");
}
Account account = accountService.findById(request.accountId());
if (account == null) {
return BalanceResponse.error("账户不存在: " + request.accountId());
}
return BalanceResponse.success(
account.getBalance(),
account.getCurrency()
);
} catch (UnauthorizedException e) {
return BalanceResponse.error("无权限查询该账户");
} catch (Exception e) {
log.error("查询余额失败: {}", request.accountId(), e);
return BalanceResponse.error("系统繁忙,请稍后再试");
}
};
}
public record BalanceRequest(
@JsonPropertyDescription("账户ID") String accountId
) {}
public record BalanceResponse(
boolean success,
BigDecimal balance,
String currency,
String errorMessage
) {
public static BalanceResponse success(BigDecimal balance, String currency) {
return new BalanceResponse(true, balance, currency, null);
}
public static BalanceResponse error(String message) {
return new BalanceResponse(false, null, null, message);
}
}
}监控Function调用
/**
* Function调用监控
* 用AOP包装Function,记录调用情况
*/
@Aspect
@Component
@Slf4j
public class FunctionCallMonitor {
private final MeterRegistry meterRegistry;
/**
* 拦截所有实现Function接口的Bean方法
*/
@Around("execution(* java.util.function.Function+.apply(..))")
public Object monitorFunctionCall(ProceedingJoinPoint pjp) throws Throwable {
String functionName = pjp.getTarget().getClass().getSimpleName();
long startTime = System.currentTimeMillis();
log.debug("Function调用: {} | 入参: {}", functionName, pjp.getArgs());
try {
Object result = pjp.proceed();
long duration = System.currentTimeMillis() - startTime;
log.info("Function调用成功: {} | 耗时: {}ms | 结果: {}",
functionName, duration, result);
meterRegistry.counter("function.call.success", "function", functionName)
.increment();
meterRegistry.timer("function.call.duration", "function", functionName)
.record(duration, TimeUnit.MILLISECONDS);
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
log.error("Function调用失败: {} | 耗时: {}ms | 错误: {}",
functionName, duration, e.getMessage());
meterRegistry.counter("function.call.failure",
"function", functionName, "error", e.getClass().getSimpleName())
.increment();
throw e;
}
}
}Spring AI的Function Calling通过Spring Bean机制让工具注册变得清晰,依赖注入也很自然。和LangChain4j的@Tool相比,Spring AI的方式更灵活(可以在调用时动态选择Function),但配置也稍微繁琐一些。
选择哪种方式主要看你的框架选型——用了Spring AI就用Function,用了LangChain4j就用@Tool。
