首页 › tutorials › 文档漂移检测Skill构建实战

概述

本教程带你从零构建一个名为 docs-drift-detector 的自定义 Claude Code Skill,用于检测项目文档(README/ Wiki)与真实源代码之间的"漂移"——文档里写了但代码已经不存在的符号(过期文档),以及代码里有但文档没提的符号(文档缺口)。你将完整走通 Skill 的目录结构、SKILL.md 前置元数据设计、渐进式披露(Progressive Disclosure)、捆绑脚本、结构化输出与触发调优。适用人群:已能正常运行 Claude Code、希望掌握 Skill 工程化开发的开发者;无需 MCP 或插件经验。

前置条件


一、理解 Skill 的三段式加载机制

动手前必须先理解 Skill 为什么这样组织。Claude Code Skills 采用渐进式披露架构,分三段加载:

阶段 时机 加载内容 Token 成本
阶段1 元数据扫描 会话启动 仅 name + description 约 30–50 tokens/个
阶段2 完整正文 模型判定相关时 整个 SKILL.md 主体 通常 < 5000 tokens
阶段3 资源/脚本 显式访问或执行时 参考文件、脚本输出 按需,脚本代码本身不进上下文

关键推论:description 是 Skill 的"广告位",它常驻每个会话的系统提示。100 个 Skill 的元数据约 5000 tokens,可接受;但描述写得模糊,Skill 装了也永远不会被触发——这是社区报告的头号问题。

常见问题:为什么我的 Skill 装好了却不触发? - 检查 description 是否同时写了"做什么"和"什么时候用"; - 是否包含用户真实会用的触发词(中英文都要覆盖); - 改完文件必须重启 Claude Code,发现扫描只在初始化时发生一次; - 用 --debug 类启动参数可以看到加载日志,YAML 解析错误会导致 Skill 静默消失。


二、第一步:规划目录结构

一个生产级 Skill 不只是一个 SKILL.md,而是一棵按渐进式披露组织的树。先建目录:

# 个人级 Skill:全局生效,跨项目可用
mkdir -p ~/.claude/skills/docs-drift-detector/{scripts,references}

# 项目级 Skill:随 git 分发给团队
# mkdir -p .claude/skills/docs-drift-detector/{scripts,references}

最终结构:

docs-drift-detector/
├── SKILL.md                      # 必须:核心指令(阶段2加载)
├── scripts/
│   └── drift_check.py            # 确定性扫描器(阶段3,仅输出进上下文)
└── references/
    ├── extraction-rules.md       # 按需加载:各语言符号抽取规则
    └── output-format.md          # 按需加载:报告 schema 说明

设计要点:把"详尽的规则"和"格式说明"挪到 references/,是因为它们只在 Claude 需要解释某条结果时才读。塞进 SKILL.md 主体会让阶段2 每次加载都浪费 token。

常见问题:个人级和项目级怎么选? - 仅自己用、跨多个项目 → ~/.claude/skills/(个人级,全局)。 - 团队共享、随代码演进 → .claude/skills/(项目级,纳入版本控制)。 - 同名时优先级:个人 > 项目 > 插件;同名通常意味着配置错误,应避免。


三、第二步:写 SKILL.md 前置元数据(最关键的一步)

前置元数据(YAML)是 Skill 的身份证。规则很硬:

创建 ~/.claude/skills/docs-drift-detector/SKILL.md:

---
name: docs-drift-detector
description: Detect documentation drift by comparing README/docs against actual source code to find symbols documented but no longer implemented (stale docs) and symbols implemented but missing from docs (gaps). Use when the user asks to check if docs are up to date, verify README accuracy, find outdated documentation, audit doc-vs-code consistency, or says "文档漂移"/"文档和代码不一致"/"检查 README 是否过期". Runs a bundled Python scanner and emits a structured drift report.
allowed-tools:
  - Read
  - Grep
  - Bash
---

# Docs Drift Detector

检查项目文档是否仍然与真实源代码一致。

## 何时使用
当用户想验证 README/docs 准确性、或寻找文档与代码的漂移时运行本 Skill。

