🟡 进阶

用 skill 维护老项目:排查、修复、迭代

不讲"你应该先画地图、再补测试"这类道理,讲具体怎么调:每个 skill 你说什么话它会启动、它内部按哪几步跑、产出长什么样、你怎么验收。12 个 skill,分三个阶段。

更新于 2026-09-16
12
个 skill,逐个拆到能直接用
0–10
有一个 skill 会给你的改动打分
100 行
生成的 CLAUDE.md 该有的上限
8 问
一张自检表,答"否"就停手

01速览与选表

读完这一篇,你能做到一件事:拿到一个你不熟的老仓库,知道在每一个卡点上该调哪个 skill、用什么话把它叫起来、它跑完你该看什么来判断"这次可以信"。

🎯 先记住这三句

① skill 不是魔法,是一份写好的说明书。它不提升 AI 的智商,它把"每次都要重讲一遍的纪律"固化下来(skill:一段叫 SKILL.md 的文本,AI 在合适的时候自动读它并按里面的步骤干活)。

② 判断一个 skill 值不值钱,只看两处:它的步骤里有没有"可验证的关卡",它默认会不会动你的代码。

③ 三个阶段各有一批:排查(只读不写)→ 修复(每步有红绿)→ 迭代(合并前的闸门)。不要跳阶段。

你现在的处境 → 调哪个 skill

你现在的状态调这个在哪一节
刚接手,打开仓库不知道从哪看codebase-onboarding§04
代码是自己(和 AI)写的,但业务已经说不清了code-to-doc-generator§04
同一个词,两个人理解不一样;"当年为什么这么写"没人记得domain-modeling§04
要往一块没测试的老代码里加功能working-with-legacy-code§05
想删一批没人用的文件/导出/依赖refactor-clean§05
只缺"把现状钉住"那层测试characterization-test-generator§05
线上有个复现不出来的 bug / 说不清为什么变慢了diagnosing-bugs§05
这块已经有测试了,要继续加功能tdd§05
清了一大堆改动,准备合并code-review§06
走通几条路了,想知道下一步挖哪里improve-codebase-architecture§06
清理计划超过一天工时,需要分几次做to-spec → to-tickets§06
issue 和外部 PR 堆了一堆,不知道该先看哪个triage§06

下面 30 秒速览,把三个阶段串成一条线:

codebase-onboarding
只读:拿地图
→
code-to-doc-generator
只读:拿业务文档
→
working-with-legacy-code
改:先补测试再动
→
refactor-clean
改:一次删一个
→
code-review
闸门:合并前两轴审

02为什么这套纪律必须被固化下来

这一节很短,但它是"为什么要用 skill"的答案。如果你已经确信这一点,可以直接跳到 §03。

老项目难改,不是新问题

2000 年 4 月,Joel Spolsky 写了一篇影响很大的文章《Things You Should Never Do》(有些事你永远不该做),核心论点一句话:把软件从头重写,是一家软件公司能犯的最严重的战略错误。他举的例子是网景——它在 4.0 和 6.0 之间决定重写浏览器,结果 4.0 到 6.0 隔了大约三年(从来没有一个叫 5.0 的版本),这三年里只能看着对手把市场份额吃掉。

关键是机制:老代码里那些"丑"的地方,很多是买来的知识——两页长的函数、看起来多余的判断,不少是多年来一个个真实 bug 的补丁,每个当时可能花了好几周才定位。你重写一遍,等于把这些知识原地扔掉,而且你不会记得它们存在过。

还有一句题眼:"读代码比写代码难。" 正因为读起来痛苦,程序员就会产生"这坨东西之所以难读,是因为写得很糟"的错觉。Joel 的判断是:这个错觉经常是错的。

AI 在这上面又加了一层

目前最常被引用的一份证据是 GitClear 的《AI Copilot Code Quality》2025 年度报告:2.11 亿行代码改动,2020 年 1 月到 2024 年 12 月,来源含 Google、Microsoft、Meta 等企业的匿名私有仓库和 25 个最大的开源项目。

4×5 行以上的重复代码块,2024 年的出现频率约为 2020 年的 4 倍
8.3% → 12.3%重复(克隆)行占全部改动行的比例
25% → <10%重构相关改动占比,2021 到 2024

最有信息量的一条是:2024 年,"复制粘贴"第一次超过了"搬动代码"。 历史上开发者搬动代码(把函数提取成公共的、把这边的东西挪到那边)一直多于复制粘贴,而搬动代码是"有人在整理结构"的信号。这两条线交叉,说明整理结构的动作整体变少了。

机制:为什么 AI 会制造重复

你让它"给订单加个优惠券功能",它有两条路:A 先翻遍整个项目找到已有的算价逻辑再复用它(要求它先理解全局);B 直接生成一段完整的、自己就能跑通的算价代码(不要求它理解任何东西)。它能看到的上下文是有限的(上下文窗口:模型一次能"看到"的文字总量),而项目往往比这个窗口大得多。所以在绝大多数时候,B 是它唯一有把握的路。

结果就是你项目里会长出这种东西:

// src/services/order.ts —— 半年里陆续长出来的
export function calcTotal(items)           // 3 月加的,现在只有两个地方还在用
export function calcTotalV2(items)         // 5 月加的,线上在用的是这个
export function calcTotalWithCoupon(items) // 7 月加的,复制了 V2 又改两行
export function calcTotalForRefund(items)  // 8 月加的,又复制了一份

这四份都能跑,也都在被用。这就是它最难的地方:不是"报错那种坏",是"改了一份、忘了另外三份"那种沉默的坏。

⚠️ 数字口径(这一节要带着这个读)

GitClear 这份报告是观察性的:它统计的是已合入代码的改动形态在几年间的聚合趋势,不是控制变量的实验。所以它支持"AI 普及的同时,代码重复度在上升、结构整理在减少"这个推断,但不能证明"是 AI 导致了所有这些下降"。

你还会看到其他口径的数字(比如"AI 生成的 PR 等待审核时间是人类的 4.6 倍、接受率只有 32.7%"这类,来自 2026 年对约 810 万个合并请求的分析)。别把这些和 GitClear 的直接相加或对比——一个看合入之后代码的形态,一个看改动在流程里怎么流转。

所以结论是:靠人记不住,要写下来

上面这些病,对策其实早就有了——Feathers 2004 年那本书里全写着:先补表征测试、找到接缝、一次只改一处、重构和功能分开提交。问题从来不是"不知道",是"每次都要重讲一遍"。

你对 AI 说一次"先跑测试再改",它会照做;说二十次,它会开始漏。这就是 skill 存在的意义:把纪律从"你的记忆力"里搬到"文件"里。

03skill 到底是什么:怎么装、怎么触发、怎么判断能不能用

它是一个文件,放在两个地方之一

一个 skill 就是一个名叫 SKILL.md 的文本文件,放在某个目录里:

放哪路径什么时候用这一档
项目级 <项目根>/.claude/skills/<名字>/SKILL.md 这个 skill 只对这个项目有意义(比如"我们这套订单模块的改法")。可以跟着仓库提交,团队共享。
个人级 ~/.claude/skills/<名字>/SKILL.md 你自己所有项目都想用(比如"怎么给老代码补表征测试")。装一次,处处有效。

文件开头有一段用三条短横线包起来的元信息(叫 frontmatter,就是"文件头"),里面最重要的是 description 那一行——AI 就是靠这一行决定要不要启动这个 skill 的。所以一个描述写得好的 skill,你根本不用记它的名字,说对话它自己就出来了。

三种把它叫起来的方式

🤖
① 自动触发
你说的话命中了它的描述,它自己启动。比如你说"帮我看看这个仓里到底有些什么",codebase-onboarding 就可能自己接上。
⌨️
② 显式调用
直接敲名字,比如 /code-review、/to-tickets。最可控,也是我下面推荐的方式。
💬
③ 自然语言点名
"用 working-with-legacy-code 那个 skill 来改这块"——直接指名,绕过自动匹配的不确定性。
🎯 一个容易忽略的坑:有些 skill 永远不会自己启动

你会在 SKILL.md 的头部看到一行 disable-model-invocation: true。它的意思是:只有你明确敲名字它才会跑,AI 不能自己决定启动它。本文提到的 to-spec、to-tickets、triage、improve-codebase-architecture、wayfinder 都是这样——因为它们会往你的任务追踪系统里写东西,让 AI 自己触发太危险。

看到这一行,就说明你得记住它的名字。

怎么装一个社区 skill

本文会提到一批社区 skill(不是官方内置,是别人写了开源的)。装法基本就一种——把 SKILL.md 放进上面那两个目录之一:

# 装到个人级(所有项目都能用)
mkdir -p ~/.claude/skills/working-with-legacy-code
curl -fsSL "<那个 skill 的 SKILL.md 原始链接>" \
  -o ~/.claude/skills/working-with-legacy-code/SKILL.md

# 或者用 skills 工具装(有些仓库支持)
npx skills add https://github.com/<作者>/<仓库> --skill <名字>
⚠️ 装之前必须做的一件事:读一遍它的 SKILL.md

它们只有几百到几千字,五分钟能读完。而且同名 skill 往往有好几份不同实现——我在查的时候,codebase-onboarding 至少见到三份、refactor-clean 至少有四份。名字一样,步骤的严格程度并不一样。

读的时候只看两处:① 它的步骤里有没有"跑测试看红绿"这种可验证的关卡;② 它有没有"不确定就跳过"这类兜底规则。两条都没有的,别用在老项目上。

📌 本文提到的 skill 分三类

本机已装(你机器上大概率已经有了,直接敲名字就能用):code-review、diagnosing-bugs、tdd、domain-modeling、code-to-doc-generator、improve-codebase-architecture、to-spec、to-tickets、triage、wayfinder、codebase-design、research。

社区(要去装):working-with-legacy-code(作者 wondelai,v1.2.0,MIT)、refactor-clean、codebase-onboarding、characterization-test-generator。

内置(随 Claude Code 一起):simplify、security-review、init。

不确定自己装了哪些,列一下目录就知道:ls ~/.claude/skills/

skill 之外还有一层:CLAUDE.md 里最值钱的是"不做什么"

