复杂软件系统的Vibe Coding实践二:拆计划与TDD实现
规格写完了。按系列第一篇的路子——真跑 superpowers 拿到的是一份 RBAC 全栈框架规格(examples/prd2spec/02-spec.md),已经把「AI 会猜错」的岔路口清掉了大半。可接下来这步,是整套流程里最爽、也最坑的一步:让它写代码。爽在从文档到能跑的程序,就差一句「开始吧」;坑在 Claude 的默认动作,是先写实现、再回头补测试。它补的测试不是来验证需求的,是来给已经写完的代码盖橡皮章的——测试全绿,覆盖全是假的。
所以我的判断是反着来的:让 Claude 写代码之前,得先逼它写「会失败的测试」。
系列第一篇把整条路画给你看过:需求 → 规格 → 拆计划 → TDD 实现 → 验收 → 迭代。本篇走第二格和第三格——spec 到手之后的两步:先把规格拆成有依赖顺序的任务清单,再逐任务用 TDD(Red→Green→Refactor)实现。全流程地图不重复贴,往上翻第一篇就有。全程我用第一篇真跑出来的那个项目当实例:拆计划用真实落盘的 examples/prd2spec/04-plan.md,TDD 循环用后端真实实现里的 requirePermission 中间件,代码全从仓库里截的,能照着抄。
读这篇前,你手里得先有一份 spec。没有就先回去读第一篇,把需求和验收标准补齐再回来。这一篇的假设是——你要写的是一个模块互相依赖的复杂系统,不是一个一次性脚本。
拆计划:先治「全局失序」
系列地图里,spec 的下一格是拆计划。一句话:把一份 spec 拆成有依赖顺序、每个任务都能单独验收的小任务。别小看这步,它治的是 vibe coding 最大的死因——全局失序。
不拆计划直接让 Claude 按 spec 一把做完,会怎么样?它每块代码单独看都合理,但中间没有任何验收点:一个假设在第三行错了,后面两百行全建在错假设上。你验收时看到的不是「一个 bug」,是「一个系统性的错位」,从哪改起都不知道。拆计划就是把「从哪改起都不知道」变成「最多错一个任务」。
打个比方。不拆计划让 Claude 一把梭,等于让一个不看路标的人开长途。它每段路都开得稳稳的,你坐在后座,等发现方向不对时,已经下了三个高速口。拆计划是在每个出口提前立好路标——走错,最多错一个出口就能发现。代价是每个出口你都得多看一眼,收益是你永远不会在五十公里外才发现偏航。
三个原则
拆计划我压成三条原则,照着拆不会散:
-
一个任务一个可验证的产出。任务不是「写授权模块」这种大词,而是「实现 requirePermission 中间件,让无权限调用返回 403」这种能独立验收的句子。判断标准就一条:这个任务做完了,你能不能单独验证「做完了」。不能单独验证的,拆。
-
依赖顺序 = 能测的先做,地基先行。权限系统按「建表 → 认证 → 授权 → 角色 → API → 前端」排:前一个任务是后一个任务的验收前提。建表没落地,认证没法测;认证没落地,授权没法测。顺序不是你喜欢什么先做什么,是「什么先能被测出来」。
-
每任务绑定验收测试。从 spec 的验收标准(用户故事 US,如 US6 无权限点访问被拦)和测试清单里,把对应条目摘到任务头上。任务完成的标准,不是「代码写完了」,是「它绑定的那条验收从红到绿」。这一个绑定,是拆计划和 TDD 之间的接缝——拆的时候就把「怎么算做完」钉死,实现的时候才有得验。
为什么这个接缝重要?第一篇讲过「一个脊柱、两道闸」:test 是第一道闸。可闸门得有东西可闸——拆计划就是在 spec 和 test 之间铺轨道,让每个任务在开工前就知道自己的验收标准是什么。验收标准拖到实现完再补,就是让 AI 自己给自己判分,判出来的分你不敢信。
RBAC 全栈框架拆计划实例
回到第一篇那份规格(examples/prd2spec/02-spec.md)。它的验收标准是六条用户故事——US1 管理员能管理用户/角色/权限点、US2 运营只能管理商品、US3 运营只看自己创建的商品、US4 管理员看全部商品、US5 未登录访问被拦、US6 无权限点访问被拦——外加一张 29 个用例的测试清单。我按上面的原则把它拆成十一个任务(真实计划在 examples/prd2spec/04-plan.md),验证绑定直接抄 spec 的条目:
| 任务 | 内容 | 验证 |
|---|---|---|
| T1 后端骨架 | package.json、config、db 连接 + 建表 + 种子数据 | 启动建表,种子存在 |
| T2 认证 | 登录 API + JWT 签发 + auth 中间件 | 登录返回 JWT;无 token 401 |
| T3 权限 | permission 中间件 + dataScope 中间件 + 权限点 API | 无权限点 403;SELF 过滤生效 |
| T4 用户管理 | CRUD + 分配角色 | 集成测试通过 |
| T5 角色管理 | CRUD + 绑定权限点 + data_scope | 集成测试通过 |
| T6 商品模块 | CRUD + 数据范围过滤 | 集成测试通过 |
| T7 后端测试 | 单测 + 集成(supertest) | npm test 全绿 |
| T8 前端骨架 | Vite + Vue3 + UnoCSS + 路由 + pinia + axios | npm run dev 可起 |
| T9 登录页 | 登录页 + 导航守卫 + token 持久化 | 登录跳转、401 拦截 |
| T10 用户/角色管理页 | 列表/增删改/绑定 | 页面功能可操作 |
| T11 商品管理页 | 按钮按权限显隐 | 权限按钮显隐正确 |
依赖关系是这张图(箭头指「必须先于」):

