Spring AI FunctionCallback源码:工具注册、Schema生成与调用链路
Spring AI FunctionCallback源码:工具注册、Schema生成与调用链路
适读人群:正在使用Spring AI框架的Java开发者,希望深入理解工具调用机制 | 阅读时长:约18分钟
开篇故事
开始用Spring AI做Agent的时候,我对它的工具注册机制很困惑:只需要写一个@Bean返回Function<Input, Output>,框架就自动生成了JSON Schema,自动处理了LLM的工具调用回调。这背后是怎么实现的?
带着这个问题,我花了两天时间把Spring AI的FunctionCallback源码从头到尾看了一遍。原来它的设计非常优雅——利用Java的泛型信息和Jackson的Schema生成能力,把开发者的Java代码自动转化成LLM能理解的JSON Schema,还负责整个调用链路的编排。
今天把这套机制讲清楚,让你用Spring AI做Function Call时心里有底。
一、Spring AI的工具注册方式
Spring AI提供了两种注册工具的方式:
方式一:通过@Bean注册Function<I, O>
@Bean
@Description("获取指定城市的天气信息")
public Function<WeatherRequest, WeatherResponse> getWeather() {
return request -> weatherService.getWeather(request.location(), request.unit());
}方式二:实现FunctionCallback接口
FunctionCallback callback = FunctionCallbackWrapper.builder(weatherService::getWeather)
.withName("get_weather")
.withDescription("获取指定城市的天气信息")
.withInputType(WeatherRequest.class)
.build();两种方式最终都会转化成FunctionCallback对象,注册到FunctionCallbackContext中。
二、源码核心路径解析
2.1 FunctionCallback接口
// spring-ai-core/src/main/.../model/function/FunctionCallback.java
public interface FunctionCallback {
// 工具名称(对应JSON Schema中的name)
String getName();
// 工具描述(对应JSON Schema中的description)
String getDescription();
// 工具的输入JSON Schema(对应JSON Schema中的parameters)
String getInputTypeSchema();
// 执行工具调用,输入是JSON字符串,输出是JSON字符串
String call(String functionInput);
}2.2 FunctionCallbackWrapper:从Java Function生成Schema
// FunctionCallbackWrapper.java(Spring AI核心类,简化)
public class FunctionCallbackWrapper<I, O> implements FunctionCallback {
private final String name;
private final String description;
private final Class<I> inputType;
private final Function<I, O> function;
private final ObjectMapper objectMapper;
private final JsonSchemaGenerator schemaGenerator;
// 生成输入参数的JSON Schema
@Override
public String getInputTypeSchema() {
try {
// 使用Jackson的JsonSchemaGenerator从Java类生成JSON Schema
JsonSchema schema = schemaGenerator.generateSchema(inputType);
return objectMapper.writeValueAsString(schema);
} catch (JsonMappingException | JsonProcessingException e) {
throw new RuntimeException("Failed to generate input schema", e);
}
}
// 执行调用
@Override
public String call(String functionInput) {
try {
// 1. 把JSON字符串反序列化成输入类型
I input = objectMapper.readValue(functionInput, inputType);
// 2. 调用实际的Java Function
O output = function.apply(input);
// 3. 把输出序列化成JSON字符串返回给LLM
return objectMapper.writeValueAsString(output);
} catch (JsonProcessingException e) {
throw new RuntimeException("Error processing function call", e);
}
}
}2.3 完整调用链路
2.4 FunctionCallbackContext:工具注册中心
// FunctionCallbackContext.java(Spring AI核心,简化)
@Component
public class FunctionCallbackContext implements ApplicationContextAware {
private ApplicationContext applicationContext;
// 按名称查找FunctionCallback
public FunctionCallback getFunctionCallback(String functionName) {
// 1. 先从Spring容器中查找同名的FunctionCallback Bean
try {
return applicationContext.getBean(functionName, FunctionCallback.class);
} catch (NoSuchBeanDefinitionException e) {
// ignore
}
// 2. 查找同名的Function<?, ?> Bean,包装成FunctionCallbackWrapper
try {
Object functionBean = applicationContext.getBean(functionName);
if (functionBean instanceof Function<?, ?> function) {
// 通过反射获取泛型类型(关键!)
return buildFunctionCallbackFromFunction(functionName, function);
}
} catch (NoSuchBeanDefinitionException e) {
// ignore
}
throw new IllegalStateException("No FunctionCallback found for name: " + functionName);
}
private FunctionCallback buildFunctionCallbackFromFunction(String name,
Function<?, ?> function) {
// 获取@Description注解
String description = "";
Method applyMethod = function.getClass().getMethods()[0];
Description descAnnotation = function.getClass().getAnnotation(Description.class);
if (descAnnotation != null) {
description = descAnnotation.value();
}
// 通过反射获取Function的泛型参数(输入类型)
Type[] genericInterfaces = function.getClass().getGenericInterfaces();
// 解析 Function<Input, Output> 中的 Input 类型
Class<?> inputType = resolveInputType(genericInterfaces);
return FunctionCallbackWrapper.builder(function)
.withName(name)
.withDescription(description)
.withInputType(inputType)
.build();
}
}三、完整代码示例
3.1 用Spring AI的完整工具调用实现
// 1. 定义工具的输入输出类型(用Jackson注解描述Schema)
@JsonClassDescription("查询订单的请求参数")
public record OrderQueryRequest(
@JsonProperty(required = true, value = "order_id")
@JsonPropertyDescription("订单ID,格式为纯数字,如:12345678")
String orderId,
@JsonProperty(value = "include_items")
@JsonPropertyDescription("是否包含订单商品列表,默认false")
Boolean includeItems
) {}
@JsonClassDescription("订单查询结果")
public record OrderQueryResponse(
String orderId,
String status,
BigDecimal totalAmount,
String createTime,
List<OrderItemDTO> items
) {}
// 2. 注册工具Bean
@Configuration
public class AiToolsConfig {
@Autowired
private OrderService orderService;
@Bean
@Description("查询指定订单的详情信息,包括订单状态、金额、商品列表等。" +
"当用户询问订单状态、查询订单信息时使用。" +
"需要提供订单ID。")
public Function<OrderQueryRequest, OrderQueryResponse> queryOrder() {
return request -> {
Order order = orderService.findById(request.orderId());
if (order == null) {
return new OrderQueryResponse(request.orderId(), "NOT_FOUND",
BigDecimal.ZERO, null, null);
}
List<OrderItemDTO> items = request.includeItems() != null && request.includeItems()
? orderService.getOrderItems(request.orderId())
: null;
return new OrderQueryResponse(
order.getId(),
order.getStatus().name(),
order.getTotalAmount(),
order.getCreateTime().toString(),
items
);
};
}
@Bean
@Description("取消指定订单。只有状态为PENDING或PAID的订单可以取消。" +
"当用户要求取消订单时使用。")
public Function<CancelOrderRequest, CancelOrderResponse> cancelOrder() {
return request -> {
try {
orderService.cancel(request.orderId(), request.reason());
return new CancelOrderResponse(request.orderId(), true, "取消成功");
} catch (BusinessException e) {
return new CancelOrderResponse(request.orderId(), false, e.getMessage());
}
};
}
}
// 3. 在ChatClient中使用工具
@Service
public class OrderChatService {
@Autowired
private ChatClient chatClient;
public String chat(String userId, String userMessage) {
return chatClient.prompt()
.system("你是一个订单客服助手,专门帮助用户查询和处理订单问题。" +
"用户当前ID:" + userId + "。" +
"在查询订单之前,请确认用户提供了订单ID。")
.user(userMessage)
.functions("queryOrder", "cancelOrder") // 指定可用工具
.call()
.content();
}
}3.2 动态注册工具(运行时添加)
// 不是所有工具都适合静态注册,有时需要动态注册
@Service
public class DynamicToolService {
@Autowired
private ChatClient.Builder chatClientBuilder;
public String chatWithDynamicTools(String userMessage, List<String> allowedCategories) {
// 根据用户权限动态构建工具列表
List<FunctionCallback> tools = new ArrayList<>();
if (allowedCategories.contains("order")) {
tools.add(FunctionCallbackWrapper.builder(
(OrderQueryRequest req) -> queryOrder(req))
.withName("query_order")
.withDescription("查询订单信息")
.withInputType(OrderQueryRequest.class)
.build());
}
if (allowedCategories.contains("product")) {
tools.add(FunctionCallbackWrapper.builder(
(ProductSearchRequest req) -> searchProduct(req))
.withName("search_product")
.withDescription("搜索商品")
.withInputType(ProductSearchRequest.class)
.build());
}
return chatClientBuilder.build()
.prompt()
.user(userMessage)
.functions(tools.toArray(new FunctionCallback[0]))
.call()
.content();
}
}四、踩坑实录
坑1:泛型擦除导致Schema生成失败
现象:工具函数的输入类型是泛型的(如Function<List<String>, Result>),Schema生成时报错。
根因:Java泛型在运行时会擦除,Spring AI通过反射获取泛型参数时,无法得到具体类型。
解决:避免在工具的输入/输出类型上使用Java泛型,用具体类型替代。实在需要列表,定义一个具体的Wrapper类:
// 不要:Function<List<String>, Result>
// 要:Function<QueryRequest, Result>,QueryRequest里有List<String>字段
@JsonClassDescription("查询请求")
public record QueryRequest(
@JsonProperty(required = true, value = "ids")
@JsonPropertyDescription("查询的ID列表")
List<String> ids // 具体类型,不是泛型
) {}坑2:工具执行抛异常LLM不知道
默认情况下,工具执行抛出的异常会导致整个对话失败,LLM无法了解工具失败原因。
应该捕获异常,返回错误描述:
@Bean
public Function<OrderQueryRequest, Object> queryOrderSafe() {
return request -> {
try {
return orderService.findById(request.orderId());
} catch (Exception e) {
// 返回包含错误信息的响应,让LLM知道失败了
return Map.of(
"success", false,
"error", e.getMessage(),
"orderId", request.orderId()
);
}
};
}坑3:Schema过于复杂LLM理解困难
嵌套对象层级太深、字段太多,LLM会填充错误。
建议:工具输入类型保持扁平,必要参数不超过5个:
// 太复杂(嵌套太深)
public record ComplexRequest(
UserInfo user, // 里面有userId, name, address...
OrderFilter filter, // 里面有dateRange, status, category...
PaginationInfo page // 里面有pageNum, pageSize, sortBy...
) {}
// 更好:扁平化
public record SimpleOrderRequest(
@JsonProperty(required = true) String userId,
String status, // optional
int page, // optional, 默认1
int pageSize // optional, 默认20
) {}坑4:同名工具Bean覆盖
如果有多个名为getWeather的Bean(可能来自不同的@Configuration类),Spring只会注册最后一个,前面的被覆盖。工具注册时要确保名称唯一。
五、总结与延伸
Spring AI的FunctionCallback核心链路:
@Bean Function<I,O>
→ FunctionCallbackContext自动发现
→ FunctionCallbackWrapper包装(含Schema生成)
→ ChatModel构建tools参数
→ LLM返回tool_calls
→ FunctionCallback.call(json) 执行
→ 结果返回给LLM
→ 最终回答Spring AI的价值在于:把繁琐的JSON Schema生成、工具调用循环、结果序列化全部封装掉,让开发者只需要关注业务逻辑(Java Function的实现)。
下一篇聊如何生成高质量的JSON Schema,让LLM更准确地调用你的工具。