## 工作流
1. 确认文档文件(默认 `README.md`)与源码根目录(默认 `.`)。
2. 运行扫描器——源码不进上下文,只有报告输出进上下文:
   `python3 scripts/drift_check.py --doc README.md --src src`
3. 阅读报告中 "Stale docs"(文档写了但代码没有)与 "Undocumented"(代码有但文档没提)。
4. 对每个过期符号,用 Grep 在代码库中确认:是被改名、删除,还是从未存在?给出具体的文档修改建议。
5. 用简短表格汇总漂移;编辑文档前先征求用户同意。

## 规则
- 以扫描器输出作为"代码中已定义"的事实依据,不要凭记忆猜符号。
- 各语言抽取规则见 `references/extraction-rules.md`;报告 schema 见 `references/output-format.md`。
- 非零退出码表示发现漂移,可直接用于 CI。

为什么 allowed-tools 只给 Read/Grep/Bash:这是一个只读分析 Skill,无需 Write/Edit,遵循最小权限原则。即便 Skill 被提示注入,也无法直接改文件。

常见问题:description 怎么写才稳? - 反例:Analyze documentation.(太模糊,永远不会触发)。 - 正例(如上):动词开头 + 明确对象 + "Use when…" + 中英文触发词。 - 经验:用用户真实会说的词,而不是开发者行话。"README 是否过期"比"文档一致性审计"更可能被普通人说出。


四、第三步:编写确定性脚本(脚本代码不进上下文)

这是渐进式披露的最大红利:一个 200 行脚本,只有它的 stdout(报告)进入上下文,脚本本身一个 token 不占。把下面的内容保存为 scripts/drift_check.py:

#!/usr/bin/env python3
# scripts/drift_check.py
"""Deterministic docs-drift scanner.
NOTE: This source code never enters Claude's context window —
only this script's stdout (the report) does. That is progressive disclosure."""
import re, sys, json, argparse
from pathlib import Path

# 每种语言"定义符号"的正则(按需扩展,详见 references/extraction-rules.md)
LANG_PATTERNS = {
    ".py": r"^\s*(?:def|class)\s+([A-Za-z_][A-Za-z0-9_]*)",
    ".ts": r"^\s*(?:export\s+)?(?:function|class|const|interface)\s+([A-Za-z_$][A-Za-z0-9_$]*)",
    ".js": r"^\s*(?:export\s+)?(?:function|class|const)\s+([A-Za-z_$][A-Za-z0-9_$]*)",
    ".go": r"^\s*func\s+(?:\([^)]*\)\s+)?([A-Za-z_][A-Za-z0-9_]*)",
}
SKIP_DIRS = {"node_modules", ".git", "dist", "build", "vendor", "__pycache__"}

def collect_definitions(roots):
    defs = {}  # name -> [files]
    for root in roots:
        for path in Path(root).rglob("*"):
            if not path.is_file() or path.suffix not in LANG_PATTERNS:
                continue
            if any(seg in path.parts for seg in SKIP_DIRS):
                continue
            try:
                text = path.read_text(encoding="utf-8", errors="ignore")
            except OSError:
                continue
            for m in re.finditer(LANG_PATTERNS[path.suffix], text, re.M):
                defs.setdefault(m.group(1), []).append(str(path))
    return defs

def collect_documented(doc_path):
    """文档声称存在的符号:反引号代码片段 + 围栏代码块里的签名。"""
    text = Path(doc_path).read_text(encoding="utf-8", errors="ignore")
    names = set()
    for span in re.findall(r"`([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)`", text):
        names.add(span.split(".")[-1])  # 取 a.b.c 的最后一段
    for block in re.findall(r"```[a-z]*\n(.*?)```", text, re.S):
        for line in block.splitlines():
            m = re.search(r"\b(?:def|function|class|const|func)\s+([A-Za-z_][A-Za-z0-9_]*)", line)
            if m:
                names.add(m.group(1))
    return names

