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反射调用

为什么要搞两个实现?因为这两种工具定义方式,在"怎么触发执行"这件事上完全不同。

如果把它们真正执行时的分工摊开看,差异主要集中在"参数如何还原"和"业务代码如何触发"这两步:

2-Learning/05-项目/08-企业级项目深读/01-SuperAgent/06-系统讲解AI相关技术/04-FunctionCall详细解析/assets/Fkd_hbYPpa7uruI0Kk5dW5znR1eW-930c8f53

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()——直接调用你定义的函数式接口。因为FunctionBiFunction这些都是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是在哪个时机执行工具调用的?

整个流程是这样的:

2-Learning/05-项目/08-企业级项目深读/01-SuperAgent/06-系统讲解AI相关技术/04-FunctionCall详细解析/assets/FqWtaEgYaT5VcKN2_qtuuTeH5TO7-81bcffe7

重点在中间那几步:

  • 模型返回的响应如果包含tool_call,Spring AI会识别出来
  • 然后委托给ToolCallingManager执行
  • Manager找到对应的ToolCallback,调用它的call()方法
  • 拿到结果后,自动发起第二次请求,把工具结果告诉模型
  • 模型根据工具结果生成最终回答

所以当你用默认配置时,一次用户请求可能在背后触发多次API调用——这点要有心理准备,会影响响应时间和费用。

默认自动执行模式下,一次用户请求会触发至少两次模型 API 调用(第一次获取工具调用指令,第二次根据工具结果生成最终答案)。在高并发或工具链较长的场景中,需要关注响应延迟和 Token 费用的累积。

小结

这一节我们深入源码,搞清楚了:

  • ToolCallback是Spring AI对工具的统一抽象
  • FunctionToolCallback通过函数式接口回调执行,对应Function Bean方式
  • MethodToolCallback通过反射执行,对应@Tool注解方式
  • 工具调用发生在模型返回tool_call响应之后,由ToolCallingManager协调执行

理解这些底层原理,能帮你更好地排查问题、优化性能。下一节我们来聊聊怎么设计一个"好用"的工具。


企业级项目导航:⬅️ 01-Spring AI工具调用实战 | 02-ToolCallback源码解析 | ➡️ 03-工具设计原则与最佳实践