art agent: enforce json schema
This commit is contained in:
@@ -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、2K;gpt-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"])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user