<!-- title: 从零打造首个自定义Skill -->

## 概述

本教程面向已经用 Claude Code 跑通过项目、想把自己的重复劳动固化成"一次编写、自动触发"能力的开发者。学完你将掌握一个自定义 Skill（技能）的完整解剖结构、`description` 触发工程的写法、高/中/低三档自由度的取舍、渐进式披露的多文件组织方式，以及最关键的——评测驱动的迭代闭环。全程用一个可落地的 `documenting-functions`（函数文档字符串生成）技能做载体，从空目录一路带到发布。

## 前置条件

- 已安装 Claude Code（CLI 或 VS Code 扩展均可），能跑通基本对话
- 了解 `~/.claude/skills/`（个人级）与项目 `.claude/skills/`（项目级）两个技能目录的区别
- 一点点 YAML 和 Markdown 常识；脚本示例用 Python 3，不装额外依赖

---

## 一、先搞清楚：一个 Skill 到底是什么

很多教程一上来就让你"建个 SKILL.md"，但没说清技能的本质，导致写出来的东西不触发、或触发了也用不对。官方把 Skill 定义得很明确：

> 一个 Skill 就是一个**文件夹**，里面有指令、脚本和资源。文件夹的入口是 `SKILL.md`，它带一段 YAML 头部（frontmatter），告诉 Claude 这个技能**做什么、什么时候用**。

技能和普通提示词最大的区别在于**渐进式披露（progressive disclosure）**这条运行时规则：

- 启动时，系统**只**把所有技能的 `name` 和 `description` 预加载进系统提示词；
- 只有当某个技能被认为"与当前任务相关"时，Claude 才会去读它的 `SKILL.md` 正文；
- 正文里引用的其它文件，按需再读，读之前**零 token 占用**。

这意味着两件事：第一，`description` 是技能能不能被选中的唯一门票；第二，技能里塞再多文件都不怕，因为不读就不花钱。下面这张图把这套机制画清楚了：

```mermaid
flowchart TD
    A[启动: 加载所有技能的 name+description] --> B{用户发请求}
    B -->|描述命中| C[触发目标技能]
    C --> D[读取 SKILL.md 正文]
    D --> E{需要更多细节?}
    E -->|是| F[按需读 reference / 执行 scripts]
    E -->|否| G[执行任务]
    F --> G
    B -->|描述不命中| H[该技能文件零 token 占用]
```

理解了机制，我们再看结构。一个成熟技能的目录长这样：

```
documenting-functions/
├── SKILL.md            # 主指令（触发后才加载）
├── reference/          # 参考资料（按需加载）
│   ├── python.md
│   ├── javascript.md
│   └── go.md
└── scripts/            # 工具脚本（执行，不进上下文）
    └── extract_signatures.py
```

## 二、YAML 头部：两条铁律

`SKILL.md` 顶部的 frontmatter 只有两个**必填**字段，但每条都有硬约束，违反就直接校验失败或无法触发。

### `name` 字段

- 最多 64 个字符
- **只能**包含小写字母、数字、连字符（hyphen）
- 不能含 XML 标签
- **保留词禁用**：不能出现 `anthropic`、`claude`

命名建议用动名词形式（gerund，动词+-ing），因为它最能表达"这个技能提供的能力是一种活动"。官方给的正面例子是 `processing-pdfs`、`analyzing-spreadsheets`、`testing-code`；可接受的备选是名词短语 `pdf-processing` 或动作型 `process-pdfs`。要避开的是 `helper`、`utils`、`tools` 这种含糊名，以及一个技能库里命名风格前后不一致。

### `description` 字段

- 非空，最多 1024 个字符
- 不能含 XML 标签
- **必须同时写清"做什么（what）"和"什么时候用（when）"**

这是整篇教程最重要的一节，下文单独展开。

> 补充：`license` 是可选字段；旧版规范里的 `allowed-tools` 在新版里也已变为可选。先只关心 `name` 和 `description` 就够了。

