<!-- title: 从零造代码异味扫描Skill -->

# Claude Code Skills 开发实战：从零造一个「代码异味扫描」Skill

## 概述

本教程带你从零创建一个可被 Claude Code 自动发现并调用的自定义 Skill——`code-smell-scanner`（代码异味扫描）。你会掌握 Skill 的目录结构、`SKILL.md` frontmatter 写法、渐进披露（Progressive Disclosure）三层拆分、脚本挂载、描述调优与 eval 验证的完整闭环。适用人群：已经能用 Claude Code 完成基本编码任务、想把团队经验沉淀成「可复用、可触发」能力的开发者。

## 前置条件

- Claude Code 已安装并能正常运行（命令行输入 `claude` 可进入交互界面）
- 对 Python 或任一脚本语言有基本读写能力
- 一个测试用的真实代码仓库（拿来当扫描样本）
- 已理解「Skill ≠ 插件 ≠ MCP」的区别（下文会顺带厘清）

---

## 第一步：先厘清边界——什么时候该造 Skill

很多人第一次想「扩展 Claude Code」时，会在四个原语之间纠结：Skill、Subagent、Slash Command、MCP。选错原语，后续维护成本会很高。判断标准只有三条：

| 你的诉求 | 选什么 | 为什么 |
|---|---|---|
| 把一套**判断标准/工作流**固化下来，让 Claude 在合适时机自动套用 | **Skill** | 描述驱动触发，模型按需加载 |
| 给 Claude 一个**独立上下文**去做某类任务（避免污染主对话） | Subagent | 隔离的 context + 限定工具集 |
| 想用 `/foo` **显式快捷触发**一段固定提示 | Slash Command | 显式调用，不依赖语义匹配 |
| 接一个**外部系统/数据源**（数据库、API、文件系统） | MCP | 标准协议，跨客户端复用 |

代码异味扫描的本质是「一套判断标准」——它需要的是知识（什么算长函数、什么算深嵌套），而不是一个新的进程或外部数据源。所以它天生属于 **Skill**。这一步想清楚，后面就不会把简单问题做复杂。

## 第二步：规划技能职责——画一个清晰的能力边界

写 Skill 最常见的失败模式是「什么都想做」。一个合格的 Skill 应该能用一句话说清边界。我们把 `code-smell-scanner` 的职责锁定为：

- **做什么**：扫描单个源文件的 6 类异味（长函数、深嵌套、超长行、参数过多、巨型文件、注释残留），给出定位与建议。
- **不做什么**：不做跨文件的架构级评审（那是另一个 Skill 的活）；不自动改代码（只诊断、不动手）。

边界清晰，触发描述才写得准，Claude 才不会在不该用时误触发。

## 第三步：搭骨架——目录结构与 frontmatter

个人级 Skill 放在 `~/.claude/skills/`，项目级放在 `.claude/skills/`。我们在项目里建：

```
.claude/skills/code-smell-scanner/
├── SKILL.md              # 入口，必填
├── references/
│   └── smell-catalog.md  # 异味详表（渐进披露第二层）
├── scripts/
│   └── scanner.py        # 自动扫描器（脚本层）
└── evals/
    └── cases.md          # 验证用例
```

`SKILL.md` 是唯一被「按需加载」的入口。它的开头是 YAML frontmatter，只有两个字段是关键：`name` 和 `description`。**`description` 决定了 Skill 能否被正确触发**，是整个文件最重要的部分。

frontmatter 硬性规则要记牢：

- `name`：小写字母、数字、连字符，最长 64 字符，**必须与目录名一致**
- `description`：最长 1024 字符，用第三人称写「什么时候该用它」，不要写「我能帮你」这种第一人称
- frontmatter 后留一个空行，再写正文

## 第四步：写 SKILL.md 正文——渐进披露第一层

`SKILL.md` 的正文（frontmatter 之外的部分）是「第一层披露」：当 Claude 判定该 Skill 相关时，整份正文会被载入上下文。所以它要**精炼、可执行**，建议控制在 5000 字符以内——把详尽的参考资料拆到单独文件去，别把入口文件撑爆。

下面是完整的 `SKILL.md`：

