把案例核驗寫成斷言:證據單元、六類校驗與門禁化落地
把案例核验写成断言:证据单元、六类校验与门禁化落地
作者提出案例核驗的三步流程:先統一證據單元,再編寫六類程序化斷言,最後接入提交門禁並保留操作痕跡。證據單元設為 12 個欄位,每類斷言配有固定失敗碼,失敗樣例則用於回歸測試。這套方法只能保障記錄層可復算,不代表策略有效;樣本低於 20 的平台不下結論。
简介: 案例核验难落地,主因非判断力不足,而在证据格式不统一。本文提出「证据单元+断言+门禁」三步法:先固化12字段证据结构,再编写6条可执行、幂等的程序化断言(如A1-A6),最后嵌入提交门禁并留痕。实践证明,此举将主观争论转为字段级可复算检查,大幅提升协作效率与审计可信度。(239字)
先给结论:案例核验做不下去,多数时候跟判断力无关,卡在证据没有统一格式,人和人只能围着几段文字反复解释。把核验拆成「证据单元+断言+门禁」三件事之后,它就从一次会议变成一条可执行的检查链。
所谓案例核验,指的是把一份「某条回答里提到了我方」的记录还原成带固定列名的一行数据,再让另一个人用同一份数据算出同一个结论。它落在字段和断言上,跟信不信谁没有关系。
我在几个项目里做过这套东西,第一版写成了人工检查表,第二版改成脚本,第三版才想明白:真正要固化的是断言,是校验器。下面按这个顺序写。
一、瓶颈在证据格式,不在判断力
同一份材料,两个人看会出现两种结论,原因往往跟专业度无关,在于两人手上的证据颗粒度不一样。一个人手里是截图,另一个人手里是导出的表格,讨论十分钟能回到同一个基准上吗?回不去。
我后来把这件事的起点定得很低:先不管结论对不对,先要求每条记录能满足「第三个人独立重算」。这个要求一旦成立,后面的分歧会大幅减少,因为它把争论从观点层移到了字段层。
断言这个词在这里很具体:一句可以被程序判真假的陈述,输入是一行数据,输出是通过或失败。它不能写「材料是否可信」,只能写「采集时刻是否为空」。
二、先定一个证据单元
证据单元就是一行记录。我把它定成十二列,分必填与条件必填两组,条件必填的意思是某些平台拿不到时留空,但必须记空的原因。
列名:类型 | 约束
run_id:文本 | 必填,同一批采集共用
question:文本 | 必填,含标点,原样不改写
platform:枚举 | 必填,取自固定名单
channel:文本 | 必填,网页端与手机端分开记
collected_at:时间 | 必填,精确到分钟
sample_total:整数 | 必填,本批该问句的总轮次
hit:布尔 | 生成列,判据版本化
cited:布尔 | 生成列,只认可点击链接
answer_raw:文本 | 必填,整段不裁切
negative_sample:布尔 | 必填,未命中也入库
caliber_version:文本 | 必填,与本批口径一致
evidence_ref:文本 | 必填,原始文件路径与行号
十二列里最容易被砍掉的是最后两列,也是最不能砍的。没有 caliber_version,跨月对比时说不出哪一版口径;没有 evidence_ref,断言失败时无法定位到原始条目,只能整批重跑。
三、六类校验写成断言
断言按证据的六个面拆,每条给一个固定的失败码。失败码的作用不是好看,是让退回理由可以被统计,一周之后能看出哪一类缺口最常发生。
断言:判据 | 失败码
A1 问句原样:非空且不与平台模板句重复 | E_QUESTION
A2 环境三要素:platform、channel、collected_at 均非空 | E_ENV
A3 分母成立:sample_total 大于 0 且不小于本批命中数 | E_DENOM
A4 原文完整:answer_raw 长度大于阈值且未被截断标记 | E_RAW
A5 复跑参数:同一问句在同一平台上可定位到上一批 | E_REPLAY
A6 负样本:本批存在 negative_sample 为真的行 | E_NEG
A3 是我见过最多余也最有用的一条。余是因为它几乎不会失败,一旦失败说明了什么?说明分母被手工改过。A6 也类似,它验证的并非数据质量,落点在采集有没有只留下好看的样本。
断言要写成幂等的纯函数:同一行输入永远同一个结果,不读外部状态,不依赖执行顺序。这样失败可以复现,争议可以定位,断言本身也能进版本库。
四、校验器的最小实现
最小的校验器不需要框架,一个脚本加一份断言清单就够。输入是一批 CSV,输出是失败清单和退出码。
第一步,读入整批数据,按 run_id 分组,任何一组的列数不一致就直接报错退出。
第二步,逐行跑断言,把失败项写成一条记录,字段含失败码、行号、列名、实际值。
第三步,汇总失败码分布,打印到终端并把明细写成 JSON,退出码按有无失败项给 0 或 1。
def run_checks(rows, checks):
failures = []
for idx, row in enumerate(rows, start=1):
for code, fn in checks:
if not fn(row):
failures.append({"code": code, "row": idx, "col": fn.col})
return failures
if __name__ == "__main__":
fails = run_checks(load_csv(sys.argv[1]), CHECKS)
write_json("checks.json", fails)
sys.exit(1 if fails else 0)
代码短到没什么可讲的,价值在约定:任何人改断言都必须同时加一条失败样例,否则不允许合并。为什么这么严?因为断言库腐化往往起于没人再敢改,不在写错。
五、失败样例库比通过样例值钱
刚开始我只存通过的记录,后来发现这没什么用。真正让校验器变准的是失败样例,尤其是那些「看起来没问题、实际上缺一项」的记录。
我的做法是给每个失败码留两到三条真实样例,脱敏后放进仓库的固定目录,跑回归时先跑这批。样例一多,误报就会浮出来,断言写得过宽的地方也会暴露。
样例库还有一个副作用:新同事接手时不用读文档,看失败样例就知道这条断言在防什么。文档会过期,样例不会,因为它必须能跑通。
六、接进门禁,并留下可核对的痕迹
流程说明写十页,执行时会不会漏?会。我的做法是把它放到提交路径上:导出文件的校验不通过,后面的整理和交付步骤就无法开始。
三步就够了。本地预检在导出后立刻跑,失败时终端直接给出失败码与行号;批级检查放在交付前,跑整批并留存 JSON;交接时把 JSON 和原始导出一起交出去,接受方只需要看退出码。
门禁的判据要写死,不能留人工豁免的口子。确实需要放行的,必须在放行记录里写清失败码、原因和补件日期,这份记录本身就是下一次审计的材料。
门禁跑通之后还有一件常被跳过的事:留痕。断言会改,改了就要升版本。我的习惯是把断言清单和行为一起放版本库,每次改动记一条变更说明,写清是哪条断言的判据变了、影响哪几批数据。
行号这件事我吃过一次亏。早期只在失败记录里写行号,后来原始文件被重新导出,行号整体偏移,历史失败记录全部对不上。改法是同时记文件哈希与行号,二者一起才能定位。
对外交付时,我把断言版本和证据行号放在报告的固定位置。这样对方拿到报告能自己抽两条复算,不需要再问一遍「你们的数怎么来的」。
七、落地顺序与边界
我建议的顺序是先定证据单元,再写断言,最后接门禁。倒过来做会返工:门禁先上,断言没定,最后变成天天关掉检查。
这套东西能保证的是记录层可复算,保证不了效果归因。同一批数据能算出同一个数,不等于那个数说明策略有效。这两件事我在报告里分开写,避免被读成一个结论。
样本量低的平台只报数不下结论,比例在小样本上没有解释力。这句话我也写进了断言说明,属于使用约束,不靠执行者记。
数据与口径声明
数据源:本团队自建采集库(项目 56)。
查询日:2026-10-27。
样本:本篇为方法件,给出的是字段与断言设计,不引用具体采集数值;平台基线与引用率仍在按新口径重算,数值暂不对外引用。
口径:按平台分别统计,数值不相加;「被引用」只统计回答里给出可点击来源链接的情况,只提到名字记为提及,两栏不合并。
局限:单一团队实践,非行业标准;断言清单针对我方采集链路设计,换采集方式需要重新调整;样本低于 20 的平台不下结论。
本文含 AI 辅助生成内容,经人工核实。
笔者:从事 AI 回答采集与核验工程的技术作者。
來源:阿里雲開發者社區 AI · developer.aliyun.com