Files
HomeAgent/internal/plugins/seq/help.go
JianFeeeee 8ca28eb071 feat(toolcall): 工具结果只统计不裁剪(方案 B),并治掉 seq 侧的静默截断
问题(核实过):工具结果进 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 轮并发判据全绿。
2026-09-27 14:05:38 +08:00

109 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 照抄不会踩坑)。
`