```markdown
---
name: code-smell-scanner
description: Reviews a single source file for code smells — long functions, deep nesting, overlong lines, excessive parameters, oversized files, and leftover TODO/FIXME comments. Use when the user asks to "check code quality", "find smells", "review this file for maintainability", or before submitting a large PR. Returns located findings with line numbers and concrete refactor suggestions; does not modify code.
---

# Code Smell Scanner

You are a maintainability reviewer. You **diagnose, never auto-edit**.

## Workflow

1. Ask the user for the target file (or infer from context). If a directory is given, ask whether to recurse.
2. Run the bundled scanner to get a deterministic baseline:
   `python3 scripts/scanner.py <file>`
3. Read the scanner output, then **augment** it with your own semantic judgement (the scanner only catches syntactic smells; you catch design smells like duplicated logic, misleading names).
4. For each finding, cite the line number and give ONE concrete suggestion.
5. Summarize severity at the end: 🔴 must-fix / 🟡 should-fix / 🟢 nit.

## The 6 smells we track

- **Long function** > 50 lines
- **Deep nesting** > 4 levels of indentation
- **Overlong line** > 120 chars
- **Too many parameters** > 4
- **Oversized file** > 400 lines
- **Leftover markers** TODO / FIXME / XXX

Full definitions and edge cases live in `references/smell-catalog.md` — read it only when a finding needs nuance.

## Output format

Always use this table so output is machine-parseable:

| Severity | Smell | Location | Suggestion |
|---|---|---|---|
| 🔴 | Long function | auth.py:login (88 lines) | extract validateCredentials() |

Do NOT reformat, do NOT add preamble. End with a one-line total: `Total: 3 🔴 / 5 🟡 / 2 🟢`.
```

注意三个细节：第一，描述里塞进了**触发词**（"check code quality"、"find smells"），这些是 Claude 做语义匹配的锚点；第二，正文明确了「诊断不动手」的边界，防止它越界去改代码；第三，详细的异味定义被推到 `references/smell-catalog.md`，只在需要时才读——这就是渐进披露的核心。

## 第五步：拆出参考文件——渐进披露第二层

把不常用、但偶尔需要的「深度知识」放进 `references/` 目录。Claude 只在第一层正文明确指向它时，才会去读。这样可以省下大量常驻 token。

`references/smell-catalog.md` 示例片段：

```markdown
# Smell Catalog — Detailed Definitions

## Long function (> 50 lines)
Count blank lines and comments as part of the body. **Exception**: long `switch`/`match`
statements with simple arms are acceptable — flag them as 🟢 only.

## Deep nesting (> 4 levels)
Indentation level is counted from the function's own scope. A method inside a class
inside a function starts at level 2 before any control flow.

## Too many parameters (> 4)
Options object / builder pattern collapses multiple params into one. Flag the *effective*
param count, not the literal count.
```

这条「按需加载」的纪律，是 Skill 区别于「一坨巨大的系统提示」的关键。一个团队如果往 `SKILL.md` 里堆几万字，反而会让 Claude 触发变钝、上下文变贵。

## 第六步：挂一个扫描脚本——把确定性逻辑脚本化

Skill 的一个高价值模式是：**让模型负责判断，让脚本负责确定性计算**。代码行数、嵌套深度这些是确定性的，交给脚本算比让模型数 token 更准、更省。我们在 `scripts/scanner.py` 里实现：

