<!-- title: 造技术债雷达Skill：扫描TODO并自动分级排期 -->

## 概述

每个真实项目里都藏着一座「技术债」冰山：散落在代码各处的 `TODO`、`FIXME`、`HACK`、`XXX` 注释，标记着「以后再说」的烂尾逻辑。它们不会让构建立刻失败，却会在某次发版后突然引爆。本教程教你从零打造一个 Claude Code 自定义 Skill——**技术债雷达**：扫描整个代码库的债务标记，按严重度自动分级，关联 `git blame` 计算债务年龄，输出一份可排期的清单。学完你将掌握 Skill 的目录结构、YAML frontmatter 写法、渐进式披露设计、脚本集成与触发准确率调优，适合所有想给 Claude Code「装上行业专属雷达」的开发者。

## 前置条件

- 已安装 Claude Code（CLI 版本，能运行 `claude` 命令）
- 基础的命令行操作能力，能看懂 Python（脚本约 80 行，逐行有注释）
- 一个本地 Git 仓库（用于实战案例，可以是你的项目或任意克隆的开源仓库）
- Git 已配置用户信息（`git blame` 需要可读的提交历史）

无需任何 AI 训练或机器学习知识——Skill 的本质是一份「带触发条件的指令文档 + 可执行脚本」，你写规则，Claude 负责理解与调用。

## 一、先想清楚：为什么技术债需要 Skill 而不是 grep

直接用 `grep -rn "TODO"` 也能扫到所有标记，但它的局限正是 Skill 的价值所在：

| 维度 | 裸 grep | 技术债雷达 Skill |
|------|---------|------------------|
| 标记分类 | 全部平铺 | FIXME/BUG/HACK/TODO/NOTE 自动分桶 |
| 严重度 | 无 | 按标记词 + 所在路径（生产/测试/脚本）分级 |
| 债务年龄 | 无 | 关联 git blame 给出「欠了多少天」 |
| 误报 | 文档里的 TODO 也算 | 排除 `.md`、`node_modules`、`dist` |
| 输出 | 一堆行号 | 一份可排期的 Markdown 报告 |

Skill 的真正威力在于：**Claude 会在你说「看看这个项目的技术债」时自动加载这套规则和脚本**，而不必每次都重复粘贴 grep 命令和分级口径。这就是官方文档强调的「技能即按需加载的领域知识」。

## 二、创建 Skill 骨架：目录结构

Claude Code 的 Skill 存放在三个位置，按优先级覆盖：

- 个人级：`~/.claude/skills/<skill-name>/`（所有项目可用）
- 项目级：`.claude/skills/<skill-name>/`（随仓库共享，团队协作推荐）
- 插件级：通过插件包分发

本教程采用**项目级**，让团队能跟着仓库走。在仓库根目录执行：

```bash
mkdir -p .claude/skills/tech-debt-radar/scripts
cd .claude/skills/tech-debt-radar
```

一个 Skill 的标准目录结构如下：

```
.claude/skills/tech-debt-radar/
├── SKILL.md              # 唯一入口：frontmatter + 指令
├── scripts/
│   └── scan.py           # 扫描+分级脚本
└── reference/
    └── severity.md       # 渐进式披露：分级细则（按需加载）
```

`SKILL.md` 是**必须**且**唯一**的入口文件，Claude 加载 Skill 时首先读它。其余文件采用「渐进式披露」——Claude 只在需要时才去读，避免一次性灌满上下文窗口。

## 三、编写 SKILL.md：frontmatter 是触发开关

`SKILL.md` 顶部是一段 YAML frontmatter，决定 Skill **叫什么**、**何时被触发**。这两行是整个 Skill 能否生效的关键。

```markdown
---
name: tech-debt-radar
description: 扫描代码库中的技术债标记（TODO/FIXME/HACK/XXX/BUG），按严重度分级并关联 git blame 计算债务年龄，输出可排期的 Markdown 报告。当用户提到「技术债」「扫一下 TODO」「代码里有多少 FIXME」「债务清单」「tech debt」时使用。
---

# 技术债雷达

## 何时使用

当用户想了解代码库中遗留债务的整体情况、需要为下一轮迭代排期、或在合并前评估新增债务时，调用此 Skill。

## 工作流程

1. 运行 `python3 scripts/scan.py`（无参数则扫描当前目录）
2. 脚本会输出一份分级清单到 stdout 和 `tech-debt-report.md`
3. 读取报告，向用户总结：债务总数、各级别分布、最老的 3 条债务、集中在哪些文件/模块
4. 若用户给出范围（如「只看 src/auth 目录」），把路径作为参数传入脚本

## 分级口径（简版）

- **P0 紧急**：`FIXME` / `BUG`，且位于生产代码路径（src/、app/、lib/）
- **P1 高**：`HACK` / `XXX`，位于生产代码路径
- **P2 中**：`TODO`，且债务年龄 > 30 天
- **P3 低**：`NOTE` / `OPTIMIZE`，或位于测试/脚本目录

完整分级细则见 `reference/severity.md`，在用户追问某条债务为何被归为某级时再读取。
```

