第三篇 · 做出东西 / 第 8 章

从规格到上线

规格写完了,为什么还是做不完? 因为从一份文档到别人能打开的网址,中间还隔着拆票、实现、审查和上线。

预计阅读
30 分钟
前置
第 7 章
关键词
工单、实现、审查、上线

读完这一章,你能

  • 把一份规格拆成 5 张纵向工单,每张都写得出“做完的可观察结果”;
  • 一次只实现一张工单,并换一个干净会话对照规格和验收清单做审查;
  • 按上线前检查清单,把项目放到一个别人能打开的网址上。

午饭登记 · 当前状态:有 spec,还没拆票

本章地图
  • 必读:拆票——把规格切成 5 张能单独演示的纵向工单。
  • 必读:实现——一次一张,做完自己验。
  • 必读:审查(含验收)——先亲手点清单,再换干净会话挑错。
  • 必读:最小上线——托管、域名(网址)、HTTPS(网址上的小锁)和上线前检查。
  • 可选以后再看:TDD(测试驱动开发)红—绿、双轴审查、子代理、并行会话。

先做判断:你要不要拆票

在讲怎么做之前,先说一件更省事的事:你可能根本不需要做这一步。

拆票,就是把一份规格拆成若干张工单(ticket)。 一张工单是一件可以单独完成、单独验收的小活。

官方的立场很干脆

/to-spec(把对话整理成规格文件的技能)和 /to-tickets(把规格拆成工单的技能)这两步, 只在“这件事跨多个会话”的时候才走。

如果整个改动装得进一个上下文窗口,那就跳过它们,直接进入实现。 官方的说法是:单会话的改动走这套流程“什么都换不来”, 反而多付一次整理的成本,而且模型在这一步还可能跑偏。

这条对你尤其重要。因为按第 1 章的建议, 你的第一个项目应该小到根本用不上拆票—— 一个页面、一件事,一轮对话就做完了。

那什么时候才该拆?用一个很实际的判断标准:

你的情况怎么走
一个聊天框里说得完,一两轮就能做完 跳过拆票,直接做
要做的东西说不完,得分好几次聊 需要拆票,往下看
你已经在同一个对话里聊了很久,它开始忘事 这是第 4 章的“变笨区”。拆票正是治它的

换句话说:拆票的本质,是把一件“一次装不下”的活, 切成若干个“一次装得下”的小活。它是为上下文窗口服务的,不是为项目管理服务的。

阶段五 · 拆票

这一步对应 /to-tickets。它把规格拆成一组工单。

关键不在于“拆”,而在于按什么方向拆。 官方的答案是:每张工单必须是曳光弹(tracer bullet)。

什么是曳光弹

一条窄,但完整的路径,穿过这个功能的每一层—— 界面、接口、数据库、测试。

不是某一层的横切面,是纵向的一条细线。 它在落地的那一刻,就能单独演示。

官方给的拆解规则一共五条,非常具体:

  1. 每一片必须纵向,不能横向

    一条窄但完整的路径穿过每一层——界面、接口、数据库、测试。 是纵向,不是某一层的一片。

  2. 做完一片,必须能单独演示或验证

    这是判断你有没有切对的最好标准: “这一片做完,我能演示什么?”答不出来,就说明你切横了。

  3. 每一片的大小,要刚好装进一个全新的上下文窗口

    为什么?因为来捡这张工单的,是一个从没见过你规格的新会话。 它得能一口气做完。

  4. 该重构的先重构,排在最前面

    官方借了一句特别好记的话:“先把改动变容易,再做那个容易的改动。” 这个动作有个正式的名字叫 prefactoring(预先重构)—— 它不是“顺便整理一下代码”,而是为了后面那几张工单能顺利做,先付的一笔定金。

  5. 工单正文里,不许写文件路径和行号

    理由和规格里那条一样:路径会腐烂,行为不会。 写“改 src/db/schema.ts:42”,几天后那行号就不是那个意思了; 写“订单表增加一个状态字段”,永远是对的。

    唯一的例外是原型阶段已经产出的代码片段—— 那种本来就该直接搬过去用。

午饭登记怎么拆:5 张纵向工单

