Spring AI 1.x 系列【18】深入了解更多的工具规范底层组件

文章目录

  • [1. 前言](#1. 前言)
  • [2. 底层 Tool API](#2. 底层 Tool API)
    • [2.1 ToolCallback](#2.1 ToolCallback)
    • [2.2 ToolDefinition](#2.2 ToolDefinition)
      • [2.2.1 方法型工具定义](#2.2.1 方法型工具定义)
      • [2.2.2 函数型工具定义](#2.2.2 函数型工具定义)
    • [2.3 ToolMetadata](#2.3 ToolMetadata)
    • [2.3. JSON Schema](#2.3. JSON Schema)
      • [2.3.1 定义](#2.3.1 定义)
      • [2.3.2 JsonSchemaGenerator](#2.3.2 JsonSchemaGenerator)
      • [2.3.3 描述(Description)](#2.3.3 描述(Description))
      • [2.3.4 参数必填 / 可选](#2.3.4 参数必填 / 可选)
    • [2.4 ResultConversion](#2.4 ResultConversion)
    • [2.5 ToolContext](#2.5 ToolContext)
    • [2.6 ReturnDirect](#2.6 ReturnDirect)

1. 前言

Spring AI 中,工具通过 ToolCallback 接口进行建模。在前面的章节中,我们已经了解了如何借助 Spring AI 提供的内置支持,从方法和函数定义工具。

本章将深入讲解工具规范,以及如何对其进行自定义与扩展,以支持更多使用场景。

2. 底层 Tool API

2.1 ToolCallback

ToolCallback 接口提供了一种定义可被 AI 模型调用的工具的方式,同时包含工具定义与执行逻辑。如果你需要从零开始定义工具,这是需要实现的核心接口。

Spring AI 为方法型工具(MethodToolCallback)和函数型工具(FunctionToolCallback)提供了内置实现。

该接口提供以下方法:

java 复制代码
public interface ToolCallback {

    /**
     * 供 AI 模型判断「何时、如何调用该工具」的定义信息。
     */
    ToolDefinition getToolDefinition();

    /**
     * 用于提供如何处理该工具的附加信息的元数据。
     */
    ToolMetadata getToolMetadata();

    /**
     * 使用给定入参执行工具,并返回要回传给 AI 模型的结果。
     */
    String call(String toolInput);

    /**
     * 使用给定入参和上下文执行工具,并返回要回传给 AI 模型的结果。
     */
    String call(String toolInput, ToolContext toolContext);

}

2.2 ToolDefinition

ToolDefinitionAI 模型提供「可调用工具的核心元信息」, 告诉模型这个工具叫什么、能做什么、需要传什么格式的参数。每个 ToolCallback 实现必须提供 ToolDefinition 实例来定义 tool

接口定义

java 复制代码
package org.springframework.ai.tool.definition;

public interface ToolDefinition {
    String name();

    String description();

    String inputSchema();

    static DefaultToolDefinition.Builder builder() {
        return DefaultToolDefinition.builder();
    }
}

核心方法:

  • String name():工具的唯一名称。
  • String description():工具的功能描述,强烈建议提供详细描述。
  • String inputSchema():工具入参的 JSON Schema 字符串。未指定时将根据方法参数自动生成。可通过 @ToolParam 注解补充参数信息(如描述、是否必选),默认所有参数均为必选。

提供了默认实现类 DefaultToolDefinition ,可以通过 ToolDefinition.Builder 方法使用默认实现构建实例:

java 复制代码
// 1. 调用接口的静态builder方法,获取默认实现的构建器
ToolDefinition weatherTool = ToolDefinition.builder()
    // 2. 设置工具名称(唯一)
    .name("getCurrentWeather")
    // 3. 设置工具描述(核心,让模型理解功能)
    .description("查询指定城市的实时天气,支持摄氏度(C)/华氏度(F)")
    // 4. 设置入参的JSON Schema(告诉模型参数规则)
    .inputSchema("""
        {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "城市名称,比如北京、上海"
                },
                "unit": {
                    "type": "string",
                    "enum": ["C", "F"],
                    "description": "温度单位,可选C/F"
                }
            },
            "required": ["location"] // location必填,unit可选
        }
    """)
    // 5. 构建实例
    .build();

2.2.1 方法型工具定义

在方法型工具定义时,如果方法上标注了 @Tool,则名称和描述优先从注解中读取。

java 复制代码
    @Tool(name = "get_current_date_time", description = "获取用户时区下的当前日期和时间,格式为 yyyy-MM-dd HH:mm:ss")
    public String getCurrentDateTime() {
        LocalDateTime now = LocalDateTime.now(LocaleContextHolder.getTimeZone().toZoneId());
        return now.format(DATETIME_FORMATTER);
    }

如果是自定义构建 MethodToolCallback 时,ToolDefinition 也会自动生成:

java 复制代码
        Method method = ReflectionUtils.findMethod(OtherDateTimeTools.class, "getCurrentDateTime");
        ToolCallback toolCallback = MethodToolCallback.builder()
                .toolMethod(method)
                .build();

此时,从方法生成的 ToolDefinition 规则为:

  • 工具名 = 方法名
  • 工具描述 = 方法名
  • 输入模式 = 方法参数自动生成的 JSON Schema

实际开发时,还是需要自定义 ToolDefinition ,生成更加规范的工具:

java 复制代码
        Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
        ToolDefinition toolDefinition = ToolDefinitions.builder(method)
                .name("currentDateTime")
                .description("Get the current date and time in the user's timezone")
                .inputSchema(JsonSchemaGenerator.generateForMethodInput(method))
                .build();
        ToolCallback toolCallback = MethodToolCallback.builder()
                .toolDefinition(toolDefinition)
                .toolMethod(method)
                .build();

2.2.2 函数型工具定义

从函数构建工具时,ToolDefinition 同样会自动生成。使用 FunctionToolCallback.Builder 时,你可以直接指定:

  • 工具名称
  • 工具描述
  • JSON Schema

2.3 ToolMetadata

ToolMetadata(工具元数据)是为「工具」提供的结果流向配置接口,核心作用是控制工具执行后的结果,是直接返回给客户端,还是先回传给 AI 模型再处理。

接口定义:

java 复制代码
public interface ToolMetadata {
    default boolean returnDirect() {
        return false;
    }

    static DefaultToolMetadata.Builder builder() {
        return DefaultToolMetadata.builder();
    }

    static ToolMetadata from(Method method) {
        Assert.notNull(method, "method cannot be null");
        return DefaultToolMetadata.builder().returnDirect(ToolUtils.getToolReturnDirect(method)).build();
    }
}

可以通过 ToolMetadata.Builder 构建实例,定义工具的额外配置:

java 复制代码
ToolMetadata metadata = ToolMetadata.builder()
    .returnDirect(true) // 手动设置结果直接返回
    .build();

MethodToolCallback 方法型工具时进行配置示例:

java 复制代码
        Method method = ReflectionUtils.findMethod(OtherDateTimeTools.class, "getCurrentDateTime");
        ToolDefinition toolDefinition = ToolDefinitions.builder(method)
                .description("获取用户时区下的当前日期和时间")
                .build();
        ToolMetadata metadata = ToolMetadata.builder()
                .returnDirect(true) // 手动设置结果直接返回
                .build();
        ToolCallback toolCallback = MethodToolCallback.builder()
                .toolDefinition(toolDefinition)
                .toolMethod(method)
                .toolMetadata(metadata)
                .build();

函数型工具示例:

java 复制代码
        ToolCallback toolCallback = FunctionToolCallback
                // 指定工具名称和函数实例
                .builder("currentWeather", new WeatherService())
                // 配置工具描述
                .description("查询指定地区的天气信息")
                // 指定输入参数类型
                .inputType(WeatherRequest.class)
                .toolMetadata(metadata)
                .build();

2.3. JSON Schema

2.3.1 定义

AI 模型提供工具时,模型需要知道调用该工具的入参格式,这个格式就是 JSON Schema,用于让模型理解:

  • 如何调用工具
  • 如何构造参数

简单来说,JSON Schema 就是给大模型看的「工具参数说明书」,描述一段 JSON 应该有哪些字段、字段类型、哪些必填、允许哪些值、字段是什么意思。

例如,天气查询工具入参 JSON Schema

java 复制代码
{
  "type": "object",
  "properties": {
    "location": {
      "type": "string",
      "description": "城市名称,例如:北京、上海"
    },
    "unit": {
      "type": "string",
      "description": "温度单位",
      "enum": ["C", "F"]
    }
  },
  "required": ["location"]
}

2.3.2 JsonSchemaGenerator

Spring AI 通过 JsonSchemaGenerator 类自动生成 Tool Calling 所需的 JSON Schema 字符串。只需给 Java 方法 / 参数加注解,就能自动生成符合 AI 模型要求的入参规则,大幅降低 Tool Calling 的开发成本。

源码如下

java 复制代码
public final class JsonSchemaGenerator {
    private static final boolean PROPERTY_REQUIRED_BY_DEFAULT = true;
    private static final SchemaGenerator TYPE_SCHEMA_GENERATOR;
    private static final SchemaGenerator SUBTYPE_SCHEMA_GENERATOR;

    private JsonSchemaGenerator() {
    }

    public static String generateForMethodInput(Method method, SchemaOption... schemaOptions) {
        ObjectNode schema = JsonParser.getObjectMapper().createObjectNode();
        schema.put("$schema", SchemaVersion.DRAFT_2020_12.getIdentifier());
        schema.put("type", "object");
        ObjectNode properties = schema.putObject("properties");
        List<String> required = new ArrayList();

        for(int i = 0; i < method.getParameterCount(); ++i) {
            String parameterName = method.getParameters()[i].getName();
            Type parameterType = method.getGenericParameterTypes()[i];
            if (parameterType instanceof Class<?> parameterClass) {
                if (ClassUtils.isAssignable(ToolContext.class, parameterClass)) {
                    continue;
                }
            }

            if (isMethodParameterRequired(method, i)) {
                required.add(parameterName);
            }

            ObjectNode parameterNode = SUBTYPE_SCHEMA_GENERATOR.generateSchema(parameterType, new Type[0]);
            parameterNode.remove("format");
            String parameterDescription = getMethodParameterDescription(method, i);
            if (StringUtils.hasText(parameterDescription)) {
                parameterNode.put("description", parameterDescription);
            }

            properties.set(parameterName, parameterNode);
        }

        ArrayNode requiredArray = schema.putArray("required");
        Objects.requireNonNull(requiredArray);
        required.forEach(requiredArray::add);
        processSchemaOptions(schemaOptions, schema);
        return schema.toPrettyString();
    }

    public static String generateForType(Type type, SchemaOption... schemaOptions) {
        Assert.notNull(type, "type cannot be null");
        ObjectNode schema = TYPE_SCHEMA_GENERATOR.generateSchema(type, new Type[0]);
        if (type == Void.class && !schema.has("properties")) {
            schema.putObject("properties");
        }

        processSchemaOptions(schemaOptions, schema);
        return schema.toPrettyString();
    }

    private static void processSchemaOptions(SchemaOption[] schemaOptions, ObjectNode schema) {
        if (Stream.of(schemaOptions).noneMatch((option) -> {
            return option == JsonSchemaGenerator.SchemaOption.ALLOW_ADDITIONAL_PROPERTIES_BY_DEFAULT;
        })) {
            schema.put("additionalProperties", false);
        }

        if (Stream.of(schemaOptions).anyMatch((option) -> {
            return option == JsonSchemaGenerator.SchemaOption.UPPER_CASE_TYPE_VALUES;
        })) {
            convertTypeValuesToUpperCase(schema);
        }

    }

    private static boolean isMethodParameterRequired(Method method, int index) {
        Parameter parameter = method.getParameters()[index];
        ToolParam toolParamAnnotation = (ToolParam)parameter.getAnnotation(ToolParam.class);
        if (toolParamAnnotation != null) {
            return toolParamAnnotation.required();
        } else {
            JsonProperty propertyAnnotation = (JsonProperty)parameter.getAnnotation(JsonProperty.class);
            if (propertyAnnotation != null) {
                return propertyAnnotation.required();
            } else {
                Schema schemaAnnotation = (Schema)parameter.getAnnotation(Schema.class);
                if (schemaAnnotation == null) {
                    Nullable nullableAnnotation = (Nullable)parameter.getAnnotation(Nullable.class);
                    return nullableAnnotation == null;
                } else {
                    return schemaAnnotation.requiredMode() == RequiredMode.REQUIRED || schemaAnnotation.requiredMode() == RequiredMode.AUTO || schemaAnnotation.required();
                }
            }
        }
    }

    @Nullable
    private static String getMethodParameterDescription(Method method, int index) {
        Parameter parameter = method.getParameters()[index];
        ToolParam toolParamAnnotation = (ToolParam)parameter.getAnnotation(ToolParam.class);
        if (toolParamAnnotation != null && StringUtils.hasText(toolParamAnnotation.description())) {
            return toolParamAnnotation.description();
        } else {
            JsonPropertyDescription jacksonAnnotation = (JsonPropertyDescription)parameter.getAnnotation(JsonPropertyDescription.class);
            if (jacksonAnnotation != null && StringUtils.hasText(jacksonAnnotation.value())) {
                return jacksonAnnotation.value();
            } else {
                Schema schemaAnnotation = (Schema)parameter.getAnnotation(Schema.class);
                return schemaAnnotation != null && StringUtils.hasText(schemaAnnotation.description()) ? schemaAnnotation.description() : null;
            }
        }
    }

    public static void convertTypeValuesToUpperCase(ObjectNode node) {
        if (node.isObject()) {
            node.fields().forEachRemaining((entry) -> {
                JsonNode value = (JsonNode)entry.getValue();
                if (value.isObject()) {
                    convertTypeValuesToUpperCase((ObjectNode)value);
                } else if (value.isArray()) {
                    value.elements().forEachRemaining((element) -> {
                        if (element.isObject() || element.isArray()) {
                            convertTypeValuesToUpperCase((ObjectNode)element);
                        }

                    });
                } else if (value.isTextual() && ((String)entry.getKey()).equals("type")) {
                    String oldValue = node.get("type").asText();
                    node.put("type", oldValue.toUpperCase());
                }

            });
        } else if (node.isArray()) {
            node.elements().forEachRemaining((element) -> {
                if (element.isObject() || element.isArray()) {
                    convertTypeValuesToUpperCase((ObjectNode)element);
                }

            });
        }

    }

    static {
        Module jacksonModule = new JacksonModule(new JacksonOption[]{JacksonOption.RESPECT_JSONPROPERTY_REQUIRED});
        Module openApiModule = new Swagger2Module();
        Module springAiSchemaModule = new SpringAiSchemaModule(new SpringAiSchemaModule.Option[0]);
        SchemaGeneratorConfigBuilder schemaGeneratorConfigBuilder = (new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)).with(jacksonModule).with(openApiModule).with(springAiSchemaModule).with(Option.EXTRA_OPEN_API_FORMAT_VALUES, new Option[0]).with(Option.PLAIN_DEFINITION_KEYS, new Option[0]);
        SchemaGeneratorConfig typeSchemaGeneratorConfig = schemaGeneratorConfigBuilder.build();
        TYPE_SCHEMA_GENERATOR = new SchemaGenerator(typeSchemaGeneratorConfig);
        SchemaGeneratorConfig subtypeSchemaGeneratorConfig = schemaGeneratorConfigBuilder.without(Option.SCHEMA_VERSION_INDICATOR, new Option[0]).build();
        SUBTYPE_SCHEMA_GENERATOR = new SchemaGenerator(subtypeSchemaGeneratorConfig);
    }

    public static enum SchemaOption {
        ALLOW_ADDITIONAL_PROPERTIES_BY_DEFAULT,
        UPPER_CASE_TYPE_VALUES;

        private SchemaOption() {
        }
    }
}

核心静态方法:

  • generateForMethodInput:为 Java 方法入参生成 JSON Schema
  • generateForType:为任意 Java 类型生成 JSON Schema
  • isMethodParameterRequired:判断方法参数是否必填(注解优先级)。
  • getMethodParameterDescription:提取参数描述(注解优先级)。

内部枚举类枚举 SchemaOption 定义了 Schema 生成规则:

java 复制代码
public static enum SchemaOption {
    // 允许额外属性(默认禁止)
    ALLOW_ADDITIONAL_PROPERTIES_BY_DEFAULT,
    // 将 type 值转大写(比如 string → STRING)
    UPPER_CASE_TYPE_VALUES;
}

2.3.3 描述(Description)

JSON Schema 生成逻辑支持一系列注解,你可以用它们来自定义参数描述与必填性。

除给工具本身写描述外,还可以给每个入参写描述,帮助模型理解:

  • 参数格式
  • 允许的取值
  • 含义与约束

Spring AI 支持以下注解(优先级不分先后):

  • Spring AI@ToolParam(description = "...")
  • Jackson@JsonClassDescription(description = "...")
  • Jackson@JsonPropertyDescription(description = "...")
  • Swagger@Schema(description = "...")

该方式适用于方法、函数、嵌套对象。示例:

java 复制代码
@Tool(description = "Set a user alarm for the given time")
void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
    // ...
}

2.3.4 参数必填 / 可选

默认所有参数都是必填。你可以用以下注解将参数设为可选,优先级从高到低:

  • @ToolParam(required = false)(Spring AI)
  • @JsonProperty(required = false)(Jackson)
  • @Schema(required = false)(Swagger)
  • @Nullable(Spring Framework)

示例:

java 复制代码
@Tool(description = "Update customer information")
void updateCustomerInfo(
    Long id,
    String name,
    @ToolParam(required = false) String email
) {
    // ...
}

正确设置必填/可选非常关键,可以大幅减少模型幻觉:

  • 必填参数:模型必须拿到有效值才会调用
  • 可选参数:模型可以不传
  • 该填却没值时,模型容易编造数据 → 幻觉

2.4 ResultConversion

工具调用的结果会通过 ToolCallResultConverter 序列化后,再发送给 AI 模型。该接口用于把工具返回的任意对象,转为字符串。

接口定义:

java 复制代码
@FunctionalInterface
public interface ToolCallResultConverter {
    String convert(@Nullable Object result, @Nullable Type returnType);
}

注意事项:

  • 结果必须是可序列化类型
  • 默认使用 Jackson 序列化为 JSONDefaultToolCallResultConverter
  • 你可以提供自定义实现

声明式方法型工具结果转换:

java 复制代码
@Tool(
    description = "Retrieve customer information",
    resultConverter = CustomToolCallResultConverter.class
)
Customer getCustomerInfo(Long id) { ... }

编程式方法型工具结果转换::

java 复制代码
MethodToolCallback.builder()
    .resultConverter(yourConverter)
    .build()

函数型工具结果转换:

java 复制代码
FunctionToolCallback.builder()
    .resultConverter(yourConverter)
    .build()

2.5 ToolContext

Spring AI 支持通过 ToolContext API,向工具传入额外的上下文数据。这些数据可以与 AI 模型传递的工具参数一起用于工具执行中,常用于:

  • 租户 ID
  • 用户身份
  • 请求级别的环境信息

执行流程图:

在工具中使用上下文:

java 复制代码
@Tool(description = "Retrieve customer information")
Customer getCustomerInfo(Long id, ToolContext toolContext) {
    String tenantId = toolContext.getContext().get("tenantId");
    return customerRepository.findById(id, tenantId);
}

使用 ChatClient

java 复制代码
String response = ChatClient.create(chatModel)
        .prompt("Tell me more about the customer with ID 42")
        .tools(new CustomerTools())
        .toolContext(Map.of("tenantId", "acme"))
        .call()
        .content();

使用 ChatModel 直接调用 :

java 复制代码
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(customerTools)
    .toolContext(Map.of("tenantId", "acme"))
    .build();

上下文合并规则:

  • 如果默认配置和运行时配置都设置了 toolContext
  • 最终结果是两者合并
  • 运行时优先级更高,会覆盖默认配置中同名的 key

2.6 ReturnDirect

默认情况下,工具执行完的结果 → 发送给 AI 大模型 → 模型用这个结果润色、生成自然语言回答 → 最终返回给用户。可以配置 returnDirect = true 可以将工具结果直接返回给「调用者」,完全不发给 AI 模型。

适用场景:

  • RAG 检索工具:工具已经查到了精准的文档 / 数据,不需要模型再加工,直接返回结果即可,避免多余处理
  • 终止推理的工具:工具执行后就结束对话(比如完成任务、退出),不需要模型继续循环思考。

ToolCallingManagerSpring AI 专门管理工具执行全过程的组件,它负责读取 returnDirect 属性,并执行操作:

  • returnDirect = true → 结果直返调用者
  • returnDirect = false → 结果返回模型

如果同时调用多个工具:

  • 只有 所有工具的 returnDirect 都设为 true → 全部结果直返调用者
  • 只要有一个工具是 false → 所有工具的结果都会返回给模型

直接在工具方法上用 @Tool 注解,设置 returnDirect = true

java 复制代码
class CustomerTools {
    // 描述:查询客户信息,开启返回直达
    @Tool(description = "Retrieve customer information", returnDirect = true)
    Customer getCustomerInfo(Long id) {
        return customerRepository.findById(id);
    }
}

使用 ToolMetadata 构建器手动设置属性,适用于函数式工具:

java 复制代码
// 构建工具元数据,开启返回直达
ToolMetadata toolMetadata = ToolMetadata.builder()
    .returnDirect(true)
    .build();
相关推荐
希望永不加班2 小时前
SpringBoot 应用启动失败常见原因与排查思路
java·spring boot·后端·spring
AAA小肥杨2 小时前
OpenClaw 数据、设置和内存备份指南
人工智能·大模型·openclaw
ew452182 小时前
【java】基于hutool实现.Excel导出任意多级自定义表头数据
java·开发语言·excel
闻哥2 小时前
深入理解 InnoDB 的 MVCC:原理、Read View 与可见性判断
java·开发语言·jvm·数据库·b树·mysql·面试
阿泽·黑核2 小时前
Easy Vibe Coding 学习心得(六):RAG 入门——让 AI 拥有企业级知识库
人工智能·vibe coding·easy vibe
Jul1en_2 小时前
Java 集合判空方法对比
java·spring boot·算法·spring
光之后裔2 小时前
人工智能对计算机领域冲击思考
人工智能
golang学习记2 小时前
IDEA 2026.1:这些 核心功能免费开放!
java·ide·intellij-idea
我就是你毛毛哥2 小时前
Docker 安装 Jenkins JDK8 版
java·docker·jenkins