如何写好 AI Skill

我以前也把 Skill 理解成一份写给 AI 的长 Prompt:把背景、要求和几个示例放在一起,文件越完整越好。真正开始维护之后,感觉很快变了。

一个 Skill 好不好用,通常不取决于它写了多少内容,而取决于 AI 能不能在正确的任务里加载它,按步骤执行,并在出错时停下来。它更像一份可执行的流程资产,而不是一篇知识介绍。

Skill 解决什么问题

团队里的很多经验不会出现在正式文档里:某个旧接口不能直接删、某条命令上线前必须执行、某种外部输入不能直接当指令、发布后还要检查线上页面。

这些事情如果每次都重新解释,结果很容易不一致。Skill 的作用,是把其中一类重复任务固定下来,至少明确四件事:

  • 什么时候应该使用
  • 具体按什么顺序执行
  • 什么情况下应该停止
  • 完成后如何验证

一个最小的 Skill 目录可以很简单:

my-skill/
├── SKILL.md
├── scripts/
├── references/
└── assets/

SKILL.md 放触发条件、流程和边界;脚本、模板和较长的参考资料放在其他目录。不要一开始就把所有东西塞进一个文件。

先写清楚触发条件

Skill 的元数据里,description 往往比正文更早发挥作用。它至少要说清楚任务是什么、什么时候使用,以及覆盖哪些动作。

不太有用的写法:

description: 处理代码迁移

更具体的写法:

description: >
  将 Go 项目中的旧版 HTTP 客户端迁移到统一请求库。
  当项目仍引用 old-http-client,且用户要求迁移、替换或修复相关调用时使用。
  包含 import 替换、参数适配、错误处理和测试验证。

触发条件写得太宽,Skill 会在不相关的任务里出现;写得太窄,又可能根本匹配不到。我的经验是,先把“应该触发”和“不应该触发”的例子各列几条,再回头修改描述,比凭感觉写一句总结更可靠。

把规则写成可以执行的步骤

“先检查版本,再选择方案”对人来说足够,对 AI 来说通常不够。应该把判断条件和下一步写出来:

1. 读取项目版本信息。
2. 如果 Go 版本低于 1.18,使用兼容写法。
3. 如果 Go 版本不低于 1.18,使用原生泛型。
4. 修改后运行 go test ./...。
5. 测试失败时停止,不要继续修改其他文件。

每个步骤最好能回答三个问题:

  • 需要检查什么
  • 根据结果采取什么动作
  • 什么时候结束或暂停

尤其是最后一个问题。只写“完成迁移”而没有停止条件,AI 很容易在目标不明确时继续扩大修改范围。

给原因,也给改前改后

规则后面补一句原因,能减少机械执行。例如:

使用参数化查询,不要拼接 SQL 字符串。
原因:字符串拼接可能引入 SQL 注入风险。

对于代码改造,Before / After 通常比长篇解释更有效:

- import oldhttp "github.com/example/old-http-client"
+ import uhttp "github.com/example/unified-httpclient"

- return oldhttp.Do(req)
+ return uhttp.Do(req)

如果任务包含分类或分级输出,可以准备 3~5 个覆盖不同分支的样例。样例不用多,但要让 AI 看见边界:什么是严重问题,什么只是建议,什么情况应该跳过。

一个足够小的例子

下面这个 Skill 只负责为 Go 函数生成表驱动测试,没有把代码审查、依赖升级和发布流程也混进来:

---
name: go-tabletest
description: >
  为 Go 函数生成表驱动单元测试。
  当用户要求补单测、生成测试用例或提升覆盖率时触发。
  适用于常见 Go 项目。
---

# Go 单元测试生成

## 目标
为指定函数生成标准 testing 风格的表驱动测试。

## 规则
1. 仅使用标准库 testing。
2. 使用 t.Run 组织子测试。
3. 覆盖正常值、边界值和异常输入。
4. 不修改生产代码,除非用户明确要求。

## 验证
```bash
go test ./... -v
```

它已经包含了触发描述、任务边界、执行规则和验证命令。对于一个小任务,这比一份几百行但没有明确流程的总手册更容易稳定执行。

