跳到主要内容

SkillOpt: 把 skill 文档当参数来训练, 以及它在规范与可复现性上的边界

Note

如果一份 skill 文档写得不好, 为什么不能像调参一样把它"训"好? SKILL.md 是纯文本, 模型权重可以冻结, 任务分数就是现成的损失函数 —— 这条路听起来几乎无懈可击. 但真正的问题在另一头: 一个只会看任务分数的优化器, 知不知道 skill 该长成什么形状? 它怎么判断"变得更长"是进步还是灾难? 这篇文章要回答的是: 把文档当参数训练会得到什么, 又会系统性失去什么.

0x00 背景

给 agent 加能力, 主流做法是写一份 skill —— 一个目录, 里面有 SKILL.md 和可选的脚本与参考文件. 但写好一份 skill 很难, 于是出现了一类很自然的想法: 既然改提示词有效, 那能不能像训练神经网络一样"训练"这份 markdown?

SkillOpt 是这类想法里工程化程度较高的一个. 它把 skill 文档当作唯一可训练的状态, 冻结模型权重, 用 rollout -> reflect -> aggregate -> select -> update -> gate 的循环, 在某个具体 benchmark 的分数上做带有验证闸门的定向编辑, 并且完整沿用了深度学习的词汇: epoch、batch size、learning rate、梯度裁剪、学习率调度、动量.

这篇文章想回答三个问题: 它到底怎么训练、它的结果可不可复现、以及它是否知道 skill 的编写规范. 第三个问题才是真正的落点, 因为它暴露出一个结构性错配 —— 今天"优化 skill 的工具"和"skill 的规范"是分离的两件事.

0x01 核心结论

  1. SkillOpt 不是 skill 编写工具, 而是一个 skill 文档的训练器. 它把 markdown 当参数, 用 benchmark 分数当损失. 所以它的前置条件不是"你有一个成品 skill", 而是"你有一组能自动打分的任务".

  2. 它完全不知道 Agent Skills 规范的存在. 全库检索 progressive disclosureskill_specskill-authoring 等关键词零命中; 唯一的规范残留是给文档补 frontmatter 的兜底函数, 而它的注释写得很直白 —— 目的是"能被加载", 不是"符合规范". 它可以优化提示词, 但不优化文档形状.

  3. "十万字压到一万字仍然爆炸, 它却认为变好了"是设计使然, 不是 bug. 四个机制叠加: 编辑预算只管条数不管长度; 重写提示词硬性要求"除非明确说要删, 否则保留已有指导"; 验证闸门只看任务分数, 而单轮问答的 exact match 对 system prompt 变长完全不敏感; 全文唯一的长度记录只用于打印日志, 没有任何约束力.

  4. 但"正确解法是拆分而非压缩"这一点, 现在有公开规范背书. 官方 Agent Skills 规范给出三级预算: 元数据约 100 词、SKILL.md 正文少于 500 行、references/``scripts/``assets/基本无上限 —— 因为脚本可以执行而不必读入上下文. 所以真问题是"这些内容该不该在这一级", 而不是"把这一级从 15 万 token 压到 1.5 万".

  5. 可复现性上, 它只做了数据侧, 没做模型侧. 种子固定了批组成、epoch 洗牌、minibatch 切分、断点续训位置, 而且候选分数按内容哈希缓存. 但没有任何后端把采样种子发给服务端, 采样温度默认也不发送. 结果: 同一次运行内的比较是稳的, 跨运行的不是.

  6. "抽奖"的准确说法是: 在一个带噪、无置信区间的闸门上做 best-of-N 采样. 闸门每步用同一个固定的 200 条验证集、严格更大才接受 —— 这些都是反抽奖的设计. 但 200 条上的 exact match 标准差约 3.5%(约 7 道题), 大于单步真实提升(1~3 道题); 再叠加"最好结果永不回退", 就是典型的选择性偏差: 报出来的最高分会系统性偏高, 且不可复现.

  7. 作者自己承认了这个噪声问题. 配置注释里明确写着: 默认的严格闸门会让候选全部被拒、训练停滞, 对策是换用更软的指标. 换指标能缓解"因离散而停滞", 但不降噪.

  8. DSH 已有现成集成, 但只覆盖"睡眠循环"那一半. 仓库里带着一个完整的 Cordis 插件, 一条补丁命令就能装. 但 DSH 不是它支持的会话来源, 所以"让 DSH 从自己的使用历史里学习"这条闭环目前是断的.

  9. 最后回到规范: DSH 有完整的 skill 载体设施, 却不内置任何"教 agent 怎么写 skill"的 skill. 系统级槽位是空的, 而格式违规对模型完全静默 —— 名字不合法、描述缺失, 结果只是这个 skill 从目录里消失, 模型无从得知自己写错了.

  10. 两个工具合起来才完整, 而接口就是"规范"这一层. 训练器会改 skill 但不知道规范, 宿主知道规范但不提供沉淀能力. 补法是把规范变成可执行的校验, 让 agent 在交付前自己先过一遍. 具体的规范内容与编写方法另见配套篇.

