返回文章

评测阶段:打分的人,不能是写代码的人

这是五阶段系列的第三篇。母篇《从能跑到可交付:一套前端 AI 协作系统的设计笔记 》讲了整套系统的形状,这个系列逐个阶段拆开:为什么要有它,具体怎么干。上一篇是实现阶段 :开发者把任务一个个做完、逐个验真提交。这一篇讲实现之后的那道关:谁来判断这些代码到底对不对。 我用的系统是开源的 frontend-harness-cli ,文中所有规则都能在仓库里找到对应实现。

一次满分评测

先讲一次真实的翻车,来自这套系统最早的评测做法。

当时的评测很简单:让模型把刚写完的代码通读一遍,写一份评测结论。结果它给自己的代码打了 100 分。满分。

然后我们把代码真的跑起来:大概只有三成是能用的,端到端测试全灭。

满分和三成之间差的不是判断力,是位置。写下这段代码的上下文里,装着它写每一行时的全部理由——为什么这里要这么处理、为什么那个边界情况"应该没问题"。让它回头评审,它读的不是代码本身,是自己当初的意图。意图看起来总是对的。

为什么写代码的人不能给自己打分

Anthropic 在他们的长周期 Harness 设计里把这件事说得很直接:开箱即用的 Claude 是一个糟糕的 QA——它能看到真正的问题,然后说服自己"没什么大不了",照样批准。

这不是某个模型的性格问题,是位置决定的。生成者和评审者共用一个上下文时,宽容是结构性的:

第一,它知道"本来想写成什么样"。评审代码时,它拿意图当标准,而不是拿需求当标准。意图与代码不一致的地方,在它眼里会自动被意图补齐。

第二,承认错误等于推翻自己刚做的决定。它在实现时为某个取舍找过理由,评审时再判这条取舍错了,等于自相矛盾。顺着原判断说"可以接受",是阻力最小的路。

第三,它没有外部锚点。它判断"这个函数没问题"的依据,是自己对这段代码的理解;而理解错了的地方,恰好就是代码写错的地方。用同一套理解去查同一批错误,查不出来是必然的。

结论只有一条:评测必须换人。而且不能只是"换个提示词让它严格一点"——上下文是同一个,换提示词没用。要换的,是一个全新的上下文。

评测由谁做:一个全新上下文的评判者

评测阶段由**评判者(evaluator)**执行。它和开发者是两个独立的角色,跑在两个互不相通的上下文里。

评判者拿到手的只有三样东西:

  • rubric.json——澄清阶段写好的评分表,每条需求对应哪些检查项,都在里面;
  • 代码——开发者提交的最终状态;
  • 它自己当场采集的证据——下一节细说。

它没有开发者的对话记录,不知道实现过程中有过哪些犹豫和取舍。它对代码的唯一认识来自评分表和证据。这一点是刻意的:它不需要理解你为什么这么写,只需要判断写出来的东西对不对。

还有一条分工纪律:评判者只许报告问题,不许顺手改代码。 检测和修复必须分开,原因在实现阶段那篇已经埋下伏笔,这里说透:评判者一旦动了代码,这次评测就没法复现了——报告说的是哪一版代码?它改完之后,报告里记的问题还存不存在?全都说不清。报告必须对应一个固定的、被提交过的代码状态,修复是下一轮回实现阶段的事。

评分表怎么打分:三种条目

rubric 里的检查项分三种类型,跑法完全不同。

command(命令项):能用命令判的,绝不用判断。 编译、测试、grep、五维扫描(harness scan),都是真跑一遍命令,拿退出码和输出说话。“项目能编译"这条,验法是跑 tsc 看退出码是不是 0,不是让评判者"看一眼觉得能编译”。命令项的特点是没有商量余地:退出码是 1,它就是不过,评判者觉得"差不多"不算数。

judge(判断项):实在需要判断的,只许凭新鲜证据下结论。 有些事命令判不了:空状态文案说没说清下一步、页面布局和设计意图合不合。这类条目由评判者打分,但证据必须是当场重新采集的:用 Playwright 现在打开页面、现在截图;拿真实请求现在探一次接口、看返回。不许凭"读过代码,看起来没问题"给分——那正是满分评测的老路。证据比上一次评测旧的,直接作废:旧截图证明的是旧代码,和眼前这版无关。