## 三、描述工程：决定技能生死的 1024 个字符

官方文档有一句话点透了它的分量：

> `description` 字段让技能可被发现。Claude 用它从潜在 100 多个技能里挑出正确的那一个。

换句话说，这 1024 个字符是技能在"海选"阶段的唯一发言机会。写不好，正文写得再漂亮也没人加载。

### 规则一：永远用第三人称

`description` 会被注入系统提示词，人称混乱会导致识别问题。

```
✅ 好：Generates docstrings for Python functions and adds JSDoc to JS methods.
❌ 坏：I can help you write docstrings.
❌ 坏：You can use this skill to document functions.
```

### 规则二：what + when，缺一不可

只写"做什么"不够，必须补上"什么场景下该选我"。下面三组对照来自官方：

```
# PDF 处理
✅ Extract text and tables from PDF files, fill forms, merge documents.
   Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.

# Excel 分析
✅ Analyze Excel spreadsheets, create pivot tables, generate charts.
   Use when analyzing Excel files, spreadsheets, tabular data, or .xlsx files.

# Git 提交
✅ Generate descriptive commit messages by analyzing git diffs.
   Use when the user asks for help writing commit messages or reviewing staged changes.
```

反面教材是这类空话，它们等于没写：`Helps with documents.` / `Processes data.` / `Does stuff with files.`

### 规则三：塞进用户真实会打的"触发词"

`when` 子句里要尽量包含用户嘴边会冒出来的关键词。写文档技能时，人们可能说"给函数加注释"、"补上 docstring"、"这个模块缺 API 文档"、"JSDoc 写一下"——把这些词都写进 description，命中率会显著提升。

### 实操：为本教程的技能写 description

先写一个差的：

```yaml
description: Helps document code.
```

再迭代成符合三条规则的版本（这就是我们最终采用的）：

```yaml
description: Generates standards-compliant docstrings and API comments for
  functions and methods — Google/NumPy/Sphinx style for Python, JSDoc for
  JavaScript/TypeScript, godoc for Go. Use when the user asks to document,
  annotate, or add docstrings/comments to a function, class, method, or
  module, or when improving API documentation coverage.
```

注意它一次满足了：第三人称、what（生成符合规范的 docstring/JSDoc/godoc）、when（"document / annotate / add docstrings / 提升文档覆盖率"等触发词）、关键术语（Google/NumPy/Sphinx/JSDoc/godoc）。

## 四、选对"自由度"：别把开阔地当成独木桥

写正文前，先判断这个技能该给 Claude 多大的发挥空间。官方用一个比喻很到位：把 Claude 想象成一个探路的机器人——

- **独木桥两侧是悬崖**：只有一条安全路，必须给精确护栏（**低自由度**）。比如数据库迁移必须严格按序执行。
- **开阔地没有障碍**：条条大路通罗马，给个方向让它自己走（**高自由度**）。比如代码审查，上下文决定最佳做法。

三档对应到写法：

| 自由度 | 写法 | 适用场景 |
|--------|------|----------|
| 高（文本指令） | 列要点、给启发式 | 多种方法都对、依赖上下文判断 |
| 中（带参数的伪代码/脚本） | 给首选模式 + 可配置 | 有偏好模式、允许变体 |
| 低（固定脚本、几乎无参数） | "只许这么做" | 脆弱易错、一致性至上 |

我们的文档技能主要是**中到高**自由度：模板是首选（中），但具体措辞要据函数语义调整（高）。下面正文就按这个定位来写。

## 五、写正文：把 Claude 当成已经很聪明的人

正文的核心原则是**精简**。官方的原话是："上下文窗口是公共资源。"你要反复拷问每一段："Claude 真的需要这段解释吗？它本来不知道这个吗？"一个经典反面：

```markdown
# ❌ 太啰嗦（约 150 token）
PDF（便携式文档格式）是一种常见的文件格式，包含文本、图像等内容。
要提取文本，你需要用一个库。有很多库可以选，pdfplumber 比较好用……
```

