首页 › tutorials › 造代码体检Skill扫坏味道

用 Claude Code 造一个「代码体检」Skill:从零到产出可读的健康报告

概述

本教程带你从零构建一个名为 code-health-audit 的自定义 Skill:它扫描任意代码仓库的长函数、深嵌套、参数过多、超大文件和 TODO/FIXME 技术债,按问题密度给出 A–F 评级,并输出一份带文件定位的 Markdown 体检报告。适合已经用过 Claude Code、想把重复的工程检查固化为「一句话触发」能力的开发者、Tech Lead 与训练营学员。你将掌握 SKILL.md 结构、前置授权、脚本式 Skill 的渐进式加载、动态上下文注入与 eval 迭代调优。

前置条件

第一步:想清楚 Skill 到底解决什么问题

写 Skill 最大的坑不是格式写错,而是「为了写而写」。官方最佳实践反复强调一句话:Claude 本来就很聪明,只补充它不知道的、或它容易做不一致的东西。 因此动手前先回答三个问题。

  1. 痛点:每次接手老代码,都要手动翻长函数、数嵌套层级、grep TODO——这件事你反复做、且每次口径不同。
  2. 可自动化部分:扫描与计数是确定性操作(低自由度),适合交给脚本;而「该先重构哪个」是判断题(高自由度),适合交给 Claude。
  3. 触发场景:用户会说「体检一下 src」「扫扫代码坏味道」「评估可维护性」「列一下技术债」。

这套拆分直接决定了我们的 Skill 形态:一个调脚本的「高自由度 + 低自由度」混合体——脚本负责确定的体检,SKILL.md 的正文负责告诉 Claude 如何解读和排序。

设计原则映射:官方把自由度分为三档——开放文本(高自由度)、带参数的脚本(中)、固定脚本(低)。本 Skill 同时用了「低」(体检脚本必须精确)和「高」(重构建议由 Claude 给),这正是混合模式的典型用法。

第二步:创建 Skill 目录骨架

Claude Code 用目录名作为命令名。你把 Skill 放在哪一层,决定了它的作用范围:

层级 路径 适用范围
个人 ~/.claude/skills/<name>/SKILL.md 你的所有项目
项目 .claude/skills/<name>/SKILL.md 仅当前项目(提交进版本控制即可团队共享)
插件 <plugin>/skills/<name>/SKILL.md 启用该插件处

我们把体检 Skill 建在个人目录,方便跨项目复用:

mkdir -p ~/.claude/skills/code-health-audit/scripts

这会得到如下结构(渐进式加载的标准骨架):

code-health-audit/
├── SKILL.md          # 入口:元数据 + 指令(触发时加载)
└── scripts/
    └── audit.py      # 体检脚本(执行,不进上下文)

预期结果:目录创建成功。由于 Claude Code 有「实时变更检测」,在已运行的会话里增删 ~/.claude/skills/ 下的 Skill 文件无需重启即可生效;仅当本次会话启动时该 skills 目录尚不存在,才需要重启一次让它被纳入监视。

常见问题:

第三步:写 SKILL.md——frontmatter 是 Skill 的「门牌号」

SKILL.md 由两段组成:YAML frontmatter(告诉 Claude 何时用它)+ Markdown 正文(告诉 Claude 怎么用它)。先看完整文件,再逐字段拆解。

保存到 ~/.claude/skills/code-health-audit/SKILL.md:

---
name: code-health-audit
description: Scans a codebase for maintainability risks—long functions, deep nesting, too many parameters, oversized files, and TODO/FIXME debt—and writes a graded health report. Use when the user asks for a code health check, code smell scan, maintainability audit, refactor hotspot analysis, or technical debt inventory.
allowed-tools:
  - Bash(python3 *)
  - Read
  - Grep
argument-hint: "[directory]"
---

# Code Health Audit

Run the bundled audit script against the target directory (defaults to the project root):