注意 T3 内部会拆成两个 TDD 周期:先做 permission 中间件(拒掉无权限调用,US6),再做 dataScope 中间件(SELF 只放本人数据,US3)——一个周期一个行为,第四章你会看到第一个周期的完整过程。
真实团队也这么拆过。得物技术发过一篇 Spec Coding 实战(tech.dewu.com/article?id=212),记录一次前端项目从 0 到 1:10 天 2.5 万行代码、提效 36%,核心决策链是 proposal → design → specs → tasks——先规格、再拆任务,tasks 分组让 AI 聚焦当前步骤,先明确验收标准再实现。规模你不一定复刻得了,但「先拆任务、任务绑验收、再实现」这个顺序,跟我这套是同一条。
用 Claude 拆,人审
拆计划能交给 Claude,但别全交。我的做法:把 spec 丢给它,让它输出「任务清单 + 依赖顺序 + 每任务验收绑定」。它习惯在 plan mode 里只读探索、不动代码——plan mode 是干嘛的我在别处讲过,不展开。任务拆好之后,想让 Claude 按依赖顺序自动往下执行,可以用 /workflow 编排一轮任务,机制同样不展开。
它拆完,你人审三件事:依赖顺序对不对、每个任务是不是真能独立验收、验收绑定有没有把 spec 的验收标准摘漏。为什么最后一道是人审?因为拆计划的质量直接决定后面所有 TDD 的质量——拆错了,后面每个任务都在错的地基上盖房。Claude 拆出来的计划可以很顺眼,但「这个任务到底怎么算做完」,只有你(还有你的 spec)说了算。
对照 superpowers:writing-plans 就是「拆计划」的产品化
这套拆法我自己攒了好一阵,回头看 superpowers 插件,发现它把同样的动作做成了技能,名字就叫 writing-plans。它就是我说的「拆计划」的产品化实现,只是粒度更狠。
writing-plans 的玩法,跟我的三个原则逐条对上:
-
每个 step 拆到 2-5 分钟级,是「一个动作」而不是「一个任务」:写失败测试是一步、跑起来确认失败是一步、写最小实现是一步、再跑确认绿是一步、提交是一步。
-
每个任务写全:精确文件路径、完整代码块、精确命令 + 预期输出,还有 Interfaces 块(这个任务消费什么、产出什么),让相邻任务知道彼此的精确签名。
-
TDD 测试步骤内置:它把「写失败测试 → 确认失败 → 最小实现 → 确认通过 → 提交」直接写进每个任务的步骤里,你拆计划的时候顺便把测试循环也拆好了。
-
禁占位符:计划里不许出现 TBD、TODO、「加合适的错误处理」这类话——每个 step 必须给全真实内容和完整代码。为什么这么狠?因为执行计划的子代理对项目零上下文,占位符等于把决策留给了一个读不懂上下文的代理,它只能猜。
-
存盘:计划落到 docs/superpowers/plans/YYYY-MM-DD-<功能名>.md,进 git。
它和我手拆最大的区别在粒度。我拆到「一个任务一个可验证产出」就停——一个任务可能是一下午的量;它拆到 2-5 分钟一个 step。为什么它敢拆这么碎?因为执行者不同。人是执行者时,任务级够用,拆太碎反而烦;代理是执行者时,step 级才不跑偏——代理没耐心也没上下文跨越大步,每个 step 必须自带验证点。所以粒度不是越细越好,是跟执行者匹配。这条写给你,是提醒别照搬它的粒度——先想清楚执行计划的是人还是代理。
TDD 实现:为什么 TDD 治「假覆盖」
拆完计划,进第三格:TDD 实现。为什么非 TDD 不可?我先把话撂这儿:TDD 对 AI 不是锦上添花,是刹车。AI 写代码天然是下坡——它倾向一次写很多、写完整、写着写着觉得「再顺手多加点」。刹车不让你更快,但让你在每个弯道都能停下来重新看清方向。没有刹车也能到终点,只是你不敢问自己是怎么到的。
Claude 的默认写代码姿势,恰好是 TDD 的反面。社区对这个有共识,claude-code-ultimate-guide 把话说得很白:Claude Code 对实现有先天偏好(implementation-first bias)——你让它建功能,它自然先写能跑的代码,再写一套对着这堆代码必然全过的测试。这不算 bug,是训练本能。所以光说「写个测试」没用,你得显式命令它「先写 FAILING 测试」「先给失败断言再动生产代码」。这是本篇最硬的一条纪律。
假覆盖是什么?测试套件绿成一片,但断言全是迁就实现写出来的——测的是实现长什么样,不是需求要什么。像镜头盖没摘的摄像头:一直开着,一直在录,录的全是黑屏。TDD 把顺序倒过来:先写一个会失败的测试,亲眼看到它失败,再写最小实现让它变绿。失败断言是「这个行为还不存在」的硬证据。有了这个证据,绿才有意义。
requirePermission 中间件:完整 TDD 循环
实例来了,从真实仓库 examples/prd2spec/backend/ 里截。拆计划把「权限」挂在 T3,对应 spec 的 US6(有 token 但缺权限点 → 403)。现在实现 T3 的第一个周期——requirePermission 拒掉无权限调用。
Red:先写 FAILING 测试。 注意措辞,不是「写测试」,是「写 FAILING 测试」。给 Claude 的命令原文是:先写测试,别动实现,这个测试必须是因为功能还不存在而失败。测试长这样(backend/test/unit/permission.test.js):
// backend/test/unit/permission.test.js —— 周期一的失败测试
const { requirePermission } = require('../../src/middleware/permission');
test('requirePermission 拦截缺少权限点的用户返回 403', () => {
const req = { user: { id: operatorId } };
let status = null;
let body = null;
const res = {
status(s) { status = s; return this; },
json(b) { body = b; return this; },
};
requirePermission('user:list')(req, res, () => {
throw new Error('不应进入 next');
});
assert.strictEqual(status, 403);
assert.strictEqual(body.code, 403);
});
operatorId 是测试 setup 里造的一个绑定 operator 角色的用户——种子角色只有 product:* 4 个权限点,没有 user:list。这段测试引用了 ../../src/middleware/permission——这个文件还不存在。这就是「红」的正确打开方式:测试针对一个还不存在的功能。
确认失败断言。 跑测试,命令 node --test backend/test/unit/permission.test.js。预期输出:
Error: Cannot find module '../../src/middleware/permission'
红得对。因为失败原因是「功能缺失」,不是拼写错、不是依赖装错、不是测试自己写错。这一步是整个循环里我最不让你省的:确认失败断言,就是确认这个测试真的在测「功能不存在」这件事。
Steve Kinney 的课程把这步叫 watch the first test:绝不让代理写一个一跑就过的测试,在动生产代码之前,先让它把失败断言亮给你看。他还有一句更狠的:「没亲眼看着测试失败,你就不知道它测的是不是对的东西。」superpowers 的 TDD 技能里,这句话几乎是原文——这不是一个人的偏好,是 TDD 对 AI 的核心。
我的结论也放这儿:不确认失败断言就放行,等于没写测试。你想想——测试跑完是绿的,你分不清是「功能真的对了」还是「测试压根没测到点子上」。失败断言是你唯一能确认「测试在测对的东西」的时刻。这个时刻放过了,后面所有绿都不可信。
Green:写最小实现。 现在允许 Claude 动生产代码了,命令是「只写让这个测试变绿的最少代码,不要顺手加别的」。最小实现(backend/src/middleware/permission.js + 配套的权限点查询):
// backend/src/middleware/permission.js(最小实现)
const { getPermissionCodesByUserId } = require('../db/queries');
function requirePermission(code) {
return function permissionMiddleware(req, res, next) {
const codes = getPermissionCodesByUserId(req.user.id);
if (!codes.includes(code)) {
return res.status(403).json({ code: 403, message: `无权限访问,缺少权限点:${code}` });
}
return next();
};
}
module.exports = { requirePermission };
配套的 getPermissionCodesByUserId 同周期写进 backend/src/db/queries.js——它把「用户 → 角色 → 权限点」的三表 JOIN 合并成一组权限点 code,中间件才能判断。写它的原因只有一个:测试要按 operator 的真实权限点判断,得先有这个查询。
只写了让失败断言变绿的最少东西:没有参数校验、没有错误处理、没有「user 不存在怎么办」。不是这些不该想,是这一轮不想——现在加的任何一行,都是没被测试逼出来的代码,属于 YAGNI。superpowers 的 TDD 技能甚至允许 Green 阶段「作弊」——硬编码、复制粘贴都行,反正重构会收拾。你看到的是收敛版,作弊版就不贴了。
再跑 node --test backend/test/unit/permission.test.js,绿。确认绿和确认红一样重要:绿了,且只有当前这个测试从红变绿,别的测试没被碰坏。
Refactor:重构,测试保持绿。 真实实现里这一周期还做了一次「不改行为地变结构」:把查到的权限点集合挂到请求上——req.permissions = codes——后面 dataScope 中间件和商品 handler 不必再查一遍库。收敛后长这样:
// backend/src/middleware/permission.js(重构后)
function requirePermission(code) {
return function permissionMiddleware(req, res, next) {
const codes = getPermissionCodesByUserId(req.user.id);
req.permissions = codes;
if (!codes.includes(code)) {
return res.status(403).json({ code: 403, message: `无权限访问,缺少权限点:${code}` });
}
return next();
};
}
重构完重跑同一命令,绿。注意,这里的重构没加任何行为:数据权限的 SELF 过滤是另一个行为,属于下一个周期。重构只允许「不改行为地变结构」。

