弹劾的自动化

——单元测试是自带弓箭手的靶子

本系列有两句相隔多篇的话,摆在一起会更好地认出彼此。《可以被弹劾的结论》给形式逻辑的最终评价是:它不生产正确的结论,它生产可以被证明错误的结论。《断言与劝诫》给机器规矩定的标准是:只有会失败的那部分算数。前一句说的是三段论,后一句说的是单元测试——但它们几乎是同一句话。三段论的价值,是产出可以被弹劾的结论;单元测试的价值,是产出可以被判不通过的断言。

同一个灵魂

两者共享的不是修辞,是同一个波普尔式的原理:价值不在保证正确,而在让错误可被检出。 迪杰斯特拉说测试只能证明缺陷存在、不能证明缺陷不存在,听起来像贬低,其实和“生产可以被证明错误的结论”一样,是能给出的最高评价——我们从来没有办法保证任何一个判断是对的,我们只有办法让错的判断死得快一点。往下逐项对,密度会说明问题。

都是转换器。 心证是一个不透明的整体,“这个案子该原告赢”无法反驳,只能用立场去撞;三段论把它拆成大前提、小前提、涵摄,每一步变成可单独瞄准的靶点。“程序能跑”同样是一个不透明的整体,无法反驳,只能用“我这儿跑不了”去撞;测试套件把它拆成一条条可单独失败的断言。两边做的是同一件事:把私人的、整体的信心,转换成公共的、分段可攻击的对象。

都有考场前移效应。 知道推理要摊开受检的法官,写不出来的结论在心证半路就被掐灭;知道代码要过测试的程序员,测不了的设计根本写不出来。测试驱动开发的真实价值从来不是多抓几个 bug,是反向塑造设计——一个函数为了可测试,被迫把依赖摊开、把副作用剥离、把边界写清。《可以被弹劾的结论》里那句话原样适用:包装纸如果足够难穿,它就会反过来决定什么东西被生产出来。

退化形态也互为镜像。 不可弹劾的判断不算推理,只算神谕;没有测试的“能跑”不算正确性主张,只算一句无从审查的口头担保。而永不失败的测试——断言恒真的、mock 到失真的——就是《断言与劝诫》里的训示期间和“应积极配合”条款:绿灯常亮,含义为零。最阴险的镜像是最后一对:大语言模型那条事后编造的流畅推理链,等于一套测的是替身而非真实代码路径的测试——全绿,但箭射向的是假阵地。两边违反的是同一条前提:靶子必须是真的。

回归测试从哪里来

朴素的想象里,测试是照着规格说明预先写好的:先定义正确行为,再写代码去满足它。实际项目里最有分量的那部分测试不是这么来的——它们是从失败里长出来的。

标准流程只有四步。出了一个 bug,第一件事不是修,是先写一条能复现它的测试:跑一遍,看它红——红灯证明你真的抓住了它,而不是在修一个想象中的问题。第二步修复,第三步跑到绿。第四步最关键:这条测试不删,永久进入套件。从此每一次提交,无论改动的是天南地北哪一行代码,都要重新通过它——这个缺陷不可能再悄悄回来。回归测试的“回归”,指的就是防止已修复的缺陷回归。

如此积累出的套件,性质和照规格预写的测试完全不同:它编码的是否定性知识——这个系统曾经以哪些具体方式坏过。每条回归测试背后都有一次真实的故障:某个深夜的线上事故、某个边界条件的翻车、某处两个模块交界的误会。规格说明记的是系统应该做什么,回归套件记的是它实际错过什么,而后者是花了真实代价换来的。

这也决定了动它的纪律。想修改或删除一条既有测试,举证责任在动手的人:说清当初这条断言为什么错了或过时了。一条看起来多余的测试,往往携带着当初作者见过、而你现在看不见的故障信息——弄清它为什么存在之前,不要动它。

所以一个运转良好的回归套件,就是这个代码库全部失败经验的沉淀。调试的经验死于调试者的离职,除非它被写成了回归测试;一个只修 bug 不留测试的团队,每一代人都得重新踩一遍同样的坑。

靶子与弓箭手

对到这里要停一下,因为有一处关键的不同。一个可弹劾的结论是一个靶子,立在那里等人来射——对方律师、二审法院、学术批评,弓箭手全是外部的、要预约的:上诉有期限,改判要排期,批评来得更慢。一条断言不等任何人:它自己执行自己的弹劾,每次构建都开庭,失败即宣判,宣判即挡路。单元测试不是靶子的类似物,它是靶子加弓箭手合一。

那么问题就变得很具体:法律工作的成品上,能不能也装上自动开弓的弓箭手?能,分两档。

第一档,确定性断言——机械可判定,红灯即证明。 一份法律分析只要落成结构化文本,至少四类检查可以直接写成代码,《断言与劝诫》论证过升格的原理,这里把清单摆全:

这一档的纪律与工程完全相同:失败要响,失败要挡路——红灯不清零,成品不出门。

第二档,半断言式检查——不可机械判定,但可以概率地检。 涵摄对不对、定性准不准,没有算法可裁定;但未必只能退回预约制的人工审查。办法是造一个独立的机器评审者:换一个模型,给它干净的上下文——不让它读作者的推理,只给原始材料——让它独立重走一遍,再与原结论比对。不一致本身就是信号:它不告诉你谁错了,但告诉你该往哪看。这样产出的也是断言:自动执行、失败会响,只是不再绝对。要让它真正可用,有三条配套纪律。

其一,改失败语义。确定性断言的红灯是证明——引文确实不在卷宗里,这是被证实的事实;半断言的红灯只是指控,还需要再裁决。闸门相应改写:红灯不直接挡路,挡路的是“指控未经处理”。

其二,先校准再上岗。往成品里播种已知缺陷,数这个评审者抓回多少、冤枉多少——量出误差棒之前,它的产出只是一家之言;量出之后,它是一台误差率已知的仪器。法律对这种东西有现成的制度位置:鉴定意见——可采信、不当然拘束,证明力由裁判者衡量。机器评审的红灯按鉴定意见处置,不按判决处置。

其三,守住独立性。半断言的可靠度,上限不在算力,在评审者之间真实的独立程度。干净上下文隔离的是个案污染——读过作者推理的评审者,射的是《可以被弹劾的结论》里那个假阵地;换模型族稀释的是系统性偏置——但稀释不净:不同的模型共享大体同源的语料,多请几个评审压得掉随机误差,压不掉共同的成见。三个评审一致放行,可能只是三个读者戴着同一副眼镜。

两档之外剩下的——涵摄的最终当否、定性的最终取舍——归人。由此得到一根诚实的进度条:法律推理工程化的进度,不该用“AI 替代了多少法官”度量,该用弹劾被自动化的比例度量,计分按三档:确定性断言算满分,校准过的指控算半分,没有误差棒的意见不算分。

缺席的标准答案

最深的一处不对称,留在最后。

单元测试的完整形态是:给定输入,断言期望输出。sort([3,1,2]) 应当等于 [1,2,3]——这条断言写得出来,是因为有人事先知道正确答案。测试预设一个答案源,工程里管它叫 oracle:不管由谁提供——规格说明、数学定义、参照实现——总得有个地方能查到“对的输出长什么样”。

法律没有这个东西。一个案件的“正确判决”不存在可独立查询的答案源:二审不是标准答案,只是另一个法官——改判是又一次判断,不是对表。上诉审能构成 review,构成不了 oracle。终审判决之所以“正确”,用工程的话说,不是因为它通过了测试,是因为它是最后一次运行——没有下一级来断言它错。

这解释了一件否则很奇怪的事:为什么上一节第一档里列出的检查,验的全是结构而不是结果——引文在不在、要件齐不齐、语气配不配得上依据,没有一条是“原告应当胜诉”。用测试的分类学说,这不是样例测试,是类型检查和性质测试:断言的是不变量——每个主张有出处、每个要件有着落——而不是输出值。法律能自动化的弹劾至今全部属于这一类,不是因为没人想测结果,是因为结果那一栏,没有答案可填。

只有一个办法能凑出答案:把时间倒过来。已生效的判决可以当作标准答案——给定它认定的事实与援引的规范,重建推理,与法院实际的裁判对答案。这时 oracle 存在了,法律推理第一次拥有名副其实的输入-输出测试。但要看清它测的是什么。它测不出“这个判决对不对”——那仍然无人能断言;它测得出“这台推理机器像不像法院”。《AI 解决分歧》说过,模拟器的任务不是比法官更好,是像法官——用测试的语言重说一遍:拿判决当 oracle,测得出像不像,测不出该不该。oracle 在这里不是正确性的来源,是分布的来源。

尾声

现在可以把两句相隔七篇的话合成一句了。

三段论是让判断可测试的格式——法律几百年前就发明了它;单元测试是弹劾权的自动化——法律至今只在可判定的边角拥有它。工程能做的,是把这块边角一寸寸扩大:每多一条会红的断言,就少一处只能靠许愿的地方。工程不能做的,是给“该不该”那一栏填上答案——那一栏的空白不是技术债,是分工,《断点》已经把它划给了谁。

《可以被弹劾的结论》的结尾说:宁要一个会被推翻的判决,不要一个无法审查的正确。现在可以补上下半句——宁要一条会红的断言,不要一份流畅的自查报告。 两句话,一句是法律在几百年前做出的选择,一句是工程在几十年前做出的选择。它们是同一个选择。