```python
#!/usr/bin/env python3
"""Deterministic code-smell baseline scanner. Language-agnostic-ish."""
import sys, re

THRESHOLDS = {"long_func": 50, "deep_nest": 4, "long_line": 120,
              "max_params": 4, "max_file_lines": 400}

def scan(path):
    findings = []
    lines = open(path, encoding="utf-8", errors="ignore").read().splitlines()
    if len(lines) > THRESHOLDS["max_file_lines"]:
        findings.append(("🟡", "Oversized file", f"{path} ({len(lines)} lines)",
                         "split into focused modules"))
    # overlong lines + leftover markers
    for i, ln in enumerate(lines, 1):
        if len(ln) > THRESHOLDS["long_line"]:
            findings.append(("🟢", "Overlong line", f"{path}:{i} ({len(ln)} chars)",
                             "wrap or extract"))
        if re.search(r"\b(TODO|FIXME|XXX)\b", ln):
            findings.append(("🟡", "Leftover marker", f"{path}:{i}",
                             "resolve or track in issue tracker"))
    # long functions + deep nesting + params (brace-based heuristic)
    depth = 0; func_start = None; func_name = None; params = 0
    for i, ln in enumerate(lines, 1):
        m = re.search(r"def\s+(\w+)\s*\(([^)]*)\)", ln)
        if m:
            func_name, args = m.group(1), m.group(2)
            params = len([a for a in args.split(",") if a.strip()])
            if params > THRESHOLDS["max_params"]:
                findings.append(("🟡", "Too many params",
                                 f"{path}:{func_name} ({params})",
                                 "introduce options object"))
            func_start = i
        prev_depth = depth
        depth += ln.count("{") - ln.count("}")
        if depth > THRESHOLDS["deep_nest"]:
            findings.append(("🟡", "Deep nesting", f"{path}:{i} (level {depth})",
                             "extract early-return guard"))
        if prev_depth > 0 and depth == 0 and func_start:
            if i - func_start > THRESHOLDS["long_func"]:
                findings.append(("🔴", "Long function",
                                 f"{path}:{func_name} ({i-func_start} lines)",
                                 "extract helper(s)"))
            func_start = None
    return findings

if __name__ == "__main__":
    for sev, smell, loc, sug in scan(sys.argv[1]):
        print(f"{sev} | {smell} | {loc} | {sug}")
```

`SKILL.md` 里已写过 `python3 scripts/scanner.py <file>` 的调用方式，Claude 在触发后会自己跑这条命令、读 stdout，再用语义判断补充。**预期结果**：对一份 80 行的 `auth.py`，输出类似：

```
🔴 | Long function | auth.py:login (88 lines) | extract helper(s)
🟡 | Deep nesting | auth.py:42 (level 5) | extract early-return guard
🟡 | Leftover marker | auth.py:67 | resolve or track in issue tracker
```

**常见问题**：脚本报 `FileNotFoundError` —— 因为 Skill 触发后工作目录是项目根，而非 skill 目录。解决：在 `SKILL.md` 里写明使用相对 skill 目录的调用方式，或在脚本里用 `__file__` 解析绝对路径。另一个坑：Python 缩进计数用花括号 `{}` 只对 C 系语言准；扫描 Python 时把 `{}` 换成缩进判断。按你的主力语言取舍即可。

## 第七步：调优 description——让触发又准又稳

`description` 是 Skill 的「广告位」，写不好会出现两类故障：**漏触发**（该用时没被调用）和**误触发**（不该用时被调用）。调优就是在这两端找平衡。

一个常见的烂描述：

```yaml
description: Helps improve code quality.
```

问题：太宽泛、没有触发词、没说边界。Claude 在「重构这段代码」时可能触发（其实你想让它直接改），在「检查这个文件质量」时反而可能不触发。

改写后（即上文 SKILL.md 里那版）写法要点：

- **列出场景触发词**：用引号包出用户真实会说的短语（"check code quality"、"find smells"）
- **写明产出物**：located findings with line numbers
- **声明边界**：does not modify code（防止越界）

自检方法：把 description 念给同事听，问「你会猜它处理哪类问题」。如果对方答非所问，就是没写清楚。

## 第八步：用 evals 验证——别靠感觉

Skill 上线前用 eval 跑一遍，量化「触发准不准、输出对不对」。在 `evals/cases.md` 里准备几组用例：

| 用户输入 | 期望触发 | 期望输出特征 |
|---|---|---|
| 「帮我看看 auth.py 代码质量」 | ✅ 触发 | 含表格、含行号、含总结行 |
| 「把这段代码重构成 async」 | ❌ 不触发 | 直接动手重构 |
| 「review 这个 PR 的可维护性」 | ✅ 触发 | 先跑 scanner |
| 「写一个新模块」 | ❌ 不触发 | 直接写代码 |

