判定不能只靠绿灯:变异测试+全量自证,我们怎么给评测门禁上锁
系列:人形机器人大脑体系 · 验证工程专项
前一篇《长稳测试 12h 金标准》讲的是「跑得够久才算数」。这一篇讲一个更早的问题:评测本身错了怎么办? 一套门禁全绿,可能意味着系统没问题,也可能意味着——门禁已经死了,而你不知道。
目录
一、先讲一个场景 1
二、把「检查会失效」变成可以被抓住的事 1
三、「缺失表现为通过」:同一个病的五种长相 1
四、全量自证:三个状态,少一个都不行 1
五、指纹对账:登记表没动,等于没改过 1
六、验证装置的装置,也要被验证 1
七、给后来者的四条可执行纪律 1
一、先讲一个场景
假设你有一套自动化评测,十几条检查规则,每次跑完输出一行「总计 15/15,全部通过」。团队很安心,参数敢改了,代码敢动了。
然后有一天,有人往规则集里加了一条新检查。为了确认新检查本身是有效的,他只跑了一个「专门触发新检查」的坏样本——红了,拦截成功。于是他认为新检查上线了。
三个月后复盘才发现:那次改动顺手重构了旧检查的匹配逻辑,三条老检查从那天起就已经失效。但每次跑评测,结果还是全绿——因为回归测试跑的样本里,没有一个能触发那三条老检查。
子集跑全绿,不等于验收。 这不是哪个工程师粗心,而是这类装置的天然缺陷:你只验证了「新加的东西活着」,没人验证「原有的东西还活着」。
二、把「检查会失效」变成可以被抓住的事
我们的做法是把每条检查规则配上专属的反例样本:一条规则,至少一组「构造出来的坏数据」,跑过去必须红。
这就是变异测试的思路,但用在一个容易被忽略的方向上——不是拿变异去测业务代码,而是拿变异去测评测装置本身。业务代码错了,评测会红;可评测装置自己错了呢?评测永远绿。所以变异的目标是门禁,不是被门禁保护的东西。
每组变异做三件事:
[拦截] 变异描述(构造了什么坏数据、违反哪条约束)
· 自证✓ —— 确认装置的检查路径真的被执行过
· 红 —— 装置抓住了它
· 红集=[规则X] —— 而且抓它的正是预期的那条,不是别的规则误伤
注意「红集」这一行。坏样本被拦下还不够,必须是被对的那条规则拦下。有一次我们加了一条新规则,回归时坏样本也红了,红集却显示是被一条老规则兜住的——新规则根本没触发,只是老规则的判定范围恰好覆盖了同一类错误。这种「看起来生效了」的误判,比不生效更难发现。
三、「缺失表现为通过」:同一个病的五种长相
跑了几个月,我们把踩过的坑归了类。同一类失效出现了五次,长相各不相同,本质是同一个:
「没找到」「没匹配」「没读到」都被当成了「没问题」。

长相 现场

1 字段改名后,检查去找旧字段名,找不到 → 跳过 → 通过 一条规则只覆盖了部分字段,漏改的字段永远不触发
2 判据编号重排后,检查按编号找,匹配不上 → 通过 编号是活的,检查写死了
3 匹配正则写 \d,编号从一位数涨到两位数 → 匹配不到 → 通过 两位数编号出现后所有匹配全部失效,没有任何报错
4 清单里同名键被旧值覆盖 → 新登记等于没登记 追加登记时插在旧条目前面,被静默吃掉
5 统计脚本用正则数自检条数,数到 0 → 还是打印「全部一致」 0 根本没进判定,空被当成了正常
第 5 次最扎心:那个统计脚本是专门为了防前四次而写的。它第一版自己就犯了同一种病。
这件事让我们确信:这不是某个人能靠「下次仔细点」解决的问题。写代码时的默认倾向就是把「空」当「没问题」。所以修法不能靠自律,只能靠装置——让「空」自己参与判定:数到 0 条、不足预期条数,直接判红。
由此固化成一条纪律,写进了规则集本身:
所有「必须存在某标识」的断言,都要同时验证该标识不存在时会红。只验证「存在时通过」,等于没有验证。
四、全量自证:三个状态,少一个都不行
规则配反例之后,还剩一个问题:怎么知道「今天这次回归」跑的是全部变异,而不是一个越缩越小的子集?
我们的装置输出三态,写在退出逻辑里,不靠人看:
 少跑:本次运行数 < 登记数 → 子集不成立,退出码非 0;
 多跑:本次运行数 > 登记数 → 有人加了变异没登记 → 退出码非 0;
 相等:全部变异已跑 → 判据存活证据成立。