把第 7 章那份 spec.md 拿出来,按“每张都能单独演示”的标准切,得到 5 张票。 注意每张票的“做完的可观察结果”都是你能亲手做一遍的动作。

序号工单做完的可观察结果依赖
1 表单 同事打开网页,填姓名和菜名,点提交;当天列表立刻出现这一条(先存在服务器内存里)。 无
2 SQLite 持久化(SQLite 是一个存在文件里的轻量数据库) 重启服务后,之前提交的记录还在;刷新页面仍然能看到。 票 1
3 管理端 组织者能看到当天列表,并删除一条填错的记录;删除后列表立刻更新,刷新后也不再出现。 票 2
4 按日期筛选 选择或输入一个日期,能看到那天的全部登记;切换日期,列表跟着变。 票 2
5 部署 把网址发给一个同事,他用手机打开并提交一条,你在自己电脑上能看到;服务重启后数据还在。 票 1–4
为什么票 1 先用内存也算纵向

票 1 的路径是“页面 → 提交接口 → 数据(暂时放内存)→ 当天列表”, 它已经穿过每一层,也能单独演示。票 2 只做一件事:把内存换成 SQLite。

每张票都能单独演示,就是切对了。 如果一张票的验收标准要伸手到下一张票,说明你又切横了。

一张合格工单和一张不合格工单,并排看

光看规则还是抽象。技能档案里放了两张真实的工单并排对比, 左边是垂直切片、右边是水平切片,差别一眼就能看出来: 技能档案 · 主链五步。

为什么不能按层切

你可能会想:那我按“先做数据库、再做接口、再做界面”的顺序拆,不是很自然吗?

这正是最典型的错误切法。官方给了一个真实案例来说明代价:

反例:一个团队按层切了 26 张工单

一个团队按层切了 26 张工单(数据层、生产层、汇总层、查询层……)。

结果:每关闭一张工单,平均要跑约 20 次 agent 运行,其中大约四分之三是在返工。

他们自己复盘时,把每一类失败都追溯到了“按层切”这件事上—— 不是实现本身有问题。

对照一下你的午饭登记:它只有 5 张票,而且每张都同时碰页面、接口、数据、验收。

为什么按层切会这么惨,看图就明白了:

按层切(错) 按功能切(对) 界面 接口 数据 测试 ① ② ③ ④ ① ② ③ 4 张工单,没有一张能单独跑通 全部做完之前,什么都动不了 3 张工单,每张做完都能演示 而且每一张都同时改了四层
左边每张工单只碰一层,所以它的验收标准必须伸手到别的工单的地盘里—— 这就是返工的来源。右边每张工单自己拥有自己的一切。

唯一的例外

有一种改动天然没法纵向切:一次机械的全局替换(比如改一个列名), 它的影响范围会扇出到整个代码库,一次性打断上千个调用点—— 没有任何纵向切片能让代码保持可运行。

这种情况官方给了一个专门的名字和打法,叫 expand–contract(先扩后收):

  1. Expand(扩)

    把新写法加在旧写法旁边。什么都不坏。

  2. Migrate(迁)

    调用点分批迁移。因为旧写法还在,每一批做完代码都还是好的。

  3. Contract(收)

    确认没有调用者了,再删掉旧写法。

这个模式零基础的你可能一时用不上,但它的思路值得记住: 当一件事没法一步做完,就先让新旧并存,再一点点搬过去。

阻塞关系与并行

每张工单还要声明一件事:它被谁挡着(blocking edges)。

这件事的价值远超你的想象。因为有了这张依赖图,就自然出现了两个东西:

顺序自己浮出来

不用你排计划

哪个先做、哪个后做,是从依赖关系里长出来的, 而不是你拍脑袋排出来的。

最关键的收益

可以并行 以后再看

依赖图上的叶子节点——所有前置条件都已完成的那些工单—— 可以各自开一个会话同时做。

这是“多个 AI 同时干活”的正确来源

不是“多开几个窗口就行”,而是依赖图告诉你哪几张可以同时开。

官方原话:“任何阻塞都已完成的那张工单,就在前沿上。” 这个“前沿”(frontier)就是可以立刻开工的那一批。

但有个坑:这一步只是产出清单,不负责推进