跑法：用 `/eval` 或 skill-creator 自带的评估流程，对每组输入检查触发与输出。重点看两类失败：误触发会让 Claude 在写新功能时莫名其妙跑去扫描；漏触发会让你的 Skill 形同虚设。迭代 description 直到表格全绿。

## 第九步：安装与实战调用

项目级 Skill 无需安装，把目录提交进仓库，团队 `git pull` 后即生效。个人级放 `~/.claude/skills/`。验证发现情况：进入 Claude Code 输入 `/` 看是否列出该 skill，或直接说「帮我检查 src/payment.py 的代码异味」——若描述写得好，Claude 会自动触发。

## 实战案例：扫描一个真实烂文件

假设团队有个 `legacy/billing.py`（1200 行、一个函数 200 行、嵌套 6 层、5 个 TODO）。对它说「扫一下 billing.py 的代码异味」：

1. Claude 触发 `code-smell-scanner`，载入 SKILL.md
2. 运行 `python3 scripts/scanner.py legacy/billing.py`，拿到确定性基线（Oversized file 🟡、多处 Long function 🔴、多处 Deep nesting 🟡、5 个 Leftover marker 🟡）
3. Claude 补充语义判断：发现 `calculate_total` 里有大段重复的折扣逻辑（脚本抓不到的设计级异味），追加一条 🔴
4. 输出表格 + 总结行 `Total: 4 🔴 / 9 🟡 / 1 🟢`

整个过程：模型负责语义判断、脚本负责确定性计数，各司其职。这就是「Skill 把团队经验沉淀成可复用能力」的落地形态。

## 工作流程图

```mermaid
flowchart TD
    A[用户输入] --> B{Claude 语义匹配}
    B -->|不相关| Z[不加载 Skill]
    B -->|匹配 description| C[载入 SKILL.md 第一层]
    C --> D[执行工作流]
    D --> E[运行 scripts/scanner.py]
    E --> F[确定性基线:行数/嵌套/参数]
    D --> G[模型语义补充:重复/命名/设计]
    F --> H{需要细节?}
    H -->|是| I[读取 references 第二层]
    H -->|否| J[合并发现]
    I --> J
    G --> J
    J --> K[输出表格+严重度总结]
    K --> L[/eval 持续验证触发精度/]
```

## 延伸阅读

- **官方 Skill 编写最佳实践**（platform.claude.com）——描述写法、渐进披露的权威指南
- **Claude Help Center：如何创建自定义 Skill**（support.claude.com）——字段约束、安装范围
- **`/skill-creator`** 内置技能——用它脚手架化生成新 Skill、自动跑 eval、做方差分析，比手写更省心
- **Skills vs Subagents vs MCP 对照**——理清四个原语边界，避免选错

## 训练营适用性评估

- **适合类型**：企业内训「Claude Code 工程化」2–3 天营、开发者工作坊（半天即可做完本案例）
- **预计教学时长**：讲解 45 分钟 + 动手 90 分钟（含把自家代码规范替换进 smell-catalog）
- **进阶衔接**：本案例完成后，可顺势接「把 Skill 打包成 Plugin 发布」「给 Skill 造量化基准测试」两节，形成完整能力沉淀链路

## 参考来源

1. [How to create custom skills — Claude Help Center](https://support.claude.com/en/articles/12512198-how-to-create-custom-skills) — 官方自定义 Skill 创建指南，字段约束与安装范围
2. [Skill authoring best practices — Claude Platform Docs](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) — 官方 Skill 编写最佳实践，含描述写法与渐进披露原则
3. [Claude Code Skills 2026 — Progressive Disclosure](https://anhtu.dev/claude-code-skills-2026-progressive-disclosure-and-how-to-standardise-workflow-for-engineering-teams-1062) — SKILL.md 解剖与三层渐进披露的工程化深度解读
4. [Ultimate guide to extending Claude Code with skills, agents, commands](https://gist.github.com/alirezarezvani/a0f6e0a984d4a4adc4842bbe124c5935) — 四原语边界对照与扩展全景指南
5. [How to Build Claude Skills: Complete Guide + MCP Comparison (2026)](https://www.thetoolnerd.com/p/how-to-build-claude-skills-step-by-step-guide-thetoolnerd) — 含端到端示例与 Skill/MCP 取舍分析

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