「多跑也红」是后来补的。一开始只防「少跑」,结果某次调试往临时目录塞了个新变异,跑完计数超了登记数,装置照样打绿——多出来的变异没进登记表,等于检查器的检查范围被人悄悄改了,而装置认为一切正常。登记数与运行数必须严格相等,多一个少一个都是异常,这才闭合。
配套的一条纪律更反直觉,但值得写下来:
修改检查器,必须跑全量变异。 修误报和加新判据一样危险——只跑「新加的那组」会全绿,而死掉的旧判据没人替它喊疼。
五、指纹对账:登记表没动,等于没改过
另一条战线是文件完整性。我们的做法很朴素:每个交付文件登记 MD5,文件变了必须补登记,登记表只追加不覆盖。
第一次意识到它有价值,是补登机制上线之前。有一次改了一版检查脚本,跑回归,结果 32/33——一条「文件指纹漂移」的红。查下来:脚本改了,登记表里还是旧 MD5。功能是对的,但按这套纪律,「登记表没跟着动」等于「这次改动没被承认」。 补登之后回绿。
这件事反过来想很有意思:那条红不是误报,是体系在工作。不是永远不出错,是出错一定红——红在当场,红在补登之前,而不是三个月后复盘时。
这套指纹体系自身也有一个经典陷阱:登记表自己要不要算指纹?算了,每次追加登记表都会变,永远漂移;不算,登记表本身就成了无锚点文件。解法是承认这是自引悖论,登记表不锚自身,其完整性由「append-only + 追加行可审计」保证——每条新登记都说明改了什么,历史不可擦除。
六、验证装置的装置,也要被验证
最后是自指问题:所有上面这些——变异、自证、指纹——都是「检查别人的装置」。装置自己坏了怎么办?
我们给装置配了五条自检,全部是反向的:
自检① 正确的判据标识 → 必须判通过(确认装置没坏到全红)
自检② 错位的判据标识 → 必须判失败(红可以,红错了不行)
自检③ 消息里去掉标识 → 必须判失败(「拦截了但说不清是谁拦的」不算拦截)
自检④ 空 expect → 必须判失败(只验存在 = 没验)
自检⑤ 合法的无标识情形 → 不误伤(防止装置红得没有边界)
还有一次更有代表性的:我们做了四条「删掉某条判据,变异应当变红(因为检查死了)」的反向实验,第一次跑完四条实验,检查装置的输出却显示一切正常。查下来是自检脚本自己有个 bug,把被测改动覆盖掉了——验证装置的脚本,自己也是装置,也要被验证。那次的结论写在纪要里:这一层暂不升级为规则,但必须记录在案。
装置没有特权。任何「用来验证别人的东西」,都要回答「谁验证你」——而且答案不能是「我自己」,得是结构。
七、给后来者的四条可执行纪律
把这套东西收拢,能带走的是四条,每条都对应至少一次真实的翻车:

  1. 每条检查配专属反例,且红集必须点名——红不够,要红对;
  2. 修改检查器必须跑全量变异,计数三态严格相等——少跑是子集,多跑是漏登,都不算过;
  3. 「缺失」永远不等于「通过」——所有存在性断言,都要验证缺失时红;
  4. 文件变更与登记表原子对账——登记表没动,等于没改过。
    以及一条价值观层面的:
    评测装置的全部意义,不在于它能让多少东西变绿,而在于它能不能让坏东西可靠地变红。绿灯是副产品,红灯才是产品。
    下一篇讲硬件侧:《关节能力宣告契约:只做能力探查,不包办动力学求解》——为什么关节层只宣告「我能查什么」,不越位替上层求解。
    系列文章:《急停优先:安全链路为什么必须独立于智能决策主线》|《人形机器人三级牺牲通行体系》|《长稳测试 12h 金标准:C2/C5 互斥与沙箱回收踩坑》|《PSSAC 契约:让运动规划读懂台阶、电梯的边界》|本文
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