第 4 章:第一个 SKILL.md
从零写一个 SKILL.md,让你的编码 agent 能自行发现并随时跑起 PR 审查助手。
小满 · 手艺作坊
小满每次都重新摸索同一套流程。今天你给它写下第一份可复用的手艺。
草稿章节。跑通格式用的第一版,正式索引前会再打磨。
本章目标
把贯穿全书的项目(PR 审查助手)打包成一个可复用的 Agent Skill。一个 skill 就是一个文件夹,里面放一份 SKILL.md。这份文件分两部分:--- 之间的 YAML frontmatter,告诉 agent 什么时候用这个 skill;下面是 markdown 指令,skill 加载后 agent 照着做。读完本章,编码 agent 就能自己发现你的 skill,判断什么时候该用,并跑出完整审查,你不用每次会话重新粘贴清单。
有两个机制先讲清楚。第一,触发完全靠 description。agent 启动时会读每个 skill 的 name 和 description,但只有当对话里有内容跟这个 description 对上了,它才会加载完整正文。第二,文件夹名就是命令:放在 .claude/skills/pr-reviewer/SKILL.md 的 skill,也能直接用 /pr-reviewer 调起。description 写对了,自动触发和手动调用你就都有了。
前置准备
- 完成第 1 到 3 章:你已理解 agent 回路与上下文工程。
- 一个支持 Agent Skills、已装好可用的编码 agent。具体路径和字段名以官方文档为准,它们会演进。
- 一个至少有一个 pull request 或 diff 可供审查的仓库。
- 若想用第 4 步的实时 diff 版本,准备好 GitHub CLI(
gh)。
原理
skill 靠渐进披露(progressive disclosure)来省成本。启动时上下文里只有名字和描述,所以一个漏掉触发词的 description 实际上等于隐形:agent 根本没有可以匹配的东西。一旦请求匹配上,完整的 SKILL.md 正文会作为一条消息注入,并在这次会话剩下的时间里一直占着上下文,所以你写的每一行都是反复花掉的 token。下面所有准则都是从这一条推出来的:让 description 多放触发词,让正文短、且是命令式的。
动手做
1. 建 skill 文件夹
在 agent 扫描的位置建一个以 skill 命名的目录。项目级 skill 放在仓库里,队友 checkout 就能拿到;个人级 skill 放在你的家目录配置里,跟着你跨项目走。
# 项目级:提交进仓库,与团队共享
mkdir -p .claude/skills/pr-reviewer
# 个人级:只属于你,在每个项目里都可用(路径示意)
mkdir -p ~/.claude/skills/pr-reviewer
2. 写触发发现的 frontmatter
打开 SKILL.md 写 frontmatter。description 是整个文件里最重要的一行:agent 就拿它来匹配,所以必须把触发场景写明白,而且要用你实际会说的词。“审查代码”太含糊。要点名对象(PR、diff、pull request)和时机(用户要求在合并前审查改动时)。
---
name: pr-reviewer
description: >-
审查一个 pull request 或 diff 的正确性、测试、安全与清晰度。
当用户要求 review 一个 PR、审查改动、合并前检查 diff,
或问"这能合并了吗"时使用。
---
description(连同任何 when_to_use 文本)在 skill 列表里会被截断到一个文档里写明的字符上限,所以把关键用例放最前面。触发词放前面,整段控制在几句话内。
3. 写精炼、命令式的指令
在 frontmatter 下面用纯 markdown 写审查流程。说做什么,别说为什么。因为正文一旦加载就一直占着上下文,把每一行都当成每轮都要花 token 的常驻指令来写。一份聚焦、有明确输出格式的清单,比一大段啰嗦的文字管用。
# PR 审查助手
从四个维度审查范围内的 diff。每条发现给出文件与行号、一句话问题陈述、
一个具体的修复建议。
## 清单
1. 正确性:逻辑错误、差一错误、空值处理、API 用法错误。
2. 测试:新路径覆盖了吗?现有测试还成立吗?标出未测分支。
3. 安全:注入、未校验输入、代码里的密钥、不安全的反序列化。
4. 清晰度:命名、死代码、过大的函数、缺失的错误信息。
## 输出格式
- 按严重度分组:阻塞、应修、小瑕。
- 末尾给一句结论:APPROVE、APPROVE WITH NITS 或 REQUEST CHANGES。
- 若 diff 为空或取不到,直说,而不是编造发现。
4. 喂给它真实的 diff(可选但推荐)
skill 可以在 agent 读取之前,用一种文档里写明的内联 shell 语法,把实时数据拉进提示词。这样审查就基于真实的工作树,而不是 agent 从打开的文件里猜出来的东西。具体语法以官方文档为准;形态是一个用反引号包住、前缀 ! 的命令。
## 待审 diff
!`gh pr diff`
## 改动文件
!`gh pr diff --name-only`
skill 加载时,agent 会先跑这些命令,把每一行替换成命令的输出,所以指令送到时,真实 diff 已经内联在里面了。模型看不到命令本身,只看到结果。
5. 触发发现,再调试不触发
开一次会话,用你平时的说法让 agent 审查一个 PR,确认它自己加载了 pr-reviewer。如果没触发,去改触发器,别改正文,因为不触发几乎总是 description 的问题。
1. 问 agent "有哪些 skill 可用?",确认 pr-reviewer 在列。
2. 在列但不触发:说明 description 缺了你用的词。
把触发短语("审查这个 PR""检查 diff")加进去。
3. skill 很多时,列表里的描述可能被截断。
精简低优先级的 skill,或裁短自己的,让触发词留存。
4. 兜底办法:直接调起 /pr-reviewer
习得「固化手艺」那份四维审查清单不用你每次会话重新粘贴了,你把它写进了一个 SKILL.md,小满凭 description 自己判断该不该用,再照着正文跑完整审查。
如何验证
最直接的验证是用 SDK 把它跑起来:项目级 skill 放在 .claude/skills/ 下,要让 SDK 会话能发现它,得让 SDK 加载项目设置源。Python 用 skills=["pr-reviewer"] 这一个开关(SDK 会替你把项目设置源和 Skill 工具一起配好);TypeScript 用 settingSources: ["project"] 加载 .claude/skills/,并把 Skill 列进 allowedTools。然后用平常的话下达任务,看消息流里有没有出现对 Skill 工具的调用。
import anyio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock, TextBlock
async def main():
async for message in query(
prompt="审查这个 PR。", # 不点名 skill,靠 description 自动触发
options=ClaudeAgentOptions(
skills=["pr-reviewer"], # 唯一开关:自动配好项目设置源与 Skill 工具
cwd="./repo",
),
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, ToolUseBlock) and block.name == "Skill":
print(f"[触发 skill] {block.input}") # 看它自己加载了 pr-reviewer
elif isinstance(block, TextBlock):
print(block.text)
anyio.run(main)
import { query } from "@anthropic-ai/claude-agent-sdk";
const q = query({
prompt: "审查这个 PR。", // 不点名 skill,靠 description 自动触发
options: {
settingSources: ["project"], // 加载 .claude/skills/ 下的项目 skill
allowedTools: ["Skill", "Read", "Grep", "Glob", "Bash"],
cwd: "./repo",
},
});
for await (const message of q) {
if (message.type === "assistant" && message.message) {
for (const block of message.message.content) {
if (block.type === "tool_use" && block.name === "Skill") {
console.log("[触发 skill]", block.input); // 看它自己加载了 pr-reviewer
} else if (block.type === "text") {
console.log(block.text);
}
}
}
}
- 用平常的话说”审查这个 PR”,确认 agent 声明它在用
pr-reviewerskill,而不是临场发挥。 - 检查输出覆盖全部四类,并以你指令要求的那句结论收尾。
- 换种说法(“合并前帮我检查下这些改动?”),确认 skill 还能触发。这说明 description 能泛化,而不是只认某一句固定话术。
- 直接跑
/pr-reviewer,确认手动调用也产出同样结构的审查。
习得「确认它真触发」你能换几种说法说「审查这个 PR」,看小满主动声明它在用 pr-reviewer,再跑一次 /pr-reviewer,确认自动和手动两条路产出同样结构的审查。
小结
你把一次性的提示词变成了一个能被发现、能复用的 skill:一个文件夹、一段触发词丰富的 description、一份精炼命令式的正文,需要的话再喂上实时 diff。什么时候用由 agent 自己决定,你也能按名字触发。机制就是渐进披露:描述始终加载,正文匹配上才加载,所以一句准的描述加一份精简的正文,就是全部诀窍。下一章把这个 skill 配上参数和工具权限,再把 hooks 与 slash-command 接成一套可重复的工作流。
常见坑
- description 含糊。 不点明对象和时机,agent 没有可匹配的东西,永远不会加载该 skill。点名 PR、diff、审查、合并。
- 正文臃肿。 因为加载后的正文每轮都占着上下文,长
SKILL.md是一笔反复缴的税,还会让意图变模糊。保持命令式、简短;把长篇参考材料拆到单独的附带文件里,由SKILL.md引用。 - 文件夹放错位置。 放错位置的 skill 是不可见的。对照官方文档确认项目级与个人级路径。
- skill 太多、描述被截断。 skill 列表拥挤时描述会被裁短,你的触发词可能被切掉。精简低优先级的 skill,让常用的保留完整文本。
你写下第一份 SKILL.md,小满第一次无师自通地走完整套流程,完事还难得地省掉了所有免责声明,干脆利落。手艺作坊,亮了。
刚点亮 手艺作坊 · 地图已点亮 5 / 16
来源
- Anthropic Agent Skills 官方文档 · official
- anthropics/skills(示例 skill 仓库) · official