0x02 关键细节

一、它到底是什么: 一场把 markdown 当参数的训练

SkillOpt 的定位可以用一句话概括: 冻结目标模型, 把 skill 文档当作唯一的可训练状态. 官方说法是"不动模型权重", 但把这套流程和深度学习逐项对照, 会发现它几乎是刻意的一一映射:

深度学习SkillOpt 对应物
模型权重skill 文档 (markdown)
前向传播rollout: 目标模型执行任务
损失 / 梯度reflect: 优化器模型分析轨迹, 产出编辑补丁
梯度裁剪编辑选择: 学习率 = 每步最大编辑条数
梯度下降步把补丁应用到 skill 文档
验证集在 selection split 上做闸门评估
学习率调度cosine / linear / constant
epoch 与动量多轮训练 + 慢更新 + 跨轮元技能记忆

六个阶段的分工是: rollout 让目标模型跑任务并存下轨迹; reflect 让优化器模型读失败与成功的轨迹, 产出增删改补丁; aggregate 分层合并多个 minibatch 的补丁; select 用模型给补丁排序, 只留前 L 条; update 把补丁应用到文本; gate 在验证集上打分, 只有严格提升才接受.

最终交付物就是一个 best_skill.md —— 一份紧凑的 markdown 文件, 运行时对未改动的目标模型零额外调用. 论文里声称在 6 个 benchmark、7 个目标模型、3 种执行环境上全面占优, 在 GPT-5.5 上把无技能基线提升了 23.5 个百分点(直接对话)、24.8(Codex 智能体循环内)、19.1(Claude Code 内).

二、怎么开始用: 三条入口, 但都不解决"规范"问题

前置条件比想象的硬. 除了 Python 3.10+, 你必须至少配置一个模型后端. 优化器和目标模型是两个可独立配置的角色, 支持 Azure OpenAI、任意 OpenAI 兼容端点、Claude 命令行适配器、Qwen、MiniMax, 以及 codex/claude/cursor/copilot 四种命令行执行器.

路径有三条:

  1. 跑通示例 —— 用 SearchQA 这个纯文本问答 benchmark 最快见效, 装上数据依赖、物化数据划分、导出环境变量、训练、评估.
  2. 用自己的任务 —— 这条是研究路径, 也是大多数人会撞墙的地方: SkillOpt 没有"输入 skill 加对话记录, 输出更好的 skill"这种通用接口. 你必须先把任务变成可打分的环境: 写数据加载器、rollout 辅助函数、环境适配器, 再加一份配置. 官方文档给出的最小实现约 200 行.
  3. 睡眠循环 —— 这是唯一贴合"我手上已经有一个 skill"这个场景的入口. 它扫描项目里已有的 skill 目录, 收获会话记录、挖掘重复任务、重放、再把学到的内容以提案形式落盘, 人工确认后才覆盖活文件.

三条路的选择很清晰: 想看看它怎么跑用第一条; 有一套能自动打分的任务且想涨点用第二条; 手上有本地 skill 想离线演化用第三条.

三、它为什么不可能知道规范: 代码里的证据

这是全文最硬的部分. 逐个检查之后, 结论是没有歧义的.

关键词零命中. 全库检索"渐进式披露"、"skill 规范"、"skill 编写"、"skill 格式"等表述, 全部为空. 关于"开放 Agent Skills 标准"全文只有一处实质命中, 而且是在讲某个命令行后端能够加载这种格式, 不是校验、不是编写规范、更不是对优化器的约束.

frontmatter 只有两处, 且目的不是规范. 唯一一处是给目标文档补一个 namedescription 的头. 注释写得很清楚: 为了让本地 agent 能加载它. 目的是"能被加载", 不是"符合规范" —— 这两件事看起来接近, 实际完全不同.

优化器的提示词里没有任何结构约束. 把全部 22 个提示词文件逐个读完, 与 skill 质量相关的约束只有零星的措辞: "简洁"、"优先强化已有章节"、"去重"、"优先合并而不是让文档变长". 没有一句提到章节结构、职责边界、什么内容该外置、token 预算、字数目标、渐进式披露.