/to-tickets 的技能停在“清单”这一步, 它不会自动派活。数出前沿上有几张工单、开几个会话——那是你的活。

另外它还提醒你:这些工单是为 agent 量身定做的, 所以不要对它们再跑一遍“分诊”——分诊是给“别人送来的工作”用的。

阶段六 · 实现

这一步对应 /implement。它的定位非常清楚,清楚得近乎偏执。

它从不重开计划。 没有访谈、没有澄清轮、不会提议换一个做法。

上游谈定的一切就是它的输入,这个技能的全部工作,就是把它变成一个提交。

这正是它区别于“在新会话里说一句『把这个做了』”的地方—— 后者会很乐意在动手的同时,顺手把这件事重新设计一遍。

一次运行,五个节拍

  1. 读懂这张工单,弄清要在哪些接缝(seam)上测试。

    接缝就是你从外面能观察、能挂测试的那个边界,比如“提交表单”这个动作。 它不关心表单内部怎么实现,只关心“点提交之后发生了什么”。

  2. 在预先约定好的接缝上驱动测试,一次一个“红—绿”循环
  3. 频繁做类型检查,边做边跑单个测试文件
  4. 结尾跑一次完整的测试套件
  5. 跑审查,然后提交到当前分支
午饭登记的最小测试长什么样

先别管测试框架。一个最小测试就是一条能自动判断真假的句子: 不填姓名、点提交,页面应该出现“请填写姓名”,而且列表里不应该多出一条记录。

先让它失败(红),再写刚好让它通过的最少代码(绿)。 这就是“测试驱动”的全部意思;剩下的细节可以以后再看。

进阶:红—绿循环到底在做什么 以后再看
“红—绿”到底是哪两件事

红 = 先写一个会失败的测试。这时功能还没做,测试跑起来当然是红的—— 你要亲眼看到它红一次,才能确认这个测试真的在测东西。

绿 = 只写刚好让它通过的最少代码。不多写一行。 多写的那些没有测试盯着,等于没验过。

然后是最反直觉的一点:这个循环里没有第三个字。

大多数讲测试驱动开发的材料都写作“红—绿—重构”,三步一体。 这套方法偏偏把“重构”拿掉了——整理结构归 阶段七 · 审查(含验收)去管。

理由是:一边修东西、一边改结构,这两件事会互相掩盖。 测试红了,你分不清是功能没做对,还是刚才那次结构调整弄坏了什么。

完整的技能档案(包括官方点名的三种测试坏味道)见 技能档案 · 四个底层原语。

一次调用,只做一张工单。做完就开一个新会话做下一张—— 因为每张工单都是自包含的,上一张的对话是可以扔掉的。

它最弱的一个关节,官方自己承认

“实现技能内部,没有任何东西来约定接缝。”

“接缝”就是上面说的那个公共边界——测试住在那里。 但 /implement 自己不管这件事:约定接缝的是另一个技能。

所以如果上游哪里都没约定过,这个前提就永远不会触发, 这次运行会悄悄地退化成“就是把代码写出来”。

官方的建议是:把接缝写进规格里,这是唯一能防止它退化的办法。 午饭登记里,“提交表单”和“删除一条记录”就是两个合适的接缝。

两个会让你困惑的行为

它不关工单

它没有完成步骤。做完一个提交就停下,从不碰工单状态,也不勾验收标准。

→ 后果很连锁:既然什么都没被关闭,就永远没有东西会变成“已解锁”,“前沿”也就永远不更新。

对账是你自己的活。

它不建分支、不问

它直接提交在你当前所在的分支上。没有“要不要开分支”这一步,也没有内建的 PR 模式(PR 是拉取请求,合并前请人看一遍改动的机制)。

很多人觉得这太急了——代码在他们验证之前就落地了。

办法是在调用时自己说清楚。

一条实操警告:别在同一个目录里并行跑

有人试过在一个工作目录里同时跑几个实现会话,结果出现了: 一个会话的改动落到了另一个会话的提交上、暂存内容凭空消失、提交跑到错误的分支—— 全在一个下午之内发生。

因为它们共享同一个工作目录、同一个索引、同一个 HEAD。 索引(staging area)是“已经准备好、等着提交的那批改动”; HEAD 是“当前分支最新的那个提交”。

