<!-- title: 造补注释Skill：扫无注解函数秒出docstring -->

## 概述

本教程带你从零造一个名为 `docstring-forge` 的自定义 Claude Code Skill：给它一个 Python 文件，它自动扫描出所有缺 docstring 的函数/方法，再按你团队的风格（Google / NumPy）批量补齐注释。在这个过程中你会彻底吃透 Skill 的四大核心机制——`SKILL.md` 骨架、frontmatter 元数据、渐进式披露（progressive disclosure）、捆绑脚本调用。适合已经会基础命令、想把自己的重复操作沉淀成可复用能力的开发者，预计动手时长 60–90 分钟。

## 前置条件

- 已安装 Claude Code（终端能跑 `claude --version`）；
- 本机有 Python 3.8+（捆绑的分析脚本用 `ast` 标准库，无需额外依赖）；
- 一个用来练手的 Python 项目目录（没有就新建一个空文件夹即可）；
- 对 Markdown、YAML 有基本认识，看得懂 `def` 和函数签名。

---

## 第一步：先建立正确的"心智模型"

很多人上手就照着模板抄一份 `SKILL.md`，跑不通再来调，根因是没想清楚 **Skill 到底是什么、和 CLAUDE.md / Hook / 子代理 有什么区别**。先厘清三句话：

1. **一个 Skill = 一个目录，核心是一个 `SKILL.md` 文件。** 目录名就是命令名，Claude Code 启动时会扫描 `skills/` 下所有目录，把每个 `SKILL.md` 顶部的 frontmatter（`name` + `description`）登记进"菜单"。
2. **Skill 正文是按需加载的，这正是"渐进式披露"的价值。** CLAUDE.md 的内容每次都进上下文，而 Skill 的正文只有被触发时才加载。所以一个 500 行的详细操作手册可以塞进 Skill 而不拖慢日常对话。
3. **Skill 是"提示词驱动"的能力扩展，不是死命令。** 它告诉模型"遇到这类任务该怎么做"，模型再调用 Read/Bash/Edit 等工具去执行。这与内置命令（如 `/clear`，固定系统逻辑、即时执行）本质不同。

**什么时候该造 Skill？** 官方文档给了一个非常准确的信号：当你发现自己**反复把同一段说明、检查清单或多步流程粘贴进对话**，或者 CLAUDE.md 里某一段已经从"陈述事实"演变成了"操作流程"——就该把它抽成 Skill。补 docstring 正是典型场景：你不会希望 CLAUDE.md 每次都背着一大段"如何写注释"的规范，但它确实是高频且需要一致性的任务。

| 机制 | 加载时机 | 谁触发 | 适合放什么 |
|------|----------|--------|------------|
| CLAUDE.md | 每次都加载 | 自动 | 项目事实、规矩、约定 |
| Skill（模型自动触发） | 命中 description 时 | 模型判断相关性 | 高频任务的标准化流程 |
| Skill（显式调用） | `/skill-name` 时 | 你手动 | 想精确控制的流程 |
| Hook | 特定事件 | 系统事件 | 强制护栏、自动后处理 |

---

## 第二步：搭骨架——创建目录与文件

Skill 可以放两个位置，作用域不同，新手最容易踩这个坑：

- **个人级**：`~/.claude/skills/<名字>/SKILL.md`——所有项目都能用；
- **项目级**：`<项目根>/.claude/skills/<名字>/SKILL.md`——只在该项目生效，可随 Git 提交给团队共享。

我们做一个**项目级**的，方便随仓库带走。在项目根目录执行：

```bash
mkdir -p .claude/skills/docstring-forge/scripts
```

得到这样的目录树（这是 Skill 最小骨架）：

```
.claude/
└── skills/
    └── docstring-forge/
        ├── SKILL.md          ← 主文件：元数据 + 操作指令
        └── scripts/
            └── analyze.py     ← 捆绑脚本：用 AST 找无注释函数
```

> **预期结果**：目录建好后，启动或重启 Claude Code，输入 `/` 应该能在菜单里看到 `docstring-forge`。如果看不到，多半是位置放错或 YAML 有语法错误，见第七步排查。

---

## 第三步：写 frontmatter——三个字段决定 Skill 的生死

`SKILL.md` 顶部是一段 YAML frontmatter，用 `---` 包起来。其中**最关键的不是正文，而是 description**——它直接决定 Skill 能不能被模型自动触发。