skill 是"一件事的说明书",CLAUDE.md 是"这个项目的常驻须知"。两者要一起用:skill 管流程,CLAUDE.md 管纪律。

写这份文件时,最反直觉的一条是:写"不做什么",比写"做什么"值钱得多。 因为"做什么"通常读代码就能看出来,而"这个目录不能删""这个写法是故意的,不要改成标准的"——这类信息代码里完全看不出来,而 AI 会非常自信地把它"改正"。

# CLAUDE.md(这份文件只写"从代码里看不出来"的东西)

## 项目
Node 20 + TypeScript,包管理器用 pnpm(不要用 npm install)。

## 常用命令
- 装依赖:pnpm i
- 类型检查:pnpm typecheck
- 跑测试:pnpm test -- --no-cache

## 护栏(这些是硬约束,优先级高于任何"顺手改进")
- legacy/ 目录已废弃,但被 3 个线上模块引用:不要删除、不要搬移其中任何文件。
- 认证走的是自定义中间件,不要"改写成标准的 Express 中间件"。
- 改任何文件之前,先说明你打算改哪些文件、为什么,等我确认。
- 只改我点名的文件。如果你觉得别的文件也该改,先列出来问我,不要直接动手。
- 一次提交只做一件事:重构的提交里不要夹功能改动。
🎯 这份文件的写法只有一条规则

问一句:"这件事 AI 读代码能不能自己看出来?" 能,就删掉。

只留它猜不到、而且会影响操作的东西:用哪个包管理器、测试必须带什么参数、以及这个老项目特有的、代码里看不出来的禁区("legacy/ 别删"就是典型——它读代码只会看到"这个目录好像没人用")。

规模参考:那个生成 CLAUDE.md 的 skill 自己定的上限是 100 行;Anthropic 的建议是不超过约 200 行;前沿模型的"指令预算"大约是 150–200 条。

什么时候该自己写一个

不用一上来就去找别人的 skill。最好的来源是你自己——你在这个项目上重复口述第三次的那段话,就是一个 skill 的雏形。

比如这几句,如果每次让 AI 改老代码你都要说一遍,那它就该被写下来:

## 改这个项目的代码之前,永远先做这三件事
1. 先跑 `pnpm test -- --no-cache`,把结果贴给我(红也要贴,我要知道基线)。
2. 只改我点名的文件。想改别的,先列出来问我。
3. 改完再跑一次同样那条命令,贴结果。红了就 `git revert`,不要手改回去。

把它存成 .claude/skills/<名字>/SKILL.md,开头加上用三条短横线包起来的 frontmatter(至少要有 name 和 description),就成了你自己的 skill。写法本站已有专篇:《定制自己的 Skill:把重复劳动变成一句话命令》。

04① 排查:三个只读的 skill

这一批的共同点是产出是文件,不是改动。所以风险最低,第一次接老项目就从这里开始。

/codebase-onboarding
从一个陌生仓库,拿到一张地图和一份 CLAUDE.md 草稿
社区 · 只读,最后写两个文件
它把"读一个陌生代码库"拆成四个阶段,而且第一阶段刻意不读源码——只读配置文件。为什么?因为"README 描述的是意图(而且经常过期),清单文件描述的是现实"。
它内部按这四步跑
1
侦察(并行检查,不读源码)

六项扫描同时跑:清单文件(package.json / go.mod / Cargo.toml / pyproject.toml / pom.xml / build.gradle / Gemfile / composer.json / mix.exs / pubspec.yaml)· 框架指纹(next.config / nuxt.config / angular.json / vite.config / Django settings / Flask app factory / fastapi main)· 入口点(main.* / index.* / server.* / cmd/ / src/main/)· 两层目录快照(跳过 node_modules、vendor、.git、dist、build、__pycache__、.next)· 配置与工具(eslint / prettier / tsconfig.json / Makefile / Dockerfile / docker-compose* / .github/workflows/ / .env.example)· 测试结构(tests/ / __tests__/ / *_test.go / *.spec.ts / jest / vitest 配置)。

2
架构映射

给出技术栈(语言版本、框架、数据库/ORM、构建工具、CI/CD)· 架构形态(单体 / monorepo / 微服务 / serverless)· 关键目录各自管什么 · 以及追踪一条真实请求:从入口 → 校验 → 业务逻辑 → 数据库。

3
约定检测

命名约定(文件、组件、测试)· 代码模式(错误处理、依赖注入还是直接 import、状态管理、异步写法)· Git 约定(分支命名、提交风格、PR 流程)。如果这个仓库没有提交历史或者只有很浅的历史,它会跳过这一节并明确说"git 历史不可用或太浅,无法检测约定"——而不是猜一个给你。

4
产出两样东西

① 一份上手导览:概览 / 技术栈 / 架构 / 关键入口 / 目录地图 / 请求生命周期 / 约定 / 常见任务 / 该去哪找。
② 一份 CLAUDE.md 草稿:技术栈 / 代码风格 / 测试 / 构建与运行 / 项目结构 / 约定。如果已经存在 CLAUDE.md,它会先读、然后增强,保留你原有的指令并标出改了哪里——不会直接覆盖。

怎么调它(它的 SKILL.md 里给了三种说法)
"Onboard me to this codebase"
→ 跑完整四阶段,导览输出在对话里,CLAUDE.md 写进项目根目录

"Generate a CLAUDE.md for this project"
→ 只跑第 1–3 阶段,只产出 CLAUDE.md,不写导览

"Update the CLAUDE.md with current project conventions"
→ 先读现有文件,跑第 1–3 阶段,合并,标出新增了什么
它自己写的铁律和反模式(这是判断一份实现好不好的依据)
  • 用 Glob / Grep 扫,不要对每个文件调 Read——只在有歧义时才选择性读文件。
  • 代码和配置冲突时,信代码。
  • 尊重已有的 CLAUDE.md——增强,不替换。
  • 导览要"2 分钟能扫完"。
  • 不确定的地方标出来,不要猜。
  • 反模式:CLAUDE.md 超过 100 行 · 把每个依赖都列出来 · 解释一眼就懂的目录名 · 抄 README。
你怎么验收

它产出的那份 CLAUDE.md 打开数一下行数——超过 100 行,就是它在往里塞代码里本来就看得见的东西(第 §03 那两条判断标准)。然后从它的结论里挑 3 条去核对,比如它说"认证在 src/middleware/auth.ts",你就去那个文件看一眼。

/code-to-doc-generator
从代码倒推出"这块业务到底在干什么"
本机已装 · 只读,产出文档
老项目最缺的往往不是"代码看不懂",是"业务说不清"。这个东西专门干倒推:从代码和界面里把业务逻辑提取出来,写成一份不同阶段的人能看的文档。
它有两种模式,选错了会白跑
模式什么时候会进这个模式它做什么
A · 生成 你说"生成文档 / 写文档 / 没有文档 / 帮我写个 HLD" 从代码逆向提炼,四轮走完:识别项目类型与入口 → 结构扫描提取信息骨架 → 逐模块深度分析 → 生成文档。
B · 反向同步 你说"更新文档 / 同步文档 / 文档和代码对不上 / 文档漂移" 以代码为真相源去校准已有文档。注意:它会直接跳去另一份流程文件,不再走四轮。
它有一条硬规则,你要知道
⚠️ 文档类型必须由你明确说出来

它的 SKILL.md 里写着一条硬阻断:文档类型不得从上下文推断或自主选择,在类型确认前分析步骤禁止启动。你只说了"帮我写个文档",它会停下来问你。

所以开口时就把类型说清楚,能省一轮:帮我写 HLD / 生成 PRD / 出一份运营手册。可选的类型覆盖了整条交付链路——BRD/MRD(立项)· PRD(需求)· HLD 概要设计(架构)· LLD 详细设计 · DDD 领域设计 · 编码指南 · 测试文档(BDD / SIT / E2E / UAT)· 运营手册 · SLI/SLO 监控 · CI/CD。

你怎么验收

拿到文档后做一件事:找出里面至少 3 条"你不知道、而且是靠读代码才能得出"的结论,去代码里核实。如果通篇都是你本来就知道的东西,说明这次倒推的深度不够,把范围收窄到某一个模块再要一次。

/domain-modeling
把"当年为什么这么写"固定下来
本机已装 · 产出 CONTEXT.md 与 ADR
它产出两类文件:CONTEXT.md(这个项目里每个词是什么意思的术语表)和 ADR(架构决策记录——"当初为什么这么选"的短文档,放在 docs/adr/ 下)。注意它区分得很清楚:仅仅读 CONTEXT.md 取词汇不算这个 skill,那只是一行习惯;这个 skill 是"你在改变这个模型"的时候用的。
它在会话里做的五件事
  • 拿术语表挑刺:你说的词和 CONTEXT.md 里定义的不一致,它当场指出来——"你的术语表把 'cancellation' 定义为 X,但你似乎指的是 Y。是哪个?"
  • 把模糊说法磨尖:你说"account",它会问"你指的是 Customer 还是 User?这是两个不同的东西"。
  • 用具体场景压测:故意构造边界场景,逼你把概念之间的边界说清楚。
  • 和代码交叉核对:你描述的行为和代码对不上时,它会把矛盾摆出来——"你的代码是整单取消,但你刚说支持部分取消。哪个是对的?"
  • 当场更新 CONTEXT.md,不攒着批量写。

另外一条纪律值得注意:CONTEXT.md 里不许有任何实现细节——它只是术语表,不是规格、不是草稿本。

ADR 只在三个条件同时成立时才建(这条最实用)
① 难以反悔
以后改主意的代价是实打实的。
② 没有背景会困惑
未来的读者会问"他们当年到底为什么这么做?"
③ 是真实取舍的结果
当时确实有别的选择,你因为具体理由挑了这一个。

三条缺任何一条,就不建 ADR。 这个门槛是它防"文档泛滥"的机制——ADR 一多就没人看了。

你把老项目接过来时怎么用它

最好的时机是你终于从某个老同事嘴里问出"当年为什么要这么写"的那一刻——趁热让它把这句话落成一个 ADR。这类知识不写下来,就随人一起走掉了。

