perf(agent): 工具 schema 瘦身 - 去掉重复 oneOf 并压缩冗余描述 (#569)

Original-Commit: 042d06f02215c29771360e73bc17c99d132ddfda
perf(agent): 工具 schema 瘦身 - 去掉重复 oneOf 并压缩冗余描述

工具表每一步都要发、还在前缀缓存最前面,属于固定开销:

- canvas_apply_ops 的 ops.items 同时给顶层 type 枚举和一份 oneOf,同一条约束下发两遍;
  工具描述与服务端按 action 的强校验已经表达了它,删掉 oneOf。
- generate_media 的 snapshotHash / sourceNodeId / referenceNodeIds 描述压成短句。
- remember_lesson 的 steps[].tool / steps[].note 描述压成短句。

契约不变(字段、required、enum、服务端校验都未动)。实测:20 个工具的平台表
22885 → 22438 字节(−447)。新增体积守卫用例(断言无重复 oneOf + 总体积 ≤24000)。
This commit is contained in:
AetherNo2332
2026-09-21 07:57:00 +08:00
committed by ddCat
parent 73ef84ffc8
commit 13707c06b9
2 changed files with 57 additions and 8 deletions

View File

@@ -0,0 +1,54 @@
package app
import (
"encoding/json"
"strings"
"testing"
)
// 工具 schema 是每一步都要发出去(并且是前缀缓存的第一段)的固定开销,因此值得钉住体积:
// 平台工具全集按 auto + canvas + 技能构造(覆盖条件暴露的工具)。
func platformToolSchema(t *testing.T) ([]map[string]any, []byte) {
t.Helper()
req := CloudAgentRequest{PermissionMode: "auto", ContextScope: []string{"canvas"}, SkillIDs: []string{"capability-list"}}
req.Budget.MaxGenerationTasks = 1
tools := cloudAgentTools(req)
raw, err := json.Marshal(tools)
if err != nil {
t.Fatal(err)
}
return tools, raw
}
func TestCloudAgentToolSchemaStaysCompact(t *testing.T) {
tools, raw := platformToolSchema(t)
t.Logf("platform tool schema: %d tools, %d bytes", len(tools), len(raw))
// 顶层 type 枚举 + 服务端按 action 强校验已经表达了"每种操作要哪些字段",
// 再挂一份 oneOf 只是重复:同一个约束会被下发两遍。
for _, tool := range tools {
function, _ := tool["function"].(map[string]any)
if function["name"] != "canvas_apply_ops" {
continue
}
parameters, _ := function["parameters"].(map[string]any)
ops, _ := parameters["properties"].(map[string]any)
items, _ := ops["ops"].(map[string]any)
opItem, _ := items["items"].(map[string]any)
if _, exists := opItem["oneOf"]; exists {
t.Fatal("canvas_apply_ops 的 ops.items 不该再带重复的 oneOf")
}
if props, ok := opItem["properties"].(map[string]any); !ok || len(props) == 0 {
t.Fatal("canvas_apply_ops 的 ops.items 必须保留 properties")
}
}
// 体积预算:当前 20 个工具约 22.4 KB,这里留 ~7% 余量;新增工具或字段时请重新测量并
// 有意识地调整这个数字,而不是让 schema 悄悄膨胀(它每一步都要发、还在前缀最前面)。
if len(raw) > 24000 {
t.Fatalf("平台工具 schema 体积 %d 字节超出预算 24000:请压缩描述或显式调整预算", len(raw))
}
if strings.Count(string(raw), "oneOf") > 2 {
t.Fatalf("工具 schema 里出现了过多 oneOf:%s", string(raw[:200]))
}
}

View File

@@ -191,9 +191,9 @@ func compileCloudAgentTools(req CloudAgentRequest, includeProfileTool bool) []ma
"steps": map[string]any{"type": "array", "maxItems": 12, "items": map[string]any{
"type": "object",
"properties": map[string]any{
"tool": str("这一步用哪个工具(工具名)"),
"tool": str("工具名"),
"action": str("这一步做什么"),
"note": str("可选:坑/前提/注意"),
"note": str("可选:坑或前提"),
},
"required": []string{"tool", "action"}, "additionalProperties": false,
}},
@@ -251,11 +251,6 @@ func compileCloudAgentTools(req CloudAgentRequest, includeProfileTool bool) []ma
"properties": opProperties,
"required": []string{"type", "id"},
"additionalProperties": false,
"oneOf": []map[string]any{
{"properties": map[string]any{"type": map[string]any{"const": "add_node"}}, "required": []string{"nodeType"}},
{"properties": map[string]any{"type": map[string]any{"const": "update_node"}}, "required": []string{"patch"}},
{"properties": map[string]any{"type": map[string]any{"const": "connect_nodes"}}, "required": []string{"fromNodeId", "toNodeId"}},
},
}
add("canvas_apply_ops", "创建空白节点、修改提示词或建立引用连线,不提交生成任务、不产生生成费用;先读取画布并传 snapshotHash。提交媒体生成使用 generate_media。每次最多20项,禁止删除、任意 metadata 和媒体 URL。每项都需要 type 和 id:add_node 还需要 nodeType,update_node 还需要按节点能力清单填写 patch,connect_nodes 还需要 fromNodeId 与 toNodeId。连线是生成输入关系,不会改变已提交任务的输入;来源须 canSource,目标须 canTarget 且接受来源 inputKind,能力以注册表为准。", map[string]any{"snapshotHash": str("canvas_get_state返回的snapshotHash"), "ops": map[string]any{"type": "array", "maxItems": 20, "items": opItem}}, "snapshotHash", "ops")
}
@@ -264,7 +259,7 @@ func compileCloudAgentTools(req CloudAgentRequest, includeProfileTool bool) []ma
"mode": map[string]any{"type": "string", "enum": cloudAgentGenerationModeNames()}, "prompt": str("完整生成提示词;引用素材时在对应描述中使用 @图片1、@视频1、@音频1,各类型按 referenceNodeIds 中出现顺序独立编号,文本来源不占媒体编号。服务端会为遗漏的已选素材补齐引用标签,不推断素材用途"),
"logicalModelId": str("selection.logicalModelId;与channelId/channelModelKey互斥"), "channelId": str("selection.channelId"), "channelModelKey": str("selection.channelModelKey"),
"durationSeconds": map[string]any{"type": "integer", "minimum": 0}, "size": str("模型支持的画幅,例如9:16"), "quality": str("目录支持的分辨率或质量"), "videoGenerateAudio": map[string]any{"type": "boolean", "description": "是否生成音频,仅视频可用"},
"snapshotHash": str("可省略;省略时服务端使用当前画布内容快照。也可传 canvas_get_state 的 mediaSnapshotHash 或上一写操作返回的 snapshotHash"), "nodeId": str("可续用的未提交媒体草稿ID;无草稿时才使用新唯一ID"), "title": str("媒体节点名称"), "sourceNodeId": str("仅文本/镜头提示词节点ID;无文本来源则留空,绝不能填图片/视频/音频ID"), "referenceNodeIds": map[string]any{"type": "array", "maxItems": 16, "items": str("当前画布媒体参考节点ID;参考图只放此处,保持引用顺序")}, "referenceTransientIds": map[string]any{"type": "array", "maxItems": 4, "items": str("由 image_annotation_render 返回的临时参考图ID")},
"snapshotHash": str("可省略:省略时用当前画布内容快照"), "nodeId": str("可续用的未提交媒体草稿ID;无草稿时才使用新唯一ID"), "title": str("媒体节点名称"), "sourceNodeId": str("仅文本/镜头提示词节点ID;不要填媒体节点"), "referenceNodeIds": map[string]any{"type": "array", "maxItems": 16, "items": str("画布媒体参考节点ID,按引用顺序")}, "referenceTransientIds": map[string]any{"type": "array", "maxItems": 4, "items": str("由 image_annotation_render 返回的临时参考图ID")},
}, "mode", "prompt", "nodeId", "title", "referenceNodeIds")
}
return tools