**写 description 的三条铁律**：

1. **第一句说做什么**，Claude 用它做语义匹配
2. **列出触发词**（中英文都给），这是命中率最高的杠杆
3. **保持在 1024 字符内**，过长会被截断

frontmatter 里**不要**写 `version`、`author` 等非官方字段——它们不会被解析，纯属噪音。官方规范只认 `name` 和 `description`（部分场景支持 `allowed-tools` 限制工具）。

## 四、编写扫描脚本：scripts/scan.py

Skill 的脚本是把「确定性逻辑」从大模型手里拿回来的手段——分级、计数、git blame 这些规则清晰的事，交给 Python 比交给概率模型更可靠。Claude 只负责理解用户意图和总结报告。

```python
#!/usr/bin/env python3
"""技术债扫描器：扫描标记、分级、关联 git blame。
用法: python3 scan.py [目标路径...]   # 默认当前目录
"""
import os, re, sys, subprocess
from datetime import datetime, timezone
from pathlib import Path

# 标记词 -> 基础权重（越大越严重）
MARKERS = {
    "BUG": 4, "FIXME": 4,
    "HACK": 3, "XXX": 3,
    "TODO": 2,
    "NOTE": 1, "OPTIMIZE": 1,
}
# 排除目录与文件类型
EXCLUDE_DIRS = {".git", "node_modules", "dist", "build", ".venv", "vendor", "__pycache__"}
EXCLUDE_EXT = {".md", ".lock", ".min.js", ".min.css", ".svg", ".png"}
# 生产代码路径前缀（影响升级）
PROD_PREFIXES = ("src/", "app/", "lib/", "internal/", "cmd/")

PATTERN = re.compile(r"\b(BUG|FIXME|HACK|XXX|TODO|NOTE|OPTIMIZE)\b[:\s]*(.*)")

def is_prod(rel_path: str) -> bool:
    return rel_path.replace("\\", "/").startswith(PROD_PREFIXES)

def blame_age_days(file_path: str, line_no: int) -> int | None:
    """通过 git blame 获取该行最后修改时间，返回距今天数。"""
    try:
        out = subprocess.run(
            ["git", "blame", "-L", f"{line_no},{line_no}", "--line-porcelain", file_path],
            capture_output=True, text=True, check=True,
        )
        for line in out.stdout.splitlines():
            if line.startswith("author-time "):
                ts = int(line.split()[1])
                return (datetime.now(timezone.utc).timestamp() - ts) // 86400
    except Exception:
        return None
    return None

def classify(marker: str, prod: bool, age: int | None) -> str:
    w = MARKERS[marker]
    if w >= 4 and prod: return "P0"      # BUG/FIXME 在生产路径
    if w >= 3 and prod: return "P1"      # HACK/XXX 在生产路径
    if marker == "TODO" and age and age > 30: return "P2"
    if w <= 1: return "P3"              # NOTE/OPTIMIZE
    return "P2"

def scan(roots):
    debts = []
    for root in roots:
        for dirpath, dirs, files in os.walk(root):
            dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
            for f in files:
                if Path(f).suffix in EXCLUDE_EXT: continue
                fp = os.path.join(dirpath, f)
                rel = os.path.relpath(fp)
                try:
                    with open(fp, encoding="utf-8", errors="ignore") as fh:
                        for i, line in enumerate(fh, 1):
                            m = PATTERN.search(line)
                            if not m: continue
                            marker, text = m.group(1), m.group(2).strip()
                            age = blame_age_days(fp, i)
                            sev = classify(marker, is_prod(rel), age)
                            debts.append((sev, rel, i, marker, age, text))
                except (UnicodeDecodeError, OSError):
                    continue
    return debts

def main():
    roots = sys.argv[1:] or ["."]
    debts = sorted(scan(roots), key=lambda d: (d[0], -(d[4] or 0)))
    # 输出到文件 + stdout
    lines = ["# 技术债报告", "", f"共 {len(debts)} 条债务", ""]
    lines.append("| 级别 | 文件 | 行 | 标记 | 年龄(天) | 内容 |")
    lines.append("|------|------|----|------|---------|------|")
    for sev, rel, i, marker, age, text in debts:
        lines.append(f"| {sev} | {rel} | {i} | {marker} | {age if age else '-'} | {text[:60]} |")
    report = "\n".join(lines)
    Path("tech-debt-report.md").write_text(report, encoding="utf-8")
    print(report)
    # 摘要
    from collections import Counter
    c = Counter(d[0] for d in debts)
    print(f"\n摘要: " + ", ".join(f"{k}={v}" for k, v in sorted(c.items())))

if __name__ == "__main__":
    main()
```