两个会话同时改同一份索引和 HEAD,就像两个人同时在同一张纸上改字、还各自保存—— 后保存的会把前一个覆盖掉。

要并行,就开独立的目录,每个会话只碰自己那一份。

阶段七 · 审查(含验收)

最后一步不是“让 AI 说没问题”,而是先验收,再审查。 验收看行为,审查看代码与规格;两件事都做完,这一阶段才算通过。

先验收:把清单亲手点一遍

拿出 spec.md 里的用户故事,一条一条点。 每条只回答“看到 / 没看到”,不回答“应该没问题”。

午饭登记的验收长这样
  • 不填姓名点提交 → 页面出现“请填写姓名”,列表不多一条。
  • 填“小王 / 牛肉面”提交 → 当天列表出现这一条。
  • 刷新页面 → 这一条还在。
  • 组织者删除这一条 → 列表立刻少一条,刷新后仍然少一条。
  • 换到昨天 → 看到昨天的登记,今天的记录不出现。

再审查:换一个干净的眼睛

验收通过不代表代码没问题。你需要一个没有参与实现的新会话, 拿规格和清单来挑错——同一个上下文审自己,只是确认偏误。

零基础版:最小审查

开一个全新会话,给它三样东西:spec.md、验收清单、这次改动的 diff(改了什么)。 让它逐条对照验收清单,只报告“没做到”和“可能弄坏了”的地方。

你拿到报告后,逐条亲手验证。报告是线索,不是判决。

报告是线索,不是判决

不管用哪个审查命令,它给出的发现都可能引错位置,或者夸大了影响。 行动之前,先点开它引用的那段代码或规格看一眼。

进阶:双轴审查、子代理与 12 个坏味道 以后再看

它为什么要分两个轴

它审查的不是“这次改动好不好”,而是同时沿两个轴审查:

这次的改动 轴一 · 标准 “建得对吗?” 对照本仓库自己的编码规范 轴二 · 规格 “是对的东西吗?” 对照原始的规格文档 ← 一个独立的子代理 ← 一个独立的子代理 → 报告 A 报告 B 两份报告并排放 · 不合并 · 不排序 · 不选出一个“总冠军”
为什么要两个独立子代理?官方说得很直白: “这样它们就不会污染彼此的上下文。”

为什么非要分开?因为一份改动完全可能通过一个轴、却挂掉另一个轴:

标准过,规格挂

它遵守了每一条编码规范,但实现的是错的东西。

→ 代码很漂亮,功能不是你要的。

规格过,标准挂

它完全按需求做了,但破坏了项目的约定。

→ 功能对了,代码库被弄乱了。

这一句是整章的设计精髓

“分开报告,是为了阻止一个轴掩盖另一个轴。”

正因如此,它明令不许合并、不许重新排序, 结尾只给“每个轴各自最糟的问题”,拒绝跨轴选出一个总冠军。

这里的子代理(subagent),是主会话派出去、带自己独立上下文的另一个 AI 实例。 它看不到主会话的推理,所以能减少“顺着自己刚才的思路找理由”的偏差。

标准轴的兜底:12 个坏味道

就算你的项目什么文档都没写,标准轴也不会空转——它会用一份通用清单兜底, 来自 Fowler 的《重构》,12 个“代码坏味道”。其中几个零基础也能看懂:

坏味道什么感觉
神秘的名字名字看不出它是干嘛的
重复的代码同一段逻辑出现在好几个地方
依恋情结一个函数老是伸手去掏别人的数据
数据泥团那几个字段总是手拉手一起出现
夸夸其谈的通用性为“以后可能要用”加了抽象,可现在根本用不上
中间人一个类基本上只是在往下转发
两条约束很重要

一是“仓库说了算”:你项目自己的规范永远优先。 如果规范里明说了某种写法,就不该被坏味道清单标记。

二是“永远是判断题”:每个坏味道都是带标签的启发式—— 报告写的是“可能有依恋情结”,绝不是硬性错误。

