> 别人说"修好了"要在自己的产物上复跑一遍——最可能悄悄缺席的,正是那条护栏
一轮真实的协作:我在审稿里发现 bibguard 的一串缺陷,另一个会话修,我复测,四个来回。 每一轮的"修好了"我都在自己那五篇 PDF 上复跑过,四轮里三轮声明全部属实, 唯一不属实的那一轮,缺的恰好是护栏本身。四条可迁移的规矩:
结论先行¶
一轮真实的协作:我在审稿里发现 bibguard 的一串缺陷,另一个会话修,我复测,四个来回。 每一轮的"修好了"我都在自己那五篇 PDF 上复跑过,四轮里三轮声明全部属实, 唯一不属实的那一轮,缺的恰好是护栏本身。四条可迁移的规矩:
- "修好了"要在你自己的产物上复跑,不要在对方的测试用例上确认。 对方的 54 个测试全过、126/126 的 backend check 全绿,而我要的那条护栏在两条路径上都够不到。
- 最可能悄悄缺席的功能,是那种"今天不装也不会坏"的功能—— 护栏、审计记录、逃生舱。它们只在将来出事时才有用,所以缺席当天没有任何症状。
- 你每写下一个
grep -v去压掉固定噪声,那行噪声大概率是一个没人报的缺陷。 更一般地:工具静默给你空结果,比给你错结果更危险(见下面"静默空结果"那一节)。 - 在验证类工具里,误指控和漏报不是一个量级的错误,修法也不一样。 下结论之前先找到被声明的范围是什么——这条救过我一次(见下)。
为什么¶
第 1、2 条。 对方报告 0.8.0 做了"抑制可审计"(被压掉的条目要计数并列出)。
功能性的声明我复测全部属实:抽取数从 7/7/9/16/3 变成 41/33/66/29/35、逐篇对上我的人工 ground truth;
扫描从 5 篇全 FAIL 变成 4 CLEAN + 1 WARN;--no-whitelist 确实能恢复。
但那条护栏:默认报告 13 行里 grep -i suppress 命中 0,而 --no-whitelist 证明这篇被压掉了 18 条;
scan --json 照样打文本报告而且 exit 0,解析方静默拿到坏数据。
也就是说白名单压掉 18 条,读报告的人看不到这件事发生过——正好把护栏要防的风险放了回来。
这不是对方偷懒:功能有症状、护栏没有,所以写完测试跑绿就以为完了。
第 3 条,我自己的教训。 那个 scan --json 失败的根因之一是:
以旧名 fitz 导入 PyMuPDF 会把弃用警告打到 stdout。
我在这整个审稿会话里一直写 | grep -v 'fitz API' 把它滤掉,当成环境噪声绕过去了,
从没想过它污染的是 stdout 而不是 stderr。它就是那个 bug 的症状,我和它共处了一整天。
第 4 条。 最早那轮,bibguard 的 DOI 正则把句末句号吃进 DOI,
10.1038/s41586-024-08025-4.(Nature 的真 DOI)解析失败后被报成 "likely hallucinated",
一批文档里由此产生十余条假指控。在评审场景里这不是普通假阳性——
不少会议明确把"LLM 生成的不实指控"列为可处分行为。
修法也和一般假阳性不同:不能靠调阈值,要靠"单一来源的 404 不足以下结论"。
最终的实现是 Crossref 404 之后再问 doi.org 的 handle API(覆盖所有注册机构),
doi.org 不认才 FAIL、不作答则 UNVERIFIED。鉴别力没丢:我真造的
10.9999/this.is.not.a.real.doi.12345 仍然 FAIL。
两个后来补上的实例¶
一次被拦下来的误指控。 核对一份声明了"预算 ≤ N"的产物时,我按交付的整个对象 计 token,得数超过 N——看起来是违规,而且是个漂亮的 finding。 下结论前去读了那个预算的实现:它的 docstring 写明计的只是其中几个字段, 明确排除传输信封。按它声明的范围重算,逐 token 等于它自己报的数,确实没超。 不是违规,是我量错了对象。 规矩:任何"超了 / 不符 / 对不上"的结论,第一步是找到对方声明的那个量到底是什么范围。 (信封开销本身值一句备注,不值一条 weakness——量级要诚实。)
第 3 条的另一个形态:静默空结果。 同一轮里栽了两次:
pymupdf抽出来的 PDF 文本含控制字节,file判为data,于是grep什么都不输出 ——不是报 0 匹配,是静默不输出。我因此一度以为文中找不到某个关键词, 差点据此改结论。要用grep -a。- 一个分析脚本在缺少输入文件时静默返回
{}:原本该有几百条记录的表,重算全是 0, 不报错不告警。照 README 跑的人会拿到空表而毫不知情。
规矩:拿到"空"的时候,先分清是"真的没有"还是"根本没查到"。 这两个在终端上长得一模一样,而含义完全相反。 写自己的工具时,"输入缺失"必须和"结果为空"走不同的出口。
怎么用¶
接收"做完了"时的复跑清单:
- 在你自己触发这个问题的那份数据上跑,不是对方的 fixture。我一直用同一组五篇 PDF,所以四轮可比。
- 功能和护栏分开验:功能看输出变没变,护栏要专门构造"本该被记录的事件"再看它有没有被记录。
(我的办法:
--no-whitelist拿到真实被压条数 18,再回默认模式找这个 18。) - 逃生舱要亲手拉一次(
--no-whitelist从 0 injections 恢复到 18 + 2 warnings,才算真的存在)。 - 对方给的数字能对就逐格对:它给的 19 = 18 分数串 + 1 模板页脚,我复跑逐项相符才算过。
写自己的工具时:
- 压制/白名单必须自带三件套:计数 + 按原因分项 + 一个关掉它的开关。 缺任何一件,压制就不可审计。
--json这类机器接口要有一个用子进程跑真实入口、解析 stdout 的测试。 进程内测试抓不到"第三方库往 stdout 打警告"这类污染。- 验证类工具的误指控路径要单独设计:任何一条"这是假的"的结论,都不能建立在单一来源的否定答复上。
- 白名单不能碰真信号。这次保住的那类是"直接对审稿人或 LLM 说话的指令性文本"—— 被引用的 prompt 和被注入的 prompt 只有人能区分,所以那条保持 WARN 而不是压成 CLEAN,是对的。
- "输入缺了"和"结果是空的"要走不同出口。 缺输入要响亮地失败并说缺了什么, 不能返回一个和"真的没有"无法区分的空值。这是第 2 条(最可能缺席的是护栏)在数据层的同一件事。
出处¶
2026-10,一段密集使用自己工具链的时期里,围绕 bibguard 0.6.0 → 0.8.1 的四轮往返。 这篇讲的是自己的工具怎么验收,不涉及任何被检查的文档的内容。 顺带一条:对方纠正了我的验收办法——参考文献表以名开头,该查姓非递减, 我原来写的"首条 A 开头末条 Z 开头"只是碰巧成立(已在 引用幻觉藏在作者列表和 venue 里,不在标题里——只比标题的批量校验会漏 里更正)。 别人反过来挑出你的方法错在哪,是这种往返最值钱的部分。 相关笔记:引用幻觉藏在作者列表和 venue 里,不在标题里——只比标题的批量校验会漏、断言正则项是活的——Fisher 代码从未被调用、penalty 静默为零,而所有表照样出了数、 一个方法在"它的校准退化"的那个区间里看起来最稳健——那是假象,不是鲁棒性。