做法:把那段对话原样讲给它听("我们当年用双写而不是事务,是因为要兼容一个已经不存在的旧系统"),它会按三个条件判断该不该建、该建在哪、怎么命名(docs/adr/0001-event-sourced-orders.md 这种格式)。

🎯 这一阶段的正确预期

跑完这三个 skill,你大概会花掉 半天到一天,一行代码都没改。换来的是:一份导览、一份业务文档、一份术语表、可能还有几个 ADR。

这个投入值不值?只要这个项目你还要维护超过两周,就值——因为接下来每一次你和 AI 的对话,都建立在这些文件之上,而不是从零开始解释。

手工版:不想装 skill,或者它的输出你不满意时

codebase-onboarding 内部做的事并不神秘,下面这套你可以自己跑一遍。而且它有一个 skill 做不到的好处:每一步你都亲眼看了。

① 侦察:先问"哪里有什么",别急着问"怎么运转"

# 只看两层目录,别陷进去
ls -la
find . -maxdepth 2 -type d -not -path '*/node_modules*' -not -path '*/.git*' | sort

# 读"清单文件"——它描述现实;README 描述意图,而且经常过期
cat package.json        # JS / TS 项目
cat pyproject.toml      # Python 项目
cat go.mod              # Go 项目

# 近半年哪些文件被改得最勤?热点通常就是核心
git log --since="6 months ago" --name-only --pretty=format: \
  | sort | uniq -c | sort -rn | head -30

# 哪些文件最长?最长的几个通常最需要小心
git ls-files | xargs wc -l 2>/dev/null | sort -rn | head -20

② 跑起来,然后故意弄坏它

pnpm i && pnpm dev   # 目标只有一个:让它在本机跑起来

# 跑起来之后,在三个位置打点:路由入口、核心计算函数、写库之前
console.log('[打点] 算价前', JSON.stringify(items))
console.log('[打点] 算价后', total)

跑不起来也是收获——跑不起来的原因本身就是重要线索(缺哪个环境变量、依赖哪个外部服务、哪个包被钉死在旧版本)。而"跑起来之后故意弄坏它"这一步产出的理解,比读十个文件都结实。

③ 产出必须是一个文件,不是一个记忆

这一步最容易被跳过,代价也最大:理解只存在你脑子里或某次对话里,那么这次对话结束它就没了,下一个人(包括三个月后的你)要从头再来一遍。

# CODEBASE-MAP.md
# 这份文件记录"现在实际是这样",不是"应该是什么"。

## 技术栈
Node 20 + TypeScript 5 + PostgreSQL 15;包管理器 pnpm(不要用 npm install)

## 入口
- 服务启动:src/server.ts
- HTTP 路由:src/routes/index.ts
- 定时任务:src/jobs/

## 一条请求怎么走
POST /orders → routes/orders.ts → services/order.ts → repositories/order.repo.ts → Postgres

## 隐藏行为(读代码看不出来的)
- 库存扣减是异步的,失败会静默重试 3 次,不会抛错给调用方
- /v1/users 已废弃,但被 3 个线上模块引用,不要删

## 怎么跑起来
pnpm i && pnpm dev   # 需要 .env 里的 DATABASE_URL

④ 让 AI 画地图的话术

读这个仓库(先不要改任何文件)。只回答四件事:

1. 入口在哪里。一条请求从进来到返回,按顺序经过哪几个文件。
2. 哪些文件是"被依赖最多的"——列出被 import 次数最多的前 10 个。
3. 有没有循环依赖:如果有,具体是哪几个文件在互相引用。
4. 你判断哪些是核心业务逻辑、哪些是工具/胶水代码。

不确定的地方写"不确定",不要猜。
每个结论后面附上你看到它的文件路径和行号。
🎯 最后两句是整段话术的关键

"不确定就写不确定" + "每条结论附文件路径和行号"。这两句把 AI 的输出从"看起来挺靠谱"变成了可以核对。没有这两句,你拿到的是一段无法验证的自信陈述。

拿到之后做一次抽查 3 条:从它的结论里挑 3 条,去它给的行号看一眼。3 条里错 1 条 → 把整张图降级成"待验证";错 2 条 → 扔掉重来,换一个更小的范围再问一次(比如只问"订单这一块怎么走")。

05② 修复:五个带红绿关卡的 skill

这一批会改代码。判断该不该用它,就看前面那两条:它有没有每步验证,它默认改多大范围。

先装上这一层:不管用哪个 skill,版本控制上的四件事都要做

这是所有"安全带"里最便宜的一层——不需要写任何测试,四条命令。skill 不会替你记住它们,但你可以在 CLAUDE.md 里写死。

# 1. 永远不在主干(main 分支,也就是线上跑的那份)上做清理,开一条专门的分支
git switch -c refactor/order-total

# 2. 逐个代码块决定要不要进这次提交(而不是整个文件一起进)
git add -p

# 3. 出事了整段撤掉,不要手动改回去
git revert <commit>

# 4. 改之前先看一遍自己到底改了什么
git diff --stat

其中 git add -p 值得单独解释:它让你逐个"代码块"决定进不进这次提交,而不是整个文件一起进。为什么清理时特别需要它——因为你经常遇到"同一个文件里,既有你想提交的那个重命名,又有 AI 顺手改的别的东西"。

git revert 和"手动改回去"的区别也值得说清:revert 是生成一次新的提交,把之前那次的效果反向撤销,历史完整可查;手动改回去则是又产生一次你想不起来的修改。回退时选前者。

/working-with-legacy-code
本阶段最重要的一个:先补测试再改,而且会给你打分
社区 · wondelai · v1.2.0 · MIT
它是照着 Michael Feathers 那本《Working Effectively with Legacy Code》做的。它的核心宣言只有一句:"遗留代码就是没有测试的代码"——不是老旧,不是丑,是没测试。所以它的做法是:先打通依赖、装上测试,然后才动真格的。它自己的口号叫 "cover and modify, never edit and pray"(先覆盖再修改,永远不要"边祈祷边改")。
最有特色的一点:它会给你的改动打分(0–10)

它要求每次改动后按原则自评,报告当前得分,以及达到 10 分需要做的具体步骤。这是本文所有 skill 里最"可验收"的一个——你不需要判断它做得对不对,你只需要看它给自己打了几分、以及差在哪。

分数它描述的状态这说明什么
9–10改动点在动之前就被表征测试覆盖;行为改动和重构拆成两步各自验证;依赖打破用了最小侵入的手法这就是你要的状态
7–8大部分改动点有测试,但偶尔把重构和行为改动混在一次提交里,或者做了没必要的大手术能用,让它拆开提交
5–6有一些表征测试,但关键路径还是靠"信"改的;sprout 出来的代码在堆积,却没有偿还计划停下,先补测试
3–4边祈祷边改 + 手工验证;测试是改完之后补的,断言的是新代码自己的行为这次结果不可信
0–2在纠缠的代码里做无测试改动;重构和行为改动混在一个提交里;提出"不如重写"而不是去写测试它走偏了,重来
🎯 怎么用这个分数

每次让它改完,直接问一句:"按这个 skill 的标准,这次改动打几分?差在哪几步?" 分数低于 7 就别合并——让它把差的那几步补上。这比你自己盯着 diff 判断快得多。

它的核心算法:五步(顺序不能换)

它把这个叫 "遗留代码的两难":安全地改需要有测试,但装上测试又需要先改代码。它的解法是把顺序钉死:

1
定位改动点

你这次到底要动哪一行、哪个分支。越具体越好。

2
找测试点

测试点通常和改动点不是同一个地方——因为效果会沿着调用链扩散出去。你要找一个能"观察到这次改动效果"的位置。

3
打通依赖

把拦住你写测试的东西弄开(下一节讲手法)。这一步的改动必须"保守且机械",而且签名一个字不改——这样编译器会帮你指出所有受影响的地方。

4
写测试

到这一步才开始写表征测试。前面三步都是在为它铺路。

5
做改动 + 重构

只有到这一步,真正的改动才发生——而且此时此刻它是在安全网里面动的。

它给这个顺序配了一句解释,值得记住:"编程就是一次只做一件事的艺术"——因为每一步都能被单独验证。反过来,行为改动和结构改动混在一个步骤里,出问题时就再也分不清是谁造成的。

没测试也要加功能:Sprout 和 Wrap

如果那块地方今天真的测不了,它的建议不是"那就别写测试了",而是不要把新逻辑编进那坨没测试的东西里:

❌ 直接往老函数里塞

在一个 400 行的 process() 里加一段滞纳金计算。这段新逻辑没测试,而且它和周围 400 行没有任何边界。

✅ Sprout(发芽)

新逻辑写成一个全新的、自带完整测试的小函数,从老代码里用一行调用它。

好处:新代码可以照常测试驱动开发;宿主只改了一行,所以"没被验证的爆炸半径"就一行。

Wrap(包裹)是另一个手法:把老函数改个名收起来(pay() → rawPay()),然后重建一个同名的 pay() 当外壳,在调用前后加新行为(审计日志之类的)。适合有多个调用点、或者那个类已经太臃肿的时候。

⚠️ 这个 skill 自己诚实标了一个取舍

它明说:宿主那块老代码仍然没有测试,你只是"在一个坏街区里建了一栋好房子",sprout 出来的东西要记成技术债,下次再碰这块的时候要还。

它还有一条防滥用的提醒:sprout 是改动算法内部的一个战术动作,不是覆盖率的替代品。 你不能一直发芽、永远不给宿主补测试。

打通依赖的八种手法(它带一份目录)

按侵入性从小到大:提取接口 · 把构造函数参数化 · 把方法参数化 · 提取并覆写工厂方法/取值方法 · 引入实例委托 · 适配参数 · 把方法拆成独立对象 · 子类化并覆写方法。

它的选择规则是:永远挑"能解开当前问题的最小侵入手法"。 其中两个最常用——

  • 把构造函数参数化(它是主力手法):给它加一个参数并提供一个生产环境默认值,现有调用点一个字都不用改就能编译通过。
  • 提取接口(它是最安全的动作):因为"加一个接口"这件事本身不可能改变行为。

