首页 › tutorials › 造补注释Skill:扫无注解函数秒出docstring

概述

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

前置条件


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

很多人上手就照着模板抄一份 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 可以放两个位置,作用域不同,新手最容易踩这个坑:

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

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 能不能被模型自动触发。

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

逐字段拆解:

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


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

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

# 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,零依赖):

#!/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:

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 为空。最终产物:

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——这正是"先理解、再注释"的副产品:补注释的过程常常顺手暴露逻辑问题。


工作流程全景图

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["报告完成 / 暴露逻辑问题"]

延伸阅读


训练营适用性评估

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

参考来源

  1. 使用 skills 扩展 Claude — Claude Code 官方文档 — Skill 创建、管理与共享的权威说明,含 frontmatter 与渐进式披露机制。
  2. 第二章:自定义 Skills — Claude Code 手册 — 讲清 Skill 与内置命令的差异、调用控制与子代理执行。
  3. Claude Code Skills 完全指南:用 SKILL.md 打造自定义工作流 — 含 SKILL.md 编写方法、即用模板与 Skills/CLAUDE.md/Hooks 协同技巧。
  4. Claude Code Skills 完全指南(2026 最新版)— 知乎 — 覆盖手动安装、插件市场与热门 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学通信知识星球海报-扫码加入