每个真实项目里都藏着一座「技术债」冰山:散落在代码各处的 TODO、FIXME、HACK、XXX 注释,标记着「以后再说」的烂尾逻辑。它们不会让构建立刻失败,却会在某次发版后突然引爆。本教程教你从零打造一个 Claude Code 自定义 Skill——技术债雷达:扫描整个代码库的债务标记,按严重度自动分级,关联 git blame 计算债务年龄,输出一份可排期的清单。学完你将掌握 Skill 的目录结构、YAML frontmatter 写法、渐进式披露设计、脚本集成与触发准确率调优,适合所有想给 Claude Code「装上行业专属雷达」的开发者。
claude 命令)git blame 需要可读的提交历史)无需任何 AI 训练或机器学习知识——Skill 的本质是一份「带触发条件的指令文档 + 可执行脚本」,你写规则,Claude 负责理解与调用。
直接用 grep -rn "TODO" 也能扫到所有标记,但它的局限正是 Skill 的价值所在:
| 维度 | 裸 grep | 技术债雷达 Skill |
|---|---|---|
| 标记分类 | 全部平铺 | FIXME/BUG/HACK/TODO/NOTE 自动分桶 |
| 严重度 | 无 | 按标记词 + 所在路径(生产/测试/脚本)分级 |
| 债务年龄 | 无 | 关联 git blame 给出「欠了多少天」 |
| 误报 | 文档里的 TODO 也算 | 排除 .md、node_modules、dist |
| 输出 | 一堆行号 | 一份可排期的 Markdown 报告 |
Skill 的真正威力在于:Claude 会在你说「看看这个项目的技术债」时自动加载这套规则和脚本,而不必每次都重复粘贴 grep 命令和分级口径。这就是官方文档强调的「技能即按需加载的领域知识」。
Claude Code 的 Skill 存放在三个位置,按优先级覆盖:
~/.claude/skills/<skill-name>/(所有项目可用).claude/skills/<skill-name>/(随仓库共享,团队协作推荐)本教程采用项目级,让团队能跟着仓库走。在仓库根目录执行:
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 顶部是一段 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 的三条铁律:
frontmatter 里不要写 version、author 等非官方字段——它们不会被解析,纯属噪音。官方规范只认 name 和 description(部分场景支持 allowed-tools 限制工具)。
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
主 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)下的所有标记
回到仓库根目录,启动 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 会随仓库分发。
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[完成]
https://code.claude.com/docs/en/skills —— 权威的 SKILL.md 规范、目录约定、/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 命令交互式创建、改写、跑 eval,是打磨触发准确率的利器| 维度 | 评估 |
|---|---|
| 适合的训练营类型 | 全栈开发营、DevOps/工程效能营、代码质量营、AI 辅助开发入门营 |
| 难度 | 中低(核心脚本 80 行 Python,Skill 文档无需 AI 知识) |
| 建议教学时长 | 2-3 小时(含动手实做一个自己的 Skill) |
| 学员产出 | 每人一个可运行的项目级 Skill + 一份真实仓库的技术债报告 |
| 延展练习 | 让学员改造成「安全债雷达」(扫描硬编码密钥、弱密码)或「文档债雷达」(扫描缺失文档的导出函数) |
本教程尤其适合作为「AI 工程化」的第一课:它同时展示了 Skill 的三个核心能力——领域知识封装(分级口径)、确定性工具集成(Python 脚本)、按需加载(渐进式披露),且产出物立刻能在学员自己的项目里产生价值。
/skills 命令[!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 等场景全覆盖。
