Agent Skill 的 description 怎么写?275 个样本里 94.5% 漏了关键一句
更新时间:2026-10-22 | 本文所有数字都可复现,脚本和完整数据在文末
结论先说
- Agent Skill 被加载时,host 只读
description,不读SKILL.md正文。 正文写得再好,在「要不要加载」这一步是看不见的。 - 只写「我能做什么」,等于把所有语义相近的场景都变成候选。 结果是误激活,而且不报错。
- 解法只有一句:在
description里写明「什么时候不该用」。 写在正文里拦不住,因为 host 决定加载时根本不读正文。 - 我扫了本机 275 个已安装 skill,只有 15 个(5.5%)写了这句。 厂商 curated 包是 21.1%,社区是 4.3%。
- 改这一句的成本约 1 分钟。 它减少的是一类特定故障:agent 安静地做错事,然后报告成功。
一、起因:一次镜像不同步
我的仓库里有一份 skill,存了两个位置:
plugins/<name>/skills/<skill>/ ← canonical,改这里
skills/<skill>/ ← 镜像,skills.sh 和手动安装读这里
为什么两份?因为 Claude 插件市场要求 plugins/<name>/skills/<skill>/ 这个结构,而 npx skills 和手动 cp -r 的人读的是仓库根目录的 skills/。两边都得有。
于是就有了一个每次发版都会犯的错:改完 canonical,忘了同步镜像。
结果不是报错,是装了的人拿到旧版本,而且没人知道。这种 bug 最贵——它不报警,只是慢慢让所有人跑在不同的代码上。
我想把这个流程沉淀下来,让它变成一件不用记的事。这个决定后面引出了整篇文章。
二、host 是怎么决定要不要加载一个 skill 的
先明确机制:host 分两级加载,第一级能看到的只有 name 和 description。
以 Claude Code、Codex、Cursor 为例:
| 阶段 | host 做什么 | 此时能看到的内容 |
|---|---|---|
| 1. 索引 | 扫描所有已安装 skill 的 frontmatter | name、description |
| 2. 匹配 | 判断哪个 description 与当前对话相关 | 同上,仅此两项 |
| 3. 加载 | 命中后才读取 SKILL.md 正文 | 正文、references/、scripts/ |
第 2 步是分水岭:正文在这一步不存在。
所以「我的 skill 写得很详细但不生效」,问题几乎都在第 2 步,不在正文。
三、只写「我能做什么」会发生什么
所有语义相近的场景都会成为候选,误激活不会报错,只会安静地做错事。
最直接的做法是对 AI 说「把我们同步镜像的流程写成一个 skill」,然后你会得到类似这样的东西:
- 检查 canonical 目录和镜像目录是否一致
- 如果不一致,将 canonical 的内容复制到镜像
- 验证同步结果
读起来没问题,但它不能执行。「检查是否一致」用什么检查?diff -r 吗?它会因为 .gitignore 里的文件报一堆噪音。「验证同步结果」验证到什么程度算通过?
你在教它做事,但没给它任何可执行的证据。
更贵的是边界问题。如果 description 只写「同步 skill 目录」,下面这些请求都会命中它:
| 用户可能说 | 实际该路由到 |
|---|---|
| "Sync my fork with upstream." | git 操作,不是镜像同步 |
| "Copy this directory to another drive." | 文件复制,不是发版流程 |
| "Publish the skill to skills.sh." | 发布流程,不是镜像同步 |
第三种最危险。用户说要发布,agent 很勤快地去做了镜像同步,然后回复「已完成」。
看起来做了事,实际做错了事,而且不报错。 这类错误比「不会做」贵得多——不会做至少会停下来问你,做错了会安静地把结果交给你。
四、换成从证据里抽,不从记忆里编
可靠写法的共同点是:每一句都能追到一个真实执行过的命令和真实输出。
这个仓库里真实存在的东西是:
| 证据 | 来源 |
|---|---|
scripts/sync_mirror.py --check 退出码 1 = 有漂移 | 脚本实现 |
| 退出码 0 = 已同步 | 脚本实现 |
| 第一次跑会创建镜像并打印源/目标路径 | 本次会话输出 |
第二次跑打印 mirror is in sync | 本次会话输出 |
--check 在漂移时列出具体差异文件 | 脚本实现 |
于是流程变成三步,每一步都有退出条件:
python scripts/sync_mirror.py --check # 退出 0 或 1,1 时列出漂移文件
python scripts/sync_mirror.py # 镜像
python scripts/sync_mirror.py --check # 必须打印 "mirror is in sync"
写不出证据的步骤,就不写。 这是「从证据里抽」和「凭记忆描述」的分界线。
这个 skill 最后是 103 行、约 890 tokens。
五、275 个已安装 skill 的实测:只有 5.5% 写了边界
我写了个脚本,扫本机所有 host 目录下已安装的 SKILL.md,看 description 里有没有任何形式的负向触发(Do not use / Not for / Avoid using / Not intended for):
SKILL.md 文件数: 374
去重后唯一 skill: 275
有 description: 275
含负向触发: 15
负向触发率: 5.5%
374 个文件去重成 275 个,是因为同一个 skill 常被同时装进 .claude/skills、.agents/skills、.cursor/skills、.codex/skills。不去重会让装得最广的几个 skill 被反复计数,把结果带偏。
94.5% 的 skill,没有告诉 agent「什么时候不该用我」。
按来源拆开,差距明显:
| 来源 | 数量 | 写了负向触发 | 比例 |
|---|---|---|---|
| 微软 Azure 系列(厂商 curated) | 19 | 4 | 21.1% |
| 社区与其他 | 256 | 11 | 4.3% |
厂商包比社区规范约 5 倍。但这个「规范」也只是相对而言——即便精心维护的 Azure 包,也有 79% 的 skill 没写边界。
样本边界必须说清:这是一台 Windows 机器上的安装记录,主要构成是几个公开合集(Anthropic 官方示例、微软 Azure 包、superpowers、gstack)加零散安装,不是随机抽样,也不是全网普查。你在自己机器上重跑,数字会变。但 21.1% 对 4.3% 这个量级的差距,不太可能来自样本偶然。
六、description 怎么写:一个可直接套的模板
description 要装三件事:做什么、什么时候用、什么时候不用。
description: >-
<动词开头,一句话说清做什么>.
Use when <场景 1>, <场景 2>, <场景 3>.
Do not use for <最容易被误判的相邻任务> (use <那个 skill> instead),
or <另一种边界情况>.
逐行说明:
| 行 | 作用 | 判断标准 |
|---|---|---|
| 第一行 | 说清职责 | 动词开头,一句话,不含形容词 |
Use when | 正向触发 | 场景具体到能分辨,不写「相关任务」 |
Do not use for | 负向触发 | 写出你最怕被误用的那个场景 |
反例与正例:
# 反例:所有 sync 相关请求都会命中
description: Sync skill directories.
# 正例:镜像与发布被明确分开
description: >-
Mirror a canonical skill directory into its published copy.
Use when SKILL.md or references changed and a release tag is imminent.
Do not use for publishing (use publish-skill instead),
or for repositories that keep only one copy.
关键是最后那句。想不出边界,通常意味着这个 skill 的职责还不够窄。
七、怎么验证边界写对了:触发测试
至少要 3 条正向 + 3 条负向,每条都有明确的预期行为。
| 类型 | 用户可能说 | 预期 |
|---|---|---|
| 正向 | "The skill directory drifted again, mirror it." | 激活 |
| 正向 | "I changed references/, sync the published copy." | 激活 |
| 正向 | "About to tag a release, keep the mirror current." | 激活 |
| 负向 | "Sync my fork with upstream." | 不激活 |
| 负向 | "Copy this directory to another drive." | 不激活 |
| 负向 | "Publish the skill to skills.sh." | 不激活 |
每条都要有可判定的预期,不能写「表现正常」。负向测试的价值在于:没有它们,sync 和 mirror 这两个词会把所有东西都吸进来。
八、8 道门禁:那个 skill 的完整打分
写完之后我没有直接发布,而是拿一套评分卡逐项打分,每项 0/1/2 分,满分 16:
| # | 门禁 | 得分 | 依据 |
|---|---|---|---|
| 1 | 触发精度 | 2/2 | 3 个正向触发 + 1 个排除项 |
| 2 | 渐进披露 | 2/2 | 103 行,没有内联参考资料 |
| 3 | Token 预算 | 2/2 | ~890 tokens,远低于 5k 核心预算 |
| 4 | 可执行性 | 2/2 | 5 步,每步都有命令和退出条件 |
| 5 | 证据锚定 | 2/2 | 来自 sync_mirror.py 和两次真实运行 |
| 6 | 安全 | 2/2 | 不删源数据;只写镜像;无密钥、无网络安装 |
| 7 | 可移植性 | 2/2 | 路径从仓库根发现,无绝对路径 |
| 8 | 可证明性 | 2/2 | 5 个正向 + 3 个负向触发;dry run 跑了两次 |
16/16,可以发。
但注意第 1 道和第 8 道门都在说同一件事——触发。这两道门才是大部分 skill 真正失败的地方,而不是正文。
九、这套流程在哪
上面提到的门禁、打分卡、触发测试模板和统计脚本,我整理成了一个开源仓库:
skill-forge — github.com/YaBoom/skil…
里面具体有什么:
| 内容 | 路径 | 用来做什么 |
|---|---|---|
| Skill 本体 | plugins/skill-forge/skills/skill-forge/ | 把已完成的工作流蒸馏成 skill |
| 103 行完整示例 | examples/sync-skill-mirror/ | 本文第四节引用的那个 skill 的全部源码 |
| 8 道门禁打分记录 | docs/dry-run-scorecard.md | 16/16 的逐项依据,可对照 |
| 统计脚本 | scripts/survey_triggers.py | 复现 275 / 15 / 5.5% 这三个数 |
| 确定性检查脚本 | scripts/skill_audit.py | 检查行数、token 估算、负向触发缺失、绝对路径 |
| 英文版长文 | docs/boundary.html | 完整数据集和门禁表,可直接引用 |
安装(Claude Code):
/plugin marketplace add YaBoom/skill-forge
/plugin install skill-forge@skill-forge-marketplace
任意 Agent Skills host:
npx skills add YaBoom/skill-forge -s skill-forge -g
MIT 协议。不想安装也能用:统计脚本和检查脚本可以直接 clone 下来跑,本文所有数字都能这样复核。
十、常见问题(FAQ)
Q1:什么是 Agent Skill?
Agent Skill 是一个目录,核心是 SKILL.md:frontmatter 里写 name 和 description,正文写步骤,旁边可带 references/、scripts/、assets/。Claude Code、Codex、Cursor 这类 host 会读取它,作为可复用的工作方法加载进上下文。
Q2:Agent Skill 和 Prompt、MCP、Tool 有什么区别?
| 类型 | 是什么 | 是否落盘 | 加载方式 |
|---|---|---|---|
| Prompt | 一次性对话指令 | 否 | 每轮手动提供 |
| Tool / MCP | 可调用的能力接口 | 服务端定义 | 按需调用 |
| Skill | 可复用的工作方法与判断标准 | 是,SKILL.md | 按 description 匹配后加载 |
三者不冲突。一个 skill 里可以调用 MCP 工具,它负责的是「什么时候调、按什么顺序调、调完怎么验证」。
Q3:description 写多长合适?
长到能装下三件事:做什么、什么时候用、什么时候不用。没有固定字数限制,但第三件事通常需要一整句,这也是带负向触发的 description 普遍比不带的更长的原因。
Q4:为什么把「不要用我」写在正文里没用?
因为 host 决定是否加载时只读 description,正文在这一步不可见。写在正文里的排除说明,只有在该 skill 已经被加载之后才会被读到——那时已经误激活了。
Q5:怎么写触发测试?
两组:正向 3 条以上(用户会怎么说法,应该激活),负向 1 条以上、最好 3 条(用户会怎么说法,但不该激活)。每条都要有明确预期,不能写「表现正常」。
Q6:有没有工具能自动检查?
有一个确定性检查脚本,能查出负向触发缺失、绝对路径、token 超预算这类可判定问题:
python plugins/skill-forge/skills/skill-forge/scripts/skill_audit.py \
plugins/skill-forge/skills/skill-forge --repo .
退出码 0 表示没有阻断性问题。需要人工判断的部分(触发精度、可证明性)仍需自己看。
Q7:5.5% 这个数字怎么来的?
扫描本机所有 host 目录下的 SKILL.md,按目录名去重,检查 description 是否包含 Do not use、Not for、Avoid using、Not intended for。脚本公开,可在自己机器上重跑,数字会不同。
Q8:skill 应该写多长?
本文那个例子是 103 行、约 890 tokens。经验值是把核心 SKILL.md 控制在 5k tokens 以内,超出的内容拆到 references/,正文只留一句「需要时读哪个文件」——这叫渐进披露,避免每次加载都吃掉全部上下文。
十一、4 条可以立刻改的
- 在
description末尾加一句Do not use for ...。 具体到能分辨的场景,不写「不适用于无关任务」。 - 至少写一条负向触发测试。 想不出反向场景,说明职责还不够窄。
- 给每一步写死完成条件。 不是「确认同步成功」,而是「
--check打印mirror is in sync」。 - 数字优先于形容词。
~890 tokens比「很轻量」有用,退出码 1比「检查失败」有用。
数据来源
| 内容 | 位置 |
|---|---|
| 统计脚本 | scripts/survey_triggers.py |
| 8 道门禁打分记录 | docs/dry-run-scorecard.md |
| 103 行完整示例 | examples/sync-skill-mirror/ |
| 仓库 | github.com/YaBoom/skil… |
| 英文版(含完整数据集与门禁表) | yaboom.github.io/skill-forge… |
本文所有数字均来自上述公开文件,可逐条核对。样本为单机安装记录,非随机抽样,重跑结果会变化。
如果只改一件事:改 description 里的负向触发。它便宜、见效快,而且是唯一一条能直接减少 agent「做错事」的改动。