```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/audit.py $ARGUMENTS

What you get

A markdown report graded A–F by issue density (issues per 1000 lines). Sections: - 问题分布 — counts by category - Top 重灾区 — the 10 files with the most issues - 具体问题清单 — file:line:detail for the first 30 issues

How to respond

  1. Run the script above (pass a path via $ARGUMENTS, e.g. src/).
  2. Read the report it prints to stdout.
  3. Summarize the grade and the 3 most impactful hotspots in 2–3 bullets.
  4. For each top hotspot, propose ONE concrete refactor: extract function, guard clause to flatten nesting, parameter object, split file, or resolve the TODO. Cite file:line.
  5. This is a read-only audit—do not edit files unless explicitly asked.

Tuning thresholds

Defaults sit at the top of scripts/audit.py: LONG_FUNC_LINES=50, DEEP_INDENT=6, LONG_LINE=120, LONG_FILE_LINES=500. Surface them if the user wants a stricter or looser bar.

**字段拆解**:

- `description`(强烈推荐):这是 Claude 在上百个 Skill 里挑中你的唯一依据。务必第三人称、把**做什么**和**何时用**都写进去,并放进用户会自然说出的关键词(「code smell」「maintainability audit」「技术债」)。注意 `description` 与 `when_to_use` 合计在 Skill 列表里被截断到 **1536 字符**,所以把最关键的用例放在最前面。
- `name`:仅作展示名,默认等于目录名;命令名始终来自目录,而非此字段(插件根 `SKILL.md` 是唯一例外)。
- `allowed-tools`:**前置授权**——Skill 激活期间,列出的工具免逐次确认即可调用。这里放行 `Bash(python3 *)`,让体检脚本无须每次点头就能跑。注意它只「预授权」而不「限制」:其他工具仍可调用,最终仍受你的权限设置约束。
- `argument-hint`:自动补全时显示的参数提示,让用户知道可以传一个目录。
- 正文里的 `${CLAUDE_SKILL_DIR}`:在运行时被替换为该 Skill 自身所在目录,保证脚本路径无论装在个人级、项目级还是插件级都能正确解析。
- 正文里的 `$ARGUMENTS`:用户在 `/code-health-audit src/` 里跟在后面的 `src/` 会被替换进来;若正文没有 `$ARGUMENTS` 占位符,参数会被追加为 `ARGUMENTS:` 附在末尾。

**预期结果**:保存后,无需重启,新会话或当前会话都能用 `/code-health-audit` 调用,或在你问「体检一下代码」时被自动触发。

**常见问题**:

- *Skill 不触发?* 八成是 `description` 没覆盖用户原话。先用 `What skills are available?` 确认它被列出;再把用户的真实措辞补进 description。frontmatter YAML 写错时,Claude Code 会以空元数据加载正文,`/code-health-audit` 仍能用但自动匹配失效——用 `claude --debug` 可看到解析错误。
- *正文太长怎么办?* 官方建议 SKILL.md 正文控制在 **500 行以内**,因为一旦加载,正文会**在整个会话常驻**,每行都是反复的 token 成本。超长就拆成 `reference.md` 等附属文件,在正文里用一行链接指过去(引用保持**一层深**,不要 A 引 B、B 引 C)。

## 第四步:写体检脚本——确定性部分交给代码

体检是「窄桥两边是悬崖」的典型:必须精确、可复现,所以用固定脚本(低自由度),而不是让 Claude 每次临场写扫描逻辑。脚本遵循官方「**解决它,别甩给 Claude**」原则:自己处理文件读取异常,而不是把错误抛回去让模型兜底。

保存到 `~/.claude/skills/code-health-audit/scripts/audit.py`:

```python
#!/usr/bin/env python3
"""code-health-audit: scan a codebase for maintainability risks, emit a graded markdown report."""
import ast, sys, re
from pathlib import Path
from collections import Counter

IGNORE_DIRS = {'.git', 'node_modules', '__pycache__', '.venv', 'venv',
               'dist', 'build', '.idea', '.vscode'}
