agentscope-ai / agentscope-ai/agentscope-java

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

オープン
#1,379 コメント 1 件 リアクション 0 件 担当者 1 名 @itxaiohanglover が担当を希望しています GitHub で見る
area/core/tool enhancement
主要言語
Java
スター
5.6k
フォーク
1.3k
平均マージ
4日 12時間
マージ済み PR(30日)
77

説明

## 问题背景

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` 不受影响

コントリビューションガイド

コントリビューションガイドを開く

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。