一份可以核对的账
2026 年 6 月 18 日,仓库第一条提交是 chore: init voicedrop repo;到 9 月 1 日走到 738 条,如今 740 出头。中间 76 天。按天数一遍:最猛的是 6 月 23 日 70 条(去掉合并提交还有 55 条),其次 6 月 20 日 43 条、7 月 14 日 40 条,其间 43 次合并,以及十几次明确标着 revert 的回滚。产物也不止一个 iOS App:还有 Cloudflare 上的三套服务、一个排序旁车、东京 VPS 上的公众号中继、一套网页书架、一个给别的代理调用的接口服务。
这堆数字容易被读成「AI 让人写得快」。它的真实含义是另一件事——当写代码不再是瓶颈,瓶颈会往上游跑。
并行的前提是隔离
提交历史里反复出现 Merge branch 'worktree-swift-jingling-pike',十几次。worktree(工作树)是 git 的一个功能:同一个仓库同时签出到好几个目录,各占一个分支,互不打扰。因为同时开着好几个 AI 会话,共用一个目录就会互相踩脚——一个刚写完文件,另一个的编译还在读旧版本。
并行不是「多请几个人」,是「给每个人一张自己的桌子」。桌子不够,人越多越慢。
代码便宜了,规格反而贵了
740 条提交里有一套反复出现的节奏:docs(spec) 写清楚要什么、边界在哪 → docs(plan) 拆成一串带验收条件的任务 → 一串 feat,每个任务先写一个必然失败的测试,再写代码让它变绿(这就是 TDD)→ 评审打出的 fix → docs(state) 收口。仓库里躺着 27 份 spec、30 份计划与任务报告、41 条 docs(state);算力计费、设备配对登录、持久编辑队列、接受分享、语音指令、追问、提示词注册表、邀请奖励、分享码,九个大功能全走完这套。
代码写得越快,不是越该「直接开干、错了再改」吗?恰恰相反:想清楚的成本没变,把它变成代码的成本掉到了零头,规格于是成了整条流水线上最贵的一段。
以前打字慢,含混的想法在敲键盘的过程里会被自己发现。现在打字不要钱了,含混会被原封不动地铸成产品——而且长得很像对的:变量名整齐、注释完整、测试全绿。
洞在计划里,不在代码里
最值钱的一条发现出现在 7 月 13 日。计划文档里照惯例给了一段「参考实现」,约束本身没错:标签不超过 40 字,整棵树不超过 200 个节点。然后对抗性评审上场——它的任务不是确认代码能跑,是想办法把它弄坏。三轮对抗评审连着打穿六处,挑三处说:
- 标签长度量的是去掉空格后的串,
「 」.repeat(10000)+「A」就绕过了 40 字上限; - 某类节点挂的子节点既不校验也不计数,
{ref:「sys_cartoon」, children:[500 条垃圾]}整包混进存储; - 传一个空对象
children:{},遍历它的循环直接抛异常,本该返回 400(「你请求错了」)的接口变成 500(「我崩了」)。
关键在于这几刀砍中的位置。三条修复提交的正文写着同一句自白:
这些洞是计划本身写错的(实现者是逐字照抄的)。
实现没走样。你的实现者会逐字照抄你的计划,包括你计划里的错。人照着一份有毛病的设计写代码,往往写着写着「觉得哪里怪怪的」就顺手改了;代理不会。
由此写死进计划两条规矩。校验器必须是全函数——对任何输入都得给出确定答复,「过」或「不过」,不能自己先炸;它一崩,400 就变成 500,而 500 意味着这个洞可以被利用。输出是要落盘的,必须永远满足自己的校验器——恢复默认值居然能产出一份连自己都会拒收的文档。
还有一个动作值得单说:每次修完代码都回头改计划文档,理由只有五个字——「免得再被照抄一遍」。评审的火力要往上游移:审代码只救这一次,审计划才救下一次。
测试从「保险」变成「契约」
CLAUDE.md 里有条铁律,6 月 24 日加的:任何改动前后各跑一遍单测。iOS 第一个测试 target 是 7 月才建的,从 0 涨到 125,如今 214。一批「决策逻辑」被刻意抠成纯函数再钉死:何时推荐升档订阅、何时弹评分请求、要不要问署名、走哪条线路、社区搜索怎么过滤。测的是行为契约,不是实现细节——实现细节明天就会被某个代理重写。
服务端的规矩叫「向下兼容即契约」:永远伺候老数据和老客户端,老契约钉绿才推新版本。最有仪式感的一条是删掉一段遗留代码之前,先删它的测试。顺序是关键:先删测试,你必须写下「我知道我在放弃这种老数据」;反过来就成了「被测试挡了路,把挡路的搬开」。同样两行删除,一个是决策,一个是事故。
「有响应」不等于「能用」
语音改稿换了听写后端,两次上线都通过了冒烟测试(最粗的一层验证:点着火看冒不冒烟),两次都完全不工作,用户反馈是「说话和没说一样」。根因是中转服务把二进制音频帧隐式转成了字符串,每帧都变成 「[object Blob]」 这十三个字符。而冒烟测试之所以说「通过」,是因为它只发配置帧、只检查「有没有字节回来」——回来的正是对这十三个垃圾字符的回应。合格的冒烟测试必须流真实数据、断言解码出来的结果。
当外部世界给你限流
苹果给每个 App 大约 20 次/24 小时的上传额度,高强度迭代日里「每次 push 就发一版」直接烧光,CI 成片返回 409。修法不是加重试,而是把默认值翻过来:默认只跑不签名的编译校验,提交信息带 [tf] 才真打包上传——整个历史里带这标记的只有 51 条。但这条闸只活了 24 天。8 月 2 日它被撤销了——恢复每次推送都自动发 TestFlight,理由写在提交里:苹果那约 20 包/24 小时的限额当然还在,撞上 409 就是额度到了,等窗口刷新就好,但不要为此把默认值改成「默认不发布」。这个反转比原来那条方法论更值得记:限流是外部世界的成本,而「每次推送都能出包」是你自己的节奏——宁可偶尔吃一个 409,也不要让节奏迁就限额。唯一没变的那句是:CI 绿不等于出包。另一条纪律踩了两次才写:某个版本号一过审,那趟「列车」就永久关闭,同号构建再也传不上去;第二次当天就加了守卫——构建时查在售版本,工程版本没高过它就就地加一。
一个新的失效模式:上下文污染
写书引擎接某家模型服务时,请求被风控判成「高风险」直接拒绝。二分了整整一天才定位到元凶:编程助手会自动往上下文里注入项目记忆笔记,而那个目录的笔记里恰好带着某本书的内容——风控读到的是那段内容,不是你写的提示词。它跟文件名、目录名统统无关:搜代码搜不到,看日志看不到。修法是让这条腿跑在一个独立的零记忆工作目录里,每次开跑前清空。
代理的输入不只是你传的参数,还包括系统提示词、项目记忆、工具返回值、上一轮对话。「我传了什么」和「它收到了什么」第一次成了两件事。
人在哪儿
这不是「AI 全自动」的故事。「用户当场指出」「用户拍板」「用户要求还原」是提交历史里的高频词,而且每次都落在关键位置:
- 写书功能拿一个身份查询接口当门槛,用户一眼看穿:那个接口只做归因,不是门槛——匿名令牌是客户端自造的随机数。三个补丁之后是最朴素的答案:收钱。新账号送 200 算力,一本书 320,伪造账号天然被余额挡住。
- 实时预览做了四版,用户一句「引入多个难预料的 bug」,当天全部下线。
- 线路选择做完六条单测的竞速探测,用户说不如「判定简单可预期」,一周后整体删除(详见「减法」那章)。能查的东西不要测。
- 拍照按钮一天改五版,用户说「越改越不好」,还原到昨天。
共同点:全都不是「代码写错了」,而是「这个方向本身不对」。代理能把任何方向执行得又快又漂亮,包括错的那个。人的角色于是从「写代码的」变成「定义什么是对的」。
给未来的代理留账
长期文档里,一条从来没真跑通过的路被诚实写成「待验证」,后来第一次拿两台真机跑当场挂了两次——所有「待验证」本质上都是「已知会坏,只是还没坏给你看」。一次「合并 → 回滚 → 一模一样重做」的曲折被专门写进文档,理由是「免得 git log 误导后人」。有条提交信息里专设一节叫 Honest scope (not faked):照着设计稿做,但不照着设计稿撒谎。一次整体回滚里还写明「一并撤掉的还有一个独立的真 bug 修复」。留账是这套工作方式的必要成本,不是美德——收益在三周后,下一个会话不管是人还是代理,读到的是「为什么」。
真正变了的三件事
76 天七百多次提交,速度是这故事里最不重要的部分。真正变了的是三件:规格成了最贵的东西——「你想清楚了没有」成了唯一的瓶颈;评审的火力要往上游打——实现者会逐字照抄你的计划,包括错的那部分;人必须留在「什么是对的」这个位置上——方向、取舍、什么时候该停、什么该整体删掉,没有一条能从提交数里读出来,也没有一条能外包。