把它存为 `.claude/skills/tech-debt-radar/scripts/scan.py` 并赋予执行权限：

```bash
chmod +x scripts/scan.py
```

## 五、配置渐进式披露：reference/severity.md

主 `SKILL.md` 故意只放了简版口径，详细的判定边界放进 `reference/severity.md`。Claude 默认不加载它，只在用户问「为什么这条 FIXME 是 P0 而那条是 P2」时才去读。这就是渐进式披露——把上下文窗口留给常用信息，把长尾细节按需展开。

```markdown
# 严重度判定细则

## P0 紧急（必须本迭代清掉）
- `BUG` / `FIXME` 出现在生产路径（src/ app/ lib/ internal/ cmd/）
- 出现在认证、支付、事务等关键模块，无论标记词

## P1 高
- `HACK` / `XXX` 出现在生产路径
- 任何标记词出现在最近一次生产事故相关文件

## P2 中
- `TODO` 且债务年龄 > 30 天
- 非生产路径的 `HACK` / `XXX`

## P3 低
- `NOTE` / `OPTIMIZE`
- 测试目录（test/ tests/ *_test.go）下的所有标记
```

## 六、验证 Skill：能否被正确触发

回到仓库根目录，启动 Claude Code：

```bash
cd /path/to/your/repo
claude
```

然后**用自然语言**测试三种触发强度（不要说「运行 tech-debt-radar」，那是作弊）：

**弱触发**（语义相近）：
```
> 这个项目欠了多少技术债？
```

**中触发**（命中关键词）：
```
> 帮我扫一下代码里的 TODO 和 FIXME
```

**范围触发**（带限定条件）：
```
> 只看 src/auth 目录的技术债，按严重度排一下
```

预期结果：Claude 会调用 Skill、运行 `python3 scripts/scan.py`（或带路径参数），然后读取 `tech-debt-report.md` 并总结。

**预期输出**（节选）：
```
# 技术债报告

共 17 条债务

| 级别 | 文件 | 行 | 标记 | 年龄(天) | 内容 |
|------|------|----|------|---------|------|
| P0 | src/auth/login.py | 42 | FIXME | 89 | 临时绕过 OAuth 校验，上线前必须修 |
| P0 | src/payment/refund.go | 118 | BUG | 12 | 并发退款会重复扣款 |
| P1 | lib/cache.py | 7 | HACK | 203 | 用全局变量模拟线程局部存储 |
...
摘要: P0=2, P1=5, P2=7, P3=3
```

## 七、常见问题与解决方案

**问题 1：Claude 没识别到 Skill，直接用 grep 了。**
原因多半是 `description` 里的触发词没覆盖你的问法。解决：把用户实际会说的短语补进 description，例如「代码异味」「烂尾」「欠的债」。

**问题 2：脚本扫描巨慢。**
`git blame` 对每条债务单独调用很慢。解决：给 `scan.py` 加一个 `--no-blame` 开关快速出全量，只在前 20 条债务上做 blame。也可以缓存 blame 结果。

**问题 3：误把文档里的 TODO 算进去了。**
检查 `EXCLUDE_EXT` 是否包含 `.md`，以及是否排除了文档目录（如 `docs/`）。本脚本已排除 `.md`。

**问题 4：Skill 没出现在 `/skills` 列表里。**
确认目录名与 frontmatter 的 `name` 一致，且 `SKILL.md` 的 frontmatter 用的是 `---` 三横线包裹。运行 `/skills` 查看已加载的 Skill 列表。

**问题 5：团队其他人 clone 后 Skill 不生效。**
确认 `.claude/skills/` 已被提交到 Git，且未被 `.gitignore` 排除。项目级 Skill 会随仓库分发。

## 八、进阶：用 eval 打磨触发准确率

Skill 写完不等于好用。官方推荐用 **eval（基准测试）** 量化触发准确率：准备 20 条用户问句，一半该触发、一半不该触发，跑 N 轮统计命中率与误报率。

写一个 `evals/trigger.jsonl`，每行一个测试用例：

```json
{"input": "扫一下所有 FIXME", "should_trigger": true}
{"input": "技术债雷达跑一下 src", "should_trigger": true}
{"input": "这段代码时间复杂度多少", "should_trigger": false}
{"input": "帮我写个 TODO 应用", "should_trigger": false}
```