反过来,它点名了接缝死掉的地方:构造器里干实事、全局变量、静态变量、写死的 I/O。以及一个判断:一个没有"启用点"的接缝是没用的——所谓启用点,就是"在这里决定用哪个实现"的那个位置。

最能说明问题的是"把构造函数参数化"这个最常用的手法。注意改完之后,生产代码的行为一个字没变:

// 改之前:这个函数没法单独测,因为它一跑就去连真数据库
function getOrderTotal(orderId) {
  const db = new RealDatabase(process.env.DATABASE_URL)
  return db.query('...')
}

// 改之后:多了一个参数,这个参数就是"接缝"
// 测试里可以传一个假的 db,生产代码里走默认值,行为完全不变
function getOrderTotal(orderId, db = defaultDb) {
  return db.query('...')
}

它对这个手法的评价是"主力手法",原因就是那句:现有调用点一个字都不用改就能编译通过——所以它属于"保守且机械"的那一类前置改动。

看不懂代码的时候:三个便宜的搞懂手法
  • 效应草图:给每个变量、每个方法画一个气泡,谁影响谁就画一个箭头,一直追到所有"泄漏点"。目的是找到收窄点——那个"少量测试就能覆盖大片行为"的窄口。
  • 特征草图:看一个巨大的类里面,方法和字段是怎么聚成几团的——团与团之间通常就是天然的拆分边界。
  • 草稿式重构:在一条件上随便重构,目的只是为了看懂,然后整条扔掉。"洞见会留下来,代码不会。"

它的判断是:遗留代码工作的瓶颈是"理解",不是"打字"。

它自带的一张自检表(答"否"就停手)
问自己如果答案是"否"
测试覆盖了你要改的代码吗?你就是在"边祈祷边改"——先跑算法,动之前把行为钉住
你能在测试里把这个类构造出来吗?依赖挡住了隔离——把构造函数参数化、提取接口、或者 sprout 出一个新类
测试能"感知到"你这次改动的效果吗?效果对断言不可见——去找一个可感知的位置
这次提交是"只有行为"还是"只有结构"?混了——拆开,两次提交中间跑一遍测试
你知道这次改动可能影响到哪些地方吗?爆炸半径未知——画效应草图,在收窄点上测
你的断言写的是"观察到的行为"吗?你在测试你的愿望——先探一次,读失败信息,把真实值钉下来
你选的这个接缝,是可选里最便宜的那个吗?做了没必要的手术——优先构造函数参数和 import 接缝
这次改完之后,这块的覆盖比之前好了吗?下次改它的成本和这次一样高——至少在最近的测试点留一个钉住测试
它列出的八条常见错误(照单避开)
错误为什么会失败
重构和行为改动放在一步里做你分不清是哪一次编辑弄坏的
给老代码写"应该怎样"的测试想象中的规格会大量失败,然后你会去"修"那些其实在承重的行为
什么都 mock测试钉住了实现细节,之后每次重构都会把它弄红
用大爆炸重写代替增量覆盖旧系统还在动;重写上线晚,还会漏掉边界情况
做表征测试时顺手把 bug 修了调用方和报表可能正依赖着那个"错"的行为
测试还不存在就先做侵入式清理手工改动在没有任何网的情况下拿行为冒险
一直发芽,从不偿还宿主始终没测试,芽会硬化成新的遗留代码
等一个专门的"补测试项目"立项它永远不会被批;覆盖率永远不出现——改成跟着每一次改动长
怎么调它

它的触发词里包含这些原话:legacy code、no tests、untested codebase、how do I test this、seams、characterization tests、golden master、sprout method、afraid to change this code、monster method、inherited a messy codebase。所以你可以直接说:

"这块代码没有测试,我不敢改。用 working-with-legacy-code 的方法带我做:
 先告诉我改动点和测试点分别在哪、要打通什么依赖。"

# 它跑完之后,追问这一句:
"按这个 skill 的标准给这次改动打分,并列出达到 10 分还差哪几步。"

一条边界要知道:它明确把"改造已经有测试的代码"路由给另一个 skill(refactoring-patterns),把"日常代码质量"路由给 clean-code。 这是好事——说明它知道自己管哪一段。

三组最容易搞混的词(混了会让整条流程走错方向)

AB混了会怎样
重构 vs 重写 外部可观察行为一个字不变,只改内部结构。小步、可回退、每个中间状态都能发布。 丢掉旧的,按新思路再写一遍。中间状态是残缺的,无法发布。 选 B 的话,你会把老代码里那些"丑但承重"的历史补丁一起扔掉(§02 的网景案例)。分界线是那一句:"你能不能在每个中间状态都发布?"
删死代码 vs 清理冗余 删的是没有任何地方引用的东西。工具能替你判断,风险低。refactor-clean 管这一段。 处理的是被引用很多次、而且几乎一样的东西。工具只能帮你找出来,不能替你决定合不合并。风险高。 用删死代码的心态去处理重复代码——你会把两段"碰巧长得一样"的代码合了,几个月后它们需求分叉,那个函数就变成一个怪物。顺序是先 A 后 B。
表征测试 vs 单元测试 写的是现状("空订单现在返回 -1")。它红了,说明你把它改坏了,或者现状变了——它不判断这个值对不对。 写的是需求("空订单应该返回 0")。它红了,说明代码错了,去修代码。 拿表征测试当需求测试看,你会忍不住"顺手把 -1 改成 0"——然后这一次纯重构就变成了一次行为改动,安全带当场失效:测试红了,但这次红不是"你改坏了",是"你真的改了行为",而这两件事看起来一模一样。
🎯 第三条的规矩

表征测试红了 → 先假设是自己改坏了 → 查清楚 → 如果确认"我确实想改这个行为",那就把它当成一次独立的功能改动:单独提交、单独工单,并且把那个表征测试改写成真正的单元测试(写明"应该是 0")。

/refactor-clean
删死代码,一次只删一个,红了就还原
社区 · affaan-m/ECC 等 · 会改代码
它的描述只有一句:"安全地识别并删除死代码,每次改动之后都验证。" 这里说的死代码,指"没有任何地方引用它"的代码。
它内部按这六步跑
1
按项目类型选工具,扫出死代码

knip(没被用到的导出、文件、依赖,JS/TS 最全)· depcheck(只查没用到的 npm 依赖)· ts-prune(TS 的未用导出)· vulture src/(Python)· deadcode ./...(Go)· cargo +nightly udeps(Rust)。如果一个都没装,它会退化成用 Grep 去找"导出了但没人 import"的东西。

2
分三档

安全档:没人用的工具函数、测试辅助函数、内部函数 → 可以直接删。
小心档:组件、API 路由、中间件 → 先查动态引用和外部使用者。
危险档:配置文件、入口文件、类型定义 → 动之前先搞清谁在用。

3
安全删除循环(每个"安全档"项目走一遍)

① 先跑完整测试套件,建立基线(全绿才继续)→ ② 用编辑工具外科手术式删掉这一个 → ③ 立刻重跑测试 → ④ 失败就 git checkout -- <文件> 当场还原并跳过这一项 → ⑤ 成功就处理下一个。

4
"小心档"要先查四件事

搜动态引用(import() / require() / __import__)· 搜字符串引用(配置文件里的路由名、组件名)· 判断这个符号是不是通过包的公开接口导出的 · 如果是发布的包,确认没有外部使用者。

5
最后才处理重复

找相似度 80% 以上的近似重复函数来合并、合并冗余的类型定义、删掉"没有增加任何价值"的包装函数和无意义的转发导出。

6
出一份报告

统计删掉了多少(函数 / 文件 / 依赖)、跳过了哪些(并注明原因是"测试失败")、省了多少行,最后一行是测试是否全绿。

第 1 步那些命令,你可以自己先跑一遍看看规模(不改任何东西):

# JS / TS:没被用到的文件、导出、依赖(覆盖面最广,先用它)
npx knip --reporter compact

# 只查没用到的 npm 依赖
npx depcheck

# Python:没被用到的代码
# --min-confidence 是"置信度门槛":调高只报更确定的,调低会有一堆误判
vulture src/ --min-confidence 80

先跑一遍、看一眼数量,再决定要不要让它动手。 如果 SAFE 档有 300 项,别一次跑完——分批,每批之后停下来看一眼 diff。

它的四条规则,值得直接抄进自己的习惯
🚫 四条硬规则

① 跑测试之前绝不删任何东西。

② 一次只删一个。(原子化的改动才容易回滚。)

③ 不确定就跳过——"宁可留着死代码,也不要搞坏生产"。

④ 清理的时候不要重构——先清理,重构以后再说,两件事分开。

一个必须知道的边界

这个 skill 只管"确定没人用的"。它会顺带做一点重复代码的合并(第 5 步),但那是最浅的一层——它不会替你判断"这两段重复该不该合并"。

而 AI 时代最典型的债务恰恰是后一种:那四份 calcTotal 全都在被引用,静态工具对它们完全无感。要处理那一层,得回到 working-with-legacy-code 的流程,一个个判断。

怎么调它
"跑一遍 refactor-clean,只处理安全档的项目,每删一个都跑测试。"

# 想更保守,可以自己加限制:
"跳过所有组件和路由,只删工具函数和测试辅助函数。"

refactor-clean 管不到的那一层:AI 造出来的重复

这是本文开头那个病(calcTotal 有四份)真正的解法,而它不在 refactor-clean 的射程里——那四份代码全都在被引用,静态工具对它们完全无感。

找它们要用另一类工具(克隆检测,即跨文件找出高度相似的代码块):

# jscpd:找出跨文件的高度相似代码块
# --min-lines 20 --min-tokens 50 表示"至少 20 行、50 个词"才算,避免报一堆 import
npx jscpd src/ --min-lines 20 --min-tokens 50 --reporters console

# 在自动流水线(CI,每次提交后自动跑的那套检查)里设一道闸:超 10% 重复率就直接失败
npx jscpd . --threshold 10

关于阈值:常见说法是健康范围在 5% 以下;有项目把闸门设在 3%,也有项目按更严格的 0.5% 要求自己。挑一个,然后在 CI 里卡住它——只有卡住了,趋势才会停下来。

🚫 工具只能替你"找出来",不能替你决定"合不合并"

这是整个清理工作里最需要动脑、也最不能交给工具的一步。判断标准是三类:

① 完全一样(字面上一模一样)→ 直接提取成公共函数,没什么好犹豫的。

② 同一件事(写法不同,但表达的是同一条业务规则,比如"满 199 免运费"在三个地方各写了一遍)→ 合并成唯一来源。这是收益最大的一类。但合并前先确认它们现在的行为真的一样——有时候两条规则看着一样,实际差一个四舍五入。

③ 碰巧一样(结构相似但背后是两件不相干的事,比如"用户列表分页"和"商品列表分页")→ 大概率不该合并。 硬合并会造出一个"既要满足用户需求、又要满足商品需求"的函数,以后两边需求一分开,它就变成一个满是 if 的怪物。别把"代码整洁"当教条用。

🎯 合并前的那一问(比什么原则都管用)

"这两段代码以后会不会朝不同的方向演变?" 会 → 别合。不会 → 合。

业务规则("满 199 免运费")答案是"不会";两个不同页面的分页逻辑答案是"很可能会"。还有一条叫"三"法则:同样的东西出现一两次先放着(那时你还没足够信息判断属于哪一类),出现第三次再动手。

拿不准就先别合——这是正确答案,不是拖延。 保留一份重复的成本是"以后改两处";错误合并的成本是"造出一个要同时满足两种需求的函数,而且它会越来越难改"。后者更贵。

/characterization-test-generator
窄而专:只负责把现状钉住
社区 · 只写测试文件
表征测试(characterization test)是把代码当前的行为记录下来,不判断它对不对。这个 skill 只干这一件事:给已有代码生成捕获当前行为的测试,好让后面的 AI 辅助重构有个安全网。
怎么写:一个"先断言一个荒谬的值"的配方

很多人卡在"我不知道它应该返回什么,所以写不了测试"。这个配方精妙的地方就是它不需要你知道:

1
随便断言一个荒谬的值

写 expect(calcTotal([])).toBe(-999)。对,故意写一个明显不对的数。

2
跑一次,读失败信息

测试红了。失败信息会写着"期望 -999,实际收到 -1"。那个 -1 就是现状。

3
把真值填回去,注释里说清这是现状

toBe(-1),并写一行注释解释这个值的来历。行为就被钉住了。

4
连跑两次,确认完全一致

不一致就说明测试里有不确定的东西,先解决它。

// order.test.js —— 表征测试:记录的是"现在是这样",不是"应该是这样"
import { describe, it, expect } from 'vitest'
import { calcTotalV2 } from './order.js'

describe('calcTotalV2 —— 现状记录,不是需求', () => {
  it('空订单返回 -1(是的,返回 -1,不是 0)', () => {
    // 这个 -1 是现状。看起来像 bug,但先钉住,单独开一张单子去修。
    expect(calcTotalV2([])).toBe(-1)
  })
})
两条硬指标(不满足就别往下走)
🚫 两条不可跳过的门槛

① 连跑两次,两次全绿且结果完全一致。 只要有过一次闪红,先解决那个不确定性再继续。带抖动的测试不是安全带,是噪音源。

② 在你动手改之前,这个测试得先红过一次。 做法:故意改坏一行,看测试有没有反应。没反应说明它根本没测到你关心的东西——很多人写了一个永远绿的测试而不自知。

输出很复杂怎么办:黄金标准测试

如果这个函数输出的是一份报表或者一大坨 JSON,没法逐字段断言,就用黄金标准测试(也叫 approval testing、快照测试):第一次跑把完整输出原样存成文件 → 人工看一遍确认"对,这就是现在的样子"(这步不能省)→ 提交进仓库 → 以后每次都和它比。

⚠️ 存快照之前必须先清噪音

输出里如果有时间戳、随机 ID、随机排序、本机路径,你钉住的就是噪音——测试会天天红,然后你就会开始习惯性地"更新快照",安全带就作废了。做法:把这些不稳定的部分替换成固定值(时间冻住、随机数种子固定、网络和文件读写挡掉)再存快照。

覆盖哪里:跟着改动走,不跟着文件走

新手最容易做错的一件事:以为"补测试"就是"给这个模块补一套完整测试"。不是。只覆盖你这次要动的那些分支。

理由很实际:给整个模块补完整测试要花两周,而你的任务是三天内加一个优惠券功能。要两周的测试你根本不会写,于是你会跳过安全带直接上手——那才是最危险的结果。

/diagnosing-bugs
复现不出来的 bug:先造一个红绿信号,再谈别的
本机已装 · 六个阶段
它开头就写了这个 skill 的全部要点:第一阶段建一个"反馈回路",其余都是机械劳动。 所谓反馈回路,就是一个能跑出红绿的命令——它跑在真实的 bug 代码路径上、断言的是用户描述的那个具体症状(不是"没报错"),所以它能在有 bug 时变红、修好后变绿。
它反复强调的一句狠话
🚫 "如果你发现自己在回路存在之前就开始读代码找理论,停下"

它的原话是:"直接跳到假设,正是这个 skill 要防止的失败。" 没有能变红的命令,就不许进入第二阶段。

它还写了一句判断:如果你有一个"紧"的反馈回路,你一定会找到原因——二分查找、假设检验、插桩,全都只是在消费这个回路。如果没有,盯着代码看多久都救不了你。

建反馈回路的十种办法(按推荐顺序)
  1. 在能触及 bug 的接缝上写一个失败的测试(单元 / 集成 / 端到端都行)
  2. 对跑着的开发服务器写 curl / HTTP 脚本
  3. 用固定的输入跑一次 CLI,把标准输出和"已知正确"的快照做 diff
  4. 无头浏览器脚本(Playwright / Puppeteer)驱动界面,断言 DOM / 控制台 / 网络
  5. 重放一次抓到的请求:把真实的网络请求、载荷或事件日志存到磁盘,单独喂进那条代码路径
  6. 临时脚手架:起一个最小子系统(一个服务、依赖全 mock),用一次函数调用跑通 bug 路径
  7. 属性测试 / 模糊测试循环:如果 bug 是"有时输出不对",跑 1000 个随机输入找失败模式
  8. 二分查找脚手架:bug 是在两个已知状态之间出现的,就自动化"在状态 X 启动、检查、重复",这样你能直接 git bisect run
  9. 差分循环:同一个输入分别走旧版本和新版本,diff 输出
  10. 人来点的最后手段:如果必须有人点,就写脚本去驱动人,让回路仍然是有结构的

然后它要求你把这个回路当产品打磨:能不能更快?能不能让信号更锋利(断言具体症状,而不是"没崩")?能不能更确定(把时间钉住、把随机数种子固定、隔离文件系统、冻结网络)?——"一个 30 秒的抖动回路比没有回路好不了多少;一个 2 秒的确定性回路是调试超能力。"

六个阶段与各自的出口条件
阶段做什么出口条件
1 建回路上面十种办法里挑一种造出红绿信号能报出一条命令,你已经跑过至少一次(附输出),且它:能对这个 bug 变红 · 确定 · 快(秒级)· 能无人值守跑
2 复现 + 最小化跑回路,看着它变红;然后把复现缩到"还能红的最小场景"——逐个砍输入、调用方、配置、数据、步骤,每砍一次重跑一次剩下的每一个元素都是承重的:去掉任何一个,回路就变绿
3 提出假设在测任何一条之前,先写 3–5 条排好序的假设(单条假设会锚死在第一个想法上)。每条必须是可证伪的:写出它的预测——"如果是 X 导致的,那么改 Y 会让 bug 消失"写不出预测的,那是感觉不是假设,丢掉或者磨尖它。把排序后的清单给用户看一眼再测
4 插桩每个探针都必须对应第 3 阶段的一条预测。一次只改一个变量。 优先调试器/REPL(一个断点胜过十条日志),其次是在能区分假设的边界上打定向日志,绝不"全都打日志再 grep"每条调试日志打上唯一前缀(如 [DEBUG-a4f2]),清理时一条 grep 就能删干净。性能问题走另一条支线:先建立基线测量,再二分——先测量,后修
5 修复 + 回归测试先写回归测试,再修——但前提是存在正确的接缝正确接缝 = 测试跑的是真实调用点上真实发生的 bug 模式。如果只有太浅的接缝(bug 需要多个调用方,你只能单调用方测),在那里写回归测试只会给你假的信心。如果没有正确接缝,那本身就是发现——记下来,说明这个架构阻止了这个 bug 被锁死
6 清理宣布完成之前必须做的原始复现不再复现 · 回归测试通过(或者"没有合适接缝"已被记录)· 所有 [DEBUG-...] 插桩都删了(grep 前缀)· 临时脚手架删掉 · 把最终被证实的那条假设写进提交信息,让下一个调试的人学到东西
它内置的一条安全纪律
🚫 展示任何东西之前先脱敏

它明确要求:所有要展示的命令、输出、抓下来的东西,先把密钥替换成 <REDACTED>;把循环建在环境变量上,让凭据留在环境里而不是出现在展示内容里;抓下来的东西可能带认证头,只引用有信号的那几行。如果脱敏之后信息不够诊断,它会说清楚并来问你——而不是悄悄把明文贴出来。

怎么调它
"线上偶发:提交订单时大约 5% 的概率金额算成 0。复现不出来。诊断一下。"
→ 它会先去造能变红的回路,而不是先猜原因

"这两个月这个接口的 P95 从 120ms 涨到 400ms,找不到是哪次改动引入的。"
→ 走性能支线:先建基线测量,再二分
/tdd
有测试的那块地,怎么继续加功能
本机已装 · 红-绿-重构循环
它是红 → 绿循环的参考手册:什么算一个好测试、测试该写在哪、有哪些反模式、循环的规则是什么。它开宗明义:"每一节在每一个循环里都适用:在循环之前和之中查它们,而不是之后。"
它有一条别的 skill 没有的纪律:先把接缝定下来
🎯 "只在前置商定的接缝上写测试"

它的原话是:在写任何测试之前,先把要测的接缝写下来,和用户确认。没有确认过的接缝,不写任何测试。

理由很实在:你不可能什么都测,所以把接缝提前商定下来,是让测试精力落在关键路径和复杂逻辑上、而不是每个边界情况上的唯一办法。

