工程方法论复盘 · Anti-Drift

每一片都绿,
整体却漏了六成

一次 spec → plan → workflow 一次性构建的根因排查:18 个单元全部通过对抗验收,可交付给用户的价值只剩四成。没有一个环节作弊。问题出在方法论的结构里——以及我们怎么把它从根上补上。

项目 一键开店 日期 2026-07-14 作者 epingpong + Claude 状态 已复盘 · 已落地

一句话

一个流程严谨的构建(先写测试、独立对抗验收、逐片过闸)产出了 18/18 全绿的结果,却漏掉了约 61% 的用户可见价值。我们一开始以为是"验收标准没写全",查下来发现验收标准写得很全、连端到端旅程都有——真正的病根是:把工作切成小片、并逐片定义"什么算做完"的,是同一份 plan。切片的盲区会自动变成验收的盲区。

解法不是"把标准写得更好",而是一个结构性倒转:让验收权威回到 spec、凌驾于 plan 之上;让分片纵切;让"做完"等于整条用户旅程能走通,而不是各片各自绿。再用一个 hook 保证这套东西下次真的被触发,而不是又变成一条没人执行的规矩。

Part 1 · 缘起

01一个"全过"的失败

"一键开店"是一个帮商户在新加坡从零开一家餐厅的产品——从公开测算、注册、问卷,到选址报告、证照预审、持牌撮合,一直到开业交接,一条 23 步的旅程。

我们用一套自认为很硬的流程来造它:先把需求写成 spec,再拆成一片片小任务写成 plan,最后交给一个多智能体的 workflow 一次性跑完——每一片都"先写会失败的测试、再实现到绿、再由一个全新的智能体对抗验收",闸门全绿才进下一片。

结果 workflow 报告:18 个单元,18 个通过。然后一个人照着真实的开店流程,从注册开始一屏一屏点下去做走查——发现大约 六成的用户可见价值根本没交付:后端引擎一个个都造好了、都有测试,但真正的界面要么是"敬请期待"的占位,要么根本没接进任务板,用户点不到。

18/18
单元通过对抗验收
~61%
用户可见价值未交付
0
环节作弊 / 造假

最让人不安的是第三个数字。这不是"谁偷懒了"能解释的事故。每个实现智能体都写了真测试、跑了真绿;每个验收智能体都独立复核、甚至看见了缺口(比如"通知铃点不动、跳不到材料页"),但都判了"非阻塞、放行",理由高度一致:"这不在本片闸门的断言范围内。"

当一个严谨的流程稳定地产出"全过但没用"的结果,而且人人合规——那问题就不在人,在结构。这份文档记录我们怎么把这个结构性病根挖出来,以及怎么从根上补上它。

Part 2 · 先讲清楚方法论

02spec → plan → workflow 是什么

要看懂后面的分析,得先把这三个阶段讲清楚。它们各自回答一个不同的问题。

Spec
做到什么算完
只钉 WHAT:业务目标、边界、验收标准(EARS 条款 + 至少一条端到端场景)。不写怎么实现。
Plan
拆成一片片
把 spec 切成能独立完成的小单元,每片写清范围这一片怎么验
Workflow
逐片实现+验收
每片:实现智能体先写失败测试→实现到绿;另一个智能体对抗验收;过闸才进下一片。
图 1 三个阶段像一条流水线:spec 定义目标,plan 把目标拆成小块并规定每块怎么检查,workflow 逐块实现并逐块把关。这套设计的每一环都很有道理——正是这一点让后面的失败特别值得研究。

为什么这套流程被认为很硬?因为它把三条业界公认的防错手段叠在了一起:

  • 测试先行(TDD):先写会失败的测试并单独提交,再实现到绿。谁偷改测试作弊,一看提交记录就露馅。
  • 全新上下文的对抗验收:验收的智能体没参与实现,只看代码和验收标准——避免"自己批改自己作业"。
  • 薄切片、逐片过闸:把大任务切小,一片一片验,让"半吊子"早点暴露。