```yaml
---
name: docstring-forge
description: 扫描 Python 文件中缺少 docstring 的函数与方法，并按指定风格（Google/NumPy）批量补写注释。当用户要求"补注释/补 docstring/给函数加文档/规范注释"或抱怨"这些函数没说明"时使用本 skill。
allowed-tools:
  - Read
  - Grep
  - Glob
  - Bash
  - Edit
---
```

逐字段拆解：

- **`name`**：必须与目录名一致，全小写、连字符分隔（`docstring-forge`）。
- **`description`（重中之重）**：模型只看这一段来决定"要不要在当前对话激活这个 Skill"。它要回答两个问题——**这个 Skill 能做什么**，以及**什么时候该用它**。诀窍是显式写出**触发词和场景**（"补注释""给函数加文档""没说明"），而不是泛泛地写"处理代码"。
- **`allowed-tools`**：白名单，限定 Skill 执行期可用的工具。原则是**最小权限**——这个 Skill 要读文件（Read）、运行脚本（Bash）、改文件（Edit），就只列这五个，绝不写 `*`。

> **常见坑**：description 写成"一个强大的代码工具"，模型永远猜不到该用它，于是你只能手动 `/docstring-forge`。把触发场景写进 description，自动触发率会立刻上升。

---

## 第四步：写正文指令——教模型"怎么做"

frontmatter 之下就是正文。正文只在 Skill 被触发时加载，所以可以写得详细，但仍要**结构清晰、可执行**。核心是告诉模型三件事：**输入是什么、步骤是什么、输出长什么样**。

```markdown
# docstring-forge

为 Python 源文件中缺失 docstring 的函数/方法批量补写注释。

## 工作流程

1. 确认目标：向用户确认要处理的文件或目录，以及希望的 docstring 风格
   （默认 Google 风格；可选 NumPy）。若无明确风格，优先沿用文件中已有 docstring 的风格。
2. 运行分析：对目标调用捆绑脚本 `scripts/analyze.py`，拿到缺注释函数清单（JSON）。
   命令形如：`python3 scripts/analyze.py <目标路径>`
   脚本路径相对于本 skill 目录。
3. 阅读上下文：用 Read 读取每个目标函数及其上下文（调用方、被调用方、类型提示），
   理解其真实行为，杜绝凭函数名臆测。
4. 生成注释：按选定风格生成 docstring，必须包含：
   - 一句话功能摘要（祈使句，首字母大写）。
   - Args：每个参数一行，含类型与含义。
   - Returns：返回值类型与含义；无返回值写 None。
   - Raises：仅当函数确实显式抛出异常时才写。
5. 落盘：用 Edit 把 docstring 插回原位，缩进与函数体对齐，绝不改动业务逻辑代码。
6. 复核：重新跑一次 analyze.py，确认无注释函数数量下降到 0（或仅剩用户确认跳过的）。

## 风格示例（Google）

def transfer(amount, src, dst):
    """从一个账户向另一个账户转账。

    Args:
        amount: 转账金额，单位元，必须为正。
        src: 源账户 ID。
        dst: 目标账户 ID。

    Returns:
        转账成功后的流水号字符串。

    Raises:
        ValueError: amount 非正数时抛出。
    """

## 禁止事项

- 不得修改函数签名、函数体、返回逻辑。
- 不得臆造参数含义；看不懂就标注"待确认"并提示用户。
- 单行函数/lambda 不强求注释。
```

要点：**用祈使句写步骤**（"运行分析"而非"你可以运行"）、**给正反例**（风格示例 + 禁止事项）、**把判断点明确化**（"无返回值写 None"）。这种"说明书式"正文正是 Skill 区别于零散 prompt 的地方。

---

## 第五步：捆绑分析脚本——把"脏活"交给确定性代码

模型擅长写自然语言注释，但不擅长精确地"数哪个函数没注释"——它会漏、会数错。这就是**捆绑脚本**的价值：把确定性的部分交给代码，把需要理解力的部分交给模型。这就是渐进式披露在工程上的最佳实践——**复杂逻辑拆到脚本里，正文只做编排**。

在 `scripts/analyze.py` 写入（用标准库 `ast`，零依赖）：