CODE_EXTS = {'.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.go', '.rb',
             '.php', '.c', '.h', '.cpp', '.cc', '.cs', '.rs', '.kt', '.swift'}
TODO_RE = re.compile(r'\b(TODO|FIXME|HACK|XXX|BUG)\b')
LONG_LINE = 120        # chars per line
DEEP_INDENT = 6        # indent levels (1 level ~= 4 spaces)
LONG_FUNC_LINES = 50   # python functions longer than this
LONG_FILE_LINES = 500

LEGEND = {
    'long-function': '超长函数', 'many-params': '参数过多',
    'deep-nesting': '嵌套过深', 'long-file': '超大文件',
    'tech-debt': 'TODO/FIXME 债务', 'long-line': '超长行',
}

def walk(root):
    for p in Path(root).rglob('*'):
        if any(part in IGNORE_DIRS for part in p.parts):
            continue
        if p.is_file() and p.suffix in CODE_EXTS:
            yield p

def scan_file(path):
    issues, metrics = [], {'lines': 0, 'todos': 0, 'deepest': 0, 'long_lines': 0}
    try:
        text = path.read_text(encoding='utf-8', errors='replace')
    except OSError:                       # solve, don't punt: skip unreadable files
        return issues, None
    lines = text.splitlines()
    metrics['lines'] = len(lines)
    for i, line in enumerate(lines, 1):
        if TODO_RE.search(line):
            metrics['todos'] += 1
            issues.append((i, 'tech-debt', line.strip()[:80]))
        stripped = line.lstrip()
        levels = (len(line) - len(stripped)) // 4
        metrics['deepest'] = max(metrics['deepest'], levels)
        if levels >= DEEP_INDENT:
            issues.append((i, 'deep-nesting', f'nesting depth {levels}'))
        if len(line) > LONG_LINE:
            metrics['long_lines'] += 1
            issues.append((i, 'long-line', f'{len(line)} chars'))
    if metrics['lines'] > LONG_FILE_LINES:
        issues.append((metrics['lines'], 'long-file', f'{metrics["lines"]} lines'))
    if path.suffix == '.py':              # AST precision for Python
        try:
            tree = ast.parse(text)
        except SyntaxError:
            tree = None
        if tree:
            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    length = getattr(node, 'end_lineno', node.lineno) - node.lineno + 1
                    if length > LONG_FUNC_LINES:
                        issues.append((node.lineno, 'long-function', f'{node.name}() = {length} lines'))
                    params = len(node.args.args) + len(node.args.kwonlyargs)
                    if params > 5:
                        issues.append((node.lineno, 'many-params', f'{node.name}() has {params} params'))
    return issues, metrics

def grade(density):                       # issues per 1000 lines, lower is better
    return 'A' if density < 5 else 'B' if density < 15 else \
           'C' if density < 30 else 'D' if density < 50 else 'F'

def main():
    root = Path(sys.argv[1] if len(sys.argv) > 1 else '.').resolve()
    all_issues, total_lines, per_file, type_counter = [], 0, [], Counter()
    for path in walk(root):
        issues, metrics = scan_file(path)
        if metrics is None:
            continue
        total_lines += metrics['lines']
        per_file.append((path, metrics, len(issues)))
        for _, typ, _ in issues:
            type_counter[typ] += 1
        all_issues.extend((path, ln, typ, det) for ln, typ, det in issues)
    density = len(all_issues) / total_lines * 1000 if total_lines else 0
    print('# 🩺 代码体检报告\n')
    print(f'**扫描目录**: `{root}`  ')
    print(f'**代码行数**: {total_lines:,}  **问题数**: {len(all_issues)}  '
          f'**密度**: {density:.1f}/千行  **评级**: **{grade(density)}**\n')
    print('## 问题分布\n| 类型 | 数量 |\n|------|------|')
    for t, n in type_counter.most_common():
        print(f'| {LEGEND.get(t, t)} | {n} |')
    print('\n## Top 重灾区(按问题数)\n| 文件 | 行数 | 问题数 |\n|------|------|--------|')
    for path, metrics, n in sorted(per_file, key=lambda x: -x[2])[:10]:
        if n:
            print(f'| {path.relative_to(root)} | {metrics["lines"]} | {n} |')
    print('\n## 具体问题清单(前 30 条)\n| 文件 | 行 | 类型 | 详情 |\n|------|----|------|------|')
    for path, ln, typ, det in sorted(all_issues, key=lambda x: x[1])[:30]:
        print(f'| {path.relative_to(root)} | {ln} | {LEGEND.get(typ, typ)} | {det} |')
    print(f'\n---\n_由 code-health-audit Skill 生成,共 {len(all_issues)} 条问题。_')

