第三篇 · 做出东西 / 第 8 章
从规格到上线
规格写完了,为什么还是做不完? 因为从一份文档到别人能打开的网址,中间还隔着拆票、实现、审查和上线。
读完这一章,你能
- 把一份规格拆成 5 张纵向工单,每张都写得出“做完的可观察结果”;
- 一次只实现一张工单,并换一个干净会话对照规格和验收清单做审查;
- 按上线前检查清单,把项目放到一个别人能打开的网址上。
午饭登记 · 当前状态:有 spec,还没拆票
- 必读:拆票——把规格切成 5 张能单独演示的纵向工单。
- 必读:实现——一次一张,做完自己验。
- 必读:审查(含验收)——先亲手点清单,再换干净会话挑错。
- 必读:最小上线——托管、域名(网址)、HTTPS(网址上的小锁)和上线前检查。
- 可选以后再看:TDD(测试驱动开发)红—绿、双轴审查、子代理、并行会话。
先做判断:你要不要拆票
在讲怎么做之前,先说一件更省事的事:你可能根本不需要做这一步。
拆票,就是把一份规格拆成若干张工单(ticket)。 一张工单是一件可以单独完成、单独验收的小活。
/to-spec(把对话整理成规格文件的技能)和
/to-tickets(把规格拆成工单的技能)这两步,
只在“这件事跨多个会话”的时候才走。
如果整个改动装得进一个上下文窗口,那就跳过它们,直接进入实现。 官方的说法是:单会话的改动走这套流程“什么都换不来”, 反而多付一次整理的成本,而且模型在这一步还可能跑偏。
这条对你尤其重要。因为按第 1 章的建议, 你的第一个项目应该小到根本用不上拆票—— 一个页面、一件事,一轮对话就做完了。
那什么时候才该拆?用一个很实际的判断标准:
| 你的情况 | 怎么走 |
|---|---|
| 一个聊天框里说得完,一两轮就能做完 | 跳过拆票,直接做 |
| 要做的东西说不完,得分好几次聊 | 需要拆票,往下看 |
| 你已经在同一个对话里聊了很久,它开始忘事 | 这是第 4 章的“变笨区”。拆票正是治它的 |
换句话说:拆票的本质,是把一件“一次装不下”的活, 切成若干个“一次装得下”的小活。它是为上下文窗口服务的,不是为项目管理服务的。
阶段五 · 拆票
这一步对应 /to-tickets。它把规格拆成一组工单。
关键不在于“拆”,而在于按什么方向拆。 官方的答案是:每张工单必须是曳光弹(tracer bullet)。
一条窄,但完整的路径,穿过这个功能的每一层—— 界面、接口、数据库、测试。
不是某一层的横切面,是纵向的一条细线。 它在落地的那一刻,就能单独演示。
官方给的拆解规则一共五条,非常具体:
-
每一片必须纵向,不能横向
一条窄但完整的路径穿过每一层——界面、接口、数据库、测试。 是纵向,不是某一层的一片。
-
做完一片,必须能单独演示或验证
这是判断你有没有切对的最好标准: “这一片做完,我能演示什么?”答不出来,就说明你切横了。
-
每一片的大小,要刚好装进一个全新的上下文窗口
为什么?因为来捡这张工单的,是一个从没见过你规格的新会话。 它得能一口气做完。
-
该重构的先重构,排在最前面
官方借了一句特别好记的话:“先把改动变容易,再做那个容易的改动。” 这个动作有个正式的名字叫 prefactoring(预先重构)—— 它不是“顺便整理一下代码”,而是为了后面那几张工单能顺利做,先付的一笔定金。
-
工单正文里,不许写文件路径和行号
理由和规格里那条一样:路径会腐烂,行为不会。 写“改
src/db/schema.ts:42”,几天后那行号就不是那个意思了; 写“订单表增加一个状态字段”,永远是对的。唯一的例外是原型阶段已经产出的代码片段—— 那种本来就该直接搬过去用。
午饭登记怎么拆:5 张纵向工单
把第 7 章那份 spec.md 拿出来,按“每张都能单独演示”的标准切,得到 5 张票。
注意每张票的“做完的可观察结果”都是你能亲手做一遍的动作。
| 序号 | 工单 | 做完的可观察结果 | 依赖 |
|---|---|---|---|
| 1 | 表单 | 同事打开网页,填姓名和菜名,点提交;当天列表立刻出现这一条(先存在服务器内存里)。 | 无 |
| 2 | SQLite 持久化(SQLite 是一个存在文件里的轻量数据库) | 重启服务后,之前提交的记录还在;刷新页面仍然能看到。 | 票 1 |
| 3 | 管理端 | 组织者能看到当天列表,并删除一条填错的记录;删除后列表立刻更新,刷新后也不再出现。 | 票 2 |
| 4 | 按日期筛选 | 选择或输入一个日期,能看到那天的全部登记;切换日期,列表跟着变。 | 票 2 |
| 5 | 部署 | 把网址发给一个同事,他用手机打开并提交一条,你在自己电脑上能看到;服务重启后数据还在。 | 票 1–4 |
票 1 的路径是“页面 → 提交接口 → 数据(暂时放内存)→ 当天列表”, 它已经穿过每一层,也能单独演示。票 2 只做一件事:把内存换成 SQLite。
每张票都能单独演示,就是切对了。 如果一张票的验收标准要伸手到下一张票,说明你又切横了。
光看规则还是抽象。技能档案里放了两张真实的工单并排对比, 左边是垂直切片、右边是水平切片,差别一眼就能看出来: 技能档案 · 主链五步。
为什么不能按层切
你可能会想:那我按“先做数据库、再做接口、再做界面”的顺序拆,不是很自然吗?
这正是最典型的错误切法。官方给了一个真实案例来说明代价:
一个团队按层切了 26 张工单(数据层、生产层、汇总层、查询层……)。
结果:每关闭一张工单,平均要跑约 20 次 agent 运行,其中大约四分之三是在返工。
他们自己复盘时,把每一类失败都追溯到了“按层切”这件事上—— 不是实现本身有问题。
对照一下你的午饭登记:它只有 5 张票,而且每张都同时碰页面、接口、数据、验收。
为什么按层切会这么惨,看图就明白了:
唯一的例外
有一种改动天然没法纵向切:一次机械的全局替换(比如改一个列名), 它的影响范围会扇出到整个代码库,一次性打断上千个调用点—— 没有任何纵向切片能让代码保持可运行。
这种情况官方给了一个专门的名字和打法,叫 expand–contract(先扩后收):
-
Expand(扩)
把新写法加在旧写法旁边。什么都不坏。
-
Migrate(迁)
调用点分批迁移。因为旧写法还在,每一批做完代码都还是好的。
-
Contract(收)
确认没有调用者了,再删掉旧写法。
这个模式零基础的你可能一时用不上,但它的思路值得记住: 当一件事没法一步做完,就先让新旧并存,再一点点搬过去。
阻塞关系与并行
每张工单还要声明一件事:它被谁挡着(blocking edges)。
这件事的价值远超你的想象。因为有了这张依赖图,就自然出现了两个东西:
不用你排计划
哪个先做、哪个后做,是从依赖关系里长出来的, 而不是你拍脑袋排出来的。
可以并行 以后再看
依赖图上的叶子节点——所有前置条件都已完成的那些工单—— 可以各自开一个会话同时做。
不是“多开几个窗口就行”,而是依赖图告诉你哪几张可以同时开。
官方原话:“任何阻塞都已完成的那张工单,就在前沿上。” 这个“前沿”(frontier)就是可以立刻开工的那一批。
/to-tickets 的技能停在“清单”这一步,
它不会自动派活。数出前沿上有几张工单、开几个会话——那是你的活。
另外它还提醒你:这些工单是为 agent 量身定做的, 所以不要对它们再跑一遍“分诊”——分诊是给“别人送来的工作”用的。
阶段六 · 实现
这一步对应 /implement。它的定位非常清楚,清楚得近乎偏执。
它从不重开计划。 没有访谈、没有澄清轮、不会提议换一个做法。
上游谈定的一切就是它的输入,这个技能的全部工作,就是把它变成一个提交。
这正是它区别于“在新会话里说一句『把这个做了』”的地方—— 后者会很乐意在动手的同时,顺手把这件事重新设计一遍。
一次运行,五个节拍
-
读懂这张工单,弄清要在哪些接缝(seam)上测试。
接缝就是你从外面能观察、能挂测试的那个边界,比如“提交表单”这个动作。 它不关心表单内部怎么实现,只关心“点提交之后发生了什么”。
- 在预先约定好的接缝上驱动测试,一次一个“红—绿”循环
- 频繁做类型检查,边做边跑单个测试文件
- 结尾跑一次完整的测试套件
- 跑审查,然后提交到当前分支
先别管测试框架。一个最小测试就是一条能自动判断真假的句子: 不填姓名、点提交,页面应该出现“请填写姓名”,而且列表里不应该多出一条记录。
先让它失败(红),再写刚好让它通过的最少代码(绿)。 这就是“测试驱动”的全部意思;剩下的细节可以以后再看。
进阶:红—绿循环到底在做什么 以后再看
红 = 先写一个会失败的测试。这时功能还没做,测试跑起来当然是红的—— 你要亲眼看到它红一次,才能确认这个测试真的在测东西。
绿 = 只写刚好让它通过的最少代码。不多写一行。 多写的那些没有测试盯着,等于没验过。
然后是最反直觉的一点:这个循环里没有第三个字。
大多数讲测试驱动开发的材料都写作“红—绿—重构”,三步一体。 这套方法偏偏把“重构”拿掉了——整理结构归 阶段七 · 审查(含验收)去管。
理由是:一边修东西、一边改结构,这两件事会互相掩盖。 测试红了,你分不清是功能没做对,还是刚才那次结构调整弄坏了什么。
完整的技能档案(包括官方点名的三种测试坏味道)见 技能档案 · 四个底层原语。
一次调用,只做一张工单。做完就开一个新会话做下一张—— 因为每张工单都是自包含的,上一张的对话是可以扔掉的。
它最弱的一个关节,官方自己承认
“接缝”就是上面说的那个公共边界——测试住在那里。
但 /implement 自己不管这件事:约定接缝的是另一个技能。
所以如果上游哪里都没约定过,这个前提就永远不会触发, 这次运行会悄悄地退化成“就是把代码写出来”。
官方的建议是:把接缝写进规格里,这是唯一能防止它退化的办法。 午饭登记里,“提交表单”和“删除一条记录”就是两个合适的接缝。
两个会让你困惑的行为
它不关工单
它没有完成步骤。做完一个提交就停下,从不碰工单状态,也不勾验收标准。
→ 后果很连锁:既然什么都没被关闭,就永远没有东西会变成“已解锁”,“前沿”也就永远不更新。
对账是你自己的活。
它不建分支、不问
它直接提交在你当前所在的分支上。没有“要不要开分支”这一步,也没有内建的 PR 模式(PR 是拉取请求,合并前请人看一遍改动的机制)。
很多人觉得这太急了——代码在他们验证之前就落地了。
办法是在调用时自己说清楚。
有人试过在一个工作目录里同时跑几个实现会话,结果出现了: 一个会话的改动落到了另一个会话的提交上、暂存内容凭空消失、提交跑到错误的分支—— 全在一个下午之内发生。
因为它们共享同一个工作目录、同一个索引、同一个 HEAD。 索引(staging area)是“已经准备好、等着提交的那批改动”; HEAD 是“当前分支最新的那个提交”。
两个会话同时改同一份索引和 HEAD,就像两个人同时在同一张纸上改字、还各自保存—— 后保存的会把前一个覆盖掉。
要并行,就开独立的目录,每个会话只碰自己那一份。
阶段七 · 审查(含验收)
最后一步不是“让 AI 说没问题”,而是先验收,再审查。 验收看行为,审查看代码与规格;两件事都做完,这一阶段才算通过。
先验收:把清单亲手点一遍
拿出 spec.md 里的用户故事,一条一条点。
每条只回答“看到 / 没看到”,不回答“应该没问题”。
- 不填姓名点提交 → 页面出现“请填写姓名”,列表不多一条。
- 填“小王 / 牛肉面”提交 → 当天列表出现这一条。
- 刷新页面 → 这一条还在。
- 组织者删除这一条 → 列表立刻少一条,刷新后仍然少一条。
- 换到昨天 → 看到昨天的登记,今天的记录不出现。
再审查:换一个干净的眼睛
验收通过不代表代码没问题。你需要一个没有参与实现的新会话, 拿规格和清单来挑错——同一个上下文审自己,只是确认偏误。
开一个全新会话,给它三样东西:spec.md、验收清单、这次改动的 diff(改了什么)。
让它逐条对照验收清单,只报告“没做到”和“可能弄坏了”的地方。
你拿到报告后,逐条亲手验证。报告是线索,不是判决。
不管用哪个审查命令,它给出的发现都可能引错位置,或者夸大了影响。 行动之前,先点开它引用的那段代码或规格看一眼。
进阶:双轴审查、子代理与 12 个坏味道 以后再看
它为什么要分两个轴
它审查的不是“这次改动好不好”,而是同时沿两个轴审查:
为什么非要分开?因为一份改动完全可能通过一个轴、却挂掉另一个轴:
标准过,规格挂
它遵守了每一条编码规范,但实现的是错的东西。
→ 代码很漂亮,功能不是你要的。
规格过,标准挂
它完全按需求做了,但破坏了项目的约定。
→ 功能对了,代码库被弄乱了。
“分开报告,是为了阻止一个轴掩盖另一个轴。”
正因如此,它明令不许合并、不许重新排序, 结尾只给“每个轴各自最糟的问题”,拒绝跨轴选出一个总冠军。
这里的子代理(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 会自己去做的事。
三句话带走
- 拆票按纵向:每张票穿过页面、接口、数据、验收,做完就能单独演示。
- 实现一次一张票、开新会话;做完换一个干净会话,先验收再审查。
- 上线不是新阶段,是阶段七 · 审查(含验收)通过后的动作:按清单查密钥、环境变量、备份和日志。
自测:三个问题
一张“先做数据库表”的工单为什么不合格?
它是横向切片,只碰数据层,做完不能单独演示,验收标准还得伸手到别的工单。 应该改成“提交一条午饭记录,并在当天列表里看到它”——这条路径同时穿过页面、接口和数据。
实现完成后,为什么要换一个新会话做审查?
同一个上下文里,AI 会顺着自己刚才的思路找理由,这是确认偏误。 新会话没有参与实现,只拿规格和验收清单来对照,才更容易发现“做错了但做得很好”的情况。
上线前最低要检查哪三件事?
密钥不在前端、验收清单全过、错误能看到(日志里有记录)。 如果项目有数据库,还要确认有备份;这些检查每一条都能在五分钟内做完。
动手 10 分钟
把午饭登记的 spec.md 拆成 5 张工单,每张只写两行:
- 做什么:一句话。
- 做完后我能看到什么:一句能亲手验证的话。
写完检查一遍:有没有哪张票的验收要等下一张票? 有就说明切横了,重切;没有,就挑票 1,按它的验收标准做一次最小实现。
如果你看到 X,大概率是 Y
- 先建数据库表、再写接口、最后做页面 → 横向切片,返工。改成一条能单独演示的纵向路径。
- 让一个会话连着做完 5 张票 → 上下文变笨,后一张的改动会覆盖前一张。一次一张,开新会话。
- 在同一个项目目录并行开几个会话 → 它们共享索引和 HEAD,提交会打架。要并行就开独立目录。
- 审查时只说“帮我看看有没有问题” → 没给规格和验收清单,它只能挑风格。把它当审查员,不是当保洁。
- 上线前把密钥写进前端代码 → 打开网页源代码就能看到。密钥只放服务器环境变量。
- 部署完就发链接 → 没看日志,出错只能猜。先自己走一遍,再看日志确认。