还有两条来自实践者的经验,值得一并知道:

  • 要换一个干净的会话来审。 最好的说法来自一位读者:“同一个上下文审自己不是审查, 是用一个斜杠命令包装起来的确认偏误。”
  • 不要指望它“审到干净”。 每次跑都能发现新问题,因为修复本身会制造新的表面。 把一次审查当成线索清单,处理掉背后有明确规则的, 然后就停——它不会收敛。
一个已知的失控情况

它会派两个子代理去审。而那两个子代理没有被禁止再次调用审查技能, 于是它们可能各自再派一批……有报告记录到一次跑出了 50 多个代理。

如果你让它无人值守地跑,盯一下代理数量。

一个会把你搞糊涂的撞名

Claude Code 自带一个叫 /code-review 的命令。 它和这一节讲的不是同一个东西,做的事也不一样:

Claude Code 自带的这个库的
看什么 这次改动(diff)里有没有 bug 规格符合性 + 仓库规范
问的问题 “这段代码写错了吗” “做的是不是当初要的那件事”

装了这个库之后,两个同名命令会打架。谁赢,取决于你当初怎么装的 (见上一章的装法):

路线 A · 插件装的

所有技能被挂在 mattpocock-skills: 前缀下面。 内建的那个在无前缀的短名字上就够不着了,冲突不明显。

路线 B · skills.sh 装的

本地文件赢。也就是这个库的 /code-review 会遮蔽掉内建的那个。

有个更干脆的解法

有实践者干脆把 Claude Code 的内建技能整个删掉—— 冲突就不存在了,而且还能省下相当一块上下文 (内建技能也占你那 15 万 token 的工作台)。

如果你只是偶尔用,不折腾也行。但至少要知道有两个同名命令, 别在看到一个只挑 bug 的报告时,以为是自己装错了。

最小上线:放到别人能打开的网址上

审查通过、验收清单全过,最后一步才是上线。 上线不是新的阶段,它是阶段七 · 审查(含验收)通过后的动作。 这一节只讲最小可用版本,不讲运维。

先选托管方式:只看两个问题

你的情况选什么判断标准
只有网页文件,数据存在浏览器里 静态托管 没有后端程序,也没有数据库;把文件传上去就能访问
有后端程序,数据要存服务器 托管平台 平台能运行你的程序,并给你数据库、环境变量(放在服务器上、不写进代码的配置值)和日志

午饭登记有后端和数据库,属于第二种。选平台时只看四件事: 能不能跑你的程序、有没有数据库、能不能配环境变量、出错时能不能看日志。 产品名不重要;具体怎么比,见 进阶:架构与选型。

域名和 HTTPS,各一段人话

域名(domain)是网址里你买下来的那一段,比如 lunch.example.com。 它通过 DNS(把名字翻译成服务器地址的系统)指向你的服务器。 刚开始用平台送的免费子域名就行,等真的有人天天用了,再买自己的域名。

HTTPS 是浏览器地址栏里的小锁:它把用户和网站之间的内容加密, 别人中途截获也看不懂。现在的托管平台基本都能一键开启; 找不到就搜平台的“HTTPS / 证书”设置。没有小锁,不要让人填真实姓名。

上线前检查清单

密钥是 API Key(调用服务时用的通行证)、数据库密码这类不能公开的字符串; 环境变量是放在服务器上、不写进代码的配置值,比如数据库地址和密码。

检查项怎么查不过会怎样
密钥不在前端 在浏览器里打开页面源代码,搜 key、token、password 前端文件任何人都能看,别人可以拿你的账号花钱
环境变量已配 在平台设置里确认数据库地址等变量存在,且本地的 .env 没有提交到 Git 程序启动报错,或者连到错误的数据库
验收清单全过 用手机和电脑各点一遍 spec.md 里的每条用户故事 上线的是一个没验过的版本
数据库有备份 平台有自动备份就打开;没有就手动导出一份 删错数据无法恢复
错误能看到 故意访问一个不存在的地址,确认平台日志里有记录 出问题只能猜,不知道从哪查
上线后的第一小时

自己用手机完整走一遍:提交一条、刷新、删除、换日期。 再把网址发给一个同事,让他真的提交一次。然后打开日志,确认你看到了这两次访问。

这三步做完,才算“上线了”,而不是“部署命令跑完了”。

上下文纪律

