首页 › tutorials › 造技术债雷达Skill:扫描TODO并自动分级排期

概述

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

前置条件

无需任何 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 存放在三个位置,按优先级覆盖:

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

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 能否生效的关键。

---
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 只负责理解用户意图和总结报告。

#!/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 并赋予执行权限:

chmod +x scripts/scan.py

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

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

# 严重度判定细则

## 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:

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,每行一个测试用例:

{"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 后端项目为例。先制造几条债务样本(或直接在真实代码上跑):

# src/auth/login.py
def login(user):
    # FIXME: 临时绕过 OAuth 校验,上线前必须修
    return token(user)
// src/payment/refund.go
func Refund(orderID string) {
    // BUG: 并发退款会重复扣款
    processRefund(orderID)
}

运行:

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

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

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

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

工作流程图

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[完成]

延伸阅读

训练营适用性评估

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

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

参考来源

  1. Extend Claude with skills — Claude Code Docs — 官方 Skill 创建、管理与分享权威规范,含 SKILL.md 结构与 /skills 命令
  2. Agent Skills Overview — Claude Platform Docs — Skills 作为开放标准的模块化能力定义与跨产品一致性说明
  3. Progressive Disclosure in AI Agent Skill Design — Towards AI — 渐进式披露设计哲学,把 SKILL.md 写成目录而非正文的实践
  4. Claude Agent Skills Framework Guide — DigitalApplied — 覆盖 SKILL.md 结构、渐进式披露与触发词设计的综合指南
  5. AI Agent Skills Guide 2026 — The Prompt Index — 含 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学通信知识星球海报-扫码加入