agentscope-ai / agentscope-ai/agentscope-java

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

Abierto
#1,379 1 comentario 0 reacciones 1 asignado Reclamado por @itxaiohanglover Ver en GitHub
area/core/tool enhancement
Lenguaje dominante
Java
Estrellas
5.6k
Forks
1.3k
Merge medio
4 d 12 h
PR fusionados (30 d)
77

Descripción

## 问题背景

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`:

```java
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 语义一致:

```java
// 默认严格模式,拒绝额外参数
@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`,包括嵌套对象的 `properties`、`items`、`oneOf/anyOf/allOf`、`$defs` 等。仅在顶层加不够,因为 LLM 伪造的参数往往出现在嵌套对象内部。

## 参考实现

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

### 1. `Tool.java` — 新增 `additionalProperties` 属性

```java
Class converter() default DefaultToolResultConverter.class;

/**
* Whether to allow additional properties beyond those defined in the schema.
*
*

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.
*
*

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` — 新增重载方法 + 递归注入

```java
Map generateParameterSchema(Method method, Set excludeParams) {
return generateParameterSchema(method, excludeParams, false);
}

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

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

return schema;
}

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

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

Object itemsObj = schema.get("items");
if (itemsObj instanceof Map) {
addAdditionalPropertiesFalseRecursively((Map) 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) item);
}
}
}
}

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

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

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

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

## 影响范围

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

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.