manual(人工项):留给人的条目。 有些判断自动化不该碰——比如这个功能的取舍符不符合产品意图。这类条目明确标给人,系统不假装能测。

三种条目之外还有一条统一的证据纪律:每条检查都必须给出证据——具体到哪个文件、哪一行、哪张截图、哪次命令输出。没有证据,就不算通过。 不是"扣分",是直接不成立。“看起来没问题"这句话,在评测报告里被明文禁止出现。

一票否决:总分救不了的两种死法

评分表最后会算一个总分,但有两种情况,总分再高也救不回来。

第一种:essential 条目得 0。 rubric 里标为 essential 的条目,是这个功能的命根子——比如"付款后订单状态正确更新”。假设一个功能总分 92,看起来很优秀,但这条 essential 的证据显示:付完款,订单状态还停在待支付。这条得 0,整个功能直接判失败。92 分描述的是其余条目的平均水准,掩盖不了核心功能根本没通这件事。就像驾照考试总分再高,闯了红灯就是不过——有些错误不参与平均。

第二种:pitfall 条目被触发。 pitfall 是事先写明的作弊与塌方清单,比如为了让类型检查通过而贴 @ts-ignore 消音报错。扫描一旦发现,功能直接判失败,不进入打分。道理和上一条一样:用绕过检查的方式"通过"检查,如果还能靠其他条目的分数平均回来,等于系统在奖励作弊。

所以总分的定位要说清:它描述的是没有被否决的前提下,这批代码的质量梯度——用来比较、用来排修复优先级。它不是生死线。生死线是那两条否决规则。

报告纪律:写完不许改

每份评测报告一旦写完,就不许再改。重新评测,写一份新报告,旧的原样留着。

这条纪律是为了日后能算一笔账:某个功能上线后翻了车,翻出当时的评测报告对照——

  • 如果旧报告里已经预警过这个问题、却没人跟进,那问题出在路由和修复环节:评测是对的,流程没接住;
  • 如果所有旧报告里从来没提过这个问题,那问题出在评测本身:评分表有盲区,该补条目。

没有不可变的报告,这笔账永远算不清:记录一直在被改写,你分不清"是当时没发现"还是"发现了被删掉"。报告不可变,复盘才有证据可依。

不通过之后:只修点名的那些条

评测不通过,内核不会把功能笼统地"打回去重做"。它把功能送回实现阶段,同时附上开放项清单:就是这次评测里没过的那些条目,一条不多。

开发者用 fix-from-report 这份 skill 修复,纪律是只修点名的条。不许顺手重构、不许"顺便优化"清单之外的东西。原因很实际:清单之外的改动没有经过这次评测的检验范围,顺手改的东西出了问题,下一份报告里无从对照——你不知道它是新引入的,还是原来就有的。修复的边界,就是评测的边界。

还有一种特殊情况:评判者认为评分表本身写错了——条目和真实需求对不上。这时它不许自己改 rubric,而是提交"rubric 改进建议"给规划者裁定。而且方向锁死:建议只能收紧,不能放松;任何放松评分标准的修改,必须人来批准。这一条防的是元层面的自我宽容:如果评测方能一边考一边把考题改简单,整个评测迟早会退化成另一种满分评测。

最后,所有回路都有预算上限:评测-修复的往返次数是写死的。次数耗尽还没通过,内核把功能置为暂停(paused),停下来等人决定——是需求本身有问题,还是实现路线要换。绝不让它在一个死胡同里无限重试下去。

评测过了,为什么还要确认

评测通过,功能还不能交付。因为一次通过的证据强度有限:judge 条目的证据是这一次采集的,命令项跑的是这一版的代码——它证明的是"这一次,全都对上了"。一次采样对不对得上运气,只有再换一个人、把证据全部重新生成、再连着过几轮,“稳定"才成立。

那就是下一个阶段的事:确认(verify)。评测解决的是"打分的人必须是外人”,确认解决的是"过一次不算数"。下一篇讲确认阶段具体怎么换人、怎么再生证据、要连过几轮才放行。