然后用 skill-creator 或 eval 框架批量跑，统计：

- **召回率**：该触发的里，实际触发了多少
- **精确率**：不该触发的里，被误触发了多少

根据错例反复调 `description` 措辞，通常 2-3 轮就能把召回率拉到 90% 以上。这是把「能用的 Skill」打磨成「好用且不烦人」的关键一步。

## 实战案例：在一个真实仓库上跑通

以一个典型的 Web 后端项目为例。先制造几条债务样本（或直接在真实代码上跑）：

```python
# src/auth/login.py
def login(user):
    # FIXME: 临时绕过 OAuth 校验，上线前必须修
    return token(user)
```

```go
// src/payment/refund.go
func Refund(orderID string) {
    // BUG: 并发退款会重复扣款
    processRefund(orderID)
}
```

运行：

```bash
cd /path/to/project
python3 .claude/skills/tech-debt-radar/scripts/scan.py src/
```

报告会清楚显示这两条都被判为 P0，并标注债务年龄。然后你在 Claude Code 里说：

```
> 这两条 P0 债务分别在做什么？能合并到下个迭代吗？
```

Claude 会读取报告、定位文件、解释上下文，并给出排期建议——这正是「Skill + 大模型」的协同：脚本负责确定性扫描，模型负责理解与决策。

## 工作流程图

```mermaid
flowchart TD
    A[用户: '扫一下技术债'] --> B{Claude 语义匹配}
    B -->|命中触发词| C[加载 SKILL.md]
    B -->|未命中| Z[按普通对话处理]
    C --> D[运行 scan.py]
    D --> E[正则扫描标记]
    E --> F[git blame 取债务年龄]
    F --> G[分级: P0-P3]
    G --> H[生成 tech-debt-report.md]
    H --> I[Claude 读取报告]
    I --> J[总结: 分布/最老/热点模块]
    J --> K[给出排期建议]
    K --> L{用户追问分级细则?}
    L -->|是| M[渐进式加载 reference/severity.md]
    L -->|否| N[完成]
```

## 延伸阅读

- **Claude Code 官方 Skills 文档**：`https://code.claude.com/docs/en/skills` —— 权威的 SKILL.md 规范、目录约定、`/skills` 命令用法
- **Agent Skills 平台总览**：`https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview` —— Skills 作为开放标准的跨产品（Claude Code、Claude.ai、API）一致性
- **渐进式披露设计**：`https://pub.towardsai.net/progressive-disclosure-in-ai-agent-skill-design-b49309b4bc07` —— 把 SKILL.md 当成目录而非正文的设计哲学
- **skill-creator 工具**：用 `/skill-creator` 命令交互式创建、改写、跑 eval，是打磨触发准确率的利器

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| 适合的训练营类型 | 全栈开发营、DevOps/工程效能营、代码质量营、AI 辅助开发入门营 |
| 难度 | 中低（核心脚本 80 行 Python，Skill 文档无需 AI 知识） |
| 建议教学时长 | 2-3 小时（含动手实做一个自己的 Skill） |
| 学员产出 | 每人一个可运行的项目级 Skill + 一份真实仓库的技术债报告 |
| 延展练习 | 让学员改造成「安全债雷达」（扫描硬编码密钥、弱密码）或「文档债雷达」（扫描缺失文档的导出函数） |

本教程尤其适合作为「AI 工程化」的第一课：它同时展示了 Skill 的三个核心能力——**领域知识封装**（分级口径）、**确定性工具集成**（Python 脚本）、**按需加载**（渐进式披露），且产出物立刻能在学员自己的项目里产生价值。

## 参考来源

1. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — 官方 Skill 创建、管理与分享权威规范，含 SKILL.md 结构与 `/skills` 命令
2. [Agent Skills Overview — Claude Platform Docs](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) — Skills 作为开放标准的模块化能力定义与跨产品一致性说明
3. [Progressive Disclosure in AI Agent Skill Design — Towards AI](https://pub.towardsai.net/progressive-disclosure-in-ai-agent-skill-design-b49309b4bc07) — 渐进式披露设计哲学，把 SKILL.md 写成目录而非正文的实践
4. [Claude Agent Skills Framework Guide — DigitalApplied](https://www.digitalapplied.com/blog/claude-agent-skills-framework-guide) — 覆盖 SKILL.md 结构、渐进式披露与触发词设计的综合指南
5. [AI Agent Skills Guide 2026 — The Prompt Index](https://www.thepromptindex.com/how-to-use-ai-agent-skills-the-complete-guide.html) — 含 SKILL.md、Claude Code 与安全考量的 2026 综合实战指南

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