def main():
    ap = argparse.ArgumentParser(description="Docs drift detector")
    ap.add_argument("--doc", default="README.md")
    ap.add_argument("--src", nargs="*", default=["."])
    ap.add_argument("--json", action="store_true")
    a = ap.parse_args()

    if not Path(a.doc).is_file():
        print(f"ERROR: doc not found: {a.doc}", file=sys.stderr); sys.exit(2)

    defs = collect_definitions(a.src)
    documented, defined = collect_documented(a.doc), set(defs)
    stale = sorted(documented - defined)   # 文档有、代码无 → 过期文档
    gaps  = sorted(defined - documented)   # 代码有、文档无 → 文档缺口
    matched = sorted(documented & defined)

    if a.json:
        print(json.dumps({"stale_docs": stale, "undocumented": gaps,
                          "matched": matched, "total_defined": len(defined),
                          "total_documented": len(documented)}, indent=2)); return

    print(f"# Docs Drift Report — {a.doc}\n")
    print(f"Documented symbols : {len(documented)}")
    print(f"Defined in code    : {len(defined)}")
    print(f"Matched            : {len(matched)}\n")
    print(f"## Stale docs (documented but not in code): {len(stale)}")
    for s in stale: print(f"  - {s}")
    print(f"\n## Undocumented (in code but not in docs): {len(gaps)}")
    for g in gaps[:50]: print(f"  - {g}")
    if len(gaps) > 50: print(f"  ... ({len(gaps)-50} more)")
    sys.exit(1 if stale else 0)   # 发现过期文档 → 非零退出码

if __name__ == "__main__":
    main()

设计意图: - 纯标准库,零依赖,在任何能跑 python3 的环境直接运行(注意:Skill 的代码执行沙箱在某些平台无网络、不能 pip 安装,所以坚持只用标准库)。 - 输出同时支持人类可读文本与 --json,后者便于被其他工具/CI 消费。 - 退出码即结论:0=无漂移,1=有漂移,2=参数错误。

常见问题:脚本报 command not found: python3? - Claude Code 里 Claude 通过 Bash 工具调用脚本;若系统无 python3,可改写为 awk/grep 版本,或用 python(部分 macOS)。 - 路径用相对路径 scripts/drift_check.py 是因为 Skill 执行时工作目录是其所在目录的相对引用;若不确定,可在 SKILL.md 里写绝对占位并让 Claude 用 Read 定位。


五、第四步:补齐按需加载的参考文件

把详细规则放进 references/,让 SKILL.md 保持精简。references/extraction-rules.md:

# 符号抽取规则

## 源码侧("已定义")
| 语言 | 匹配 | 示例 |
|------|------|------|
| Python | `def`/`class` 名 | `def create_user` → create_user |
| TS/JS | `function`/`class`/`const`/`interface`(含 `export`) | `export const MAX` → MAX |
| Go | `func` 名(跳过接收者) | `func (s *S) Run` → Run |

跳过目录:node_modules, .git, dist, build, vendor, __pycache__。

## 文档侧("已记录")
- 反引号代码片段:`createUser`、`auth.login()`(取最后一段)。
- 围栏代码块内形如 `def/function/class/const/func X` 的签名。

references/output-format.md:

# 报告 Schema
- Stale docs(过期):文档提到、代码中已不存在 → 优先修,可能误导用户。
- Undocumented(缺口):代码存在、文档未提 → 次优先,按公开 API 重要性筛选。
- Matched:一致,无需处理。
--json 输出供 CI 解析;文本输出供人读。

为什么这样拆:用户 90% 的请求只需要看报告;只有当 Claude 需要解释"为什么某符号被判为漂移"时,才去读 extraction-rules.md。这就是阶段3 的价值——按需付费。


六、实战案例:跑通完整闭环

在临时目录造一个"文档说一套、代码做一套"的小项目:

mkdir demo && cd demo

cat > README.md <<'EOF'
# Demo API
## 端点
- `createUser(name)` 创建用户
- `deleteUser(id)` 删除用户
- `legacyAuth()` 兼容旧登录
EOF

cat > app.py <<'EOF'
def createUser(name):
    pass

def updateUser(name, email):
    pass
EOF

直接跑脚本验证(不依赖 Claude):

python3 ~/.claude/skills/docs-drift-detector/scripts/drift_check.py \
  --doc README.md --src .
echo "exit=$?"

预期输出:

# Docs Drift Report — README.md

Documented symbols : 4
Defined in code    : 2
Matched            : 1