if __name__ == '__main__':
    main()

先脱离 Claude Code 单独验证脚本能跑:

python3 ~/.claude/skills/code-health-audit/scripts/audit.py /path/to/your/repo

预期结果:终端打印一段 Markdown,包含评级、问题分布表、重灾区表和问题清单。Python 文件会因走 AST 而精确报出函数长度与参数个数;其他语言走缩进行级启发式,足以定位嵌套过深与超长行。

常见问题:

第五步:注册并测试——两种触发方式都要验

进入任意代码仓库,启动 claude。验证两条路径:

方式 A:让 Claude 自动触发。 故意用 description 里没有、但语义接近的话:

帮我看看这个模块的可维护性怎么样,有没有该重构的地方

方式 B:手动直调。

/code-health-audit src/

两种都应得到:脚本被 Bash(python3 *) 免确认执行 → 报告打印 → Claude 用 2–3 条要点汇总评级与 Top3 热点,并对每个热点给出一条带 file:line 的具体重构建议。

关键认知:「Skill 被触发」只代表 Claude 找到了它,不代表输出符合预期。 验证要在「全新会话」里做——因为写 Skill 时残留的上下文会掩盖正文里的缺口,让你误以为它很灵。

常见问题:

第六步:解读报告并迭代调优

拿到报告后,迭代聚焦三件事,顺序很关键:

  1. 先调 description:用官方 skill-creator 插件做「触发命中率」测试。安装:/plugin install skill-creator@claude-plugins-official(找不到就先 /plugin marketplace update claude-plugins-official),再 /reload-plugins。然后让它 evaluate my code-health-audit skill,它会生成 should-trigger / should-not-trigger 两组用例,量出命中率并给出 description 修改建议。
  2. 再调正文质量:同样用 skill-creator,它会把测试用例写进 evals/evals.json,每个用例在隔离子代理里跑,产出 grading.json(逐条断言通过/失败+证据)和 benchmark.json(对比有 Skill / 无 Skill 的通过率、耗时、token)。重点关注「无 Skill 时 Claude 漏掉了什么」「有 Skill 后是否真的补上了」。
  3. 最后做版本对照:改完一版,用 skill-creator 的盲测 A/B 对比新旧两版,确认这次编辑真的是改进再提交,避免越改越差。

这套「先写评测、再写指令」的评估驱动开发(evaluation-driven development),是官方反复强调的正确姿势:先建评测再补文档,确保你解决的是真实问题,而不是想象出来的问题。

第七步:进阶——让 Skill 更精准、更安全

骨架跑通后,按需叠加这些官方特性:

实战案例:给一个「重灾区」模块做体检

造一个刻意凌乱的小项目来端到端验证。在 ~/messy-demo/ 下放一个 order.py:

def process_order(customer, items, coupon, shipping, tax_rate, warehouse, priority, note):
    # TODO: 这里的折扣逻辑太绕,等 Q3 重写
    total = 0
    for item in items:
        if item.get("qty", 0) > 0:
            if item.get("price", 0) > 0:
                if item.get("sku"):
                    if coupon and coupon.get("type") == "percent":
                        if priority == "vip":
                            total += item["price"] * item["qty"] * 0.8
                        else:
                            total += item["price"] * item["qty"] * 0.9
                    else:
                        total += item["price"] * item["qty"]
    # ... 此处省略后续 60 行未重构的结算、税费、出库逻辑 ...
    return total

