art agent: enforce json schema
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Successful in 1m32s
Project CI / Native shell tests (pull_request) Successful in 1m57s

This commit is contained in:
2026-07-28 12:03:45 +08:00
parent 7f8abe02ea
commit 2b31d5e16e
10 changed files with 115 additions and 13 deletions
@@ -14,6 +14,14 @@
- 关联:相关文件、文档、提交或 Issue
```
## 工具 JSON Schema 的条件约束必须覆盖运行时默认值
- 现象:LLM 按工具 schema 生成的参数可以通过结构约束,但参数补默认值后被运行时校验拒绝,白白消耗一次工具修复轮次。例如固定 `gpt-image-2` 的 UI 工具仍暴露 `0.5K`,或视频调用省略 `model` 时 schema 允许 `1080p`,运行时却默认成 `seedance2.0-fast` 后拒绝。
- 原因:通用枚举 schema 被固定模型工具直接复用;JSON Schema 的 `if` 又用 `required: ["model"]` 排除了字段缺失场景,而 Serde 默认值只在 schema 校验之后生效。description 只能提示 LLM,不能替代 `enum` / `if` / `then` 的结构约束。
- 处理:固定模型工具使用与该模型能力一致的专用枚举;可切换模型的图片工具在对象层复用共享 `model + image_size` 条件约束。条件字段有运行时默认值时,省略字段必须落入默认模型对应的 schema 分支:默认 nanobanana2 的图片工具只在显式选择 `gpt-image-2` 时收紧尺寸,所以条件保留 `required: ["model"]`;默认 fast 的视频工具则利用字段缺失时 `properties.model.const` 条件成立的语义,不额外要求 `model` 存在。运行时校验仍保留为最终防线。
- 验证:锁定 `generate-ui-design.image_size = ["1K", "2K"]`,三个可切换图片模型的工具都接入共享 `gpt-image-2 -> image_size = ["1K", "2K"]` 条件,以及视频 fast 条件没有内层 `required`、其 `then.resolution = ["480p", "720p"]`;同时保留运行时拒绝 `gpt-image-2 + 0.5K``seedance2.0-fast + 1080p` 的测试。
- 关联:`server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs``server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs``server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs``docs/【编辑器】画布Agent对话面板-2026-07-03.md`
## 图片生成的 K 档不能靠回图后缩放实现
- 现象:用户选择 2K 时占位框看起来是 2K,最终资源元数据也显示为 2K,但模型请求实际仍是固定 1K 或竖版回落尺寸;画面只是后端放大后的低分辨率结果。
@@ -28,6 +28,7 @@
- 用户要求“规范图 / 视觉规范图 / 风格规范图 / 素材规范展板”时,规划默认选择 `generate_image`,并在 prompt 中明确要求生成规范展板,包含统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等可落地的视觉规范元素。
- 用户要求“角色规范图”且语义是角色的规范展板、风格展板或设定板时,仍走 `generate_image`,不要误分流到 `generate_character`;只有实际生成角色立绘、角色主形象或角色视觉资产时才走 `generate_character`。用户要求多个图标素材、图集或 spritesheet 时才走 `generate_icon_spritesheet`
- 所有生成必须走 `execute_billable_asset_operation_with_cost` 与模型定价配置,禁止绕过定价收口。
- function-calling 的 JSON Schema 必须与参数默认值和运行时校验保持一致,不能只在 description 中提示会被运行时拒绝的组合。`generate-ui-design` 固定 `gpt-image-2`,因此 `image_size` 只暴露 `1K / 2K`;其它可切换图片模型的工具通过共享条件 schema 在显式选择 `gpt-image-2` 时同样把 `image_size` 限制为 `1K / 2K`,省略模型时仍按默认 nanobanana2 允许 `0.5K``generate-video` 省略 `model` 时按默认 `seedance2.0-fast` 约束 `resolution``480p / 720p`,显式选择其它模型时仍使用其现有分辨率范围。运行时强类型校验继续作为最终防线。
- 图层操作及其他未注册的画板功能第一期不进入对话工具面,仍走现有面板。
## 当前分支落地状态
@@ -88,6 +88,7 @@ impl Tool for EditImageTool {
"type": "string",
"description": "编辑提示词,描述希望如何修改图片。例如「把背景换成红色」、「把人物改成坐着」。"
},
// TODO need to introduce size param, but that needs more metadata such as original image size, skip in this version
"model": {
"type": "string",
"enum": [GPT_IMAGE_2_MODEL],
@@ -6,7 +6,8 @@ use crate::agent::tools::generate_image::{
};
use crate::agent::tools::image_generation_options::{
default_image_aspect_ratio, default_image_model, default_image_size,
image_aspect_ratio_parameter_schema, image_model_parameter_schema, image_size_parameter_schema,
image_aspect_ratio_parameter_schema, image_model_parameter_schema,
image_model_size_constraint_schema, image_size_parameter_schema,
validate_image_generation_options,
};
use crate::framework::tool::ToolFailureKind;
@@ -53,7 +54,8 @@ impl Tool for GenerateCharacterTool {
"image_size": image_size_parameter_schema(),
},
"required": ["prompt"],
"additionalProperties": false
"additionalProperties": false,
"allOf": [image_model_size_constraint_schema()]
})
}
@@ -4,7 +4,8 @@ use crate::agent::tools::context::EditorToolContext;
use crate::agent::tools::image_generation_options::{
ImageGenerationOptionsError, default_image_aspect_ratio, default_image_model,
default_image_size, image_aspect_ratio_parameter_schema, image_model_parameter_schema,
image_size_parameter_schema, validate_image_generation_options,
image_model_size_constraint_schema, image_size_parameter_schema,
validate_image_generation_options,
};
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL};
@@ -133,7 +134,8 @@ impl Tool for GenerateIconSpritesheetTool {
"image_size": image_size_parameter_schema()
},
"required": ["reference_image_id", "icon_descriptions"],
"additionalProperties": false
"additionalProperties": false,
"allOf": [image_model_size_constraint_schema()]
})
}
@@ -4,7 +4,8 @@ use crate::agent::tools::context::EditorToolContext;
use crate::agent::tools::image_generation_options::{
ImageGenerationOptionsError, default_image_aspect_ratio, default_image_model,
default_image_size, image_aspect_ratio_parameter_schema, image_model_parameter_schema,
image_size_parameter_schema, validate_image_generation_options,
image_model_size_constraint_schema, image_size_parameter_schema,
validate_image_generation_options,
};
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL};
@@ -113,7 +114,8 @@ impl Tool for GenerateImageTool {
"image_size": image_size_parameter_schema()
},
"required": ["prompt"],
"additionalProperties": false
"additionalProperties": false,
"allOf": [image_model_size_constraint_schema()]
})
}
@@ -5,8 +5,8 @@ use crate::agent::tools::generate_image::{
EditorImageGenerationResult, GenerateImageError, GenerateImageToolOutput,
};
use crate::agent::tools::image_generation_options::{
default_image_aspect_ratio, default_image_size, image_aspect_ratio_parameter_schema,
image_size_parameter_schema, validate_image_generation_options,
default_image_aspect_ratio, default_image_size, gpt_image_2_size_parameter_schema,
image_aspect_ratio_parameter_schema, validate_image_generation_options,
};
use crate::framework::tool::ToolFailureKind;
use crate::framework::tool::{Tool, ToolFailure};
@@ -58,7 +58,7 @@ impl Tool for GenerateUiDesignTool {
},
"reference_image_ids": { "type": "array", "items": { "type": "string" }, "description": "image_id(s) for desc UI 风格或布局" },
"aspect_ratio": image_aspect_ratio_parameter_schema(),
"image_size": image_size_parameter_schema()
"image_size": gpt_image_2_size_parameter_schema()
},
"required": ["prompt", "model"],
"additionalProperties": false
@@ -169,6 +169,14 @@ mod tests {
parameters["properties"]["model"]["default"],
GPT_IMAGE_2_MODEL
);
assert_eq!(
parameters["properties"]["image_size"]["enum"],
json!(["1K", "2K"])
);
assert_eq!(
parameters["properties"]["image_size"]["default"],
json!("1K")
);
assert!(
parameters["required"]
.as_array()
@@ -125,9 +125,10 @@ impl Tool for GenerateVideoTool {
"additionalProperties": false,
"allOf": [
{
// model 省略时运行时会补为 fast;这里故意不要求 model 存在,
// 让 JSON Schema 对省略 model 的调用应用同一分辨率约束。
"if": {
"properties": { "model": { "const": GenerateVideoTool::SEEDANCE_2_FAST_MODEL } },
"required": ["model"]
"properties": { "model": { "const": GenerateVideoTool::SEEDANCE_2_FAST_MODEL } }
},
"then": {
"properties": { "resolution": { "enum": GenerateVideoTool::SEEDANCE_2_FAST_RESOLUTIONS } }
@@ -360,5 +361,15 @@ mod tests {
);
assert_eq!(schema["properties"]["sound"]["default"], json!("on"));
assert_eq!(schema["allOf"].as_array().map(Vec::len), Some(2));
let fast_model_condition = &schema["allOf"][0];
assert_eq!(
fast_model_condition["if"]["properties"]["model"]["const"],
json!(GenerateVideoTool::DEFAULT_VIDEO_MODEL)
);
assert!(fast_model_condition["if"].get("required").is_none());
assert_eq!(
fast_model_condition["then"]["properties"]["resolution"]["enum"],
json!(["480p", "720p"])
);
}
}
@@ -93,12 +93,36 @@ pub fn image_aspect_ratio_parameter_schema() -> Value {
pub fn image_size_parameter_schema() -> Value {
json!({
"type": "string",
"enum": ["0.5K", "1K", "2K"],
"enum": NANOBANANA_2_IMAGE_SIZES,
"default": DEFAULT_IMAGE_SIZE,
"description": "图片尺寸档位。nanobanana2 支持 0.5K、1K、2Kgpt-image-2 仅支持 1K、2K;默认 1K。"
})
}
pub fn gpt_image_2_size_parameter_schema() -> Value {
json!({
"type": "string",
"enum": GPT_IMAGE_2_IMAGE_SIZES,
"default": DEFAULT_IMAGE_SIZE,
"description": "图片尺寸档位。gpt-image-2 仅支持 1K、2K;默认 1K。"
})
}
pub fn image_model_size_constraint_schema() -> Value {
json!({
"if": {
"properties": { "model": { "const": GPT_IMAGE_2_MODEL } },
// model 省略时运行时默认 nanobanana2,仍允许 0.5K。
"required": ["model"]
},
"then": {
"properties": {
"image_size": { "enum": GPT_IMAGE_2_IMAGE_SIZES }
}
}
})
}
fn supported_image_sizes(model: &str) -> Option<&'static [&'static str]> {
match model {
NANOBANANA_2_MODEL => Some(NANOBANANA_2_IMAGE_SIZES),
@@ -157,5 +181,23 @@ mod tests {
json!(["0.5K", "1K", "2K"])
);
assert_eq!(image_size_parameter_schema()["default"], DEFAULT_IMAGE_SIZE);
assert_eq!(
gpt_image_2_size_parameter_schema()["enum"],
json!(["1K", "2K"])
);
assert_eq!(
gpt_image_2_size_parameter_schema()["default"],
DEFAULT_IMAGE_SIZE
);
let model_size_constraint = image_model_size_constraint_schema();
assert_eq!(
model_size_constraint["if"]["properties"]["model"]["const"],
GPT_IMAGE_2_MODEL
);
assert_eq!(model_size_constraint["if"]["required"], json!(["model"]));
assert_eq!(
model_size_constraint["then"]["properties"]["image_size"]["enum"],
json!(["1K", "2K"])
);
}
}
@@ -18,7 +18,9 @@ mod tests {
GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs,
};
use super::generate_character::{GenerateCharacterTool, GenerateCharacterToolArgs};
use super::generate_icon_spritesheet::GenerateIconSpritesheetToolArgs;
use super::generate_icon_spritesheet::{
GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs,
};
use super::generate_image::{GenerateImageError, GenerateImageTool, GenerateImageToolArgs};
use super::generate_sound_effect::{GenerateSoundEffectTool, GenerateSoundEffectToolArgs};
use super::generate_ui_design::{GenerateUiDesignTool, GenerateUiDesignToolArgs};
@@ -188,6 +190,18 @@ mod tests {
#[test]
fn confirmation_schemas_expose_all_finite_choices_as_enums() {
let context = EditorToolContext::default();
let image = GenerateImageTool {
context: context.clone(),
}
.parameters();
let character = GenerateCharacterTool {
context: context.clone(),
}
.parameters();
let icon = GenerateIconSpritesheetTool {
context: context.clone(),
}
.parameters();
let edit = EditImageTool {
context: context.clone(),
}
@@ -218,5 +232,16 @@ mod tests {
assert_eq!(sound["properties"]["duration"]["default"], json!(5));
assert_eq!(video["properties"]["sound"]["default"], json!("on"));
assert!(music["properties"].get("make_instrumental").is_none());
for schema in [&image, &character, &icon] {
assert_eq!(
schema["allOf"][0]["if"]["properties"]["model"]["const"],
json!(GPT_IMAGE_2_MODEL)
);
assert_eq!(
schema["allOf"][0]["then"]["properties"]["image_size"]["enum"],
json!(["1K", "2K"])
);
}
}
}