```python
#!/usr/bin/env python3
"""扫描 Python 文件，找出缺少 docstring 的函数与方法。"""
import ast, json, sys, pathlib

def has_docstring(node):
    """判断函数节点首条语句是否为文档字符串。"""
    body = getattr(node, "body", [])
    if not body:
        return False
    first = body[0]
    return (isinstance(first, ast.Expr)
            and isinstance(first.value, ast.Constant)
            and isinstance(first.value.value, str))

def analyze(path):
    """解析单个文件，返回缺注释函数清单。"""
    src = pathlib.Path(path).read_text(encoding="utf-8")
    tree = ast.parse(src, filename=str(path))
    missing = []
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            args = [a.arg for a in node.args.args]
            if not has_docstring(node):
                missing.append({
                    "name": node.name,
                    "lineno": node.lineno,
                    "args": args,
                    "kind": "method" if node.name.startswith("__") else "function",
                })
    return {"file": str(path), "total_funcs": sum(
        1 for n in ast.walk(tree)
        if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))),
        "missing": missing}

def main():
    """命令行入口，支持文件或目录，输出 JSON。"""
    if len(sys.argv) < 2:
        print(json.dumps({"error": "usage: analyze.py <path> [path...]"}))
        sys.exit(2)
    results = []
    for p in sys.argv[1:]:
        root = pathlib.Path(p)
        files = [root] if root.is_file() else root.rglob("*.py")
        for f in files:
            try:
                results.append(analyze(f))
            except SyntaxError as e:
                results.append({"file": str(f), "error": f"语法错误: {e}"})
    print(json.dumps(results, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()
```

> **预期输出**：对一段缺注释的代码运行，得到结构化结果，模型读它比读整份源码省 token、还不会数错：
>
> ```json
> [
>   {
>     "file": "src/billing.py",
>     "total_funcs": 3,
>     "missing": [
>       {"name": "transfer", "lineno": 12, "args": ["amount", "src", "dst"], "kind": "function"},
>       {"name": "__init__", "lineno": 40, "args": ["self", "account_id"], "kind": "method"}
>     ]
>   }
> ]
> ```

> **常见坑**：脚本路径要在正文里写清楚是"相对于 skill 目录"。不同工作目录下直接写 `analyze.py` 会找不到，模型调用时会用 `python3 scripts/analyze.py …` 这种相对路径，配合正文说明即可稳定运行。

---

## 第六步：触发与调用——两种入口

Skill 有两个触发入口，理解差异才能用得顺手：

1. **模型自动触发**：你在对话里说"这个 billing.py 一句注释都没有，帮我补一下"，模型比对各 Skill 的 `description`，判断 `docstring-forge` 命中，于是自动激活、加载正文、按步骤执行。**你完全不需要提"skill"这个词。**
2. **显式调用**：你输入 `/docstring-forge`，直接强制激活。适合你知道要干嘛、不想依赖模型判断时。

两种入口殊途同归，最终都走到同一条"加载正文 → 编排工具执行"的链路。

---

## 第七步：调试与迭代——90% 的"不工作"都在这

| 现象 | 根因 | 解决 |
|------|------|------|
| `/` 菜单里看不到这个 Skill | 目录位置错 / frontmatter YAML 语法错 / name 与目录名不一致 | 放对 `.claude/skills/<name>/`；用 YAML 校验器检查；改完重启会话 |
| 说了"补注释"却不自动触发 | description 没写清触发场景与关键词 | 把"补注释/补 docstring/给函数加文档"等真实说法写进 description |
| 模型没用脚本，自己硬数函数 | 正文没强调"必须先跑 analyze.py" | 在流程第 2 步加粗、并在禁止事项里写"不得人工枚举函数" |
| 脚本报路径找不到 | 用了绝对路径或写错相对路径 | 正文统一写 `python3 scripts/analyze.py <目标>`，相对 skill 目录 |
| 改完文件没生效 | 正文有缓存 | `/reload` 或新开会话 |
| 自动触发太频繁（误触发） | description 过于宽泛（如"处理代码"） | 收窄为"**仅**缺 docstring 的场景"，加排他条件 |

迭代的关键是**把 description 当成给模型的"广告位"来打磨**——它一句话决定命中率。想量化命中率，可以用 eval 基准（构造一批"该触发/不该触发"的对话样本），这是更进阶的话题。

---

## 实战案例：给一个真实模块补齐注释

准备一个故意没注释的文件 `src/billing.py`：

```python
class Account:
    def __init__(self, account_id, balance=0):
        self.account_id = account_id
        self.balance = balance

    def charge(self, amount):
        if amount <= 0:
            raise ValueError("amount must be positive")
        self.balance += amount
        return self.balance


def transfer(amount, src, dst):
    src.charge(-amount)
    dst.charge(amount)
    return f"{src.account_id}->{dst.account_id}:{amount}"
```