## Stale docs (documented but not in code): 3
  - deleteUser
  - legacyAuth
  - Demo        # 来自标题里的反引号无关词,属噪声,见下文优化
## Undocumented (in code but not in docs): 1
  - updateUser
exit=1

语义解读:deleteUser、legacyAuth 是过期文档(代码已删/从未实现),updateUser 是文档缺口。退出码 1 表示发现漂移。

然后重启 Claude Code让新 Skill 被发现,在新会话里用自然语言触发:

> 帮我检查这个项目的 README 是否过期

Claude 会自主判定 docs-drift-detector 相关 → 加载 SKILL.md → 运行脚本 → 读报告 → 用 Grep 逐个确认 deleteUser 是否真不存在 → 汇总成表并建议改 README。

常见问题:自然语言没触发 Skill? - 临时手段:显式点名,用我的 docs-drift-detector skill 检查…,可绕过匹配问题用于排错。 - 长期手段:回到 description,把这次用户实际说的话补进去,再重启。

常见问题:报告里有噪声(如上例的 Demo)? - 反引号抽取会误抓非符号词。优化方向:在 extraction-rules.md 里加"必须形如 xxx( 或 class Xxx 才算符号"的规则,或维护一个停用词表。这正是把规则外置的好处——改规则不用改 Skill 主体。


七、整体工作流与架构图

flowchart TD
    A[会话启动] --> B[阶段1: 扫描 ~/.claude/skills 与 .claude/skills]
    B --> C[仅加载 name+description<br/>~30-50 tokens/个]
    C --> D{模型判定: 用户请求<br/>与某 Skill 相关?}
    D -- 否 --> E[该 Skill 保持休眠<br/>零额外成本]
    D -- 是 docs-drift --> F[阶段2: 加载 docs-drift-detector/SKILL.md]
    F --> G[运行 scripts/drift_check.py]
    G --> H[阶段3: 仅脚本 stdout 进入上下文<br/>源码/脚本本身不进]
    H --> I[读 references/extraction-rules.md<br/>仅当需解释某结果]
    I --> J[Grep 逐个核实过期符号]
    J --> K[输出漂移表格 + 文档修改建议]

    style C fill:#e8f5e9
    style H fill:#fff3e0
    style K fill:#e3f2fd

三层加载(绿→橙→蓝)对应三个 token 成本台阶:元数据常驻、主体按需、资源与脚本输出按需。


八、上线前的自检与调优清单

发布前逐项打钩,能避免 90% 的 Skill 陷阱:

进阶:把 SKILL.md 控制在 500 行以内;脚本复杂计算全部下沉到脚本而非让模型"用文本生成"去算(排序、统计、校验这类,代码执行远比 token 生成可靠且省 token)。


九、延伸阅读


十、训练营适用性评估

维度 评估
适合的训练营类型 AI 工程化实战营、开发者效率工具营、Claude Code 进阶营(中段课程)
学员基础 会命令行与一种语言(Python/JS/Go 任一),已跑通 Claude Code 首跑
预计教学时长 讲解 45 分钟 + 动手 60 分钟 + 调优/盲测 30 分钟,合计约 2.5 小时
可交付成果 每人一个可触发、可运行、带参考文件的完整 Skill,并接入 CI(用退出码做质量门禁)
衍伸练习 加一种新语言规则;接 MCP 做远程仓库扫描;为 Skill 加 Evals 盲测集

参考来源

  1. Extend Claude with skills — Claude Code Docs — 官方 Skill 创建、管理与共享权威文档,含目录约定与触发机制。
  2. Claude Code Skill Frontmatter: Every YAML Option Explained — 逐字段讲解 15 个前置元数据字段(name/description/allowed-tools/version 等)及校验规则。
  3. Claude Code Skills: Complete Technical Deep-Dive — 三段式渐进式披露、发现扫描、状态模型与安全模型的技术深 Dive。
  4. Agent Skills in the SDK — Claude Code Docs — Agent SDK 中 Skill 的自主调用与组合机制说明。
  5. Claude Code Skills: The Complete Guide (2026) — 2026 年全景指南,覆盖 SKILL.md 规范、安全控制与实战案例。

[!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学通信知识星球海报-扫码加入