```markdown
# ✅ 精简（约 50 token）
## 提取 PDF 文本
用 pdfplumber：
import pdfplumber
with pdfplumber.open("file.pdf") as pdf:
    text = pdf.pages[0].extract_text()
```

精简版默认 Claude 已经知道 PDF 是什么、库怎么用。两条硬指标记住：**正文不超过 500 行**，超了就拆文件；**引用只允许一层深**（SKILL.md 直接指向 reference 文件，不要 reference 互相套娃，否则 Claude 会用 `head -100` 之类部分读取，漏掉关键信息）。

此外几个反模式要绕开：

- 路径一律正斜杠 `reference/guide.md`，**绝不**写 `reference\guide.md`，Windows 上也用正斜杠
- 别一次抛出五六个备选库，给一个默认值 + 一个逃生口就够
- 引用 MCP 工具时用**全限定名** `ServerName:tool_name`，否则多服务器时会"工具找不到"

## 六、实战：从零构建 `documenting-functions`

现在把前面所有概念落到一个真实可跑的技能上。目标：用户说"给 `utils.py` 的函数补文档"，技能自动生成符合各语言规范的注释。

### 步骤 1：建目录

```bash
mkdir -p ~/.claude/skills/documenting-functions/{reference,scripts}
```

### 步骤 2：写 `SKILL.md`

```markdown
---
name: documenting-functions
description: Generates standards-compliant docstrings and API comments for
  functions and methods — Google/NumPy/Sphinx style for Python, JSDoc for
  JavaScript/TypeScript, godoc for Go. Use when the user asks to document,
  annotate, or add docstrings/comments to a function, class, method, or
  module, or when improving API documentation coverage.
---

# Documenting Functions

## Quick start
1. Determine the target language from the file extension.
2. Run the signature extractor to inventory every function.
3. Write a docstring/comment following that language's convention.

## Conventions
- **Python** (.py): Google-style docstrings → see [reference/python.md](reference/python.md)
- **JavaScript/TypeScript** (.js/.ts/.tsx): JSDoc → see [reference/javascript.md](reference/javascript.md)
- **Go** (.go): godoc conventions → see [reference/go.md](reference/go.md)

## Signature extraction
List every function/method and its parameters before writing docs:

    python scripts/extract_signatures.py path/to/file.py

## Rules
- Document every **public** function or method. Skip names starting with `_`
  unless the user explicitly asks.
- Infer parameter and return types from the code; mark unknowns as `(unknown)`
  rather than guessing.
- Keep summaries to one imperative sentence.

## Workflow checklist
Copy and track your progress:

    [ ] Identified the language
    [ ] Extracted signatures
    [ ] Wrote a docstring for each public function
    [ ] Listed Args / Returns / Raises (or the language equivalent)
    [ ] Did not document trivial one-line getters
```

### 步骤 3：写各语言的 reference（渐进式披露的"第二层"）

`reference/python.md`：

```markdown
# Python docstrings (Google style)

## Template

    """One-line summary in imperative mood.

    Optional extended description.

    Args:
        param_name (type): Description.

    Returns:
        type: Description.

    Raises:
        ExceptionType: When this happens.
    """

## Rules
- First line ends with a period and fits on one line.
- Document every argument, return value, and raised exception.
```

`reference/javascript.md`：

```markdown
# JSDoc

## Template

    /**
     * One-line summary.
     *
     * @param {Type} paramName - Description.
     * @returns {Type} Description.
     */

## Rules
- Always include @param and @returns for non-void functions.
- Use {Type} braces for type annotations.
```

`reference/go.md` 留作练习，写 godoc 约定即可。这样设计的好处是：用户只写 Python 时，JS 和 Go 两个文件**完全不被读取**，零 token 浪费。

### 步骤 4：写工具脚本 `scripts/extract_signatures.py`

按官方"提供工具脚本"原则——能预制的脚本比让 Claude 现写更可靠、更省 token、更一致。同时遵守"自己处理错误，别甩给 Claude"：

```python
#!/usr/bin/env python3
"""List every function/method signature in a Python file."""
import ast
import sys


def main(path: str) -> None:
    try:
        with open(path, encoding="utf-8") as f:
            tree = ast.parse(f.read(), filename=path)
    except FileNotFoundError:
        print(f"file not found: {path}", file=sys.stderr)
        sys.exit(1)

    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            args = [a.arg for a in node.args.args]
            kind = "method" if isinstance(node, ast.FunctionDef) else "function"
            print(f"{kind} {node.name}({', '.join(args)})  # line {node.lineno}")


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("usage: extract_signatures.py <file.py>", file=sys.stderr)
        sys.exit(1)
    main(sys.argv[1])
```

注意它示范了官方强调的两个好习惯：`try/except FileNotFoundError` 显式兜底而不是抛给模型；参数个数校验给出明确的 usage 信息。

### 步骤 5：验证触发与执行

新开一个 Claude Code 会话（让技能被重新加载），扔一句不含技能名的请求：

```
帮我把 utils.py 里所有函数都补上规范文档
```

**预期结果**：Claude 选中 `documenting-functions`，先跑 `extract_signatures.py` 列出函数，再按 Google 风格逐个补 docstring，跳过 `_` 开头的私有函数。

如果没触发，第一反应永远是回去改 `description` 的 when/触发词，而不是改正文。

## 七、评测驱动迭代：别先写文档，先写评测

这是新手最常踩的坑——凭想象写一大堆指令，结果真实任务里根本用不上。官方的**评测驱动开发（evaluation-driven development）**顺序是反过来的：

1. **找缺口**：不带任何技能，让 Claude 跑代表性任务，记录它哪里翻车
2. **写评测**：针对这些缺口造**至少三个**场景
3. **定基线**：测量"无技能"时的表现
4. **写最小指令**：只写够通过评测的内容
5. **迭代**：跑评测、对比基线、精修

评测本身是一个结构化文件，官方给出的格式如下（我们为文档技能改写）：

```json
{
  "skills": ["documenting-functions"],
  "query": "Add Google-style docstrings to every public function in utils.py",
  "files": ["utils.py"],
  "expected_behavior": [
    "Reads utils.py and lists all public functions (skips names starting with _)",
    "Generates Google-style docstrings with Args/Returns/Raises sections",
    "Infers parameter and return types from the code",
    "Does not document trivial one-line getters unless asked"
  ]
}
```

文档里特别说明：目前没有官方内置的评测运行器，需要你自建一套（社区里 `skill-creator` 插件就提供了评测 + 方差分析的能力，可作为脚手架）。评测就是衡量技能效果的"真相之源"。

### Claude A / Claude B 双实例迭代环

官方推荐的高效迭代模式是用**两个** Claude 实例：

```mermaid
flowchart TD
    A["Claude A（作者）: 撰写/改进 SKILL.md"] --> B["Claude B（执行者）: 带上技能跑真实任务"]
    B --> C{观察 Claude B 行为}
    C -->|发现问题| D["记录: 哪里没触发 / 哪里出错 / 漏读哪个文件"]
    D --> A
    C -->|表现良好| E[发布或分享给团队]
```

关键在于**基于观察改，而不是基于假设改**。比如你发现 Claude B 给私有函数 `_helper` 也补了文档，那回到 Claude A，把 SKILL.md 里"跳过 `_` 开头"这条规则**上移并加粗**（官方建议用 `MUST skip` 而不是 `always skip`，措辞强度也影响执行率）。再比如你发现 Claude B 从不点开 `reference/go.md`，那要么是没人写 Go，要么是正文里的链接不够显眼——按真实观察决定，而不是猜。

还有个常被忽略的点：**观察 Claude 是怎么用技能的**。它读文件的顺序和你预期一样吗？有没有反复读同一个文件（说明那部分该挪进 SKILL.md 正文）？有没有某个 reference 从没被碰过（说明可能多余或信号太弱）？这些都比凭空优化有效得多。

