Skip to content

[Feature]: @Tool 注解支持 additionalProperties 参数,实现严格参数校验 #1379

@WEIHUWEIHU

Description

@WEIHUWEIHU

问题背景

LLM 在调用工具时,有时会自行伪造(hallucinate)未定义的参数。由于当前框架对额外参数的处理规则是静默忽略,工具仍能正常返回结果,导致:

  1. LLM 误以为伪造参数生效 — 工具正常返回了结果,LLM 没有收到任何错误反馈,便认为自己的传参(包括伪造的参数)都被正确处理了
  2. LLM 对返回数据做出错误解读 — 例如 LLM 伪造了 "owners": ["admin"] 参数,工具返回了全量数据,LLM 误以为返回的是按 owner 过滤后的结果,从而对数据做出错误的判断和决策
  3. 错误链持续传播 — 由于没有任何校验拦截,LLM 不会意识到参数有误,在整个推理链路中持续基于错误的假设进行后续操作

现状分析

目前框架中设置 additionalProperties 的途径存在以下问题:

1. 注解方式注册的工具没有设置入口

使用 @Tool 注解生成的 schema 中,没有任何地方可以设置 additionalProperties,生成的 schema 默认允许额外参数。

2. Schema 方式注册的工具天然支持

使用 SchemaOnlyTool 或直接实现 AgentTool 接口时,getParameters() 返回的 schema 由用户自行构建,天然可以包含 additionalProperties: false

3. SkillBox.registration().extendedModel() 方式无法生效(疑似 Bug)

尝试通过 SimpleExtendedModel 添加 additionalProperties: false

skillBox.registration()
    .tool(toolObject)
    .extendedModel(new SimpleExtendedModel(
        Map.of("additionalProperties", false), List.of()))
    .register();

ExtendedModel.mergeWithBaseSchema() 的实现会将 getAdditionalProperties() 返回的 Map 全部合并进 schema 的 properties 字段中(见 ExtendedModel.java 第 70-72 行),导致 additionalProperties 被错误地放进 properties 里,变成了一个名为 "additionalProperties" 的属性定义,而不是顶层的 additionalProperties: false这是否是一个 Bug,请维护者确认。

期望行为

@Tool 注解上支持声明 additionalProperties,默认 false(不允许额外参数),与 JSON Schema 语义一致:

// 默认严格模式,拒绝额外参数
@Tool(name = "query_business_data")
public String queryData(@ToolParam(name = "reqVo") ReqVo reqVo) { ... }

// 显式允许额外参数(宽松模式)
@Tool(name = "query_business_data", additionalProperties = true)
public String queryData(@ToolParam(name = "reqVo") ReqVo reqVo) { ... }

当 LLM 传入未定义的参数时,ToolValidator 基于 additionalProperties: false 校验失败,返回错误信息,LLM 收到明确的校验错误反馈后可以自我纠正。

生成的 schema 应递归地为所有 type=object 节点注入 additionalProperties: false,包括嵌套对象的 propertiesitemsoneOf/anyOf/allOf$defs 等。仅在顶层加不够,因为 LLM 伪造的参数往往出现在嵌套对象内部。

参考实现

以下是我们验证过的代码改动,供维护者参考:

1. Tool.java — 新增 additionalProperties 属性

Class<? extends ToolResultConverter> converter() default DefaultToolResultConverter.class;

/**
 * Whether to allow additional properties beyond those defined in the schema.
 *
 * <p>Corresponds to the {@code additionalProperties} keyword in JSON Schema.
 * When set to {@code false} (default), any extra parameters passed by the LLM
 * that are not defined in the schema will cause a validation error.
 * When set to {@code true}, extra parameters are silently ignored.
 *
 * <p>This setting is applied recursively to all nested objects within the schema.
 *
 * @return {@code false} to disallow additional properties (default), {@code true} to allow
 */
boolean additionalProperties() default false;

2. ToolSchemaGenerator.java — 新增重载方法 + 递归注入

Map<String, Object> generateParameterSchema(Method method, Set<String> excludeParams) {
    return generateParameterSchema(method, excludeParams, false);
}

@SuppressWarnings("unchecked")
Map<String, Object> generateParameterSchema(
        Method method, Set<String> excludeParams, boolean additionalProperties) {
    // ... 原有生成逻辑不变 ...

    if (!additionalProperties) {
        addAdditionalPropertiesFalseRecursively(schema);
    }

    return schema;
}

@SuppressWarnings("unchecked")
private void addAdditionalPropertiesFalseRecursively(Map<String, Object> schema) {
    if (!"object".equals(schema.get("type"))) {
        return;
    }
    schema.put("additionalProperties", false);

    Object propsObj = schema.get("properties");
    if (propsObj instanceof Map) {
        Map<String, Object> props = (Map<String, Object>) propsObj;
        for (Map.Entry<String, Object> entry : props.entrySet()) {
            if (entry.getValue() instanceof Map) {
                addAdditionalPropertiesFalseRecursively((Map<String, Object>) entry.getValue());
            }
        }
    }

    Object itemsObj = schema.get("items");
    if (itemsObj instanceof Map) {
        addAdditionalPropertiesFalseRecursively((Map<String, Object>) itemsObj);
    }

    for (String key : new String[]{"oneOf", "anyOf", "allOf"}) {
        Object listObj = schema.get(key);
        if (listObj instanceof Iterable) {
            for (Object item : (Iterable<?>) listObj) {
                if (item instanceof Map) {
                    addAdditionalPropertiesFalseRecursively((Map<String, Object>) item);
                }
            }
        }
    }

    Object defsObj = schema.get("$defs");
    if (defsObj instanceof Map) {
        Map<String, Object> defs = (Map<String, Object>) defsObj;
        for (Map.Entry<String, Object> entry : defs.entrySet()) {
            if (entry.getValue() instanceof Map) {
                addAdditionalPropertiesFalseRecursively((Map<String, Object>) entry.getValue());
            }
        }
    }

    Object definitionsObj = schema.get("definitions");
    if (definitionsObj instanceof Map) {
        Map<String, Object> definitions = (Map<String, Object>) definitionsObj;
        for (Map.Entry<String, Object> entry : definitions.entrySet()) {
            if (entry.getValue() instanceof Map) {
                addAdditionalPropertiesFalseRecursively((Map<String, Object>) entry.getValue());
            }
        }
    }
}

3. Toolkit.java — 读取注解值传给 generator

// registerToolMethod() 中 getParameters() 的实现
@Override
public Map<String, Object> getParameters() {
    Set<String> excludeParams =
            presetParameters != null
                    ? presetParameters.keySet()
                    : Collections.emptySet();
    return schemaGenerator.generateParameterSchema(
            method, excludeParams, toolAnnotation.additionalProperties());
}

影响范围

  • 向后兼容:默认 false 即严格模式,这是更安全的默认行为。如果担心破坏现有用户,可以将默认值改为 true,让用户显式声明 additionalProperties = false 来启用严格模式
  • 仅影响注解方式注册的工具SchemaOnlyTool 和程序式注册的 AgentTool 不受影响

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    Status

    In progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions