mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 12:53:35 +00:00
问题(核实过):工具结果进 f.Msgs 时**没有任何长度上限**(task.go 直接 `Content: result`),内核也**不预检**是否超长 —— 超限由上游 API 报错。 时间线那侧有预算(ContextTokens = 0.8×窗口,进消息前就裁过),但那只管 a.context 的历史事件,**不管单条工具结果** ⇒ 一条巨大结果可能直接冲破 预算而内核不会提前发现。 为什么**不裁剪**(与方案 A 的取舍): · 截断会让模型拿到**残缺**信息,而截断位置由内核武断决定; · 模型无法得知"这里被截断了",会基于残缺数据下结论 —— 与本仓反复 吃亏的「静默降级」同族(`20s` 少引号 → 静默降级 → cmd_run 失败率 34%); · 处置权应交给调度器/上层(告警、拒绝、或让模型自己换更窄的查询), 而不是内核单方面替模型决定。 改动: · core/toolresult_budget.go: checkToolResultSize 只**计数+报告**; 阈值默认 = ContextTokens/8(一条吃掉全部预算会把其它上下文全挤掉); 报告经 toolResultReporter(可替换),默认 logReporter —— **不给模型发 消息**:那是在已花掉的 token 之上再加一条 system,且对当前这轮决策无帮助。 · 接入点在 stepToolAfter 的 toolMsg 落定**之后**(那里才是模型最终看到的 内容;stepToolExec 拿到的尚未经 after_toolcall 改写)。 · TaskFrame 记 oversizeTools / oversizeToolNames,供调度器与状态面查询 "是否有工具在稳定产出超大结果"。 ★ 顺带治掉 seq 侧一处**我自己留下的静默截断**: handlers.go 里我当初随手写了 truncate(…, 160),把变量槽静默截到 160 字 且**无任何标注** —— 正是我批评过的静默降级。 改为 renderSlot:≤160 给全;超过则显式标注「已截断:共 N 字,此处显示前 160 字」并给出改法。**槽里存的始终是完整值**,截断只影响回填文本长度。 端到端判据 TestSeqRunDoesNotSilentlyTruncateSlot 抓到了这个缺陷 ("变量槽被截到 160/5000 字却没有任何标注")。 判据(toolresult_budget_test.go,4 条): · 400KB 结果触发超限报告(含工具名与 token 数) · ★ **默认不裁剪**:200KB 结果原样进 tool 消息(方案 B 的核心不变式) · 小结果不误报(噪音会淹没有效信号) · 报告文案可执行:带工具名、token 数、改法建议 变异验证:去掉统计调用 ⇒ 两条判据 FAIL("统计没生效" + "被裁剪了")。 另:检查项报 stepToolBatch 的 goroutine 竞态,-race 实测**误报**—— 循环变量显式传参(非闭包捕获)、且按索引写各自槽位(非共享 map), `-race` 下 20 轮并发判据全绿。
157 lines
5.9 KiB
Go
157 lines
5.9 KiB
Go
package seq
|
||
|
||
import "strings"
|
||
|
||
// seqToolDefs 返回本插件导出的六个工具定义。
|
||
//
|
||
// ⚠️ 全部**不声明** ParallelSafe:seq_run / seq_call 会执行**一串**工具,
|
||
// 其中可能含写操作。标成并发安全会让内核把两条 seq_run 并发跑起来,
|
||
// 两个序列的执行顺序交错、变量表互相污染。
|
||
//
|
||
// 为独立真相源:注册、判据、文档都从这里取,避免三处各写一份。
|
||
func seqToolDefs() []toolDefInfo {
|
||
return []toolDefInfo{
|
||
{
|
||
Name: "seq_create",
|
||
Description: "创建/更新一条工具序列。**groups 与 file 二选一**:传 groups 直接给结构," +
|
||
"或用 file 加载你已写好的序列文件(适合长序列——长参数会被 max_tokens 截断,写文件更稳)。\n" +
|
||
"序列由若干 group 组成:**组内并行、组间串行**;group 拥有独立签名(in 入参 / out 出参)," +
|
||
"可被 seq_call 按名调用。\n" +
|
||
"保存时会做静态校验:as 必须已在 out 声明、$args.x 必须已在 in 声明、组名不重复、" +
|
||
"组内 ; 分隔符必须完整。任一项不满足都会报错并说明原因。\n" +
|
||
"格式:JSON;每个 group 的 tools 是一个字符串,内含若干以 ' ; ' 分隔的 JSON 对象," +
|
||
"形如 {\"tool\":\"cmd_run\",\"args\":{...},\"as\":\"槽名\"}。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"name": map[string]interface{}{
|
||
"type": "string", "description": "序列名(唯一)",
|
||
},
|
||
"description": map[string]interface{}{
|
||
"type": "string", "description": "一句话说明这条序列做什么",
|
||
},
|
||
"groups": map[string]interface{}{
|
||
"type": "array",
|
||
"description": "组数组,按数组顺序执行;与 file 二选一。" +
|
||
"⚠️ 每个 group 的 in 必须是**对象**(无入参写 {}),写成空字符串会被拒绝;" +
|
||
"tools 是字符串(内容为 ';' 分隔的 JSON 对象,每个 tool 后都要有 ';',含最后一个)。" +
|
||
"格式细节先用 seq_help 查。",
|
||
"items": map[string]interface{}{"type": "object"},
|
||
},
|
||
"file": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "序列文件路径(与 groups 二选一)。目录内文件优先用 files 工具查看",
|
||
},
|
||
},
|
||
"required": []string{"name"},
|
||
},
|
||
},
|
||
{
|
||
Name: "seq_help",
|
||
Description: "查看工具序列的完整格式说明与可照抄的示例。**写序列前先查这个** —— " +
|
||
"tools 字段的分隔符、in/out 的写法、as 与 out 的关系都在这里。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{},
|
||
},
|
||
},
|
||
{
|
||
Name: "seq_list",
|
||
Description: "列出全部可用序列:名称、描述、组数与各组的签名(in/out)。" +
|
||
"按名调用前先用它确认名称与签名。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{},
|
||
},
|
||
},
|
||
{
|
||
Name: "seq_delete",
|
||
Description: "删除一条序列。序列不存在时会报错(不会静默成功)。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"name": map[string]interface{}{
|
||
"type": "string", "description": "要删除的序列名",
|
||
},
|
||
},
|
||
"required": []string{"name"},
|
||
},
|
||
},
|
||
{
|
||
Name: "seq_run",
|
||
Description: "执行一条序列:按 groups 数组的顺序逐组执行,组内并行、组间串行。" +
|
||
"每组先求值 when 条件,为真才执行。返回逐组摘要与最终变量槽快照。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"name": map[string]interface{}{
|
||
"type": "string", "description": "要执行的序列名",
|
||
},
|
||
"args": map[string]interface{}{
|
||
"type": "object",
|
||
"description": "传给各组的入参(组内用 $args.<键> 读取)",
|
||
},
|
||
},
|
||
"required": []string{"name"},
|
||
},
|
||
},
|
||
{
|
||
Name: "seq_call",
|
||
Description: "按名调用一条序列里的 group:target 写组名(限本序列内)或 " +
|
||
"#序列名(跨序列)。返回该组的 out 槽。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"name": map[string]interface{}{
|
||
"type": "string", "description": "所属序列名(组名调用时必填)",
|
||
},
|
||
"target": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "组名,或 #序列名 表示跨序列调用",
|
||
},
|
||
"args": map[string]interface{}{
|
||
"type": "object",
|
||
"description": "传给该组的入参",
|
||
},
|
||
},
|
||
"required": []string{"target"},
|
||
},
|
||
},
|
||
{
|
||
Name: "seq_when_call",
|
||
Description: "条件按名调用:when 表达式为真才执行,否则整次调用跳过(不产出任何槽)。" +
|
||
"when 只可读传入的 args(如 $args.flag == true)。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"name": map[string]interface{}{
|
||
"type": "string", "description": "所属序列名(组名调用时必填)",
|
||
},
|
||
"target": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "组名,或 #序列名 表示跨序列调用",
|
||
},
|
||
"args": map[string]interface{}{
|
||
"type": "object",
|
||
"description": "传给该组的入参",
|
||
},
|
||
"when": map[string]interface{}{
|
||
"type": "string",
|
||
"description": "条件表达式,如 $args.flag == true",
|
||
},
|
||
},
|
||
"required": []string{"target", "when"},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
// 内部小工具:把 args 里的字符串字段取出来并 trim。
|
||
func argString(args map[string]interface{}, key string) string {
|
||
if args == nil {
|
||
return ""
|
||
}
|
||
s, _ := args[key].(string)
|
||
return strings.TrimSpace(s)
|
||
}
|