主题
构建测试集:问题分类、标准答案、出处标注
本章要回答的问题:没有标注团队,我一个人怎么搭出一套可信的测试集?
第五部分用 80 道题跑完了整条链路。数字有了,但那套题本身没被审过。回头查,有三个问题:
- 出处太集中:80 条出处里 63 条来自《安全生产法》(79%),另一份北京市工作指南一次都没用上;
- 单条复用太高:「第二十四条」一条被 4 道题共用,模型只要背熟这一条就能刷分;
- 拒答题有假货:20 道拒答题里的 R12 问「注塑工艺参数」,而专利
pat-inj-02里就写着料筒温度 180/220/240/250℃(实测,见benchmarks/evaluation/refuse-audit-v1.csv)。
这不是题不够多,是出题没有流程。这一节把 80 题扩到 300 题,重点不在数量,而在把「怎么验证一道题」写成可执行的三道闸(图 6.1-1)。
一套题要过哪四关
测试集不是「攒够多少题」,是同时满足四件事:
- 覆盖:条款、文档、题型都要散开,不能全挤在一章里;
- 题型:不同题型测的是链路上不同的环节,混在一起平均会掩盖问题;
- 出处:每条答案都要能回到一个可核验的位置:文档、条号、原文片段;
- 可判定:答案对不对,能由脚本先判一遍,人只处理剩下的争议。
数据集该记录哪些信息、怎么说明局限,已经有成熟模板(引用 [1])。
这四关不是凭空列的,是两次失败逼出来的。第一次在第五部分:只有第 1、2 关的直觉,没有第 3、4 关的约束,于是数字看起来完整,题却经不起复查。第二次是这一节要避免的:把校验写成脚本,让「出处不对」「答案抄错」在评测之前就暴露。
规模、分片与题目分布见 benchmarks/evaluation/testset-split.csv;条款覆盖见 testset-coverage.csv。
四类题对应四种失败
| 类型 | 数量 | 它在测哪一种失败 | 例子 |
|---|---|---|---|
| 直接摘取 | 75 | 检索到了,但没抄对 | 「生产经营单位的主要负责人对本单位安全生产工作负什么责任?」 |
| 条件判断 | 75 | 规则找到了,档位套错 | 「某机械厂有 80 名员工,应如何配备安全生产管理人员?」 |
| 跨段组合 | 75 | 只召回一半,答案不完整 | 「主要负责人在全员责任制和安全投入上分别承担什么职责?」 |
| 拒答 | 75 | 资料里没有,模型却敢编 | 「公司 2025 年的营业收入是多少?」 |
四类的判分方式不同:
- 直接摘取只看答案有没有命中关键句,用来定位「检索对了、生成错了」;
- 条件判断在问题里放一个具体参数(人数、金额、行业、频次),看模型会不会把 A 条款套到 B 场景;
- 跨段组合要求两个出处都命中(
joint: true),单段召回再高也不算过; - 拒答不看出处,只看有没有说出「资料中没有相关信息」。
条件题允许同一条款下出不同阈值,例如「一般事故罚多少」与「较大事故罚多少」算两道独立题——它们测的是分档能力,不是重复。
标准答案:从改写改成逐字抄
v1 的答案经过改写,读起来自然,但带来两个麻烦:一是标注时各人理解不同,二是自动判定只能靠模糊匹配。v2 改成逐字抄条款:
- 可以取连续的一两句,多句用「;」连接;
- 不加「不符合」「应当」这类判断前缀——判断留在问题里,答案只放原文;
- 跨段题必须两个出处各贡献至少一句原文,脚本会逐条检查。
代价是答案变长。test 分片里,直接题平均 54.5 字,跨段题平均 98.4 字(实测,见 benchmarks/evaluation/testset-split.csv)。5.8 用子串和 bigram 覆盖率判分,答案越长,模型「答对意思但换了词」越容易被判错。这个偏差是刻意保留的:宁可判严,也不要让评分标准跟着模型输出漂。6.3 只量出两把尺子差多少;要校准 v2 的偏差,需要人工标一批 v2 答案(本书没做)。
v1 的答案普遍经过改写;其中 4 道(C18、X02、X12、X16)与所引条款的 4-gram 覆盖率低于 45%,校验会告警但不报错。它们留在 dev 里,方便和第五部分的数字对照。
出处:先能定位,再谈标注
v2 只从三份语料出题:《安全生产法》《工贸企业有限空间作业安全规定》《北京市生产经营单位安全生产主体责任规定——工作指南》。
另外两份为什么不用?gov-checklist-hs 是 26 项检查表,条文里的编号是它引用的法条号,不是自己的条目;gov-responsibility-zs 按岗位分节、每节又从 1 开始编,两个岗位的「1」是不同职责。两者都没有「文档内稳定条款」这个概念,答案可以「对」,出处却指不回去。这和 5.9 的权限过滤是同一个问题:id 不稳定,过滤和追溯都会漏。它们仍留在检索库里当干扰项——检索要能区分「相似的法规条文」和「真正要引的那一条」。
结果(实测,见 benchmarks/evaluation/testset-coverage.csv):
| 语料 | 条款数(去重) | 被覆盖 | 引用占比 |
|---|---|---|---|
| 安全生产法 | 119 | 84(71%) | 55% |
| 有限空间规定 | 22 | 19(86%) | 16% |
| 主体责任工作指南 | 54 | 38(70%) | 29% |
| 合计 | 195 | 141(72%) | 100% |
指南也有重复条号:54 个条号里 27 个在正文出现两次以上,因为它是汇编,同一编号可能来自不同法规(第二十八条既有教育培训,也有劳动防护用品)。v2 仍然用它,但出处按「条号 + 答案原文」定位:校验脚本核的是答案能否在所引条款正文里逐字找到;6.2 判检索相关性时也要求命中段落包含答案关键词,而不是只比条号。只比条号,会把「同号的另一段」误判为命中。校验脚本对重复条号还会额外核对:答案必须只落在其中一段不同正文里(相同条文重复排版不算)。
v1 只覆盖 44 条条款、79% 引用压在安全生产法;v2 覆盖 141 条,新增 97 条。条款总数按行首条号去重后是 195,比 corpus/index.json 的条目少——索引里还有目录和正文引用造成的重复,覆盖率只按去重后的条号算;条号重复的,以答案原文定位。
规模、分片与功效
v2 一共 300 题:225 道可答、75 道拒答。四类各 75,dev/test 各 150。
分片规则只有一条:v1 保留的题全部进 dev,test 只放新题。第五部分用 v1 选过切分方式、向量维度和融合策略,这些题已经参与了决策;再把它们放进 test,测到的就是记忆。最终 dev 是 79 道 v1 + 71 道新题,test 是 150 道新题。
benchmark 的污染与统计功效不是本书独有的问题(引用 [2])。300 题能分辨多少?独立比例的分辨率只有 ±6 个百分点左右(p≈0.8,n≈150,95% 区间)。真正提高功效的是配对比较:同一道题上「方案 A 对、方案 B 错」的格子里才有信息,6.4 用配对 bootstrap 与 McNemar,不拿两个独立比例硬比(做法见 引用 [3])。
还有一个容易忽略的副作用:dev 的出处 64% 来自安全生产法(79 道 v1 + 71 道新题,149 条引用),test 是 46%(剔除 8 道缺块题后 139 条引用;按 test 全量算 47%)。两侧文档组成不同,dev 上选出来的参数不能直接套到 test 的绝对水平,只能看相对变化。这条限制会一直带到 6.4。
150 道 test 也不支持反复试错。五个预注册的方案、每个只比一次,是这个规模能承受的上限;试到第二十个,test 就被你调成了 dev。
拒答题要审计
拒答题最容易出的错,不是问得太偏,而是问到了语料里真有的东西。写题的人凭印象判断「这份资料不可能有」,但语料里还有专利、环评、事故报告,细节比你想象的多。
审计方法很直接:把每道拒答题当查询,用 5.4 的 734 个真实块检索 top-1(实测,见 benchmarks/evaluation/refuse-audit.csv):
- 可答题 top-1 相似度的中位数是 0.803,5% 分位是 0.670;
- 拒答题的中位数是 0.441,90% 分位是 0.596,最高 0.688;
- 两条分布有重叠,所以阈值不能自动判定,只能把 ≥0.65 的题标出来人工看。
75 道拒答题里标出 2 道,人工复核都不是可答:R67 问安全管理人员姓名电话,命中的事故报告只写了人数和岗位;R73 问本厂被处罚金额,命中的 62 万元是另一家企业燃气事故的罚款。
真正的收获在 v1:R12「产品的注塑工艺参数」top-1 是 0.693,命中的专利段落直接给出料筒各段温度和保压参数。这是一道可答题,却一直被当作「正确拒答」计入 5.8 的拒答率。v2 把它退役,补了 R76。没有审计的拒答题,会把「模型不肯答」误当成能力。
一个人怎么保证质量
流程是:条款索引 → 按文档和题型配额出题 → 草稿 → 三道闸 → 拒答审计 → 分片。
脚本能查的(eval/validate.py):
- 字段、id、重复题、四类配额;
- 出处是否存在、
cross是否至少两个出处; - 答案是否逐字出现在所引条款里,
cross的每个出处是否都进了答案; - 新增题在同一题型下是否复用同一条款超过 2 次;
- dev/test 数量、v1 是否全部在 dev、test 是否混进了旧题。
脚本查不了的:题目是否自然、场景参数是否合理、答案是否真的回答了问题。这部分只能人工抽检,本书抽了 20%。
最终结果:300 题 0 error / 15 warning。15 条告警里 12 条来自 4 道 v1 改写答案的覆盖率(同一批题在三个校验分组里各报一次),3 条是文档集中度(v1 79%、v2 整体 55.3%);没有一条是新增题的出处或答案错误。
局限
- 一个人出题,没有标注者间一致性(inter-annotator agreement,κ)。6.3 会用本地模型当裁判、拿 60 条人工标签对齐,只能补一部分。
- 逐字答案放大了生成端的评分偏差。答对意思但换了词会被判错,v2 的绝对正确率可能低于 v1,这是口径变化,不是模型变差。
- 只有法规文体。换到工单、手册、聊天记录,题型配额和条款定位方式都要重设。
- 指南的重复条号:27/54 个条号对应多段不同正文,只按条号算相关性会偏乐观;6.2 用答案原文匹配抵消,但出处标注本身仍不如另外两份干净。
- 拒答审计只查「检索得到」,不查「模型会不会用」。后者是 6.3 与 6.5 的事。
- 300 题支撑不了太多比较。如果团队要反复调 20 个参数,应该先扩题,而不是在 test 上试。
工程含义
把测试集当代码管。 入仓、可 diff、可校验;出题和改题走同一条 PR 流程。题目一改,历史数字就不可比,必须在提交信息里写清楚。
出处用复合键。 (doc, clause) 比文件名、页码稳定;条款号不唯一时先修索引,别硬出题——那是 5.9 权限过滤的前提。
test 必须晚于调参。 调参用 dev,报数用 test;test 只放没参与过决策的题。
拒答题要审计,而且不能只看相似度。 相似度分布会重叠,阈值只用来缩小人工范围。
评估集规模要跟决策数匹配。 5 个方案 × 150 题,比 20 个方案 × 80 题可信。
动手实验
动手实验:给你自己的语料建 100 题
假设:用 validate.py 的三道闸,能把「出处错、答案抄错」挡在评测之前;人工只需处理题目自然度和场景合理性。
做法:从 corpus/index.json 选 3 份条款结构稳定的文档,四类各 25 题;答案逐字抄;每写完一类就跑一次 python3 eval/validate.py --draft 草稿.jsonl --category direct。全部通过后,把拒答题交给 labs/06-evaluation/audit_refuse.py 跑一遍 top-1 审计。
记录:error 类型分布、返工题数、单题耗时、被标出的拒答题数。注意:审计阈值只能缩小范围, 标出的题必须逐条看原文;相似度低于阈值的也可能漏。
参考资料
- Gebru, T. et al. Datasheets for Datasets(数据集该记录哪些信息:动机、构成、采集、用途与局限,CACM 2021). https://arxiv.org/abs/1803.09010
- Bowman, S. R., Dahl, G. E. What Will it Take to Fix Benchmarking in Natural Language Understanding?(benchmark 的可复现性、污染与统计功效,NAACL 2021). https://arxiv.org/abs/2104.02145
- Dietterich, T. G. Approximate Statistical Tests for Comparing Supervised Classification Learning Algorithms(配对比较与 McNemar 检验,Neural Computation 1998). https://doi.org/10.1162/089976698300017197