返回文章

实现阶段:一次只做一个任务,每个任务留一个真提交

这是五阶段系列的第二篇。母篇《从能跑到可交付:一套前端 AI 协作系统的设计笔记 》讲了整套系统的形状,这个系列逐个阶段拆开。上一篇(澄清阶段 )的产出是一份契约:一组带编号的需求、一份任务清单、一份评分表。这一篇讲契约签完之后,代码是怎么被写出来的——以及为什么写代码这个环节,要按"干活的人说的话不能直接采信"来设计。我用的系统是开源的 frontend-harness-cli 。

一个不存在的提交

先看一个具体的场景。

你让 AI 做一个任务。它干了一阵,回来报告:任务完成,提交编号 a1b2c3d。你打开仓库查这个提交——不存在。代码确实改了,新文件、改动都在,但全躺在工作区里,一次提交都没有。那个编号是它编的。

它不一定在"撒谎"。报告的格式里有一栏要填提交编号,而生成一串看起来像模像样的十六进制字符,对它来说几乎没有成本。填空题总要填满,填的时候对不对,是另一回事。

实现阶段的整套设计,就建立在这一条认识上:执行者的自述是主张,不是证据。 它说做完了不算,它说提交了也不算。每一步都要落到一个能被程序查验的事实上。后面几条看起来有点"不近人情"的规则——看不见考题、一次只许提交一个任务、提交编号要验——都是从这一条推出来的。

干活的人为什么看不见考题

澄清阶段结束时,这个功能的一切都定好了:prd.md 里带编号的需求、tasks.json 里的任务清单、rubric.json 里的评分表。进入实现阶段,内核会给干活的角色——开发者(developer)——生成一份任务包,在一个全新上下文里开工。

这份任务包里有计划、有任务清单、有项目规范、有这一阶段要用的技能(implement-task)。唯独没有 rubric。

这是故意的。评分表是验收的量具:哪条需求对应哪项检查、什么是硬伤、什么会触发一票否决,全在里面。如果写代码的人提前看过量具,它写代码的目标就会悄悄换掉:从"把需求做对",变成"让检查通过"。这两件事在简单功能上差不多,在复杂功能上差很多——检查只能测它写明的那几项,一个盯着检查写代码的人,会精准地满足那几项,把没写进检查的真实需求漏掉。更直接的例子:检查里写"某目录下应有某文件",它就把文件建出来,内容对不对再说;检查里 grep 某个函数名,它保证函数名在,实现可以是空的。

这不是品德问题,是激励问题。人也一样:提前拿到考卷的学生,复习的是考卷,不是课程。所以这套系统把可见性当权限来管:谁需要什么才给什么。开发者按计划干活,不对着考题干活;评分表只出现在评测阶段,只出现在评判者的任务包里。

任务循环:做一个,验一个,提交一个

实现阶段的节奏被压到最小粒度:一次只做一个任务。

任务的粒度在澄清阶段就定好了,tasks.json 里一条一条列着。开发者的循环是固定的四步:

  1. 拿当前这一个任务,按 implement-task 技能把它实现;
  2. 跑这个任务的检查——项目规范要求每个任务之后 typecheck 和 lint 必须通过(这是语言层那一篇讲过的任务级门禁),不过就当场修,不许攒到后面;
  3. 做一次提交,只包含这个任务的改动;
  4. 把提交编号报给内核。内核验过(下一节细说),才把这个任务记为完成,循环才进入下一个。

一轮循环里不许顺手把下一个任务也做了,不许把三处不相关的改动揉进一个提交。听起来慢,实际算的是总账:粒度小,任何一步出问题,定位和重做的范围都被锁在"一个任务"里。粒度大,省下的是表面上的往返次数,赔进去的是出事时的排查范围。

为什么必须一次一个提交

一次一个任务、一个任务一次提交,这条规则的用途是让每一次提交都成为一个可验证的证据单元,同时也是一个回滚点。

