ToolCallback源码解析
上一节我们知道了Spring AI提供两种定义工具的方式:@Tool注解和Function Bean。那你有没有好奇过:这两种方式在底层有什么区别?Spring AI拿到你的工具定义后,内部是怎么调用的?
这一节我们就来扒一扒源码,搞清楚这些问题。
ToolCallback:工具的统一抽象
不管你用哪种方式定义工具,Spring AI最终都会把它转换成一个叫ToolCallback的东西。这是Spring AI对"可调用工具"的统一抽象。
ToolCallback 接口的两个核心方法:
getToolDefinition():提供工具的"说明书",让模型知道有这个工具可用call():真正干活的方法,接收 JSON 格式的参数,返回执行结果
不管工具是怎么定义的,只要实现了这个接口,Spring AI 就能调用它。
先来看看这个接口长什么样:
public interface ToolCallback {
/**
* 获取工具定义,包含名称、描述、参数规范等信息
* 模型会根据这些信息来决定什么时候调用这个工具
*/
ToolDefinition getToolDefinition();
/**
* 获取工具的元数据,比如是否需要用户确认、返回值是否直接给用户等
*/
default ToolMetadata getToolMetadata() {
return ToolMetadata.builder().build();
}
/**
* 执行工具调用
* @param toolInput 模型传过来的参数,JSON格式
* @return 执行结果,会被发回给模型
*/
String call(String toolInput);
/**
* 带上下文的执行方法,可以传递额外信息
*/
default String call(String toolInput, @Nullable ToolContext toolContext) {
if (toolContext != null && !toolContext.getContext().isEmpty()) {
throw new UnsupportedOperationException("不支持工具上下文");
}
return call(toolInput);
}
}
看完接口定义,几个关键点就清楚了:
getToolDefinition()— 提供工具的"说明书",让模型知道有这个工具可用call()— 真正干活的方法,接收JSON格式的参数,返回执行结果
不管工具是怎么定义的,只要实现了这个接口,Spring AI就能调用它。
两个实现类的分工
ToolCallback有两个主要实现:
| 实现类 | 对应的工具定义方式 | 内部调用机制 |
|---|---|---|
| FunctionToolCallback | Function Bean方式 | 函数式接口回调 |
| MethodToolCallback | @Tool注解方式 | Java反射调用 |
为什么要搞两个实现?因为这两种工具定义方式,在"怎么触发执行"这件事上完全不同。
如果把它们真正执行时的分工摊开看,差异主要集中在"参数如何还原"和"业务代码如何触发"这两步:
FunctionToolCallback:函数式回调
先来看Function Bean方式是怎么工作的。
当你这样定义一个工具:
@Bean
@Description("查询商品库存")
public Function<StockRequest, StockResponse> queryStock(StockService service) {
return request -> service.getStock(request.productCode());
}
Spring AI会创建一个FunctionToolCallback来包装它。看看它的核心代码(简化版):
public class FunctionToolCallback<I, O> implements ToolCallback {
// 真正干活的函数式接口
private final BiFunction<I, ToolContext, O> function;
// 入参类型,用于JSON反序列化
private final Class<I> inputType;
@Override
public String call(String toolInput, ToolContext toolContext) {
// 1. 把JSON字符串转成入参对象
I input = parseInput(toolInput, inputType);
// 2. 调用函数式接口
O output = function.apply(input, toolContext);
// 3. 把返回值转成字符串
return convertOutput(output);
}
private I parseInput(String json, Class<I> type) {
return objectMapper.readValue(json, type);
}
}
调用链路很清晰:
模型返回工具调用请求
→ Spring AI调用FunctionToolCallback.call()
→ JSON反序列化成Request对象
→ 调用你定义的Function.apply()
→ 返回结果
核心就是那个function.apply()——直接调用你定义的函数式接口。因为Function、BiFunction这些都是Java标准接口,调用起来很自然,不需要什么黑魔法。
MethodToolCallback:反射调用
@Tool注解方式就不一样了。你标注的是一个普通方法,Spring AI得想办法把它"调"起来。怎么调?反射。
假设你定义了这样一个工具:
@Component
public class WeatherTools {
@Tool(description = "查询天气")
public String getWeather(
@ToolParam(description = "城市名") String city) {
return city + ":晴,25度";
}
}
Spring AI会创建一个MethodToolCallback来包装这个方法。来看核心代码(简化版):
public class MethodToolCallback implements ToolCallback {
// 目标对象,也就是你的工具类实例
private final Object toolObject;
// 要调用的方法
private final Method method;
// 方法的参数信息
private final List<ToolMethodParameter> parameters;
@Override
public String call(String toolInput, ToolContext toolContext) {
// 1. 解析JSON,提取参数值
Map<String, Object> arguments = parseArguments(toolInput);
// 2. 按照方法参数顺序组装参数数组
Object[] args = buildMethodArguments(arguments);
// 3. 反射调用方法
Object result = method.invoke(toolObject, args);
// 4. 返回结果
return convertToString(result);
}
private Object[] buildMethodArguments(Map<String, Object> arguments) {
Object[] args = new Object[parameters.size()];
for (int i = 0; i < parameters.size(); i++) {
String paramName = parameters.get(i).getName();
args[i] = convertValue(arguments.get(paramName), parameters.get(i).getType());
}
return args;
}
}
调用链路:
模型返回工具调用请求
→ Spring AI调用MethodToolCallback.call()
→ 解析JSON获取参数
→ 组装方法参数数组
→ method.invoke()反射调用
→ 返回结果
关键在method.invoke(toolObject, args)这一行——通过反射调用你的方法。
MethodToolCallback 使用反射机制调用目标方法,有轻微的性能开销,但在工具调用场景下可以忽略不计——工具执行本身(如网络请求、数据库查询)的耗时远大于反射开销。
工具是怎么被注册进去的
知道了两种Callback的区别,再来看看Spring AI是怎么识别和注册这些工具的。
当你调用chatClient.prompt().tools(someToolObject)时,Spring AI会这样处理:
// 简化的处理逻辑
public ChatClientRequestSpec tools(Object... toolObjects) {
for (Object toolObject : toolObjects) {
// 把对象转成ToolCallback数组
ToolCallback[] callbacks = ToolCallbacks.from(toolObject);
this.toolCallbacks.addAll(Arrays.asList(callbacks));
}
return this;
}
ToolCallbacks.from()会扫描传入对象的所有方法,找出带@Tool注解的,每个方法创建一个MethodToolCallback。
而当你用toolNames("beanName")时,Spring AI会去Spring容器里找对应的Function Bean,然后包装成FunctionToolCallback。
Debug看执行过程
光看代码可能还不够直观,咱们来实际Debug一下。
在org.springframework.ai.model.tool.DefaultToolCallingManager类的executeToolCall方法打个断点:
在 DefaultToolCallingManager.executeToolCall() 方法打断点,可以实时观察工具调用的执行情况,包括工具名、传入参数以及实际使用的 Callback 类型。这是排查工具调用问题最直接的方式。
// DefaultToolCallingManager.java
public ToolCallResult executeToolCall(ChatOptions options, ToolCall toolCall) {
// 根据工具名找到对应的Callback
ToolCallback callback = findToolCallback(toolCall.name());
// 执行调用 — 在这里打断点
String result = callback.call(toolCall.arguments(), toolContext);
return new ToolCallResult(toolCall, result);
}
场景一:用tools(new TimeTools())传入对象
断点命中时,你会看到callback的实际类型是MethodToolCallback:
callback = MethodToolCallback {
toolObject = TimeTools实例
method = getTimeByZoneId(String)
...
}
说明@Tool注解方式走的是反射调用。
场景二:用toolNames("queryStockTool")指定Bean名称
断点命中时,callback的实际类型是FunctionToolCallback:
callback = FunctionToolCallback {
function = StockService::getStock(方法引用)
inputType = StockRequest.class
...
}
说明Function Bean方式走的是函数式接口回调。
执行时机的秘密
还有一个细节值得关注:Spring AI是在哪个时机执行工具调用的?
整个流程是这样的:
重点在中间那几步:
- 模型返回的响应如果包含
tool_call,Spring AI会识别出来 - 然后委托给
ToolCallingManager执行 - Manager找到对应的
ToolCallback,调用它的call()方法 - 拿到结果后,自动发起第二次请求,把工具结果告诉模型
- 模型根据工具结果生成最终回答
所以当你用默认配置时,一次用户请求可能在背后触发多次API调用——这点要有心理准备,会影响响应时间和费用。
默认自动执行模式下,一次用户请求会触发至少两次模型 API 调用(第一次获取工具调用指令,第二次根据工具结果生成最终答案)。在高并发或工具链较长的场景中,需要关注响应延迟和 Token 费用的累积。
小结
这一节我们深入源码,搞清楚了:
ToolCallback是Spring AI对工具的统一抽象FunctionToolCallback通过函数式接口回调执行,对应FunctionBean方式MethodToolCallback通过反射执行,对应@Tool注解方式- 工具调用发生在模型返回
tool_call响应之后,由ToolCallingManager协调执行
理解这些底层原理,能帮你更好地排查问题、优化性能。下一节我们来聊聊怎么设计一个"好用"的工具。