## 八、整条工作流一图总览

把从零到发布的所有环节串起来：

```mermaid
flowchart LR
    A["1.命名与定位<br/>(gerund, ≤64, 禁保留词)"] --> B["2.写 description<br/>(第三人称+what+when)"]
    B --> C["3.选自由度<br/>(高/中/低)"]
    C --> D["4.写正文<br/>(精简, ≤500行)"]
    D --> E["5.组织多文件<br/>(渐进式披露, 一层深)"]
    E --> F["6.加脚本<br/>(自己处理错误)"]
    F --> G["7.评测迭代<br/>(Claude A↔B)"]
    G -->|未达标| B
    G -->|达标| H[发布/分享]
```

## 九、常见问题与排查

**技能死活不触发。** 99% 是 `description` 问题。检查：是不是用了第一/第二人称？是不是只写了 what 没写 when？触发词和用户真实说法对得上吗？把当着用户面会说的词原样写进去。

**触发了但执行不对。** 先确认自由度档位。该用低自由度（固定脚本）的别给高自由度文本；反之亦然。再看正文是否被 Claude 部分读取——多半是引用嵌套超过一层。

**正文太长总被截断。** 超过 500 行就拆。长 reference 文件在顶部加一个目录（Contents），让 Claude 即便用 `head` 预览也能看到全貌。

**脚本找不到 / 工具报错。** 路径用正斜杠；MCP 工具用 `ServerName:tool_name` 全限定名；依赖包在 SKILL.md 里显式声明并验证可用（注意 claude.ai 能联网装包，API 环境不能）。

**技能在某些模型上表现差。** 官方明确要求"在计划使用的所有模型上测试"：Haiku 要更详细的指引，Sonnet 看是否清晰高效，Opus 看是否过度解释。同一个技能跨模型时，目标写成"三种都过得去"。

## 十、延伸阅读

- 官方技能总览与概念：`https://platform.claude.com/docs/en/agents-and-tools/agent-skills` —— 先读"技能如何工作"理解渐进式披露的底层架构
- Claude Code 中的技能管理：`https://code.claude.com/docs/en/skills` —— 个人级/项目级技能的创建与分享
- `skill-creator` 插件：`https://claude.com/plugins/skill-creator` —— 官方维护的技能开发、评测、方差分析脚手架

---

## 训练营适用性评估

- **适合的训练营类型**：Claude Code / AI 编码入门到进阶训练营的**第二模块**（学员已会基本对话和项目管理，开始做能力沉淀）。也可作为企业内部"AI 效能工程"培训的核心实验课。
- **预计教学时长**：讲授 45 分钟 + 动手实验 60 分钟（当场建出 `documenting-functions` 并跑通评测）+ 分组互测 30 分钟，合计约 2.5 小时。
- **能力梯度**：偏"方法论 + 轻量代码"，对零基础略快；建议放在"装好 Claude Code 并跑通首个项目"之后、"Hooks/自动化"之前，作为从"用工具"到"造工具"的过渡。

## 参考来源

1. [Skill authoring best practices — Claude API Docs](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) — 官方权威指南，frontmatter 规则、description 写法、自由度、评测驱动开发均出自此处
2. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — Claude Code 中技能的创建、管理与分享（官方文档）
3. [Skill Creator — Claude Plugin | Anthropic](https://claude.com/plugins/skill-creator) — 官方技能开发与评测/方差分析脚手架插件
4. [Claude Code Skills: The Complete Guide (2026)](https://duet.so/guides/claude-code-skills-complete-guide) — 社区技术博客，技能目录结构与实战场景补充
5. [Build a Claude Skill: YAML Frontmatter, SKILL.md Instructions](https://sjramblings.io/building-skills-for-claude-part-2/) — 教程类资料，手把手 SKILL.md 与 frontmatter 实操

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