它会问你一句:"公开接口是什么?我们应该测哪些接缝?" —— 所以调它之前,先想好这个问题的答案。

它点名的三个反模式(老项目里特别容易犯)
反模式它长什么样为什么在老项目里特别危险
耦合实现的测试 mock 了内部协作者、测私有方法、或者从旁路验证(去查数据库而不是用接口) 识别信号是:你重构了但行为没变,测试却红了。 老项目本来就靠重构往下走,这种测试会持续挡路
同义反复的断言 断言用和代码一样的方式把期望值重算了一遍(expect(add(a,b)).toBe(a+b)、手工按同样算法导出的快照、把一个常量和它自己比) 它从构造上就永远通过,不可能和代码不一致——等于没测。期望值必须来自独立来源:一个已知正确的字面量、一个算过的例子、或者规格
横向切片 先把所有测试写完,再写所有实现 批量测试验证的是想象出来的行为:你测的是东西的形状而不是面向用户的行为,而且你在还没理解实现之前就把测试结构定死了。要按垂直切片走:一个测试 → 一个实现 → 重复,每个测试是一发"曳光弹",根据上一轮学到的东西调整
循环的三条规则
  • 先红后绿。 先写失败的测试,然后只写刚好让它通过的代码。不要预判未来的测试,不要加投机性的功能。
  • 一次一个切片。 一个接缝、一个测试、一个最小实现。
  • 重构不属于这个循环。 它属于评审阶段(那个 skill 叫 code-review),不属于红 → 绿的实现循环。

第三条是这两篇文章的接缝:tdd 管"写新代码",working-with-legacy-code 管"改没测试的旧代码",code-review 管"改完之后"。 三个 skill 的地盘几乎不重叠。

06③ 迭代:四个装闸门的 skill

/code-review
合并前的闸门:两条轴,并行审,不许合并结论
本机已装 · 只读
它审的是 HEAD 和你指定的某个固定点之间的差异,沿两条轴:标准轴(符不符合这个仓库写下来的编码规范)和规格轴(改的东西是不是真的实现了最初那张工单或规格)。两条轴各跑一个子 agent,并行——目的是让它们不污染彼此的上下文,最后它把两份结果并排摆出来。
它为什么坚持两条轴分开(这段解释值得读)
💡 一个改动可以过一条轴、栽在另一条上

代码处处符合规范,但实现的是错的东西 → 标准轴过,规格轴挂。
完全按工单做了,但违反了本项目的约定 → 规格轴过,标准轴挂。

它的原话是:分开报告,是为了阻止一条轴掩盖另一条。 所以它在最后汇总时明确要求:不要合并、不要跨轴重排——"那是这套分离机制存在的意义所在",而且它连"选一个总冠军问题"都不做,只给每条轴各自最严重的那一条。

它自带的 12 条"坏味道"基线(这是最实用的部分)

就算你的仓库什么都没写,标准轴也总会带上这份来自 Fowler《重构》第三章的固定清单。两条约束是:仓库写了的规矩永远优先于这份基线(仓库认可某种做法时,就把对应的味道压掉);以及每一条永远是判断题(它写的是"可能的 Feature Envy",不是硬性违规),工具已经能查的项直接跳过。

味道它长什么样怎么处理
神秘命名函数、变量或类型的名字看不出它在干什么、装的是什么改名;如果起不出一个诚实的名字,说明设计本身是浑的
重复代码同一段逻辑形状在改动里出现了不止一处把共有的形状提出来,两边都调它
依恋情结一个方法访问别的对象的数据比访问自己的还多把这个方法搬到它"羡慕"的那份数据上去
数据泥团同样那几个字段或参数总是一起出现(一个想诞生的类型)把它们打成一个类型,传那个类型
基本类型偏执用一个基本类型或字符串顶替一个本该有自己的类型的概念给这个概念一个自己的小类型
重复的 switch对同一个类型的同一套 switch 或 if 阶梯在改动里反复出现换成多态,或者两边共用一个映射表
霰弹式修改一次逻辑改动逼着你在很多文件里零散地改把"总是一起变的东西"聚到一个模块里
发散式变化一个文件因为好几种不相干的原因被改拆开,让每个模块只因为一个原因变
夸夸其谈通用性为规格里根本不存在的需求加了抽象、参数或钩子删掉,内联回去,等真实需求出现再说
过长的消息链调用方依赖了 a.b().c().d() 这种长链把这个走法藏到第一个对象的一个方法里
中间人一个类或函数大部分时间只是在往下转发砍掉它,直接调真正的目标
被拒绝的遗赠子类忽略了或者覆写掉了它继承来的大部分东西放弃继承,改用组合
怎么调它:固定点必须你来给
/code-review                    # 它会问你要固定点
"review since main"             # 对比 main 的 merge-base
"review since HEAD~5"
"review since a3f9c21"          # 某个具体 commit

它内部用的是三点语法 git diff <固定点>...HEAD——对比的是合并基点,这样不会把主干上的无关变化算进来。而且它会先确认固定点能解析、diff 不为空,再开两个子 agent——"一个错误的引用或空 diff 应该在这里就失败,而不是在两个并行子 agent 里面失败"。

⚠️ 它会去找规格,但可能找不到

