首页 › tutorials › 从零造代码异味扫描Skill

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

概述

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

前置条件


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

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

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

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

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

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

边界清晰,触发描述才写得准,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 硬性规则要记牢:

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

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

下面是完整的 SKILL.md:

---
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 示例片段:

# 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 里实现:

#!/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 的「广告位」,写不好会出现两类故障:漏触发(该用时没被调用)和误触发(不该用时被调用)。调优就是在这两端找平衡。

一个常见的烂描述:

description: Helps improve code quality.

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

改写后(即上文 SKILL.md 里那版)写法要点:

自检方法:把 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 把团队经验沉淀成可复用能力」的落地形态。

工作流程图

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 持续验证触发精度/]

延伸阅读

训练营适用性评估

参考来源

  1. How to create custom skills — Claude Help Center — 官方自定义 Skill 创建指南,字段约束与安装范围
  2. Skill authoring best practices — Claude Platform Docs — 官方 Skill 编写最佳实践,含描述写法与渐进披露原则
  3. Claude Code Skills 2026 — Progressive Disclosure — SKILL.md 解剖与三层渐进披露的工程化深度解读
  4. Ultimate guide to extending Claude Code with skills, agents, commands — 四原语边界对照与扩展全景指南
  5. How to Build Claude Skills: Complete Guide + MCP Comparison (2026) — 含端到端示例与 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学通信知识星球海报-扫码加入