这三条单独看都对。但它们组合起来时,藏着一个谁都没注意到的接缝。要看清它,得先看一次失败的排查——从一个看起来最合理、其实是错的假设开始。

Part 3 · 排查过程

03第一个假设,和它为什么错

最自然的猜测是:spec 里的验收标准(EARS 条款),在拆进 plan 的一片片时,漏掉了一些——有些条款没落到任何一片上,于是没人做。

这个假设很有说服力。如果 42 条验收标准里有几条没被分配,那漏掉一部分功能就顺理成章了。于是我们做了最直接的核对:把 spec 的验收标准全集,和 plan 里那张"覆盖矩阵"逐条比对。

核对结果(反直觉)

spec 的第 9 节写了 21 条主验收标准,加上四个深功能附录各自的验收,共 42 条。plan 的覆盖矩阵——标题就叫"21 条 EARS + F1–F4 全部验收 → 切片;一行不漏"——把这 42 条每一条都点了名、分给了具体的切片。连最敏感的"受控取件"红线都挂在了某一片上。
换句话说:没有一条验收标准掉在缝里。覆盖矩阵是满的,"一行不漏"是真的。

假设被推翻了。而且推翻得很彻底:一个覆盖率 100% 的 plan,照样漏了六成价值。这反而是整件事最关键的线索——它排除了"标准没写全",把我们逼向一个更深的问题:

如果每条验收标准都被分配了、也都被"通过"了,可用户还是拿不到东西——那"通过"这个词,到底在验什么? — 排查转折点

更进一步:我们甚至发现 spec 里本来就有一条完整的端到端旅程验收——从"未注册访客扫码进来算账、留资注册"一路写到"开业、门店激活、渠道报表归因",一步不落。它完全符合我们自己定的标准("至少一条端到端场景")。spec 该有的都有。问题一定在别处。

Part 4 · 内部机制

04"通过"是怎么被掏空的

答案藏在"每一片怎么验"这行字里。我们拿一个具体例子来拆——算钱功能(开店成本测算器)。

它被切成一片,代号 S4。翻开 plan 里 S4 的"闸门"那一行,原文开头就是四个字:pytest——,整行讲的全是后端算出来的数字对不对:覆写行项要实时重算、重油烟要按工期上限、台账过期要警示……一个字都没提"用户能不能打开这个算钱的页面、看不看得到这些数字"。

然后 workflow 严格照着这行字执行。给实现智能体的指令里有一句关键的话:

# workflow 给实现智能体的原文(build #1)
跑全量回归:backend pytest 全套
  + 涉前端时 pnpm build(+ playwright 若本片有浏览器闸

盯住那半句:"playwright 若本片有浏览器闸"——意思是,只有当这一片的 plan 里明确写了要开浏览器,workflow 才会真去浏览器里点一遍。S4 那行没写,于是 workflow 只跑了后端,全绿,判过。算钱的那个页面,从头到尾没有任何一道闸看过它一眼。

写 plan 的人切出「算钱引擎」这片
脑子里想的是"造一个后端算账引擎"
顺手写下它的验收
引擎的自然验收 = 后端 pytest(数字对不对)
workflow 照着这行字执行
规则:plan 没写"要开浏览器",就只跑后端
于是
算钱页面:无人负责、无闸检查
后端绿了 · 屏幕没接上 · 判「通过」
图 2 "只测后端"不是某个总开关设定的,而是一片一片、在写 plan 时手写决定的。没有哪一片叫"把算钱页面接上并验一遍",于是这个页面的检查根本没人写;而这片手头现成的检查(后端测试)就顺理成章成了它唯一的检查。workflow 只是忠实地把 plan 的疏忽执行了出来。

所以要回答"'纯 pytest'到底在哪儿定的"——答案是两层:选择在 plan(写 plan 的人一片一片定),执行在 workflow("不写就不开浏览器"的默认,把这个疏忽变成了既成事实)。没有一个总开关,也没人作弊。

那条端到端旅程呢?它不是排在最后了吗

是的。spec 那条完整的端到端旅程验收,plan 确实分配了——排给最后一片当"收官大考",而且明明白白写着要"浏览器级 e2e"。听起来这道闸能兜住一切。问题出在时机:它只在 18 片全做完之后跑一次。可跑到那会儿,前面那些页面(算钱、选址报告……)根本没接上,"从头点到尾"物理上跑不起来——于是它被悄悄缩水成"跑跑后端数据链 + 点一个毕业页面"糊弄了过去。真正的端到端,从来没真跑过。

教训一

把"整体能不能用"的检查排在最后跑一次,等于没有早期信号。等它跑的时候,一切都已经晚了——它甚至没有能力真跑起来,只能降级放行。整体验收必须每片都跑,才能在坏掉的那一片当场拦住。

三种机制,不止一种

把算钱这一类看清后,六成的缺口其实来自三种不同的漏法:

  • ① 分了片,但闸门只验后端代理指标。验收标准名义上"被覆盖"了,可承载它的那片用后端测试顶替了用户可见面。这是大头。算钱页、选址报告页都是这类——后端 100% 建好、前端零消费。
  • ② 有些属性根本不是任何一条验收标准。"把真页面接进任务板"这层接线、"占位被真页面替换"——spec 里没有这种条款,所以覆盖矩阵里连一行都没有,不是漏填,是结构上无处可填。
  • ③ 否定式的不变量,点击式的闸门天生证不了。比如"某条违规路径不得存在"——你没法"点出"一个不存在的东西。这类红线即使分了片,可达性检查也抓不住。

三种漏法长得不一样,但都指向同一个源头。

Part 5 · 病根

05分片者,不能同时是验收定义者

用一句话概括病根:把工作切成小片的人,同时也是写"这一片怎么验"的人——而且是同一笔写下去的。

写 S4 这片时,同一个动作既画了切片的边界("这片是算钱引擎"),也定了这片的验收("测后端")。这两件事本该互相制约,现在却由同一个念头一次性产生。于是——如果切错了(把"引擎"和"页面"切成两件事、而页面那件没人认领),或者验收写弱了(只写后端),后面没有任何一道关能纠正。因为在这套流程里,你写的那行"怎么验",本身就是"什么算做完"的定义。定义它的人漏了,就没有别人能拦了。

这就是为什么覆盖矩阵补到 100% 也没用:那张表只回答"每条标准归了哪片",不回答"那片的验收,验的是后端还是用户能看到的东西"。盲区随着分片,自动复制到了验收里。

S1 ✓
S4 ✓
S5 ✓
… ✓
S18 ✓
▲ 每一片都独立通过  ✗ 把它们接成"用户能用的整体"的那层——无人负责 ✗
用户从入口走一遍 → 走不通
部分的正确,不等于整体的正确
图 3 · 组合谬误 "每一片都对"推不出"整体对"。逐片验收能保证每个零件合格,却天然看不见零件之间的接线——因为接线不属于任何单独一片。这类"整体属性"(可达性、接线、跨片一致性、否定式红线)必须由一道凌驾于分片之上的检查来守,逐片求和永远得不到它。

可达性只是露出水面的那一角

我们最初盯上的"前端可达性",其实只是这次恰好撞上的那一个整体属性。病根一天不动,换一种整体属性,同一类"全绿却带病"就会换个症状复发。

前端可达性
这次露出水面、被我们发现的症状
— — — 水面:能被人工走查发现的 — — —
病根:分片者同时定义验收 → 盲点自我复制
水面之下,同一个病根还长着许多没被发现的症状 ——
否定式红线(无旁路) 跨片数据一致性 没被切进任何片的整块能力 跨轨真数据的正确性 分支旅程(换址/退款失败)
图 4 · 症状与病根 "补可达性"只是敲掉了冰山露出水面的一角。真正的质量在于动水面之下那块——那个让所有症状不断新生的结构。只治症状,下一个功能会以另一个症状复发。
Part 6 · 关键澄清

06不是 spec 写得不好,是工作流用错了它

这一步值得单独说清楚,因为它决定了解法往哪走。一个很自然的结论是"那就把 spec / 验收标准写得更好、更全"。但这个结论是错的,证据就在 spec 自己身上:

  • spec 有 21 条验收标准,覆盖了各个部件;
  • spec 还有一条完整的端到端旅程,从头到尾把整条路串了下来;
  • 完全遵循了我们自己的标准

如果规则是"必须写一条完整的端到端验收",那 build #1 通过了这条规则——然后照样漏了 61%。所以 spec 不是那个弱点。你没法靠"把标准写得更好"来防这个 bug,因为它本来就写得够好。

病根定性

这是工作流的问题,不是 spec 质量的问题。同一条写得很好的端到端验收,被工作流用错了三件事:
① 只在最后跑一次(太晚) ② 前面每片用后端测试就放行 ③ 活按切,让"接线"没人认领。
补救全在改工作流的机制,没有一条是"把标准写得更高质量"。

Part 7 · 解法

07根治:一个倒转 + 三阶段

一句话解法:把验收权威从 plan 上移到 spec;把分片从"横"改"纵";把"做完"从"各片各自绿"改成"整条旅程绿"。

前者切断"分片者即验收定义者"这个机制;后者让"每片绿"和"集成有价值"收敛成同一件事。这张图取代了那种拿等宽字符拼出来的流程图——它要说的是谁说了算变了。

spec 的端到端旅程 = 唯一验收权威
可执行、从用户真入口点击走通、每片都跑
plan 只能细化它,不能重定义"什么算验收"
S0.5 骨架
点亮 3 / 23 步
+ 选址片
8 / 23
+ 算钱片
12 / 23
+ 材料片
仍 12 / 23 → 当场判「没做完」
23 / 23 · done
图 5 · 外循环棘轮 那条端到端旅程测试从第一片之前就写好、并"每做完一片就重跑一次",要求点亮的步数单调不减。哪一片没把自己的页面真正接上,旅程就多不了几步——那一片当场判没做完(图中"材料片"那道红杠)。drift 被卡在坏掉的那一片,而不是攒到最后。"做完"= 旅程绿,而不是各片 pass 的相加。

为什么"纵切"能从结构上消灭这个 bug

build #1 是横切——按技术层切:算钱引擎一片、选址引擎一片,页面是"另一层",顺带做、没人专门验。横切必然留下"接线无人负责"的缝。纵切反过来:每一片都是"从用户入口到一个用户能看见的产出"的一条薄竖条。这样每片天生就包含页面,而且页面没做出来,这片就没法判做完

横切(build #1)— 按层切

界面层页面 / 屏
逻辑层接线 / 导航
引擎层后端算账引擎 ← S4 只切这一条

切一片 = 横着切一层。界面层没人切到 → 页面成孤儿。

纵切(根治)— 按结果切

页面
接线
引擎
算钱片
页面
接线
引擎
选址片

每片贯穿所有层、落在一个用户可见产出上。没页面 = 没做完。

图 6 · 横切 vs 纵切 一个通俗的比方:横切像"把蛋糕分成海绵层、奶油层、糖霜层"——拿到"海绵层"的人不会端出一块能吃的蛋糕;纵切像"每人切一整块(含所有层)"——每块都是能直接上桌的成品。纵切让"每片绿"和"用户拿到价值"变成同一件事。

三阶段各自要改什么

把上面的原则落到三个阶段,就是这套新方法论的骨架:

  • spec 阶段 —— 那条端到端旅程从"散文描述"升级成可执行、从用户真入口点击走通的唯一验收权威;并写死一条元规则:plan 只能细化它,不能用"本片范围外 / 后端片"把它排除在自己的"做完"之外。
  • plan 阶段 —— ① 分片纵切(每片"入口→可见产出",禁按层横切);② 第一片是 walking skeleton(一条最薄但从头通到尾、零占位的骨架,之后只在"会走"的骨架上长肉);③ 开工前先做一遍完整性核对:每条用户可见的验收标准都要被某片的旅程验收认领,"没有任何一片天然拥有"的整体属性(否定式红线、跨片一致性……)要显式指派看守。
  • workflow 阶段 —— ① 端到端旅程测试每片都跑(图 5 的棘轮);② 验收单加一个机械字段 交付了实质价值一个能打开、但里面是空的页面 ≠ 绿);③ 后端片不能"豁免"用户可见义务,只能"转移"给点名的下游片;④ 至少一名验收智能体直接锚 spec、有权因"plan 合规但 spec 承诺没兑现"而判红;⑤ 终局走查用多只独立的眼睛,不许一个人自证。

这些不是我们拍脑袋想的。它们各自对应软件工程里成熟的做法:可执行验收凌驾于计划(Specification by Example)、外向内的双循环测试与 walking skeleton(《GOOS》)、纵切用户故事(INVEST 的 V=Valuable)、"整体可用"是塔尖属性不能下放到零件(测试金字塔)、Definition of Done 独立于任务分解(Scrum)。我们做的是把它们拼成一套能防住这个具体病根的组合拳

Part 8 · 让它真的生效

08规矩写下来,不等于会被执行

这里有一个必须诚实面对的陷阱:build #1 之所以漏,不是因为缺规矩

我们的 spec 标准里早就白纸黑字写着"端到端验收测试要真跑通整条闭环、它就是闸门"。规矩在,build #1 还是栽了。所以如果这次的补救只是"往标准里再多写几条规矩",下一次照样能被软化——这正是要避免的失败模式。

真正的强制力,只能来自一个不看执行者心情的机制。在 Claude Code 里,那就是 hook(由外壳执行,不由智能体自觉)。所以我们把这套东西分成三层,每层的"保证程度"不同:

1
标准文档(spec-standard)建议性
定义"该怎么做"。必要,但靠智能体自觉遵守——会漂。这一层单独存在时,正是 build #1 栽的地方。
2
可复用模板(workflow 模板)用了就强制
把棘轮、"交付实质价值"字段、完整性前置闸写成代码。一旦用它,闸门就没法被某一片的散文软化——但前提是"用它"。
3
Hook(挂在 Workflow 工具上)保证触发
启动一个构建时,外壳先读脚本:是多片构建却缺抗漂移闸 → 当场拦下。这是唯一由外壳执行、不看智能体心情的一层——"保证"两个字落在这里。
图 7 · 三层强制,逐级变硬 从上到下,"保证程度"递增:文档靠自觉、模板靠"用了它"、hook 靠外壳硬拦。三层叠起来,"每片绿但整体不交付"就从"靠自觉避免"变成了"得主动绕过三层才可能发生"。

这套已经落地并验证:标准升到 v1.1(补上第 5 个病、纵切规约、棘轮与机械字段);可复用模板把机械闸编码进代码;hook 用真实输入测了 7 种情形(达标放行、调研类 workflow 不误伤、缺闸构建被拦)。hook 先跑"只提醒不拦"的观察模式,确认无误伤后一个词翻成"硬拦"——这样硬闸的误伤风险基本清零。

从此以后

下次跑 feature 构建:用模板 → 三道机械闸自动在场,build #1 那种漏法结构上做不到;没用模板裸手搓 → hook 把你推回模板;跑调研/审查类 workflow → 不受影响。"每片绿但整体不交付"从一个方法论漏洞,变成了一个过不去的闸

Part 9 · 收束

09一条新的铁律

如果这次复盘只能留下一句话,就是这句:

验收权威在 spec、凌驾于 plan;
分解工作的人,不得同时定义"什么算验收"。

这条铁律不只属于"一键开店"。任何"把一个目标拆成 N 块、每块独立过闸"的流程——不管块是代码切片、微服务、还是工序——都有同一个结构性风险:拆的那一刀,会把整体属性切进块与块的缝里;而如果拆的人同时定义了验收,缝里的东西就永远没人验。

防它的办法是恒定的:让一道照着整体目标、独立于分解的检查凌驾在所有块之上,并且每完成一块就重跑它。部分的正确永远推不出整体的正确——这道跨越所有部分的检查,才是"整体真的交付了"的唯一证据。