对比另一种常见长相:AI 闷头干一下午,最后来一个两千行的大提交,十几个文件,什么都有。然后评测说"REQ-3 没做对"。问题来了:这两千行里,哪几十行是 REQ-3 的?没人说得清。评测的发现对不上改动,修复就变成在两千行里大海捞针。

一次一个提交时,这笔账是清的:评测说哪个任务有问题,就看那个任务的那一次提交,diff 通常几分钟能读完;要推翻重做,git revert 撤掉的就是这一个任务,不伤别处;要复盘"哪一步开始偏的",提交序列就是时间线。提交在这里不只是版本管理动作,它是这套系统给"做过的事"留的凭证——而且凭证必须一张一张开,不能一沓糊在一起。

为什么提交编号还要被验一遍

回到开头那个场景。开发者报出提交编号之后,内核不做任何推测,直接拿这个编号去问 Git:git rev-parse 能不能解析出这个对象。能,这个任务记为完成;不能,这个任务就没完成,报得再工整也不算。

这条规则不是为了防君子。它防的是报告格式本身的引力:只要表格里有"提交编号"这一栏,模型就有把这一栏填满的倾向,填的内容像不像真的,取决于它当时手边有没有真的。把校验放在内核一侧,事情就变简单了——编号不再是自述的一部分,而是要过验的凭证。验不过,流程停在这里,不往下走,也轮不到评测阶段去替它发现"原来代码根本没提交"这种低级错位。

同一逻辑还管着两件相关的事。一,开发者不许手改 .harness 下的状态文件:它不能把自己的任务状态从"进行中"改成"完成",所有状态变更只能走内核命令。二,它无权宣布功能完成:一个功能算不算做完,是评测和后面的交付门禁说了算,干活的人从头到尾没有给自己盖章的权限。这三条加起来是同一个设计——执行、记录、裁定,分开在三个位置上,没有任何一环能自己证明自己。

卡住时怎么调试

任务循环里检查不过、跑不通,是常态。这一阶段配的调试技能是 systematic-debugging,纪律只有一条:先找根因,再动补丁。

具体到行为上:报错了,先复现,定位是哪一层的问题(类型、数据、环境,还是自己刚写的逻辑),确认根因之后再改。不许做的事也具体:不许对着症状贴补丁——比如把报错的那行包一层 try-catch 让它不响了,或者按语言层那篇讲的,贴个 @ts-ignore 让编译器闭嘴。那类"修法"会让检查变绿,同时让问题转入地下,留到评测甚至上线再炸。

调试也受同一个任务粒度约束:修的是当前任务引入的问题,修完仍然走原来的循环——检查、提交、验编号。不因为赶进度跳过任何一步。

评测打回来时,它只能看到一张清单

实现阶段不是终点。功能进入评测阶段后如果没通过,会被打回来重修。这时开发者拿到的不是评分表,也不是评判者的完整报告,而是一张只列着未通过项的清单:哪条需求没满足、证据是什么、要修什么。修完重新提交,再回评测。

这个安排和"看不见考题"是同一条原则的延续。如果把完整 rubric 发给它,它下一轮就会对着量具逐项打补丁,越修越像应试;只给开放项清单,它修的是"这些地方没达到需求",量具本身始终在它视野之外。修复的纪律(只修清单上的项、不顺手扩大改动)属于评测回路的一部分,下一篇讲评测阶段时会展开。

这一阶段保证什么、不保证什么

边界照例说清。

实现阶段保证的是可追溯:每一份代码改动都对应一个任务、一次真实的提交;谁在什么时候改了什么、为什么改,查得到、撤得回。它不保证正确性——一个任务可以提交得干干净净、编号真实存在,而功能本身就是错的。提交真实,只证明"这件事确实以这个形态发生过",不证明"这件事做对了"。

做没做对,是下一篇评测阶段的事。而评测之所以能评得动,前提正是这一阶段留下的东西:一份它没见过的评分表,和一串能逐项对上账的提交。