唯一的"长度闸门"是编辑条数, 不是文本长度. 学习率设为 4, 意思是每步最多 4 条编辑. 排序选择函数只做两件事: 让模型按重要性排名取前 L 条, 失败就简单截断. 对候选 skill 的总长度没有任何裁剪或惩罚. 于是每步 4 条编辑, 每条可以是任意长度的 markdown —— 4 条编辑足以让文档翻好几倍.

长度只被记录, 不被约束. 训练器里确实记录了 skill 的长度, 但唯一的用途是打印一行前后对比. 它是观测指标, 不是约束条件.

最能说明问题的一句在慢更新的提示词里: "简洁但全面 —— 你没有长度限制, 但每句话都应该值得". 这句话直接和上下文预算对撞.

它真正"管"的是两个受保护区域, 也就是文本里被特殊标记包起来的两段: 一段承载 epoch 级的纵向指导(每轮覆盖), 一段承载执行提醒(累积). 但这两段的语义是"优化器的记忆字段", 跟 Agent Skills 规范毫无关系. 顺带一个结构性观察: 慢更新是覆盖写(不增长), 而主体是追加写(增长)—— 正文在结构上是单调膨胀的.

四、为什么"十万字变一万字"会被判定为"变好"

这不是一个 bug, 而是四个机制叠加的必然结果. 把它摊开看:

  1. 它没有删除的权威. 整篇重写的提示词里有硬性要求: "除非明确的建议说要移除或合并, 否则保留有效的已有指导." 在只有任务分数的信号下, 没有任何东西会"明确说要移除" —— 一条规则只要偶尔有用就不会被判死.

  2. 默认模式只会变长. 默认的更新模式是打补丁, 而分析提示词的首选操作就是追加.

  3. 闸门只看任务分数, 而且这个分数对长度不敏感. 单轮问答用 exact match 评分, skill 被原样塞进 system prompt —— 注入更长的上下文不会让分数下降. 一个不惩罚长度的目标函数, 自然不会优化长度.

  4. 唯一能全局压缩的路径也要靠模型自觉. 全文重写模式会整篇重写, 但约束只有一句"优先合并、保持简洁", 没有长度目标、没有 token 预算、没有结构模式, 而且重写后还要再过同一个闸门.

公平地说, 有一道间接刹车: "只有严格提升才接受"加上"每步最多 4 条编辑", 会让"越写越长但分数不涨"停在原地. 但这只是止损, 不是压缩 —— 它永远无法主动把十万字缩到一万字, 除非这一收缩同时在验证集上涨分. 而按规范看, 十万字和一万字都在预算之外, 所以这个"变好"判断本身就没有意义.

五、幂等只做了一半: 数据侧干净, 模型侧为零

做得很干净的一半. 种子固定了批池、epoch 之间的洗牌顺序、minibatch 的组成、慢更新与元技能的采样, 数据划分也固定. 断点续训会记下已完成步数、当前分数、最好分数和最好步数. 还有一条很关键的: 候选分数按内容哈希缓存 —— 它把"分数是 skill 文本的纯函数"这个假设直接写进了缓存键, 同一次运行内同一份文本只测一次, 后续比较复用同一个数.

在确定性世界里这是正确的优化, 在非确定性世界里它是一个强力稳定器(防止"拿这次测的数和上次测的数比"), 但也意味着闸门分数在一次运行内被冻结在首次测量值上 —— 想靠重复测量降噪, 得先改掉这个缓存键.

完全没做的一半. 关键的一点: 在模型后端目录里检索采样种子, 零命中. 没有任何后端把种子参数发给服务端. 采样温度在 Azure 路径上根本不出现, 在通用兼容后端上也只在显式设置时才发送, 默认是空 —— 也就是用服务端默认值(通常是最高的随机性). 重试机制是失败重试, 直接重新采样, 没有 seeded retry, 也没有"取第 k 个候选"的机制.

所以那个"训练种子"参数只固定了数据侧, 不固定模型侧. 代价可以量化: 用 SearchQA 的默认配置算, 每轮 10 步、共 40 步; 每步包含 40 次训练 rollout、200 次闸门 rollout(整个验证集)、约 10 次优化器调用, 再加若干次合并和排序 —— 每步约 250 次无种子模型调用, 整个运行是万数量级的独立采样. 每一步里任意一次采样改变一个字符, 就会改变候选文本、改变哈希、改变闸门结果、改变后续所有轨迹. 这是一棵逐字符分叉的树.

可以这样总结可复现性的边界:

