练习 18:为什么不让 agent 自动写 skill
真有产品这么干过。Hermes Agent 会在四种情况下自动把一次任务总结成 skill 存起来:完成一次复杂任务、走出一次死胡同、发现一套非平凡的 工作流——以及第四条,用户纠正了它的做法。正常干活,这四条哪条不是 天天发生?官方文档自己也承认这样会攒出"一堆污染目录、浪费 token 的 近似重复技能",于是加了后台清理——但清理只认一个维度:用没用过, 合并重叠的那道开关、写入前要不要经人批准那道开关,默认都是关的。 生成默认开着,两道真正管用的闸门默认关着——这不是猜的,是文档和代码 默认值逐字对过的结果。
模型手上早就有 write_file,能写到工作目录下任何路径——包括
.harness-skills/ 自己。这一章不是要争论"该不该让 agent 自动写
skill",而是把 Hermes 默认关掉的那道闸门,换成默认开着、绕不过去的:
写在哪儿,和谁点头让它生效,不能是同一步。
敲进去
在练习 17 的代码上继续写。新增一条规矩(软的)和一道闸门(硬的)——
这正是 Part 2 已经讲过的套路:basePrompt 教它怎么做,权限层拦住它
不听的那次。这一章把这套二层结构原样搬到 skill 身上。
// skillsProposedRoot 是自动写 skill 的落地位置,刻意不是 skillsRoot。
// discoverSkills 只扫 skillsRoot,这个目录里的东西不会进清单、不会占
// 任何一轮的 token,直到人用 bash mv 把它挪进 skillsRoot 才生效——
// "写"和"生效"从代码层面就是两个不同的目录,不是靠模型自觉。
const skillsProposedRoot = ".harness-skills-proposed"
// skillAuthoringGuidance 把 Hermes 的教训换成规矩:生成不难,回收才是
// 问题。这段话不因任何条件变化——即使这个项目现在一个 skill 都没有,
// 模型也要知道"写草稿"和"生效"是两个目录、两件事。
const skillAuthoringGuidance = `# 想沉淀新 skill 时
如果你判断一类任务以后会反复出现,值得写成一份新 skill 供下次复用——
可以写,但不要直接写进 "` + skillsRoot + `/<name>/SKILL.md":那个目录
里的每一份 SKILL.md,只要存在,description 就会被打进清单,从下一轮起
每一轮对话都要为它多付一点 token,不管这一轮用不用得上。
草稿写到 "` + skillsProposedRoot + `/<name>/SKILL.md",格式跟正式 skill
完全一样。这个目录不会被扫描、不会出现在清单里,写多少份草稿都不花一分
钱。写完之后告诉用户你觉得这份草稿值得转正,一句话说清楚它是什么、什么
时候该用——要不要挪进 "` + skillsRoot + `/" 生效,由用户决定,不是你。`
软的一半到这里。硬的一半拦在注册表里——练习 9 拦 bash 时已经写过一次 "读不到回答就按拒绝处理",这次原样复用,只是换个说法:
// confirm 就是练习 9 的 askApproval,改了个更通用的名字:这一次要拦的
// 不只是 bash 命令。
func confirm(prompt string) bool {
fmt.Fprintf(os.Stderr, "\n⚠️ %s\n允许吗?(y/N) ", prompt)
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return false
}
answer := strings.ToLower(strings.TrimSpace(line))
return answer == "y" || answer == "yes"
}
func askApproval(cmd string) bool {
return confirm("模型想执行: " + cmd)
}
if name == "write_file" || name == "edit_file" {
path := pathOf(args)
if path != "" && fileExists(path) && !r.hasRead[path] {
return "错误: " + path + " 已存在但这个会话里还没读过它。先用 read_file 看一眼,再来修改。"
}
if strings.HasPrefix(path, skillsRoot+"/") {
// 生效目录,见 skillAuthoringGuidance 那段规矩:写进这里的
// 东西下一轮就会算进清单的 token 账,这不是模型一个人能拍板
// 的事——跟练习 9 的 bash ask 档同一个道理,同一个函数。
if !confirm("模型想把一份 skill 写进生效目录:" + path) {
return "错误: 权限拒绝——写入生效的 skill 目录需要用户批准,这次没有批准。"
}
}
}
把 skillAuthoringGuidance 接进 composeSystemPrompt,跟清单、记忆
拼在一起,不管这个项目现在有没有 skill,这段规矩都在:
if manifest := skillManifest(skills); manifest != "" {
prompt += "\n\n---\n\n" + manifest
}
prompt += "\n\n---\n\n" + skillAuthoringGuidance
prompt += "\n\n---\n\n" + memoryGuidance
跑起来
go build -o ex18 .
在一个没有任何 .harness-skills/ 的干净目录里,给一个具体任务,同时
邀请它去沉淀一份 skill:
./ex18 "帮我把'新增了会话改名字功能'这句话记到 CHANGELOG.md 的 Unreleased 小节下面(现在时,别用'新增了'这类前缀)。如果你觉得这类'记录变更'的任务以后还会常做,按你系统提示里的规矩,去写一份新 skill 存起来。"
在自己的终端里跑,.harness-skills-proposed/ 第一次用需要先建目录,
模型多半会用 bash mkdir 去建,那条命令不在练习 9 的 allow 名单里,
会弹出真实的 y/N 让你确认——正常回答就是。下面三组转写为了看清"没人
应答时会怎样",特意在标准输入为空的非交互环境下跑过一遍。
你应该看到什么
实验一:草稿该写在哪儿。 DeepSeek 先把任务本身做对——read_file
确认没有 CHANGELOG.md 之后直接创建、写入,然后决定沉淀 skill,从头
到尾只往 .harness-skills-proposed/ 伸手:
[round 5] write_file({..., "path": ".harness-skills-proposed/changelog-entry/SKILL.md"})
[round 6] bash({"command": "mkdir -p .harness-skills-proposed/changelog-entry && echo ok"})
⚠️ 模型想执行: mkdir -p .harness-skills-proposed/changelog-entry && echo ok
允许吗?(y/N) [错误: 权限拒绝——用户没有批准这条命令。]
[round 7] write_file({..., "path": ".harness-skills-proposed/changelog-entry/SKILL.md"})
skill 草稿我写好了,但**没存成**:`.harness-skills-proposed/changelog-entry/`
目录不存在,write_file 不会自动建目录,而我用 mkdir 建目录的那条命令被
你拒绝了。草稿内容如下,你认可的话我可以建目录落盘:……
本机 Ollama,同一个任务:CHANGELOG.md 写对了(虽然顺手多加了一段没人
要求的 ## 1.0.0 初始版本),然后是这段回复:
我建议将这份 skill 草稿写入 .harness-skills-proposed/record-changelog/SKILL.md,内容如下:
(一整段 markdown……)
我是否应该将这份草稿转正?(即是否将它移动到 .harness-skills/record-changelog/SKILL.md)
finish_reason=stop,全程没有一次 write_file 调用。翻遍整个工作
目录,.harness-skills-proposed/ 这个目录压根不存在——它问"要不要
转正"的那份草稿,从来没有存在过。
实验二:想绕过闸门时会怎样。 批准 mkdir 后重跑 DeepSeek,草稿
正确落进 .harness-skills-proposed/changelog-entry/SKILL.md。再要求
"转正",这次只回答一次 y(模拟人类批准一步之后就不再理会):模型
先后试了链式 bash(mkdir && mv && rmdir)、单独一条 mkdir、最后
直接对生效路径 write_file——三种手法,全部被拦下:
转正没成功——写入正式目录 .harness-skills/ 需要你的批准,但这次系统
返回了权限拒绝。草稿没有动,还留在 .harness-skills-proposed/changelog-entry/SKILL.md。
- ✅ CHANGELOG.md:已建好
- ✅ 草稿 skill:已存在
- ❌ 转正:被拒绝,未生效
前两次撞上的是练习 9 早就存在的默认档位——不在 allow 名单里的命令
一律先问;只有第三次直接 write_file 才撞上这一章新加的那道闸门。
一次都没有漏网,模型也如实汇报了"哪些做成了、哪些没做成"。
实验三:批准之后。 把每个确认都答 y 重跑一次:转正成功,
.harness-skills-proposed/ 清空,.harness-skills/changelog-entry/ SKILL.md 生效,模型接着自己往 MEMORY.md 记了一笔——这一步没有出现
在任何提示词里:
- 有一份已生效的 skill:.harness-skills/changelog-entry/SKILL.md,
覆盖"往 CHANGELOG 追加记录"这类任务;该类任务应直接按该 skill 执行,
无需再问用户。
开一个全新会话,代价确实开始算了:
[skill 清单:1 个 skill,约 184 tokens,随 system prompt 每轮都算钱]
发生了什么
Hermes 的四个触发条件里,藏着这个问题的根:判据太宽,谁都会命中。
"用户纠正了它的做法"这一条尤其致命——被纠正是每次协作里最正常不过
的一环,如果这也算"该沉淀"的信号,那几乎每一次多轮对话都会生出一份
新 skill。清单越攒越大,skill_manifest 里可选的候选越多,练习 17
已经量过这笔账:清单每多一条,往后每一轮都要多付一点 token;而选择
本身也会变差——从五个里选和从一百个里选,命中的概率完全不是一回事。
Hermes 自己的清理机制只解决了"占地方",没解决"选不准":归档看的是
"用没用过",跟"这几份是不是说的同一件事"完全无关;真正管这件事的
"合并重叠"开关,文档写得明明白白——默认关闭。生成默认开着,两道
真正管用的闸门默认关着——默认值,就是产品对这件事的真实立场。
这一章的闸门,补的正是"写入审批"这道默认关掉的开关。 .harness- skills-proposed/ 解决"生成太容易":写草稿不用经过任何人,但也不会
进清单、不会占一分钱;skillsRoot 那道 confirm 解决"生效太容易":
不管模型用 bash mv、bash cp 还是直接 write_file,只要终点是
.harness-skills/,都要有人点头。两道加起来,Hermes 默认关闭的那道
"写入前审批",在这一章的设计里默认就是开着的,而且拦得住——DeepSeek
换了三种手法都没能绕过去。
"读到规矩"和"照着做"之间,这次多出一种新的失败方式。 练习 14 见过
"读到了但没听";练习 15 见过"做了但没做对、还谎报成功"。这次 Ollama
是第三种:整段草稿写成了自然语言回复里的一段 markdown,读起来完全像
"已经处理",但从头到尾没有调用一次 write_file——不是不听、不是做错,
是把"描述打算做的事"当成了"做了这件事"。三次失败方式都不一样,但都
指向同一件事:陈述、承诺、描述,都替代不了一次真正发生的工具调用。
常见问题
- 这一章批评的产品是自己编的例子吗:不是,Hermes Agent 官方文档
(skills / curator 两个功能页)和对应版本的源码都能查到——四个触发
条件、
write_approval: false(默认自由写)、consolidate: false(合并重叠默认关闭)、30 天标记陈旧 / 90 天归档但从不真删,都是逐字 对照过文档和代码默认值得到的结果,不是转述印象。 - 为什么不干脆信任练习 9 已有的 bash 权限,省得多写一道针对
skillsRoot的检查:实验二里已经出现过反例——模型换了三种手法, 其中两种(链式 bash、单独mkdir)撞上的是 bash 层原本就有的默认 ask,只有第三种(直接write_file)才撞上新加的这道;如果这道门 不存在,直接write_file那条路径就是敞开的。bash 层拦的是"认得出 的命令模式",skillsRoot这道拦的是"不管用什么工具,终点在哪"。 - 为什么要有
.harness-skills-proposed/这一层,而不是让模型直接 往生效目录写、每次都靠confirm拦一下就好:批准应该留给真正要 紧的那几次,不是每次草稿迭代都打断人。先在没有成本的地方把草稿写 完、写好,用户要审的时候面对的是一份具体、能读的东西。 - 模型转正成功之后主动往 MEMORY.md 里记了一笔,这是设计好的吗:
不是,这一步没有出现在
skillAuthoringGuidance或任何提示里,是 模型自己判断"这是个值得记的项目约定"——练习 15 的memoryGuidance本来就在系统提示里,两层规矩独立生效,这次撞在一起是真实发生的。
加分练习
- Ollama 那次"描述了草稿但没有真的写文件",写一个小检查:会话声称
写了某个 skill 草稿之后,自动
read_file一下那个路径,确认文件 真的存在——把这一章撞见的失败模式,变成一个能自动发现同类问题的 检查,而不是只能靠人肉翻目录才发现。 - 把这一章新加的闸门从"只看
write_file/edit_file的目标路径" 扩展到"任何 bash 命令里出现skillsRoot这个路径都要经过同一个confirm"——用一个简单的字符串包含判断就够。这是在补上"常见问题" 第二条提到的那道缝:不靠 bash 层的默认 ask 侥幸兜底,从代码上把 "终点在生效目录"这件事堵死,不管用哪个工具达到。 - 给
.harness-skills-proposed/加一个查看命令(比如./ex18 -list-proposed),列出所有还没被转正、也没被拒绝、就那么放着的 草稿——草稿目录本身如果只进不出,也会变成 Hermes 那种"只生成不 回收"的地方,只是换了个位置。 - 参照 Hermes 缺的那道"合并重叠"开关,给这一章加一个最小版本:起
两份内容重叠的草稿(比如"记录变更"和"更新 CHANGELOG",说的是同一
件事),看模型会不会在写第二份之前,先去
.harness-skills-proposed/里看一眼有没有已经写过的类似草稿——回收不只是"能删",还包括"写之前 先看看是不是已经有了"。