本教程带你从零造一个名为 docstring-forge 的自定义 Claude Code Skill:给它一个 Python 文件,它自动扫描出所有缺 docstring 的函数/方法,再按你团队的风格(Google / NumPy)批量补齐注释。在这个过程中你会彻底吃透 Skill 的四大核心机制——SKILL.md 骨架、frontmatter 元数据、渐进式披露(progressive disclosure)、捆绑脚本调用。适合已经会基础命令、想把自己的重复操作沉淀成可复用能力的开发者,预计动手时长 60–90 分钟。
claude --version);ast 标准库,无需额外依赖);def 和函数签名。很多人上手就照着模板抄一份 SKILL.md,跑不通再来调,根因是没想清楚 Skill 到底是什么、和 CLAUDE.md / Hook / 子代理 有什么区别。先厘清三句话:
SKILL.md 文件。 目录名就是命令名,Claude Code 启动时会扫描 skills/ 下所有目录,把每个 SKILL.md 顶部的 frontmatter(name + description)登记进"菜单"。/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 提交给团队共享。我们做一个项目级的,方便随仓库带走。在项目根目录执行:
mkdir -p .claude/skills/docstring-forge/scripts
得到这样的目录树(这是 Skill 最小骨架):
.claude/
└── skills/
└── docstring-forge/
├── SKILL.md ← 主文件:元数据 + 操作指令
└── scripts/
└── analyze.py ← 捆绑脚本:用 AST 找无注释函数
预期结果:目录建好后,启动或重启 Claude Code,输入
/应该能在菜单里看到docstring-forge。如果看不到,多半是位置放错或 YAML 有语法错误,见第七步排查。
SKILL.md 顶部是一段 YAML frontmatter,用 --- 包起来。其中最关键的不是正文,而是 description——它直接决定 Skill 能不能被模型自动触发。
---
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 被触发时加载,所以可以写得详细,但仍要结构清晰、可执行。核心是告诉模型三件事:输入是什么、步骤是什么、输出长什么样。
# 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 有两个触发入口,理解差异才能用得顺手:
description,判断 docstring-forge 命中,于是自动激活、加载正文、按步骤执行。你完全不需要提"skill"这个词。/docstring-forge,直接强制激活。适合你知道要干嘛、不想依赖模型判断时。两种入口殊途同归,最终都走到同一条"加载正文 → 编排工具执行"的链路。
| 现象 | 根因 | 解决 |
|---|---|---|
/ 菜单里看不到这个 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["报告完成 / 暴露逻辑问题"]
/skill-creator 让它反向帮你生成骨架,与本教程的手写法互为补充。| 维度 | 评估 |
|---|---|
| 适合训练营类型 | 偏工程实战的开发营、AI 编程提效营、团队规范落地营;不适合纯概念入门营 |
| 学员基础 | 会 Python 基本语法、用过 Claude Code 对话即可,无需 AST 经验 |
| 教学时长 | 讲解 30 分钟 + 动手 45 分钟 + 迭代答疑 15 分钟,合计约 90 分钟 |
| 可拓展作业 | ① 改造支持 JavaScript/JSDoc;② 加"只补公开 API(非 _ 前缀)"开关;③ 接 eval 测触发率 |
| 评估标准 | 能独立造出一个可在 / 菜单出现的 Skill,且自动触发率 ≥ 80%(用 10 条对话样本测) |
关注「爱卫生」获取更多 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 等场景全覆盖。