对象是否可复现
数据划分 / 批组成 / minibatch 顺序 / 续训位置
同一次运行内, 同一候选的闸门分数是(被缓存冻结)
跨运行同一个 skill 的闸门分数
40 步后的最优 skill 文件否, 实质不可复现

六、"是不是抽奖": 精确的判定

抽奖与否的边界不在"有没有随机性", 而在接受决策是不是用同一把尺子测出来的. SkillOpt 其实做了三件反抽奖的事: 闸门每步用同一个固定验证集比较(不是每步换题); 用内容哈希缓存避免"这次的数和上次的数"混比; 严格更大才接受(平手即拒).

它抽奖的地方在于: 尺子本身有噪声, 而它把噪声当信号.

具体算一下: 200 条验证集上 exact match 的标准差约 sqrt(0.5 x 0.5 / 200) ≈ 3.5%, 也就是约 7 道题. 而一步补丁的真实提升通常是 1 到 3 道题的量级. 于是步间涨落基本落在噪声带内, 而闸门没有置信区间、没有重复测量, 严格大于就接受.

两个直接后果:

  • 接受与拒绝的序列在很大程度上是噪声驱动的;
  • 最好结果永不回退 = 从 N 次带噪测量里取最大值 → 典型的赢家诅咒. 汇报出来的最好分数会系统性高于真实水平, 且不可复现.

作者对这个问题是知情的. 配置注释原文承认: 在小的 selection split 加连续奖励的场景下, 候选 skill 经常改善了逐项的软分数(例如某项从 0.06 到 0.26)但永远无法翻转离散的 hard 结果, 默认的严格闸门于是拒绝每一个候选、训练停滞. 给出的对策是切换到软/混合指标. 但换一把更敏感的尺子能降低"因离散而停滞", 不降噪 —— 噪声被更灵敏地转换成决策, 决策的方差并不会因此变小.

所以准确的说法是: 在一个带噪、无置信区间的闸门上做 best-of-N 采样. 训练过程是可持续的(有跨轮记忆、慢更新、被拒编辑缓冲这些真正的机制), 但结果不可复现.

七、DSH 集成: 已经做好了一半

DSH 是"一切皆插件"的 agent 框架, 而 SkillOpt 仓库里已经带了完整的 DSH 集成 —— 一个 Cordis 插件入口(注册 7 个原生工具)、一个 bundle 补丁层、一份 agent skill、一个自检脚本和包清单. 安装就是把它加进 profile 的 bundles, 或者用一条 --patch 命令挂上.

但有两个必须知道的限制:

第一, 它借宿主的循环, 自己不带循环. 它的后端选项全部是调起一个已安装、已认证的外部命令行工具; 它自己没有工具调用循环、没有工作区管理、没有文件读写能力. 而且它读取的是宿主已有的 skill 文件, 而不是要求你先写 benchmark —— 这是它和训练引擎最大的区别.

第二, DSH 不是它支持的会话来源. 来源参数是写死的枚举, 没有 dsh 这一项. 所以在 DSH 里可以用它编辑 skill, 但收割不到 DSH 自己的会话记录 —— 而"收割"正是整个循环的起点. 换句话说, "让 DSH 从自己的使用历史里学习"这条闭环当前是断的.

还有一个容易被忽略、但对判断价值很关键的细节: 它的重放并不是真的重跑任务. 官方文档明确写着当前实现没有 fresh-worktree 重放, 而且"目标 skill 是作为提示词文本注入, 而不是作为原生 skill 被调用". 也就是说, 闸门测的是"模型读了这段文字能不能答对", 而不是"agent 真的去干活能不能干对". 对能带工具的命令行后端尚可, 对裸 API 后端就退化成纯文本问答.

对比之下, 训练引擎那条路反而更难: 它的抽象是围着"命令行可执行文件加结构化事件流"设计的, 四个执行器适配器里以 Claude Code 覆盖最厚(约两千行, 同时实现 SDK 与命令行两条路径). 接入一个新的 agent 框架属于新增一类适配器, 不是填个配置. 而 DSH 是个 Node 插件框架, 不满足"PATH 上有一个可执行文件"这个形状.

八、规则的另一半: 公开的 skill 规范给出了可判定的标准

既然训练器不知道规范, 那"什么才算合规"就必须由外部提供. 好消息是这件事现在有明确答案: Agent Skills 已经作为开放标准发布, 有字段级的正式规范和量化预算 —— 元数据约 100 词、正文少于 500 行 / 5000 tokenreferences/``scripts/``assets/基本无上限(因为脚本可以执行而不必读入上下文).

