第13章 把经验装进说明书——Skill

小A发现团队每次做代码评审,都要跟 Agent 重复同样的话:先看影响范围、跑测试、检查权限、别急着说“可合并”。他一边复述一边想,这太浪费了。
“能不能把这一套存下来,让 Agent 每次照着走?”他问老Z。
“能。外部能力已经有了接入边界,你现在撞上的是另一件事:同一类任务会反复需要检查表、约定和资料。这类可复用的工作说明,就是常说的 Skill。”老Z说,“先提醒一句:‘Skill’没有跨平台统一定义。本章把它当一种工程模式来讨论:用可发现、可按需加载的说明组织经验,不假定某种文件格式或自动触发机制。”
先把名字对齐
| 中文术语 | 英文全称或缩写 | 本书工作定义 | 不等于什么 |
|---|---|---|---|
| 技能 | Skill | 可发现、可按需加载的任务经验说明;不是统一行业协议 | 一个可执行 Tool |
| 指令包 | instruction bundle | 适用条件、步骤、资料、工具和验收说明的组合 | 强制执行引擎 |
| 发现/触发/渐进加载 | discovery/trigger/progressive loading | 先读索引、判定适用、再加载正文的三步 | 模型必然正确选择 |
| 软约束 | soft constraint | 借文本影响模型决策的规则 | 代码级权限或校验 |
Skill 的内容与边界
“一个 Skill 里都装什么?”
“适用条件、目标、步骤、可用工具、输入输出约束、例外、验收方式,通常这几样。”老Z说,“载体不定,Markdown 文件、数据库记录、代码配置都行;要紧的是内容可审查、可版本化、可验证,格式本身不那么重要。”
“这七部分逐个说说?”
“可以。拿一个‘代码评审’Skill 当样本,逐项拆开看:”
| 组成部分 | 代码评审 Skill 中的样例 | 写糊的后果 |
|---|---|---|
| 适用条件 | “准备合并到主分支的代码变更” | 写成“代码相关任务”→ 任何任务都触发 |
| 目标 | “确认变更可安全合并,并留下可追溯的评审记录” | 缺目标→ 模型不知道做完该产出什么 |
| 步骤 | 影响范围→跑测试→查权限→输出结论 | 步骤无序→ 模型跳步或漏步 |
| 可用工具 | read、run_test、git_diff | 不声明→ 模型用错工具或不用工具 |
| 输入约束 | “变更涉及的文件列表、目标分支” | 不约束→ 模型评审了无关文件 |
| 输出约束 | “结论 + 测试结果 + 风险清单” | 无约束→ 输出一段散文,无法验收 |
| 例外 | “若无法跑测试,标记并转人工” | 无例外→ 遇到障碍就卡住或编造结果 |
| 验收 | “每步有结果,结论可被另一人复核” | 不可验收→ 不知道算不算做完 |
“这八部分不是每个 Skill 都要写全,”老Z补充,“但缺了哪部分,那部分的风险就留给运行时。适用条件和验收是两条底线:前者决定该不该加载,后者决定加载了算不算做完。其他部分越完整,模型遵循得越可靠。”
“这里面哪些最容易被写糊?”
“适用条件和验收方式。”老Z说,“适用条件含糊,模型就会在错的场景加载它,比如把‘发布前检查’用到普通草稿上;验收方式不可观察,比如‘认真检查’,加载了也判断不了做完没有。一个能用的 Skill,至少要答得上两件事:什么时候该用它,怎么算用完了。”
“Skill 和 Tool、MCP 的关系我老混。”小A说。
老Z把三样东西摆进一张表:
| 概念 | 主要回答的问题 | 约束强度 |
|---|---|---|
| Tool | 能执行什么动作 | 由实现和权限决定 |
| MCP | 怎样发现与交换外部能力 | 由协议与宿主决定 |
| Skill | 面对任务该怎样组织动作 | 通常是软约束 |
“三样东西在不同层面。”老Z指着表说,“Skill 要调用 Tool 才动得了手,可能经由 MCP 才发现得了资源,但它自己只是一份说明书,不替代任何一层。”
“写进 Skill 的‘发布前运行测试’,会被保证执行吗?”
“不会。模型会遗漏、误解,也可能干脆不理会文字说明。”老Z说,“需要强制的前置条件,落在 CI、工具校验、审批或权限控制里;Skill 的价值是让正确路径更明确,替代不了这些机制。简单的判断办法:哪一步失败会造成不可逆后果,哪一步就不能只靠 Skill,得有硬约束兜底。”
“那它和 System Prompt 什么区别?不都是写给模型的文字吗?”
“生命周期不同。”老Z说,“System Prompt 是每次请求都在场的常驻规则,声明的是始终适用的边界;Skill 按需加载,只在特定任务场景被带进上下文。System Prompt 像房子的承重墙,不能太厚;Skill 像按需取用的说明书,可以写得很细,反正不是每次都加载。”
比喻:岗位手册
手册写得再好,也不能保证每个人都照做;要确保的环节,靠的是门禁。Skill 是手册(软约束),门禁是代码(硬约束)。
发现与按需加载
“那我干脆把所有 Skill 都塞进提示词,一步到位?”
“别。全文进上下文,既挤占任务空间,又让彼此无关的规则互相干扰。”老Z说,“稳妥的分法是两层:先加载名称、描述、适用范围这些索引;任务确实匹配,或者用户明确点名,再加载正文。匹配可以交给规则、用户或模型,三种方式的误触发率和可解释性都不一样,不能笼统说成‘自动就对了’。”
“这三种触发,差别在哪?”
“代价不同。”老Z说,“规则触发可解释、可测试,条件却写死了,覆盖不到没预料到的场景;用户选择准,但交互负担在人这边;模型判断覆盖面广,可能误判,把不相干的 Skill 拽进来,轻则浪费上下文,重则带进冲突规则。多数系统混着用:关键 Skill 交给规则或用户兜底,辅助的才让模型判断,但都留日志。”
老Z把三种方式的差别列成一张表:
| 触发方式 | 准确性 | 覆盖面 | 代价 |
|---|---|---|---|
| 规则匹配 | 高(条件明确) | 低(只覆盖预定义条件) | 条件维护成本 |
| 用户选择 | 最高(人决定) | 取决于用户认知 | 交互负担 |
| 模型判断 | 中(可能误判) | 高(语义匹配) | 误触发占用上下文 |
“索引本身会不会出问题?”
“会。索引是 Skill 的快照:名称、描述、适用范围。”老Z说,“正文改了索引没跟上,模型读到的就是旧描述;索引写‘适用所有 TypeScript 项目’,某份 Skill 其实只针对测试框架。索引一漂移,触发判断就建立在过时或过宽的信息上。所以索引和正文要一起版本化,加载日志里同时记两个版本。”
“加载进来的正文呢?”
“也要标明来源和版本。回答或行为异常时,才追得清是模型推理出了问题、说明过时了,还是当初就加载错了。”
“那这份正文会在上下文里待多久?”
“一个取舍。”老Z说,“一直留着,每轮都占地方,好处是模型能反复参照;用完就删,省空间,但后面轮次模型可能把它忘了。常见做法按参考频率定:步骤型的,一次性检查表,用完就移除;规则型的,贯穿整个任务的约定,留到任务结束。不管哪种,移除时在消息历史里记一条‘某版本 Skill 已从上下文移除’,审计时有据可查。”
软约束为什么是软的
“我还没完全想通‘软约束’软在哪。”小A说,“Skill 写得那么细,模型怎么会不照做?”
“因为指令和数据走的是同一条通道。”老Z说,“模型收到上下文时,机制上分不出‘这是必须遵守的规则’还是‘这是一段参考文本’,它看到的都是 token。它按概率续写最可能的那一种,而‘最可能’不等于‘照着 Skill 走’。”
“那有没有办法让约束变硬?”
“有,换通道。”老Z说,“硬约束不走文本,走代码:工具执行器拒绝、权限层拦截、CI 强制检查。它们不依赖模型自觉,在模型之外执行,绕不过去。软约束影响的是模型的决策概率,硬约束限定的是模型的行动边界,两者配合:前者让正确路径更容易被选中,后者保证没被选中也不失控。”
要点
软约束“软”在它和普通文本共用同一条通道,模型按概率决定是否遵循。要保证的环节,用代码层硬约束,不靠文字。
“软约束失手时是什么样?”
“至少三种。”老Z列了出来:
| 失败场景 | 表现 | 根因 |
|---|---|---|
| 遗漏 | Skill 要求跑测试,模型直接跳到结论 | 步骤多时模型可能跳步 |
| 误解 | Skill 要求‘列影响范围’,模型列了一堆无关文件 | 描述歧义或模型理解偏差 |
| 抵触 | Skill 要求‘不声明可合并’,模型在结尾还是写了‘可以合并’ | 任务的惯性压过规则约束 |
“这三种都不算 bug,”老Z补充,“它们是软约束的固有特性。应对方式不是‘写更严厉的 Skill’,而是给关键步骤配硬约束:遗漏测试就在合并前门禁拦住,声明可合并就在工具层不让它输出这个词。”
把说明写成可执行的检查表
“能给个例子看看吗?”
“好。下面是示意,不绑定任何系统:”
---
name: review-change
when: "准备合并代码变更时"
---
1. 说明本次变更的影响范围。
2. 运行与受影响模块相符的测试,并记录命令和结果。
3. 若涉及权限或网络,列出新增边界并请求确认。
4. 未完成任何一项时,不声明变更可合并。“这个‘说明影响范围’算不算好步骤?”
“算,它可观察。”老Z说,“好的说明给的是能核对的完成条件,‘认真检查代码’这种要求没法验收。检验一个步骤写得行不行,有个简单办法:把它单独交给一个不了解上下文的同事,看他能不能照着做,并且说得出‘做完了’还是‘没做完’。”
几种常见写法,老Z分了类:
| 步骤写法 | 可观察? | 问题 |
|---|---|---|
| “运行受影响模块的测试,记录命令和结果” | 是 | 无 |
| “仔细检查代码质量” | 否 | 无法判断完成状态 |
| “列出本次变更的影响范围” | 是 | 范围可被另一人复核 |
| “确保没有安全问题” | 否 | “确保”无验收标准 |
“那‘仔细检查代码质量’这种,交给同事,他多半要反问:检查什么?标准是什么?”
老Z把几条模糊步骤的改写对照列了出来:
| 模糊步骤 | 同事的疑问 | 改写后 |
|---|---|---|
| “检查代码质量” | 检查什么?标准是什么? | “运行 npm test,记录通过/失败数” |
| “确保安全” | 怎么确保?什么算安全? | “列出本次变更涉及的权限变更,若无写‘无’” |
| “优化性能” | 优化什么?目标是什么? | “测量修改前后 npm run bench 的耗时,记录差值” |
| “review 代码” | 看什么?输出什么? | “读取 diff,按文件列出潜在风险及理由” |
“那危险操作呢?”
“Skill 应该把危险操作交给硬约束,而不是把密钥、绕过方法或无限权限写进上下文。”老Z说,“示意一下:Skill 里写‘部署前需要审批’,审批本身由工具或流程引擎强制执行;Skill 只负责提醒有这么一步,不承担强制责任。密钥或绕过逻辑写进 Skill,等于把敏感信息放进模型上下文,泄露面反而更大。”
“步骤之间有依赖呢?”
“把顺序写进说明,别让模型自己猜。”老Z说,“比如‘先跑测试,通过后再请求审批,审批通过后才提交’,每一步的完成都是下一步的前提。Skill 能表达顺序,但强制不了顺序;真要保证,靠工具的状态检查或流程引擎,不是说明里的编号。”
“验收方式也用同一把尺子量。”老Z补了一句,“‘看起来不错’量不出结果,‘测试全部通过’才核对得了。”
选择错误与规则冲突
“模型把 Skill 选错了怎么办?”
“选 Skill 这一步本身就是个可失败的决策:索引可能漏掉该加载的说明,语义匹配也可能把‘发布前检查’配到普通草稿任务上。”老Z说,“示意一下:两份 Skill,一份要求‘只生成预览’,一份要求‘执行提交’。冲突时,系统按任务阶段、显式用户选择或预先定义的优先级来裁决,并把裁决结果记录下来;裁不了就请求澄清,不能让模型把两份步骤混在一起,假装都完成了。”
“怎么分清是选错了,还是模型没照做?”
“把加载日志和执行日志分开记。”老Z说,“加载日志记的是在什么条件下加载了哪份 Skill、版本多少;执行日志记的是模型实际做了什么、每步什么状态。两份摆在一起对,问题才定位得了:加载对了但没照做,是模型遵循的问题;加载就错了,是触发或描述的问题。缺了这两份日志,故障只能靠猜。”
“多份 Skill 同时适用时,裁决顺序是什么?”
“按这个优先级链走:”
1. 用户显式选择 ── 最高优先级,直接采用
│(无显式选择时)
↓
2. 互斥标签检查 ── 两份 Skill 标了互斥?→ 停下请求澄清
│(不互斥时)
↓
3. 预定义优先级 ── 配置里声明了谁优先?→ 采用高优先级
│(无优先级声明时)
↓
4. 任务阶段判定 ── 当前处于哪个阶段?→ 采用匹配阶段的
│(仍无法判定时)
↓
5. 请求澄清 ── 不让模型自己混合拼接“关键在最后一步:裁不了就请求澄清,别让模型把两份步骤混合后假装都做完了。”老Z强调,“混合最危险,它会产出一份看着合理、实际上谁的要求都没满足的结果。”
“具体会混成什么样?”
“两份 Skill,A 要求‘只生成预览,不修改文件’,B 要求‘直接修改并提交’。”老Z说,“混着做,可能得到‘修改了文件但没提交,还声称只生成了预览’:既动了文件,没守住 A 的只读要求,也没满足 B 的提交要求。这种混合产物,比干脆选错一份还难排查。”
把说明当作受版本管理的输入
“那 Skill 自己怎么管?总不能写完就撒手了吧。”
“当受版本管理的输入管。”老Z说,“发布变更的 Skill 会带检查范围、测试命令、人工确认点和输出模板;发现阶段只读取名称、描述、版本和适用范围,触发阶段由用户选择、规则或模型匹配决定,加载阶段才把正文与引用资料纳入上下文。三个阶段各留日志,出了问题才分得清是描述不准确、触发错了,还是模型没照做。”
“两个 Skill 同时适用,让它们在提示词里排个先后行不行?”
“不行,那是碰运气。”老Z说,“裁决走优先级、互斥标签或合并规则;要求冲突,比如一个只读、一个写入,就停下来交给人选,别把两份说明拼在一起。”
“Skill 改了之后,怎么验证没改坏?”
“版本变更拿固定的测试集比一遍:该加载的说明加载了吗、该调的工具调了吗、不可跳过的硬约束破了吗。”老Z说,“Skill 会过时。业务流程变了、工具签名变了、安全策略变了,旧 Skill 就和现实脱节了。所以把它当受版本管理的输入,别当写一次就永远对的真理。”
“过时长什么样?”
“典型的有三种。”老Z在纸上写了三行:
| 漂移类型 | 表现 | 诱因 |
|---|---|---|
| 工具签名漂移 | Skill 写‘调用 run_test’,但工具已改名为 run_tests | 工具重构后未同步更新 Skill |
| 流程漂移 | Skill 要求‘提交到 main 分支’,但团队已改用 PR 流程 | 团队规范变了,Skill 没跟上 |
| 安全策略漂移 | Skill 写‘部署前通知 Slack’,但通知渠道已换成别的 | 基础设施变更未传播到 Skill |
“三种漂移的共同点,是 Skill 描述的世界和真实世界对不上了。”老Z说,“所以过时的 Skill 比没有 Skill 更危险:没有时,模型谨慎探索;过时时,模型自信地走错路。”
“漂移怎么发现?”
“靠两类信号。”老Z说,“一是主动巡检:定期拿 Skill 引用的工具名、分支名、命令名去和当前系统比对,对不上的标红。二是被动捕获:模型按 Skill 行动却频繁失败,工具不存在、分支不存在、命令报错,这些失败信号就是漂移的证据。两类都要落到日志里,否则漂移会一直潜伏,直到某次事故才暴露。”
“那 Hook 呢?前面说过 Skill 和 Tool、MCP 各管一层。”
“Hook 是运行时介入。”老Z说,“Skill 不替代其中任何一层,它就是一层组织经验说明,让模型面对重复出现的任务时,有一条明确的正确路径可走。”
Skill 的目录与命名空间
“Skill 多了以后,怎么管?”
“跟代码一样,会膨胀,所以要目录和命名空间。”老Z说,“几十份 Skill 平铺在一个目录里,索引就越来越难维护:描述互相覆盖,适用条件重叠,命名随意。分层是工程化的第一步,跟代码分包一个道理。”
“怎么分?按项目分?”
“按职责分。”老Z说,“常见的是三层:全局通用、团队规范、项目专用。项目专用绑定某个项目或仓库,团队规范收的是流程约定,三层的适用范围、更新节奏和权限都不一样:”
| 层 | 适用范围 | 更新节奏 | 权限 |
|---|---|---|---|
| 全局通用 | 所有会话 | 慢,需评审 | 平台管理员 |
| 团队规范 | 特定团队 | 中,团队评审 | 团队成员 |
| 项目专用 | 特定项目 | 快,跟项目走 | 项目维护者 |
“命名空间呢?”
“解决同名冲突。”老Z说,“不同团队可能都有一份 review Skill,内容完全不同。给名字加命名空间前缀,eng-review、sec-review,来源一望便知,加载日志里也一眼能认出用的是哪份。冲突不处理,模型就在两份同名 Skill 之间摇摆,日志也分不清谁是谁。”
“这些目录和命名空间,谁来维护?”
“得有明确的 owner。”老Z说,“Skill 是要维护的资产:新增要评审,变更要测试,过时要清理。没有 owner 的 Skill 目录,会像没有 owner 的代码库一样腐烂:越来越多,越来越旧,越来越没人敢动。给每份 Skill 标上 owner,是可维护的最低要求。”
“一次任务能加载多少份?有预算吗?”
“要有,这还是个常被漏掉的预算。”老Z说,“每份正文都占上下文,三份写得细的 Skill 就可能吃掉几千 token。加载预算要跟任务预算一起设:不是能加载就加载,而是这份 Skill 值不值它占的那块上下文。”
“加载多了会怎样?”
“症状是上下文臃肿,任务却没变得更清楚。”老Z说,“面对一堆 Skill,模型要么每份都沾一点,行为被稀释;要么只挑最显眼的一份,其余视而不见。加载过多和没有 Skill 一样糟,区别只是一个看着准备充分,一个明显没准备。判断标准是:加这份 Skill 之后,任务成功率或质量真的提升了吗?而不是‘该有的都有’。”
“有没有简单的预算规则?”
“有个实用的起点,但不绝对。”老Z说,“默认只加载索引;正文同时不超过 2-3 份,总字数设上限。超了预算的任务,优先用最相关的那份,其余降级为‘描述可查’。这规则不是放之四海皆准,但比不限量加载安全得多:留预算,是给任务本身留上下文空间。”
贯穿场景:一次发布评审的完整 Skill 生命周期
“能不能从头到尾串一次?”
“好。假设有一份 release-review Skill,跟踪它从被发现到任务结束的全过程。”
老Z画了一条时间线:
任务开始:用户说"帮我评审这次发布"
↓
[发现] 系统读取所有 Skill 的索引(名称、描述、适用范围、版本)
↓ `release-review` 的描述含"发布评审",进入候选
[触发] 模型判断:当前任务匹配"发布评审" → 标记为适用
↓ 加载日志记录:skill=release-review, version=3, trigger=model
[加载] 读取 `release-review` 正文(8 个步骤 + 工具清单 + 验收标准)
↓ 正文进入上下文,标明来源和版本
[执行] 模型按步骤推进:
步骤1:读取 changelog → 工具 read 返回内容 → 完成
步骤2:运行测试 → 工具 run_test 返回通过 → 完成
步骤3:检查权限变更 → 工具 git_diff 返回无变更 → 完成
步骤4:输出结论 → 生成评审报告
↓ 执行日志记录:每步的完成状态 + 工具结果摘要
[移除] 任务结束,Skill 正文从上下文移除
↓ 消息历史保留"release-review v3 已加载并执行"的记录
任务结束“这条时间线上,每一站都可能出问题。”老Z说,“发现阶段:索引过时,描述还写着旧版工具名;触发阶段:模型误判,把‘草稿评审’匹配成‘发布评审’;加载阶段:正文和索引版本不一致;执行阶段:模型跳过某步或误解某步;移除阶段:忘了移除,后续轮次还带着它占上下文。每一站的故障,只有对应的日志才能定位。”
“中途用户改主意呢?”
“比如说到一半,用户说‘算了,这次不走完整流程,快速看一下就行’。”老Z说,“已加载的 release-review 就和新意图冲突了。正确做法是把当前 Skill 标记为已中止,记下中止原因,再按新意图决定要不要加载一份更轻量的。旧 Skill 和新意图不能共存,那等于两套规则打架。”
小结
Skill 是把流程经验封装成可发现、可按需加载的说明层。一个任务反复出现、步骤和验收标准都固定,就值得把这些经验写成一份说明书,让模型需要时找得到、读得懂、照着做。它的价值在复用:写一次,多个会话都能用,不必每次靠 prompt 从零讲起。完整的 Skill 包含适用条件、目标、步骤、可用工具、输入输出约束、例外和验收方式;适用条件和验收是两条底线,前者决定该不该加载,后者决定怎么算做完。加载是渐进的:先读索引判断适不适用,适用了再加载正文,避免把所有 Skill 一次塞进上下文。三种触发方式(规则、用户选择、模型判断)各有代价,多数系统混用,把关键 Skill 交给更可靠的方式兜底。
Skill 本质上是软约束。它之所以“软”,是因为指令和数据走同一条通道,模型无法在机制上区分“必须遵守的规则”和“参考文本”,只能按概率给最可能的续写。所以模型可能遗漏步骤、误解描述、抵触约束,这三种都不是 bug,是软约束的固有特性。应对办法不是把措辞写得更严厉,而是给关键步骤配硬约束:工具执行器拒绝、权限层拦截、CI 门禁。软约束让正确路径更容易被选中,硬约束保证没被选中也不会失控。
Skill 的可靠性靠三份日志撑着:加载日志记在什么条件下加载了哪份、版本多少,执行日志记模型实际做了什么、每步完成状态,移除记录记何时从上下文移除。三份日志对应三类失效来源:触发错误、模型未遵循、上下文残留。Skill 还会随时间漂移,工具改名、流程变更、基础设施迁移,都会让旧 Skill 描述一个已经不存在的世界。过时的 Skill 比没有更危险:没有时模型谨慎探索,过时时模型自信地走错路。把它当受版本管理的输入,用主动巡检和被动捕获两类信号盯住漂移,是维持可靠性的长期工作。
Skill 的工程化不止于写正文:要有目录和命名空间,全局通用、团队规范、项目专用三层各有更新节奏和权限,命名加前缀解决同名冲突,每份 Skill 都要有 owner 负责维护;要有加载预算,默认只读索引,正文限制在 2-3 份并设字数上限,判断标准是加一份之后任务质量是否真的提升。贯穿那个发布评审场景,本章假设的一种完整生命周期是发现(读索引)→ 触发(判适用)→ 加载(读正文)→ 执行(按步骤走)→ 移除(任务结束)。这条路线用于说明接口问题,不代表所有实现或 Pi 的默认行为;pi-mono 另有包管理器发现和启动时加载路径。每一站都可能失败,要用对应日志定位。任务复杂到要同时推进多个独立目标时,下一章将讨论是否需要引入多个执行者。
动手核验
- 为一次代码评审写一个不超过十步的 Skill,给每步加可观察的完成条件。
- 将其中“必须完成”的要求分别落到测试、工具校验或审批,验证单靠文字不能代替它们。
- 为该 Skill 写索引描述,并列出一个应触发、一个不应触发的任务。