它按这个顺序找:① 提交信息里的工单引用(#123、Closes #45)→ ② 你作为参数传进去的路径 → ③ docs/、specs/、.scratch/ 下和分支名匹配的文件 → ④ 找不到就问你。

如果你说"没有规格",它会跳过规格轴并报告"没有可用的规格"。 所以想让两条轴都跑起来,先确保你这个改动是从某张工单出发的——这本身就是个值得养成的习惯。

/improve-codebase-architecture
走通几条路之后,下一步挖哪里
本机已装 · 需要手动调用
它扫一遍代码库,找出"可以挖深的地方"——也就是把浅模块变成深模块的重构机会。目标是可测性和"AI 可导航性"(让 AI 更容易在这个代码库里找对地方)。它靠项目的领域模型(也就是 domain-modeling 产出的那些文件)来加权判断。
它引用的那套词汇值得记住(来自 codebase-design)
词它到底指什么为什么这个词而不是别的
模块 任何有"接口"和"实现"的东西。刻意不分规模:一个函数、一个类、一个包,或者一个横跨多层的切片都算 不要用"组件""服务""单元"来替代——统一用词是这套词汇存在的全部意义
接口 调用方为了正确使用这个模块所必须知道的一切:类型签名,以及不变量、调用顺序约束、错误模式、必需的配置、性能特征 不要用"API"或"签名"替代——它们太窄,只指类型层面的表面
深度 接口上的杠杆:调用方(或测试)每学会一单位接口,能驱动多少行为。深 = 大量行为藏在小接口后面;浅 = 接口几乎和实现一样复杂 这是判断"这块该不该重构"的核心指标
接缝 (来自 Feathers)一个你能不修改那个地方就改变行为的位置;也就是模块接口所坐落的位置 不要用"边界"替代——那个词在领域驱动设计里已经被"限界上下文"占用了。另外:接缝放在哪是一个独立的设计决定,和"接缝后面装什么"是两件事
杠杆 / 本地性 杠杆是调用方从深度里得到的东西(一份实现回本到 N 个调用点和 M 个测试);本地性是维护者得到的(改动、bug、知识、验证都集中在一处,而不是散到各个调用方——修一次,处处修好) 这两个词分别是"给调用方的好处"和"给维护者的好处"
它的三步流程
  1. 探索——扫代码库,找架构摩擦点。
  2. 把候选摆成一份可视化 HTML 报告——让你能看,而不是读一长串文字。
  3. 拷问循环——你挑一个,它就带你把它问穿。

注意它给的是"候选清单和排序依据",不是命令。 而且它有一个前置条件:disable-model-invocation: true,也就是说 AI 不会自己启动它,你必须敲名字。

什么时候调它

不是第一次接项目的时候——那时候你连哪里疼都不知道。正确的时机是:你已经走通了三四条路,手里有测试、有地图、有业务文档,然后问"下一步该系统性地挖哪里"。

一个搭配用法:先跑 /improve-codebase-architecture 拿到候选清单,把它挑中的那一条交给 working-with-legacy-code 去实际执行——一个负责"挖哪里",一个负责"怎么安全地挖"。

/to-spec → /to-tickets
把"我想清理这一块"变成一串能单独交付的小票
本机已装 · 需要手动调用 · 会写你的任务系统
这是一对。第一个把当前对话里已经聊清楚的东西综合成一份规格并发到项目的问题追踪系统(它的 SKILL.md 明确说:不要采访用户,只综合你已经知道的)。第二个把一份计划、规格或对话拆成一串"曳光弹"式的票据,每张都声明它被哪几张阻塞。
为什么这对老项目特别有用

因为老项目清理最大的失败模式是"一次做太多"——一个几百文件的 diff,没人能审,你自己也不敢合。这两个 skill 把"一次做太多"变成了结构性的不可能:每张票是一个垂直切片,能单独交付、单独撤销。

而它们产出的票据长这样(这是它自带的模板):

# <NN>: <票据标题>

## Parent          # 属于哪张父票
## What to build  # 要做出什么
## Acceptance criteria  # 验收标准
## Blocked by      # 被哪几张票阻塞

"Blocked by"这一栏是老项目里最有价值的部分——它把"先补测试才能改这块"这种依赖关系显式写下来了,而不是留在你脑子里。

怎么调它
# 先聊清楚要做什么(可以先用 improve-codebase-architecture 挑出目标)
/to-spec          # 综合刚才聊的,出一份规格
/to-tickets       # 把规格拆成带阻塞关系的票据

两个 skill 都需要一个前置配置:问题追踪系统的接入(它们的 SKILL.md 里写着,如果 docs/agents/issue-tracker.md 不存在,会让你先跑一个初始化命令)。也就是说它们需要知道你用什么记任务——GitHub Issues、Linear、还是别的。本地也支持:每个票据一个文件、阻塞关系写成文字。

📌 一个诚实的提醒

这两个(以及下面的 triage、上面的 improve-codebase-architecture)都带 disable-model-invocation: true。它们的共同点是"会往你的任务系统里写东西"——让 AI 自己决定往你的看板里写票据,风险太大,所以做成了必须你亲手敲。

这也是本文开头那条判断标准的由来:看它默认会不会动你的东西。 会动的那些,全部需要你显式调用。

/triage
一堆 issue 和外部 PR,先看哪个
本机已装 · 需要手动调用 · 会改你的任务系统
它把 issue 和外部 PR 推过一个小型状态机,分诊到一个"角色",分类、验证、必要时拷问,最后写成一份给 agent 用的简报。
它有一个很好用的设定:PR 就是"带代码的 issue"

它的原话:"一个 PR 就是一个附带了代码的 issue"——用同一套角色、同一套状态、同一台状态机,只有少数几处标了"对 PR 而言"的差异。

它的用法
/triage              # 先摆出"有哪些需要关注"
/triage #42          # 分诊具体某一个(issue 或 PR 都行)

它内置了一份"信息不足"的模板,所以它不会硬猜——缺信息时会照模板去问。它还支持恢复上一次的分诊会话。

老项目里它最有用的一步是"写成 agent-ready 的简报":把一个含混的 issue 变成一份 AI 能直接执行的说明。这正好接上第 5 节那个"四要素任务模板"——分诊负责把话说清楚,working-with-legacy-code 负责安全地做。

🎯 这一阶段的核心是"别自己判断"

前面两个阶段都在解决"敢不敢改"。这一阶段解决的是另一个问题:改完了,你怎么知道这次可以合并?

答案不是"我看了 diff 觉得还行",而是几条互相独立的轴各出一份结论:规范轴、规格轴、安全轴(security-review)、测试轴。它们不能互相替代,也不能被合并成一句话——因为一个改动完全可以"规范全过但做错了事",也可以"做对了事但处处违规"。

07串起来:接手一个 AI 堆出来的订单后台,第一周

把上面的 skill 串成一条能照着抄的时间线。场景:一个 Node + TypeScript 的电商后台,30 多个文件,半年里由三个人(和他们各自的 AI)你一句我一句堆出来。没有测试,README 停在半年前的目录结构。

Day 1 · 只跑一个 skill,不改任何东西

/codebase-onboarding
→ 它会先并行扫六类配置,再追踪一条请求,最后给你导览 + 一份 CLAUDE.md 草稿

你的验收动作:打开它生成的 CLAUDE.md,数一下行数。超过 100 行,说明它在往里塞代码里本来就看得见的东西——删掉那些,只留"AI 猜不到而且会影响操作"的(用哪个包管理器、测试要带什么参数、legacy/ 不能删)。然后从它的导览里挑 3 条结论去核对。

Day 2 · 把"业务到底在干什么"倒推出来

"帮我写 HLD(概要设计),范围限定在订单和支付两个模块"
→ code-to-doc-generator 进模式 A:四轮走完,产出概要设计

/domain-modeling
→ 一边看文档一边把术语定下来,写进 CONTEXT.md

注意:code-to-doc-generator 有一条硬规则——文档类型必须你明确说出来,否则它会停下来问你。所以开口就把类型说清楚。

这一天你大概率会发现组织里对"订单""支付""退款"这几个词的理解并不一致。把它们当场定下来写进 CONTEXT.md——这就是 domain-modeling 的用法。如果在这个过程中你问出了"当年为什么用双写不用事务",立刻让它按三个条件判断要不要落成 ADR。

Day 3 · 装安全带,只覆盖你要动的那一小块

先人工看一眼,找到你要改的那个点。比如:算价逻辑在 order.ts 里有三份,calcTotalV2 才是线上在用的。

"这块代码没有测试,我不敢改。用 working-with-legacy-code 的方法带我做:
 先告诉我改动点和测试点分别在哪、要打通什么依赖。"

# 或者只需要安全带、不需要完整算法时:
/characterization-test-generator
→ 只生成捕获当前行为的测试

两个硬指标:连跑两次结果完全一致;动手之前它得先红过一次(故意改坏一行验证)。

Day 4 · 让 skill 给自己打分,然后才动手

"按这个 skill 的标准给这次改动打分,并列出达到 10 分还差哪几步。"
→ 分数低于 7 就别往下走,先把差的几步补上

然后按它的五步算法走:定位改动点 → 找测试点 → 打通依赖 → 写测试 → 才做改动。如果那块地方今天真的测不了,用它给的 Sprout:新逻辑写成一个全新的、自带测试的小函数,从老代码里用一行调用它。

四次提交,每次只做一件事;每次之间跑一次测试,绿了就提交,红了就 git revert:

# 提交 1:只加测试文件,零行为改动
git add src/services/order.test.ts
git commit -m "test(order): 为 calcTotalV2 补表征测试"

# 提交 2:只改名和加注释,零行为改动
git commit -m "refactor(order): calcTotal 改名 legacyCalcTotal 并标注废弃"

# 提交 3:纯结构重构(提取函数),表征测试必须保持绿色
git commit -m "refactor(order): 提取 roundToCent,行为零变化"

# 提交 4:这才是功能。前面三次提交里一行功能都不夹带
git commit -m "feat(order): 支持优惠券叠加"

Day 5 · 删死代码,然后过闸门

"跑一遍 refactor-clean,只处理安全档的项目,每删一个都跑测试。"
→ 一次只删一个,红了当场 git checkout -- 还原并跳过

"review since main"
→ code-review 两轴并行审,标准轴 + 规格轴各出一份,不许合并

如果你的清理动了鉴权、输入校验、权限判断、外部请求这几类地方,再加一条 security-review——老项目里这几块的历史写法经常是不安全的,而你在"整理"它的时候很容易整理成另一种不安全。

⚠️ 注意你没有完成什么

你没有"清理完这个项目"。项目里还有两份重复的算价逻辑,还有一堆没人引用的工具函数。

你清理的是你走过的那一条路。 这正是它能安全的原因,也是它必须持续做的原因——下一周你走另一条路,再清一块。而 improve-codebase-architecture 就是用来回答"下一块该走哪条路"的。

08出问题怎么办

这一节是给你"遇到问题回来翻"的。左边找症状,右边拿对策。

症状大概率的原因怎么办
skill 没被触发,AI 根本没理它 你的说法没命中它的 description;或者它带 disable-model-invocation: true(永远不会自动触发) 直接敲名字,或者自然语言点名:"用 working-with-legacy-code 那个 skill 来改这块"
装了两个同名 skill,行为对不上 同名往往有好几份不同实现,严格程度不一样 读一遍各自的 SKILL.md,留一份、删另一份。别让两份都装着
生成的 CLAUDE.md 有好几百行 它在把"读代码就能知道"的东西也写进去了 按那条判断法砍:"AI 读代码能不能自己看出来?能,就删掉。" 留 100 行以内
它跑完改完之后,diff 里有你没让它碰的文件 它跟着 import 走进了共享工具文件,或者"顺手"认为那里也该改 单独撤掉那些文件;把禁区写进 CLAUDE.md;任务里加一条"先列文件等我确认"
它说"已修复",但没人说得清改了什么 它把"跑通了"当成了"改对了" 要求它每次给 git diff --stat 和测试结果。只看"跑没跑通"不够
refactor-clean 删完之后测试红了一片,回滚很痛苦 它一次删了多个,或者基线本来就不是绿的 它自己的规则里就有解法:先跑基线(并记下清理前哪些测试本来就是红的)、一次只删一个、失败立刻还原并跳过。如果它没照做,把这三条明确写进你的指令里
表征测试写成了"应该返回 0" 把现状和需求搞混了 回到它最初的使命:先探一次,读失败信息,看它真的返回什么。想改行为就单独开一张单子
测试第一次红、第二次绿 里面有不确定的东西:时间、随机数、网络、文件遍历顺序 先把这些替换掉或冻住,直到连跑两次完全一致。带抖动的测试不能当安全带
code-review 只出了一份报告,规格轴不见了 它没找到规格,跳过了那条轴 它的 SKILL.md 明确说会跳过并报"没有可用的规格"。想让两条轴都跑,先让改动从一张工单出发——或者用 /to-spec 补一份
to-tickets 说缺少配置 它需要知道你的任务追踪系统接在哪 按它的提示先跑一次初始化。它支持真实的追踪系统,也支持"每张票一个本地文件"
装上 skill 之后 AI 反而变啰嗦、成本变高 上下文文件太长,agent 会过于勤勉地执行每一条指令——指令越多,它跑的测试越多、烧的推理越多 砍。一份 2026 年 2 月的研究(ETH Zurich 与 LogicStar.ai 的 Gloaguen 等人)测了四个 agent,发现自动生成的上下文文件平均让任务成功率降低约 3%、成本抬高 20% 以上;开发者手写的略好(约 +4%),但成本也涨了最多 19%
对话聊得越久,AI 越开始做奇怪的事 上下文被自动压缩后发生了任务漂移 一次只做一件事,做完开新会话。把结论落进 CONTEXT.md / CLAUDE.md,别让它依赖对话记忆
⚠️ 为什么"护栏"和"回退"这两件事不能省——有公开证据

上面那条"diff 里有你没让它碰的文件"不是个别现象,是反复出现的模式。几个公开可查的例子:

① 越界改文件。 Anthropic 的 claude-code 仓库里有一个 issue(#54240):用户只要求"给这个按钮包一层判断",agent 除了做这件事,还重构了周边代码,并且加了一条"kiosk 不应该锁定"的行为——那是从没被要求过的。用户追问时,它说不清为什么改。

② 三种反复出现的越界形状(Cursor 社区归纳,可以直接拿它当检查清单):在共享工具文件上"顺手帮忙清理"——跟着一个 import 走进某个被很多地方依赖的工具文件然后开始整理它;找错文件——改了 /v1/ 或 /deprecated/ 那个版本而不是当前在用的(老项目里这种目录特别多);级联重构幻觉——一个类型报错让它觉得"接口定义不对",改了接口,然后为了让类型对上把每个实现文件都改了,一个文件的补丁变成 20 个文件的架构级改动。

③ 长对话的任务漂移。 OpenAI 的 codex 仓库有一个 issue(#11315):对话变长后系统自动压缩上下文,压缩之后助手"回到了"更早的一个旧任务上,执行了不相关的命令。

把这三件事归纳成一句:agent 分不清"完成你要求的任务"和"因为它发现了更好的做法、所以重塑这个项目",而且它停不下来——因为在它看来,继续做看起来就是进展。一个有经验的维护者知道什么时候不写代码,目前的 agent 还没强到这个程度。

Q:我可以让 AI 自己挑 skill、自己串起来吗?
自动触发适合"顺路"的场景,但老项目维护不适合全自动。原因有两个:一是带 disable-model-invocation: true 的那批(to-spec、to-tickets、triage、improve-codebase-architecture、wayfinder)本来就不会自动跑;二是会改你代码的那批,自动触发的时机不由你控制。推荐的做法是显式调用:你自己决定现在处于哪个阶段,然后敲那个阶段该用的 skill。
Q:社区 skill 会不会有安全问题?
一个 skill 就是一份 markdown 说明书,它本身不能执行任何东西——但它会指示 AI 去执行东西。所以:① 装之前读一遍(这是本文反复强调的那条);② 注意它有没有带 allowed-tools 这类字段限制自己能调哪些工具;③ 先在一个不那么要紧的仓库上试一次,看它实际改了什么;④ 老项目上优先用"只读不写"的那一批(codebase-onboarding、code-to-doc-generator),它们跑完你一行代码都没变。
Q:我需要把这些 skill 全装上吗?
不用。按你当下的那个阶段装一个,用顺了再加。 一个最小的起步组合是:codebase-onboarding(拿地图)+ working-with-legacy-code(安全地改)+ 你机器上已经有的 code-review(合并前过闸)。这三件覆盖了"看不懂 → 不敢改 → 改完不敢合"这条主线,剩下的都是按需补。
Q:这些 skill 会替代我读代码吗?
不会,而且这是个危险的想法。所有"只读不写"的 skill,产出都必须抽查——codebase-onboarding 那份导览、code-to-doc-generator 那份业务文档,都可能在某个细节上错了。方法很简单:从它的结论里挑 3 条去核对。区别只是这里的成本更高——如果 CLAUDE.md 是错的,接下来每一个会话都会带着这个错往下走。

09什么时候 skill 不管用

一个只会说"装上就好"的方法是没用的。这一节讲边界。

你现在的情况为什么 skill 帮不上该做什么
正在赶上线,需求天天变 你会在最不能承受风险的时候引入风险;而且需求还在变,你整理出来的结构下个月又要改 先打补丁把需求满足。清理排到上线之后
这块代码马上要死了(产品下线、要被替换) 清理它的回报是零。你花一周整理的代码,两个月后被删掉 什么都不做。把精力放到会活下来的那部分
没有测试,也没有任何人能跑起来这个项目 所有"修复类" skill 的前提都是能跑一次测试。跑不起来,红绿关卡就建不起来 先解决"能跑起来"。这件事 codebase-onboarding 会帮你定位(它会明确报出缺哪个环境变量、依赖哪个外部服务),但它不修
你连"这段代码为什么长这样"都不知道 对业务理解不够、系统边界不清楚的时候动手,那不叫技术进步,那叫赌博。skill 能保证"改不坏",但保证不了"改对" 先搞清楚业务,或者去找当年写它的人聊。domain-modeling 可以帮你把问出来的东西固定住,但问本身要你去做
改动影响面很大(涉及钱、用户数据、合规) 爆炸半径大于收益。一旦出事,收益远远抵不上 先把安全带建起来,或者干脆不动。可以的话先灰度(先让一小部分流量走新逻辑)
想用 skill 替代"想清楚要做什么" 本文所有 skill 都假设你知道目标是什么。to-spec 甚至明确写了"不采访用户,只综合你已经知道的"——你脑子里没有,它就综合不出来 自己先把"要做成什么样"想清楚。这件事没有 skill 能替
团队里其他人不用 AI 工具 CLAUDE.md、CONTEXT.md、ADR 这些文件对他们是额外的、而且要维护的东西 这些文件本来就是对人也对 AI 的。但如果你判断团队接不住,就先放个人级目录(~/.claude/skills/)里,别往仓库塞
📌 一条不会变的边界

这些 skill 里,最核心的那一条纪律是 Feathers 二十多年前总结的:先覆盖,再修改;一次只做一件事;重构和行为改动分开。 skill 做的是把这条纪律的执行成本降到可以持续做下去——它没有让纪律本身变得不必要。

装了 skill 之后,分工没有变:AI 提供加速,不提供兜底。 方向判断、范围界定、质量兜底这三件事始终是人做的。skill 能做的是让"每次都要重讲一遍的纪律"不再依赖你的记忆力。

10术语表

术语它是什么为什么重要 / 什么时候用 · 别当什么用
skill 一份叫 SKILL.md 的说明书,放在 .claude/skills/ 或 ~/.claude/skills/ 下,AI 按里面的步骤干活 别当插件或程序用——它本身不执行任何东西,它是"指示 AI 去执行什么"。所以判断它值不值得装,看它的步骤有没有关卡
frontmatter 文件开头用三条短横线包起来的元信息区块,其中 description 决定 AI 什么时候启动它 为什么重要:一个 description 写得好的 skill,你不用记名字,说对话它自己就出来了。disable-model-invocation: true 也写在这里——意思是只有你敲名字它才跑
表征测试
characterization test
把代码当前的行为记录下来,不判断它对不对 何时用:你要动一块没测试的老代码,而且不知道"正确"应该是什么。别当单元测试用——它红了只说明"现状变了",不说明"谁错了"
黄金标准测试
golden master / 快照
把完整输出存成一个文件,以后每次跑都和它比对 何时用:输出是一整份报表或一大坨 JSON、没法逐字段断言时。别当万能药——存快照之前必须清掉时间戳和随机 ID 这类噪音
接缝
seam
一个"不修改那个地方、就能改变那里行为"的位置;也是模块接口坐落的地方 为什么重要:它是让老代码变得可测的唯一手段。别当设计模式用——用能解开当前问题的最小的那个。另外:接缝放在哪是一个独立的设计决定,和接缝后面装什么是两件事
启用点
enabling point
接缝上"决定用哪个实现"的那个位置 为什么重要:一个没有启用点的接缝是没用的——你能改变它,但没有地方去下这个决定。这是判断"这个接缝能不能用"的最后一问
Sprout / Wrap Sprout(发芽):新逻辑写成全新的、自带测试的函数,从老代码一行调用它。Wrap(包裹):老函数改个名收起来,重建一个同名外壳在前后加行为 何时用:宿主今天真的测不了,但你要加功能。别当覆盖率的替代品——宿主仍然没测试,sprout 出来的东西要记成技术债
反馈回路
feedback loop
一个能跑出红绿的命令,它跑在真实的 bug 路径上、断言的是用户描述的那个具体症状 为什么重要:diagnosing-bugs 的第一阶段就是造它,而且它说"其余都是机械劳动"。别当"能跑起来就算"——它必须能对这个 bug 变红,断言"没报错"不算
收窄点
pinch point
效应草图上"收窄"的地方——在这个位置写少量测试,就能覆盖上游一大片行为 何时用:你要给一个巨型方法或上帝类补测试,但不可能全覆盖。怎么找:画效应草图(每个变量/方法一个气泡,"谁影响谁"画箭头),找那个窄口
死代码
dead code
没有任何地方引用它的代码 何时用:清理的第一批目标,便宜、快、立刻缩小范围。别当"静态工具报出来的都能删"——字符串拼出来的动态引用工具看不到
深 / 浅模块 深 = 大量行为藏在小接口后面;浅 = 接口几乎和实现一样复杂 为什么重要:这是判断"这块该不该重构"的核心指标。注意:这套词汇里用"模块"指任何有接口和实现的东西(函数、类、包都算),不要换成"组件""服务"——统一用词是这个概念存在的意义
ADR
架构决策记录
"当初为什么这么选"的短文档,放在 docs/adr/ 下 何时用:三个条件同时成立才写——难以反悔 · 没有背景会让人困惑 · 是真实取舍的结果。别当日记写:缺任何一条就别建,ADR 一多就没人看了
CONTEXT.md 这个项目里每个词是什么意思的术语表 为什么重要:老项目里同一个词两个人理解不一样,是最贵的沟通成本。别当规格或草稿本——它应该完全不含实现细节
曳光弹 / 垂直切片
tracer bullet
一个测试 → 一个实现 → 重复,每一轮都根据上一轮学到的东西调整 别当"先把所有测试写完再写实现"(那是横向切片)——批量测试验证的是想象出来的行为,而且你在理解实现之前就把测试结构定死了

99一句话总结

🎯 核心要点

用 skill 维护老项目,就是按三个阶段各调一批:排查用只读的(codebase-onboarding 拿地图、code-to-doc-generator 倒推业务、domain-modeling 固定术语),修复用带红绿关卡的(working-with-legacy-code 先覆盖再改并且会给自己打分、refactor-clean 一次只删一个、characterization-test-generator 钉住现状、diagnosing-bugs 先造反馈回路),迭代用装闸门的(code-review 两轴并行审、to-spec/to-tickets 把大清理拆成能单独撤销的小票)——而判断一个 skill 值不值得信任,永远只看两处:它有没有可验证的关卡,它默认会不会动你的代码。

留给你的最后一件事

这篇文章里所有判断里,只有一条你没法在纸上验证——请拿你自己的项目做一次:

📌 现在就能做的三个动作(按顺序,第一个不用装任何东西)

① 先看看你手上有什么。 跑一行 ls ~/.claude/skills/,对照 §03 末尾那张分类表,看看本文提到的哪些你已经有了。大概率 code-review、diagnosing-bugs、tdd 已经在里面了。

② 只装一个。 装 codebase-onboarding,挑一个你已经很熟的项目跑一遍(用熟项目,这样你能一眼看出它的结论对不对)。重点看两件事:它生成的 CLAUDE.md 有几行、它给的结论有哪几条是错的。

③ 挑一个最长的函数,用 characterization-test-generator 给它补一个表征测试,然后故意改坏一行,确认它真的会红。

做完这三步,你手上就有了第一个可以安全改动的角落。这就是整篇文章的全部——只是你要再来一次,在下一个角落。

⌂