这就直接判定了前面那个问题: 十万字(约 15 万 token)超出第二级预算 30 倍. 而且它揭示了更深的一层 —— 正确解法不是压缩, 是拆分. 按"是否总是需要"把内容分到第二级和第三级, 而不是把第二级从 15 万 token 压到 1.5 万. 所以"压到一万字仍然爆炸却认为变好"是双重错误: 既没有拿预算当验收标准, 也把拆分问题当成了压缩问题.

还有一条和本文主题直接相关的对照. 规范对"怎么证明 skill 有用"给了明确要求: 让被测 agent 不知道自己在被测, 传原始产物而不是结论, 并且规定 —— 如果只有在被测 agent 看到泄漏的上下文时才能成功, 那就不能相信这个结果.

这个要求恰恰是训练器缺的那一层. 对照前面看到的 —— 200 条验证集上约 3.5% 的噪声、没有置信区间、best-of-N 取最大 —— 规范要的是"可证伪", 而它做的是"取最大值".

规范的完整内容、触发机制的原理、内容分层的判据、以及一份可执行的校验清单, 见 Agent Skill 编写最佳实践.

九、两半拼起来: 今天缺的到底是什么

把前面的发现连起来看, 图景很清楚. 今天生态里有两半, 各自完整, 但接不上:

  • 训练器那一半: 会改 skill, 但不知道规范;
  • 宿主那一半: 知道规范(甚至强制), 但不提供沉淀工具.

以 DSH 为例, 宿主侧的情况是: 载体设施很完整 —— 注册表、文件系统提供者、面向模型的消费者、人机交互入口齐备, 有扫描优先级、热更新、权限控制, 还有正式的格式契约; 但没有 authoring 能力 —— 全安装树里只有两个示例 skill, 都与宿主自身的插件开发相关, 没有一个是教怎么写 skill 的, 系统级槽位是空的; 而且格式违规对模型完全静默 —— 违规的结果只是这个 skill 从目录里消失(详见配套篇第七节).

所以当你让 agent"帮我把这个沉淀成 skill", 它手上其实没有尺子 —— 只能依赖模型的通用先验, 或者去翻源码(但那要先知道去看). 这不是模型不听话, 是环境没有把规范交给它.

十、补法: 把规范变成可执行的东西

缺口很清楚, 需要三部分:

  1. 一份符合规范、能被宿主发现的 authoring skill —— 放进系统级扫描根的那个空槽位. 它自己必须遵守它教的规则, 否则不具说服力.
  2. 一个校验器 —— 把预算表和字段契约变成可执行检查, 而不是靠人记规则.
  3. 一条把违规反馈给模型的路 —— 这是宿主目前完全没有的部分. 前两部分补上之后, agent 至少能"自己先过一遍校验再交付".

为什么必须是可执行的: 上面列出的所有失败 —— 描述太短、触发信息写错位置、正文超预算、命名不合法 —— 有一个共同特征, 全部不报错. 靠人记规则一定会漏. 这一点在实践中会立刻显现: 写完校验器后拿它跑一遍, 就能抓出肉眼看不见的问题.

前两部分的具体做法、规范细节与完整校验清单, 见配套篇.

0x03 拓展升华展望

把这篇的两个主角放在一起看, 会发现它们争的其实是同一件事的定义权: skill 到底是一段可以调优的文本, 还是一个有形状的工程对象. 训练器按前者工作, 宿主按后者装载, 于是它们能接上电, 却对不上话.

事实层面 … 今天生态里的两半各自完整: 训练器会改 skill 但不知道规范, 宿主知道规范却不提供沉淀能力, 而格式违规对模型完全静默 —— 名字不合法、描述缺失的结果只是这个 skill 从目录里消失, 模型既看不到错误, 也无法区分"不存在"与"写错了". 同时, 开放标准已经把"什么才算合规"变成了可判定的量化预算, 并把"证明 skill 有用"写成了可证伪的要求.

个人判断 … 我认为这个缺口最终会由可执行校验来填, 而不是由更好的模型来填. 原因很直接: 这类失败的特征全部是"不报错", 而一个不报错的约束, 迟早会被忽略 —— 无论写它的是人还是模型. 更远一点看, 一旦规范可以被机器检查, "优化 skill"这个问题就会从"训练一段文本"转向"在约束下重组内容分层", 那时今天这类训练器的价值会退回到它真正擅长的地方: 在既定形状内打磨措辞, 而不是决定形状.

0x04 参考来源

关联阅读

给 AI 买点 Token:
Alipay IconQR Code
Wechat IconQR Code
本文遵循 CC CC 4.0 BY-SA 版权协议, 转载请标明出处
Loading Comments...