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 轮并发判据全绿。
109 lines
5.7 KiB
Go
109 lines
5.7 KiB
Go
package seq
|
||
|
||
// 本文件提供 seq_help 的文本:格式说明 + 可照抄的完整示例。
|
||
//
|
||
// 为什么要有它(真机实跑的直接动机):模型写序列时踩了三个坑,各试了
|
||
// 1~3 次才改对 ——
|
||
// ① tools 漏末尾的 ';' → 「末尾缺少 ';'」
|
||
// ② group 的 in 传成字符串 → 重试 3 次
|
||
// ③ as 指向未声明的 out 槽 → 静态校验拦下
|
||
// 这三处都是**格式细节**,塞不进工具描述(有长度限制),却恰恰是模型最容易
|
||
// 错的地方。散落在六个描述里等于没有集中入口。
|
||
//
|
||
// ⚠️ 下面的示例**由判据校验其自身能被 Parse 接受**
|
||
// (plugin_test.go: TestSeqHelpIncludesCopyableExample)——
|
||
// 模型是照抄的,示例自己解析不过就是给模型挖坑。
|
||
|
||
// seqHelpText 返回帮助文本。
|
||
func seqHelpText() string {
|
||
return helpHeader + helpFormat + helpCondition + helpExample
|
||
}
|
||
|
||
const helpHeader = `【工具序列 seq】
|
||
|
||
⚠️ 最容易错的一处(真机实测模型在此连续失败 4 次):
|
||
|
||
group 的 in 必须是**对象**,无入参写 {} —— 不要写成字符串
|
||
|
||
"in": "" 或 "in": "{}"(字符串)一律被拒。「无入参」要表达成空**对象**。
|
||
|
||
常用操作:
|
||
seq_list 列出全部序列及其签名
|
||
seq_create 新建/更新(groups 传参 或 file 加载,二选一)
|
||
seq_run 执行(按 groups 数组顺序逐组跑)
|
||
seq_call 按名调用某个 group 或某条序列
|
||
seq_when_call 条件调用,when 为真才执行
|
||
seq_delete 删除
|
||
|
||
序列 = 若干 group,**组内并行、组间串行**;每个 group 有独立签名
|
||
(in 入参 / out 出参),可被 seq_call 按名调用。
|
||
序列存的是**解析后的 AST**:保存时做完全部静态校验,执行期不再解析文本。
|
||
`
|
||
|
||
const helpFormat = `
|
||
【格式要点 —— 这几处最容易错】
|
||
|
||
1. 顶层是 JSON:{"name":…, "description":…, "groups":[…]},groups 至少一个。
|
||
|
||
2. 每个 group 的字段:
|
||
name 必填,组名,全局唯一(它是签名名)
|
||
in 入参声明,**对象**,如 {"host":"string"};无入参写 {}
|
||
⚠️ 必须是对象 {"k":"type"};写 "" 或 "{}"(字符串)一律被拒
|
||
out 出参声明,**对象**,如 {"summary":"string"};无出参写 {}
|
||
when 条件屏障,默认 "true",只可读 $args.*
|
||
parallel 默认 true;置 false 则组内串行(保序场景用)
|
||
missing 工具不存在时的行为:fail(默认)/ skip / degrade
|
||
timeout 本组墙钟上限,如 "30s"
|
||
on_error abort(默认)/ continue / retry
|
||
tools **字符串**(不是数组!),见下
|
||
|
||
3. ⚠️ tools 是**字符串**,内部是若干以 ';' 分隔的 JSON 对象:
|
||
每个对象形如 {"tool":"cmd_run","args":{…},"as":"槽名"}
|
||
- 每个 tool 后**必须**跟 ';',**包括最后一个**。漏了报「末尾缺少 ';'」。
|
||
- 相邻两个 tool 之间也要有 ';'。漏了会让下一个工具被**静默吞掉**。
|
||
- 键必须带引号(是合法 JSON):{"tool":…} 而不是 {tool:…}。
|
||
- args 里可用 $args.<键> 引用入参;整值引用保留类型。
|
||
|
||
4. ⚠️ as 写的槽名**必须已在 out 里声明**,否则保存时报错。
|
||
非 array 的槽被同名 as 写多次也报错(组内并行会数据竞争);
|
||
需要累加就把该槽声明成 "array"。
|
||
|
||
5. groups 与 file **二选一**:短序列用 groups 直接传;长序列写文件后用 file
|
||
传路径(长参数会被 max_tokens 截断,写文件更稳)。
|
||
`
|
||
|
||
const helpCondition = `
|
||
【条件 when】
|
||
|
||
只可读本组的 $args.*(即 in 里声明过的入参),不能读别组的出参。
|
||
求值失败会**报错**(不会静默当成假),因为静默跳过会让序列少做一步而你以为跑完了。
|
||
"$args.flag == true" 布尔比较
|
||
"$args.n > 3" 数值比较
|
||
"$args.s contains \"err\"" 文本包含
|
||
"$args.host != \"\"" 非空判断
|
||
"true" / "false" 恒真/恒假
|
||
`
|
||
|
||
const helpExample = `
|
||
【可照抄的完整示例】
|
||
|
||
下面这一行是**完整合法**的序列(单行紧凑,直接照抄即可):
|
||
|
||
{"name": "巡检三节点", "description": "并行拉取三台节点状态,异常时展开", "groups": [{"name": "拉取单台", "description": "拉取一台节点的 uptime 与负载", "in": {"host": "string"}, "out": {"summary": "string", "load": "string"}, "tools": "{\"tool\":\"cmd_run\",\"args\":{\"command\":\"uptime\"},\"as\":\"summary\"} ; {\"tool\":\"cmd_run\",\"args\":{\"command\":\"date\"},\"as\":\"load\"} ;"}, {"name": "异常展开", "in": {"host": "string"}, "out": {"detail": "string"}, "when": "$args.host != \"\"", "tools": "{\"tool\":\"cmd_run\",\"args\":{\"command\":\"journalctl -x\"},\"as\":\"detail\"} ;"}]}
|
||
|
||
拆开看(仅为阅读方便,实际照抄上面那一行):
|
||
|
||
name / description 序列名与说明
|
||
groups[0] 拉取单台 组内两个工具**并行**(都声明了才并发,否则整批串行)
|
||
in {"host":"string"} ← 对象,不是字符串
|
||
out {"summary":…,"load":…} ← as 要写的槽必须在这里声明
|
||
tools 字符串里两个 {...} 之间、以及**最后一个之后**,都有 ';'
|
||
groups[1] 异常展开 when 条件:只读本组 in 声明过的 $args.*
|
||
|
||
对应调用:
|
||
seq_create(name="巡检三节点", groups=[上面那个数组])
|
||
seq_run(name="巡检三节点", args={"host":"node-a"})
|
||
|
||
⚠️ 这段示例由判据校验其**自身能被解析器接受**(model 照抄不会踩坑)。
|
||
`
|