这个函数同时踩中四类问题:8 个参数、5 层嵌套、TODO 债务、整体超长。运行:

python3 ~/.claude/skills/code-health-audit/scripts/audit.py ~/messy-demo

预期报告(节选):

# 🩺 代码体检报告

**扫描目录**: `/home/you/messy-demo`
**代码行数**: 90  **问题数**: 4  **密度**: 44.4/千行  **评级**: **D**

## 问题分布
| 类型 | 数量 |
|------|------|
| 嵌套过深 | 1 |
| 参数过多 | 1 |
| 超长函数 | 1 |
| TODO/FIXME 债务 | 1 |

## 具体问题清单(前 30 条)
| 文件 | 行 | 类型 | 详情 |
|------|----|------|------|
| order.py | 2 | tech-debt | # TODO: 这里的折扣逻辑太绕,等 Q3 重写 |
| order.py | 10 | deep-nesting | nesting depth 5 |
| order.py | 1 | many-params | process_order() has 8 params |
| order.py | 1 | long-function | process_order() = 68 lines |

随后在 Claude Code 里直接 /code-health-audit ~/messy-demo,Claude 会读取这份报告,给出类似这样的汇总与建议(高自由度部分,由模型产出):

这就达成了本 Skill 的设计目标:确定的部分(扫描、计数、评级)由脚本保证一致;判断的部分(先重构什么、怎么改)由 Claude 结合上下文给出。

工作流程与架构图

下图展示用户从输入到拿到建议的完整执行链路:

flowchart TD
    A["用户: 体检一下 src/"] --> B{"description 命中?"}
    B -->|是, 自动触发| C["加载 SKILL.md 全文入上下文"]
    B -->|否| I["用户手动: /code-health-audit src/"]
    I --> C
    C --> D["执行 python3 audit.py src/\n(allowed-tools 免确认)"]
    D --> E["脚本: AST + 行级指标扫描"]
    E --> F["输出 Markdown 体检报告"]
    F --> G["Claude 读取报告"]
    G --> H["汇总评级 + Top3 热点\n+ 每处一条 file:line 重构建议"]

下图展示 Skill 的三层渐进式加载架构——这也是「大参考文档几乎不花 token」的根源:

flowchart LR
    subgraph S0["① 启动: 仅注入元数据"]
        M["所有 Skill 的 name + description\n进入系统提示"]
    end
    subgraph S1["② 触发: 按需加载正文"]
        SK["SKILL.md 全文\n(常驻本会话)"]
    end
    subgraph S2["③ 执行: 脚本不进上下文"]
        SC["scripts/audit.py 执行"]
        OUT["仅 stdout 输出进上下文"]
    end
    M -->|"description 命中"| SK --> SC --> OUT

核心收益:metadata 全程在场但极小;正文只在被触发时加载;脚本体量再大也只有它的输出消耗 token,源码本身不占上下文。

延伸阅读

训练营适用性评估

参考来源

  1. Extend Claude with skills — Claude Code Docs — 官方 Skills 完整参考:frontmatter 字段表、目录与命令名规则、${CLAUDE_SKILL_DIR}/$ARGUMENTS 替换、allowed-tools 前置授权、动态上下文注入、子代理执行与 skill-creator 评测。
  2. Skill authoring best practices — Claude Platform Docs — 自由度三档模型、渐进式披露模式、SKILL.md ≤500 行、评估驱动开发、避免魔法常数与 Windows 路径等反模式清单。
  3. Claude Code Skills 2026 — Progressive Disclosure and How to Standardise Workflow — 工程团队视角的三层渐进式披露解读与 SKILL.md 解剖。
  4. Improving skill-creator: test, measure, and Refine Agent Skills — 官方对评测循环、基准对比与版本盲测 A/B 的说明。
  5. Evaluating skill output quality — agentskills.io — evals/evals.json 文件格式与 observe–refine–test 迭代工作流。

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