一张有答案的考卷

——没红过的测试不可信

工程可以靠单元测试验证失败,因为每条断言背后都预设着标准答案;法律却没有严格的对与错,没有什么标准答案。所以法律能自动化的检查,往往全是结构性的,而不是结果性的。顺着这个推理走,很容易得出一个结论:带答案的测试与法律无缘。然而实际上,我们还是可以考虑用近似和拟制的方法来做——先来看工程上是怎么做测试的。

测试的解剖

一条测试的标准结构是三段:布置—执行—断言。布置:把输入和环境摆好;执行:调用被测的东西;断言:拿实际输出和期望比对。行为驱动的写法换了个说法——Given(给定什么前提)、When(发生什么动作)、Then(应当看到什么)——骨架是同一副。两条纪律贯穿始终:一条测试只问一个问题,失败时的报错要能直接告诉人坏在哪。

用例从哪里来?先写一条正常路径——最典型的输入、最该对的输出。然后是边界:空的、零个的、恰好一个的、恰好装满的、超出一个的——缺陷扎堆的地方从来不在腹地,在边境线上。然后是等价类:输入有无穷多种,但程序应当同样对待的输入属于同一类,每类取一个代表就够——测试的艺术一半在于划类划得准。最后是错误路径:非法输入进来,程序应当明确拒绝,而不是沉默地吞下去算出个东西——“报错也是一种正确行为”,而且是最容易漏测的一种。

还有一条经验法则,重要到值得单列:没红过的测试不可信。 一条从写出来就一直绿的测试,可能断言写错了,可能测的是替身,可能根本没跑到目标代码——它的绿没有含义。所以测试驱动开发坚持先写测试看它红:红灯是测试自己的资格考试,证明它真的抓得住它声称要抓的东西。

答案从哪里来

三段式里最难的从来是最后一段:断言要有期望值,期望值就是答案。按答案的来源摆开,常用方法排成一道谱——对答案的要求逐级降低,换来的覆盖面逐级扩大。

手算的答案。 样例测试:sort([3,1,2]) 应当等于 [1,2,3],期望值是人算出来的。绝大多数测试属于这一类,它的极限也明显——人算得动的输入才测得了。

存档的答案。 输出庞大到手算不动时——一张渲染出的报表、一份生成的文书——办法是把某次人工确认过的输出存档,此后每次运行和存档 diff,不同即红。这叫金标准测试或快照测试。它的风险是把错误也一并存进档:“和上次一样”不等于“对”,档案本身要定期有人重新过目。

对拍的答案。 两个独立实现互为答案:一个慢而显然正确的参照实现,对一个快而复杂的优化实现,随机灌同样的输入,断言输出一致。这叫差分测试。没有谁手里有真理,但两个独立来源的一致是强证据,不一致是明确的报警。

用性质代替答案。 不知道某个输入的期望输出,也可以断言输出必须满足的不变量:排序的结果必须有序、必须是输入的重排——两条性质圈住了“正确”,一个具体答案都不用给。然后随机生成成千上万个输入去轰它。性质测试的覆盖面是样例测试的几个数量级之上,代价是想出好性质比想出好样例难。

不要答案。 谱的末端是模糊测试:灌海量畸形输入,只断言一件事——不崩溃。这是最弱的断言,却抓得住谁也想不到去测的角落。

给合同审查出题

现在回到法律。法律没有天然的 oracle,这个结论不必收回;但工程对此的回应不是认命,是拟制。法律人对这门手艺不该陌生:宣告死亡、视为送达——明知未必为真,却郑重当真来用的制度装置。给法律场景设置起标准答案作用的东西,走的是同一条路:答案不是找到的,是立起来的——要紧的只是记住它是拟制的,并看清它在哪一段撑得住。

上一篇结尾已经这么做过一次:把已生效的判决拟制为标准答案,测推理机器像不像法官。本篇做第二次,场景选在合同审查——选这里,是因为拟制在这里成色最足,有两个原因。