下一个周期。 T3 还绑着 US3:运营只看自己创建的商品。这是数据权限,独立行为,走第二个完整周期。核心是一个纯函数 buildDataScopeFilter,测试如下(backend/test/unit/dataScope.test.js):
// backend/test/unit/dataScope.test.js —— 周期二的失败测试
const { buildDataScopeFilter } = require('../../src/middleware/dataScope');
test('buildDataScopeFilter(SELF) 生成 creator_id 过滤条件', () => {
assert.deepStrictEqual(buildDataScopeFilter('SELF', 7, 'creator_id'), {
where: 'creator_id = ?',
params: [7],
});
});
先写测试(此时 dataScope.js 不存在 → 红)→ 最小实现:SELF 返回 { where: 'creator_id = ?', params: [userId] },ALL/DEPT 返回 null 不过滤 → 跑,绿 → 商品列表路由接上:router.get('/', requirePermission('product:list'), dataScope(), handler),handler 里 buildDataScopeFilter(req.dataScope, req.user.id) 拼进 SQL 的 WHERE。几处小 diff,行为加一条,全部测试保持绿。我没有把两个行为塞进一个周期,因为一次一个周期,失败时你才知道是哪个行为错了。
四条纪律,为什么不能省
上面这个循环里,有四条纪律每条都有「为什么」,不是仪式:
先写 FAILING 测试。 Claude 默认先实现后补测,补的测试是橡皮图章。「让 Claude 写测试」和「让 Claude 写 FAILING 测试」是两件事。前者它写的是辩护词,后者它写的是起诉书。你要的是起诉书——先证明现有代码有罪,再让生产代码改。
先确认失败断言。 唯一能证明「测试测的是对的东西」的时刻。绿测试能说明的东西太多(可能压根没测、可能测错了地方),只有失败断言是确定的。这条和上一条是一对:写 FAILING 测试保证测试在测新行为,确认失败保证它测的是「功能缺失」而不是「测试写错」。
一次一个 TDD 周期。 一次让 Claude 从红到绿全通过,是最大的偷懒——绿得太容易,你连它到底做了几件事都数不清。多个行为混在一个周期里,测试一红,你定位不到是哪个行为错了,只能整块返工。一个周期一个行为,最坏情况是某个行为错了,你只返工那一个。
保留 Refactor。 绿只证明行为对,不证明结构对。跳过重构,技术债在 AI 手上积累得比人快——它每次改这块代码,都会在原来的乱结构上再叠一层。thoughtbot 的文章里那个翻车现场就是这么来的:它第一次让 Claude 干,Claude 把 model、controller、route、view、migration、seeds 一次全写了,9 个文件,从红到绿一大步,没有任何重构。作者觉得这违背了 TDD 精神,把改动全部 stash 掉重来。重构不是可选项,是循环的一格。另外注意:绿测试不证明重构安全,重构完必须重跑同一验证命令——Steve Kinney 专门提醒过这条。
反模式清单:五个「假覆盖」的坑
上面四条纪律,反过来说就是五个最常见的坑。每个我都见过人踩,包括我自己。每条给你「错在哪」和「正确做法」。
写 tests,不是 failing tests。 命令是「给这个功能写测试」,Claude 写了,测试全绿——因为它照着实现写的。错在哪:测试迁就代码,测的是代码长什么样,不是需求要什么。正确做法:命令改成「先写 FAILING 测试,别动实现」,并让它把失败断言亮给你。
一次做多个功能。 一个任务里塞三个行为,让 Claude 一口气完成。错在哪:红了定位不到是哪个行为错;绿了不知道它是不是把不该做的也做了。正确做法:一个周期一个行为,最小实现只解决当前断言,别的行为下个周期见。
跳过 Refactor。 测试绿了就走,下一轮再来。错在哪:结构越来越乱,AI 下次改这处会叠加更多乱;测试是绿灯,不保证房子结构安全。正确做法:绿之后显式进重构,重构完重跑测试。
mock 当测试。 把真实依赖全替换成 mock,测试断言 mock 的返回值。错在哪:mock 永远返回你设定好的值,测试不可能失败——绿了只证明 mock 写得对,不证明代码对。superpowers 的 TDD 技能原话:除非实在躲不开,不要用 mock。正确做法:能测真实行为就测真实,mock 只用来隔离真正的外部边界(网络、时间、文件系统)。
让测试立刻通过。 测试红了,让 Claude「修一下测试」:删断言、加 .skip、放宽断言。错在哪:这是 reward hacking,把闸门本身拆了。测试红了,该改的是生产代码,不是测试。正确做法:测试一旦被确认过就冻结,红只能由生产代码变绿来解决。claude-code-ultimate-guide 专门给过对策:确认过测试文件后冻结它、审查 diff、用 Stop hook 拦住红套件不让收工。
对照 superpowers:实现的产品化
subagent-driven-development:每任务独立子代理 + 两阶段审查
我在第四章是手动守纪律:自己命令、自己看失败断言、自己跑测试。superpowers 把「实现」这步做成了另一个技能:subagent-driven-development。它把我在第四章做的事全部搬给子代理,还多加了一道我手动做起来很累的环节——独立审查。
它的玩法:每个任务派一个全新的子代理去实现(fresh subagent per task),实现完不直接进下一个任务,先过两道独立的审查闸:
第一道,spec 符合性审查(spec-compliance):审查子代理读实际代码,核对「实现的是不是任务要的东西」——该做的做全了吗?有没有加没要求的东西(YAGNI 违规)?第二道,代码质量审查(code-quality):只有 spec 符合性过了才启动,看结构、可维护性、测试覆盖。
两道审查的顺序是硬性的,不能反。为什么?因为这是两个维度——一个写得漂漂亮亮但做错了需求的函数,照样是废的。先审「做对了」,再审「做得好」,顺序反了就是浪费精力在注定要重写的代码上挑结构。这跟我「拆计划先绑验收」是同一个道理:先钉死「怎么算做完」,再看「做得怎么样」。审查不合格,打回给同一个实现子代理修,修完再审,循环到过,才进下一个任务。
它把我的「每任务验收」自动化了:我手动对照 spec 逐条过,它派一个专职审查子代理干这事,而且审查读的是代码不是实现者的汇报——这点比我手动做还稳,我手动容易听 Claude 说完「好了」就信。
但我不夸大它。superpowers 是 Jesse Vincent(GitHub 上叫 obra)写的开源 Claude Code 插件,MIT 协议,/plugin install superpowers@claude-plugins-official 就能装,一套 14 个技能。它的口碑是真实积累出来的——2026 年 6 月官方插件目录里安装量约 75 万,排在 Anthropic 自家插件后面。
它也有已知的失效模式:GitHub issue #463 里就有人报告,控制器会跳过派审查子代理这一步,理由是「这任务太简单不用审」——文字规则被模型合理化掉了。我的判断:两阶段审查是对的,但它跟所有靠自觉的纪律一样有被绕过的空间。手动做的好处是审查人是你自己,你没法骗自己。
test-driven-development:TDD 纪律的硬约束
subagent 管「每任务怎么过」,test-driven-development 技能管「每个任务里怎么实现」。它是 TDD 纪律的产品化,比我在第四章的手动约束更狠。
核心是一条铁律,原文大写:NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST——没有失败的测试,就不许写生产代码。违反的后果不是提醒,是删除:先于测试写的代码,要删掉重来,不能留着当参考、不能边写测试边改它。「删除就是删除。」它为什么这么绝?因为它认准一件事:先写的代码会污染测试的公正性——你会不自觉地写一个对已存在代码友好的测试,而不是写一个测需求的测试。
它还有一张「合理化借口表」,把跳过 TDD 的常见借口逐条堵死:「太简单不用测」「先测后再测结果一样」「我已经手动测过了」「删掉几小时的工作太浪费」——每条都给了反驳。这张表的价值不在反驳本身,在于它承认了一个事实:跳过 TDD 从来不是技术问题,是借口问题。
和我第四章的做法对着看:我手动用命令约束自己,它用铁律 + 反借口表约束代理。方向完全一致,它只是把「每次都要自己想起」变成了「技能的默认行为」。这就是 superpowers 那句口号的意义——Process over Prompt,流程比提示词重要。你手动走、我手动走、它插件走,最后到的是同一个地方:让 AI 在写代码之前,先证明它知道要写的是什么。
结论:拆计划治「全局失序」,TDD 治「假覆盖」
走完这两格,把主线的两个词收一下。
拆计划治「全局失序」:把一份 spec 拆成有依赖顺序、每任务绑定验收测试的任务清单,让 Claude 的每一步都有路标,最坏情况错一个任务,而不是错一个系统。TDD 治「假覆盖」:用「先写 FAILING 测试、先确认失败断言、一次一个周期、保留重构」把每个任务的完成标准钉成可执行的断言,让绿灯从「它说好了」变成「我看到它绿了」。
一句话:拆计划管「往哪走」,TDD 管「走到没」。前者让 AI 不跑偏,后者让 AI 不糊弄。两件事都不难,难的是每次都想起来做——这也是下一篇的主题。
「复杂软件系统的Vibe Coding实践」还有两格没走:怎么把这些纪律固化进工具(hooks、CLAUDE.md、技能),以及怎么验收、怎么迭代收尾。下一篇讲这个,关注不迷路。
你用 TDD 让 Claude 写代码时,踩过什么「假覆盖」的坑?是它把测试写成辩护词,还是你让它一口气干完、没看失败断言?评论区说说,我想看看大家各自栽在哪一条。想让下一篇展开哪块的,也评论区告诉我——hooks 怎么拦 RED、任务拆多细才不碎、验收清单怎么定,你问的我都会记下来,可能就成了下一篇的选题。
收个尾。AI 写代码像下坡,TDD 是刹车。别嫌刹车麻烦——下坡不装刹车也能到终点,只是你不敢问自己是怎么到的。
系列文章:
更多推荐




所有评论(0)