什么时候应该拆分

Skill 变长并不一定是问题,但出现下面情况时,通常应该拆:

  • 一个文件里混了多个互不依赖的任务
  • 不同部分的更新频率差别很大
  • 某些步骤可以单独复用
  • 主要流程已经很难从细节中找出来

可以让主文件负责流程编排,把具体步骤放到参考文件中:

project-migration/
├── SKILL.md
└── references/
    ├── environment-check.md
    ├── dependency-update.md
    └── api-migration.md

主流程每完成一个高风险步骤,就应该有一个检查点。例如依赖替换后运行:

go mod tidy
go build ./...

如果检查失败,先停下来处理,不要继续执行后面的步骤。这样定位问题会容易很多。

外部服务和密钥

需要访问数据库、浏览器、GitHub 或内部 API 时,可以使用 MCP,也可以在脚本中直接调用 HTTP。选择标准不必复杂:

场景更合适的方式
已经有成熟的 MCP ServerMCP
多个 Skill 都要复用同一个服务MCP
需要统一鉴权和审计MCP
一次性、简单的 API 调用HTTP 脚本

无论选择哪种方式,都不要把 API Key 写进 SKILL.md 或脚本源码。至少先检查环境变量:

if [ -z "$API_KEY" ]; then
  echo "请先设置环境变量 API_KEY"
  exit 1
fi

如果流程会改数据库、覆盖文件或删除资源,Skill 里必须写清楚备份、确认、验证和回滚。危险操作不能只用一句“谨慎执行”带过。

怎么测试一个 Skill

不要只在一次成功对话后就认为 Skill 可用了。可以准备一组小测试:

  • 10 个应该触发的请求
  • 10 个不应该触发的请求

重点看四件事:

维度检查内容
触发准确性该触发时能否触发,不相关时能否跳过
执行稳定性步骤和输出是否基本一致
验证完整性是否真的运行了检查命令
安全边界是否会跳过确认、备份或回滚

不同问题对应的修正方式也不一样:匹配不到,先改路径和 description;执行跑偏,补清楚步骤;输出不稳定,增加 Before / After;不自检,给出明确的验证命令。

结合实际维护来写

对我来说,Skill 最有价值的地方,是把已经踩过的坑提前写进流程里。比如博客发布 Skill 中,下面这些规则都不是为了让文档看起来完整,而是实际维护时确实需要:

  • 文章只能有一套 Front Matter
  • 发布前必须检查裸 URL、代码围栏和 Markdown 表格
  • 构建出现 warning 或 error 时不能继续推送
  • 图片上传失败时要明确保留原路径或停止
  • 外部文章抓取到登录墙时不能当成正文发布
  • 推送后要核对本地和远程提交是否一致

这些内容放在普通 Prompt 里,很容易在下一次对话中被遗漏;放进 Skill 后,它们就成为每次执行都要经过的检查点。

因此,我现在会优先把以下任务写成 Skill:

  • 高频重复、步骤比较固定的任务
  • 容易漏检查、出错代价较高的任务
  • 有明确输入、输出和验证方式的任务
  • 需要遵守安全边界或项目约定的任务

反过来,一次性、目标模糊、没有可验证结果的事情,不适合急着做成 Skill。

发布前检查清单

[ ] 目标和适用范围是否明确
[ ] description 能否区分应该触发和不应该触发的请求
[ ] 每一步是否能直接执行
[ ] 是否写了停止条件
[ ] 是否提供了必要的 Before / After 或示例
[ ] 关键步骤后是否有验证命令
[ ] 密钥、删除、覆盖和数据库操作是否有安全边界
[ ] 文件过长时是否应该拆成参考资料或多个 Skill
[ ] 是否用真实任务测试过,而不是只检查 Markdown 格式

好的 Skill 不需要把所有知识都装进去。先从一个边界清楚、容易验证的小任务开始,执行几次,记录哪里出错,再把这些错误变成下一版的规则。这样写出来的 Skill 才会越来越像工作流程,而不是一篇越来越长的 Prompt。