在 Claude Code 里说：**"给 src/billing.py 补上 docstring，用 Google 风格。"**

你会看到模型依次：跑 `analyze.py` 拿到 4 个缺注释函数 → Read 上下文理解 `charge`/`transfer` 的真实语义 → 生成注释 → Edit 落盘 → 复跑 `analyze.py` 确认 `missing` 为空。最终产物：

```python
class Account:
    def __init__(self, account_id, balance=0):
        """初始化一个账户。

        Args:
            account_id: 账户唯一标识。
            balance: 初始余额，单位元，默认 0。
        """
        self.account_id = account_id
        self.balance = balance

    def charge(self, amount):
        """对账户进行一笔入账或扣账。

        Args:
            amount: 金额，正数为充值，负数为扣款。

        Returns:
            操作后的账户余额。

        Raises:
            ValueError: amount 非正数时抛出（注意：扣款场景请传负值）。
        """
        if amount <= 0:
            raise ValueError("amount must be positive")
        self.balance += amount
        return self.balance
```

注意模型甚至发现了 `transfer` 里传 `-amount` 给 `charge`、而 `charge` 又拒绝非正数的**潜在 bug**——这正是"先理解、再注释"的副产品：补注释的过程常常顺手暴露逻辑问题。

---

## 工作流程全景图

```mermaid
flowchart TD
    U["用户：'给 billing.py 补 docstring'"] --> M["模型读取各 Skill 的 description"]
    M -->|"命中 docstring-forge"| L["加载 SKILL.md 正文（渐进式披露）"]
    M -->|"未命中"| F["回退为普通对话"]
    L --> S1["Bash 运行 scripts/analyze.py"]
    S1 --> J["拿到缺注释函数 JSON 清单"]
    J --> S2["Read 读取函数上下文"]
    S2 --> S3["按风格生成 docstring"]
    S3 --> S4["Edit 落盘回原位"]
    S4 --> S5["复跑 analyze.py 校验 missing=0"]
    S5 --> D["报告完成 / 暴露逻辑问题"]
```

---

## 延伸阅读

- **官方文档·使用 Skills 扩展 Claude**（code.claude.com）——权威的字段定义、目录约定与共享方式，遇到分歧以它为准。
- **Agent Skills 开放标准**（github.com/anthropics）——Claude Code Skills 基于该标准扩展，想跨工具复用 Skill 时必读。
- **skill-creator 内置 Skill**——Claude Code 自带的"造 Skill 的 Skill"，可用 `/skill-creator` 让它反向帮你生成骨架，与本教程的手写法互为补充。
- **进阶：用 eval 打磨触发准确率**——当你的 Skill 多到互相抢触发时，构造正负样本做基准测试是下一步。

---

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| 适合训练营类型 | 偏工程实战的开发营、AI 编程提效营、团队规范落地营；不适合纯概念入门营 |
| 学员基础 | 会 Python 基本语法、用过 Claude Code 对话即可，无需 AST 经验 |
| 教学时长 | 讲解 30 分钟 + 动手 45 分钟 + 迭代答疑 15 分钟，合计约 90 分钟 |
| 可拓展作业 | ① 改造支持 JavaScript/JSDoc；② 加"只补公开 API（非 `_` 前缀）"开关；③ 接 eval 测触发率 |
| 评估标准 | 能独立造出一个可在 `/` 菜单出现的 Skill，且自动触发率 ≥ 80%（用 10 条对话样本测） |

---

## 参考来源

1. [使用 skills 扩展 Claude — Claude Code 官方文档](https://code.claude.com/docs/zh-CN/skills) — Skill 创建、管理与共享的权威说明，含 frontmatter 与渐进式披露机制。
2. [第二章：自定义 Skills — Claude Code 手册](https://joeyyu23.github.io/claude-code-handbook/zh/book2-advanced/02-custom-skills) — 讲清 Skill 与内置命令的差异、调用控制与子代理执行。
3. [Claude Code Skills 完全指南：用 SKILL.md 打造自定义工作流](https://www.heyuan110.com/zh/posts/ai/2026-02-28-claude-code-skills-guide/) — 含 SKILL.md 编写方法、即用模板与 Skills/CLAUDE.md/Hooks 协同技巧。
4. [Claude Code Skills 完全指南（2026 最新版）— 知乎](https://zhuanlan.zhihu.com/p/2034659438850750016) — 覆盖手动安装、插件市场与热门 Skills 推荐，便于扩展阅读。

---

> 关注「爱卫生」获取更多 AI 编程实战教程。

> [!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)
