验收标准:什么叫“做对了”,必须在写代码之前说清
这是 Harness 系列的 skill 拆解篇,拆的这一份叫 write-acceptance-criteria。澄清阶段的著述链一共五步,前四步产出 PRD、质量愿景、技术方案、开发计划,它是第五步、最后一步,产出的东西下游就是评测阶段
直接拿去打分的评分表。母篇《从能跑到可交付:一套前端 AI 协作系统的设计笔记
》讲整套系统的形状,这个系列逐份拆 skill:为什么有它,具体怎么干。我用的系统是开源的 frontend-harness-cli
,文中所有规则都能在仓库里找到对应实现。
考完试,才出的考卷
先看一个具体的场景。
一个功能做完了,你让 AI"补一份验收标准"。它把代码通读一遍,把代码已经实现的行为一条条改写成标准:“点击提交后应保存成功”——代码就是这么写的;“列表应按时间倒序展示”——代码就是这么排的。验收的时候逐条打勾,每条都过。
这份标准没有一条是假的,但它什么也没验出来。原因不在它偷懒,在时机:标准是从代码里抄出来的,它就只能描述代码已经做到的事。 代码漏掉的,标准里同样漏掉——出题的人从头到尾没见过"本来应该是什么样",只见过"实际做成了什么样"。让学生考完试再出考卷,考卷必然满分,而且必然毫无信息量。
write-acceptance-criteria 存在的全部理由,就是把出题这件事钉在代码还不存在的时候:出题依据只有需求和愿景,考生还没进场。
这一份 skill 产什么
它把 PRD 和质量愿景变成两个文件:
rubric.json——评分表:评什么、怎么评、每条检查的级别和证据要求;e2e-cases.json——端到端用例集:哪些用户旅程必须真能走通。
前置门禁很硬:prd.md、vision.md、tech-design.md、tasks.json 必须都已存在,摘要已经人确认过。少任何一份,这一步不开工——道理很直白:需求没定,考卷没有依据;愿景没定,品质类条目没有锚点;任务没拆完,你根本不知道要考的范围有多大。
产出之后,这两份文件就是评测阶段的全部依据:评判者拿着 rubric 逐项打分,拿着 e2e-cases 逐条跑旅程。它们写得含糊,评测就只能含糊。所以这一步的功夫,全花在"把每条检查写到可执行、可复核"上。
rubric 的拼法:先抄需求,再铺四层
构建顺序是写死的,一共七个动作:
第一步,抄需求。 把 PRD 里的 REQ-n 编号和需求句子原样抄进 rubric 的 requirements。是抄,不是改写——改一个词,需求就漂移一分,评测时对着漂移后的版本打分,等于换了考题。
第二步,铺 L1(正确性)。 每条需求至少派生一个条目,其中至少一个标 essential——这一条得 0,整个功能判失败(否决规则见评测篇)。选条目类型时优先用能跑真实测试的 command。
第三步,铺 L2(健壮性)。 条目来自 PRD 里写明的异常与边界路径,加上技术方案里的错误处理设计;skill 特别点名两类最容易漏的失败:时序类和权限类。
第四步,铺 L3(UI)。 条目来自愿景的设计意图和关键页面。愿景里每个设计参照(REF-n)至少派生一个 important 或 essential 级别的 judge 条目,并把 "anchor": "REF-n" 写进条目——锚点的意义下一节说。
第五步,铺 L4(交互)。 条目来自愿景的交互范式,逐条对着一份交互启发式清单(.harness/docs/evaluation-heuristics.md 第 2 节)核,不许凭感觉想。
第六步,铺陷阱。 风险清单里每条 RL-n,至少变成一个 pitfall 条目,或者一个锚定它的 essential judge 条目;愿景里的每个反模式(ANTI-n)同样值得一个 pitfall。为什么要单列一节,后面细说。
第七步,交叉核对三条覆盖线:
- 每个 REQ → 至少一个 L1 essential;
- 每个设计意图 → 至少一个 L3/L4 条目;
- 每条 RL → 一个 pitfall,或一个锚定它的 essential judge。
三条线任何一条断了,rubric 就没写完。如果用户要求生产级,还要在 policy 里设配额(比如每层 essential 和 pitfall 的最低数量)。
五种条目:能跑的跑,能查的查,剩下的才轮到判断
rubric 的条目分五种类型,分工是按"判定方式有多硬"排的:
| 类型 | 判什么 | 判定方式 |
|---|---|---|
command | 测试、Playwright spec、脚本 | 真跑一遍,退出码 0 = 满足 |
file | 交付物存在且不是空壳 | 看 path,文件大小不低于 minBytes |
grep | 某个模式必须出现、或绝不能出现 | 在 path 里查 pattern,expect 写明 present 还是 absent |
scan | 某个传感器维度达标 | dimension 的分数不低于 minScore |
judge | 只有评判者看得见的东西 | 凭证据判断,必须带锚点 |
前四种都是机器判:退出码、字节数、模式命中、维度分数,没有商量余地。只有第五种留给判断——页面布局合不合设计意图、空状态文案说没说清下一步,这类事没有退出码可看。但 judge 条目不是"评判者随意发挥"的许可证,它有两个硬约束:
一是必须带锚点。anchor 字段写明它锚在哪个既有决定上:愿景的设计参照(REF-n)、反模式(ANTI-n)或风险项(RL-n)。没有锚点的品质判断是口味;有锚点的判断,是在核"实现有没有兑现当初定下的那条意图"——评判者对的是账,不是发表审美意见。完成标准里有一条专门验这个:每个 judge 条目的锚点必须真实存在。
二是必须留证据路径。judge 的证据固定放在 .harness/tasks/<feature>/evaluation/judge-<id>.json,评测时证据写到那里,复盘时能翻出来对。
还有一条对所有条目统一的要求:每个条目都必须带 id、requirement(它考的是哪条需求)、layer(L1–L4)、level(essential / important / optional / pitfall),以及一条 description——写明可观察的行为和阈值。不许写"登录功能正常"这种条目;要写成"错误密码提交后停留在登录页并显示错误提示"这种看得见、数得出的陈述。条目写得不可观察,评测那一关就必然退化成"看一眼觉得没问题"。
例外必须留名:豁免与不适用
覆盖线是硬的,但现实里确实有例外:一条纯后端逻辑的需求,没有任何品质面可评;一个没有界面的功能,L3 整个不适用。skill 对例外的处理不是"悄悄跳过",是留名:
- 某条需求确实没有 judge 条目可写 → 在
qualityWaivers里给它一条豁免,写清理由; - 某一层整体不适用 → 在
policy.layerNA.<L>里写清理由; - E2E 某一类不适用(下一节)→ 写进
e2eCategories.na并给理由。
三处的规矩是同一条:豁免可以,理由必须落盘;绝不允许悄悄降低最低要求。 为什么对"悄悄"这么敏感?因为降低标准这件事一旦不用留名,它就会发生在最方便的时候——赶时间的时候、过不了关的时候。留了名,豁免就是一笔可审计的决定,复盘时看得见是谁、为什么放了这一条;不留名,它和把考卷改到迁就答案没有区别。
E2E:六类旅程,各有下限
e2e-cases.json 是用例数组,每条用例带 id、category、mapsTo(它验的是哪条 REQ)、route、steps(一步步的操作)和 expect(每步该看到什么)。六个类别各有最低数量:
| 类别 | 最少 | 证明什么 |
|---|---|---|
| core | 2 | 主旅程端到端能走通 |
| negative | 2 | 错误输入、空数据、无权限、网络失败时不塌 |
| boundary | 1 | 大数据量、极端输入、分页边界、并发 |
| crossFeature | 1 | 在这里做的操作,在该出现的地方真的出现了 |
| persistence | 1 | 刷新、重开之后数据还在 |
| uiJourney | 1 | 加载、空态、反馈、响应式这些界面状态 |
两个细节值得单独说。
第一,negative 的下限和 core 一样是 2,而且两类都至少要有一条 blocking: true(不过就拦住交付)。这不是随手定的比例:AI 写用例和写代码有同一个毛病——默认只走主线。主旅程两条、失败路径也两条,是用配额把"坏情况也要验"钉成和"好情况"同等的位置。
第二,每条 uiJourney 用例的 route 必须已经在 pages.json 里;不在,就由 planner 当场补进 pages.json(那本来就是 planner 维护的文件)。用例引用了一个计划里不存在的页面,说明要么计划漏了页面、要么用例写飘了——无论哪种,都得在澄清阶段当场对齐,不能留到评测时才发现对不上。
三个设计细节:措辞、陷阱、只许收紧
细节一:考卷的措辞,会反过来塑造考生。 Anthropic 在他们的 Harness 实践里有一个发现:评分标准的措辞会影响生成者的产出方向。这不难理解——这套系统里,需求句子是从 PRD 原样抄进 rubric 的,而 PRD 同时是开发者干活的依据;一条需求写成"列表要快"还是一条 essential 写成"首屏列表渲染不超过某个阈值、附命令证据",生成者瞄准的东西完全不同。所以这一步反复强调 description 要写可观察行为和阈值:措辞含糊,含糊就会顺着链条一路传到代码里,最后在评测时变成扯皮。
细节二:陷阱要事先点名,不许事后总结。 pitfall 条目的判定方式是反着的:缺陷不存在,这条才算通过。 为什么要在代码还没写的时候,就把死法一条条列出来?因为事后能想到的塌方,基本都是已经塌过的那一个——评测篇里 @ts-ignore 消音报错就是例子,等它真发生了再补检查,黄花菜都凉了。RL 和 ANTI 是在澄清阶段对着 PRD 与技术方案列的风险和反模式,把它们逐条转成 pitfall,等于把"已知的死法"变成常驻岗哨:每一轮评测都查一遍,而不是出了事才想起来查。覆盖线里"每条 RL 必须有着落",兜的就是这件事不许漏。
细节三:过门禁的路只有一条,而且只许往紧里走。 两份文件写完,运行 harness clarify-done <feature>,它会列出所有缺口。skill 给的修复指令只有一种:按缺口补条目、或把已有条目写得更锋利——绝不许删需求、绝不许降级别来让门禁通过。 理由不用多解释:级别是出题时对重要性的判断,到了过不了关的时候去改这个判断,就是考不过改分数线。后面确实有修订的正门:replan 时,把被接受的改进建议落实到这里(更锋利的描述或阈值、新的验证方式、新条目、新级别),为评判者探索性评测中发现的新问题补 E2E 用例,并把决定记进 replan-decisions.json。但方向有约束——评测篇讲过,评判者提的修订建议只许收紧,放松必须经人批准。出题的人和考生是同一个系统时,“只许收紧"就是防自我宽容的最后一道闸。
它的边界
最后说清这一步保证不了什么。
第一,它保证覆盖,不保证眼光。 三条覆盖线全过、锚点全在、配额全满,只说明每条需求都有人查、每个风险都有岗哨。查得准不准,取决于条目写得锋不锋利,而"锋利"本身没有机器可验——clarify-done 验的是结构。所以系统留了 replan 这条回路:评测中暴露的盲区,回流成新条目和新用例,考卷靠一轮轮实战变准,而不是指望第一次就出完美。
第二,它忠实地放大上游的错误。 REQ 是从 PRD 原样抄的,PRD 写错了需求,这份考卷会一丝不苟地考错的东西,而且考得越严密,错得越自信。需求本身对不对,是著述链前几步的事,这一步只负责"把说好的事变成可查的条目”。
第三,judge 条目终究是判断。 锚点和证据把判断约束在对账的轨道上,但 L3/L4 的品质分最后还是由评判者给出,不是退出码。这套系统对此的态度是把判断关进笼子,而不是假装判断可以取消。
到这里,澄清阶段的著述链完整了:需求、PRD、愿景、技术方案、开发计划、验收标准,该落盘的文件全部落盘,clarify-done 通过,状态机才放行实现阶段。下一组拆实现侧的 skill,第一份是 implement-task——拿到任务包的开发者,具体怎么把一个任务干完、干到可验。