第一,问题形态合适。合同审查回答的不是“该判谁赢”,是“这份文本里有哪些坑”——缺陷识别,不是价值裁断。资深律师面对一条无上限的连带责任条款、一项单方任意解除权、一处约到对方主场的管辖,结论几乎不会分歧。专家在这类问题上的一致性,远高于他们在裁判结果上的一致性——一致性够高,金标准就立得住。

第二,真题充沛。合同是法律实务里流量最大的文本:每家律所、每个法务部手里都躺着大量审过的合同——原文、批注、来回修改的版本俱在。这不是需要凭空出题的场景,是真题堆到天花板、只欠把答案立起来的场景。

于是考卷的做法就清楚了:真实合同脱敏入库,由资深律师标注金标准——每处问题的位置、性质、严重级别。标注的对象就是实务里最熟的那些坑:无上限连带担保、单方任意解除权、违约金畸高、管辖异地、付款节点与交付义务错位、争议解决条款自相矛盾、定义条款与正文冲突……标注有分歧的条款经合议定稿,定不下来的剔除出卷——考卷只收专家收敛的题。历史批注是现成的起点:当年审查时标了什么、最终改成了什么,本身就是一份等待整理的答案底稿。

评分规则要认真设计,因为合同审查的错误成本是不对称的:漏掉一个致命条款,和多标三条无关痛痒的“风险提示”,不是同一量级的错。所以缺陷分级——致命、重要、提示——查全率按级加权,漏检致命项重罚;同时冤枉正常条款也要扣分:满篇皆红的审查报告等于没有审查,它把发现的成本原样转嫁回了读它的人。

一卷两用

这张考卷最值钱的地方,是同一张卷子测得了两样东西。

测规则。 审查清单——律所代代相传的那种 checklist——本身是一套规则,而规则的有效性从来靠资历背书:“这是某某合伙人定的版本。”现在可以量:这套清单对这张考卷抓回多少、漏掉的集中在哪类缺陷。修订清单之后重跑同一张卷,就是规则的回归测试——改进有数字,退步也藏不住。清单从祖传秘方变成可以迭代的工程对象。

测模型。 换一个模型、改一版提示词、升一次版本,同一张卷跑下去,能力从形容词变成数字:“新版本加权查全率从八成二到九成一,致命项漏检清零”和“感觉更聪明了”,是两个时代的说法。模型选型和提示词迭代,第一次有了可比的依据;版本升级也第一次有了硬性门槛——新版本在考卷上不得低于旧版本,否则不上线。

两个用途还可以相乘:同一张卷,横轴换模型,纵轴换规则,哪一格的组合最强,一目了然。

考卷的边界

最后照例说清楚这办法兜不住什么,否则又是一门新迷信。

考卷会被背熟。 指标一旦成为目标,就不再是好指标。题库如果进了训练语料,或者提示词被对着题库反复调优,分数就开始失真——测出的不再是能力,是记忆。对策和考试制度一样古老:保留一套不公开的盲卷,题库定期轮换,新的缺陷类型持续入库。

答案是判断的存档,不是真理。 金标准由专家标注,它继承标注者的立场和盲区。它测得出“漏没漏”,测不出“该不该改”——一条风险要不要为了促成交易吃下去,是生意判断,不在考卷的射程内,也不该在。

这条路不止一张考卷。 合同审查不是法律里天然长着答案的例外——这里的答案是专家合议拟制出来的,同样的手艺可以搬去别处。判决拟制为标准答案是一次,这张考卷是一次;往后每找到一处专家足够收敛的场景,就能再立一套。法律推理的大部分依旧在无 oracle 的旷野里,但旷野不是判词,是待办清单:凡是拟制立得起来的地方,先出卷,再上岗。

因为道理在两个行当里是同一条:没红过的测试不可信,没考过卷的审查者也一样——你不知道它的绿灯,究竟是本事,还是没遇上事。