整套流水线有一条硬性约束,违反了会出真问题。

拷问、规格、拆票,必须在同一个不被打断的上下文窗口里完成

拷问 → 规格 → 拆票这三步, 在拆票完成之前不要清空、也不要压缩上下文。

原因:这样拷问、规格、拆票全都建立在同一套思考上。 官方原话是——“实现要的是原样的推理,不是它的摘要。”

然后,阶段之间是相反的:

你在哪上下文怎么办
拷问 → 规格 → 拆票 一口气做完。不要清空、不要压缩
每一张工单的实现 开一个全新的会话。一次一张,做完就扔
在同一个阶段中途 没什么可决定的。继续,或者把剩下的拆成子任务

官方还给了一个量的感觉:模型保持推理锐利的窗口, 在目前最强的模型上大约是 15 万 token。 如果在拆完票之前就接近这个量,不要在退化里硬撑, 找个阶段边界处理一下再继续。

这些其实就是第 4 章讲的工作台和变笨区, 只不过在这里被固化成了流程的一部分。

技能的两层结构 以后再看

最后讲一个设计上的巧妙之处,它保证了你不会“点一个按钮触发一串黑箱”。

所有技能按谁能触发它分成两类:

用户调用型模型调用型
谁能触发 只有你打命令才能触发 你或 agent 都可能触发
职责 编排——决定做哪几步 承载可复用的纪律
例子 拷问、写规格、拆票、实现 拷问原语、代码审查、测试
一条递归约束

用户调用型的技能可以调用模型调用型的技能, 但永远够不到另一个用户调用型的技能。

为什么这条这么重要?因为它保证编排层是扁平的、可预期的。 不会出现“你点了一个命令,结果它在你看不见的地方触发了一串命令”。

还有一个反直觉的推论:实现技能是用户调用型的, 也就是说别的技能也调不动它。 所以当文档里说“然后每张工单跑一次实现”,那是写给你看的指令,不是 agent 会自己去做的事。

三句话带走

  1. 拆票按纵向:每张票穿过页面、接口、数据、验收,做完就能单独演示。
  2. 实现一次一张票、开新会话;做完换一个干净会话,先验收再审查。
  3. 上线不是新阶段,是阶段七 · 审查(含验收)通过后的动作:按清单查密钥、环境变量、备份和日志。

自测:三个问题

一张“先做数据库表”的工单为什么不合格?

它是横向切片,只碰数据层,做完不能单独演示,验收标准还得伸手到别的工单。 应该改成“提交一条午饭记录,并在当天列表里看到它”——这条路径同时穿过页面、接口和数据。

实现完成后,为什么要换一个新会话做审查?

同一个上下文里,AI 会顺着自己刚才的思路找理由,这是确认偏误。 新会话没有参与实现,只拿规格和验收清单来对照,才更容易发现“做错了但做得很好”的情况。

上线前最低要检查哪三件事?

密钥不在前端、验收清单全过、错误能看到(日志里有记录)。 如果项目有数据库,还要确认有备份;这些检查每一条都能在五分钟内做完。

动手 10 分钟

把午饭登记的 spec.md 拆成 5 张工单,每张只写两行:

  1. 做什么:一句话。
  2. 做完后我能看到什么:一句能亲手验证的话。

写完检查一遍:有没有哪张票的验收要等下一张票? 有就说明切横了,重切;没有,就挑票 1,按它的验收标准做一次最小实现。

如果你看到 X,大概率是 Y

  • 先建数据库表、再写接口、最后做页面 → 横向切片,返工。改成一条能单独演示的纵向路径。
  • 让一个会话连着做完 5 张票 → 上下文变笨,后一张的改动会覆盖前一张。一次一张,开新会话。
  • 在同一个项目目录并行开几个会话 → 它们共享索引和 HEAD,提交会打架。要并行就开独立目录。
  • 审查时只说“帮我看看有没有问题” → 没给规格和验收清单,它只能挑风格。把它当审查员,不是当保洁。
  • 上线前把密钥写进前端代码 → 打开网页源代码就能看到。密钥只放服务器环境变量。
  • 部署完就发链接 → 没看日志,出错只能猜。先自己走一遍,再看日志确认。