> 覆盖率这类比值能抓到不抛异常的缺陷——而"每页小节数"抓不到
一个把教材 PDF 切块进向量库的流水线,靠字号启发式抽小节标题。 一本 725 页的组合数学书,只抽出 8 个标题, 六成正文进不了索引 —— 不抛异常,不卡死,页面字符数正常,只是悄悄少了。
结论先行¶
一个把教材 PDF 切块进向量库的流水线,靠字号启发式抽小节标题。 一本 725 页的组合数学书,只抽出 8 个标题, 六成正文进不了索引 —— 不抛异常,不卡死,页面字符数正常,只是悄悄少了。
抓到它的是一个比值:coverage(小节区间覆盖的页数 / 总页数)= 0.426。
而最自然的那个替代判据是错的。"每页小节数太少"听起来等价,实测否掉了:
| 书 | 每页小节数 | coverage | 判断 |
|---|---|---|---|
| Williamson & Shmoys | 0.006 | 1.00 | 健康(小节少但每节都长) |
| Stanley《Enumerative Combinatorics》 | — | 0.426 | 病(罩不住正文) |
小节稀疏不是病,罩不住才是病。
修法是加第四级 fallback:coverage < 0.55 就强制改走固定页窗,
取覆盖更高的那个。那本书从 8 个父块 / 覆盖 0.43 变成
61 个父块 / 覆盖 1.00。
这是同一个项目里第三次靠比值指标抓到静默缺陷 (前两次是目录页排到正文前面、TOC 页码不单调)。
为什么¶
静默缺陷的定义是:它不触发任何异常,而且它的输出是一个合法的输出。 8 个小节是合法的,一本书确实可能只有 8 章。所以类型检查、 schema 校验、非空断言全都过。
能抓住它的只有不变量,而最有用的一类不变量是比值:
- 比值自带分母,所以跨规模可比 —— 一本 100 页的书和一本 1000 页的书 能用同一个阈值。绝对数("至少 20 个小节")对两者必有一个是错的。
- 比值描述的是结构关系(抽出来的东西罩住了多少原始材料), 而绝对数描述的是产出量。产出量低可以有合法理由,罩不住不行。
而"每页小节数"失效的原因正是它不是一个关于覆盖的比值: 它是"产出量 / 规模",归一化了规模,但量的仍然是产出量。 分母对了,分子错了。
选分子的判据:它要直接表达"我想要的那个性质"。 我想要的是"正文都进了索引",那分子就该是"被某个小节区间覆盖的页数", 不是"小节个数"。这一步常常被跳过,因为小节个数更容易拿到。
怎么用¶
给每个抽取/解析阶段定义一个覆盖率,并且记进产物。
coverage = 被产出结构覆盖的原始单位数 / 原始单位总数
"原始单位"按场景取:页、字符、行、token、时间长度、记录数。 低于阈值就换一条 fallback 路径,并把两条路径的 coverage 都记下来,取高的那个 —— 这比"报错退出"好,因为它仍然产出可用的东西。
候选不变量的通用形状(每一条都是比值):
| 场景 | 分子 / 分母 |
|---|---|
| 文档解析 | 被小节覆盖的页 / 总页 |
| 实体抽取 | 被某个 span 覆盖的字符 / 总字符 |
| 日志解析 | 匹配到某条规则的行 / 总行 |
| ETL | 落到目标表的记录 / 源记录 |
| 转录 | 有文字的秒数 / 总秒数 |
阈值靠实测分布定,不靠猜。 这个项目的 0.55 是在全语料上 看过分布之后定的(改完 0 本书低于 0.55)。先把所有样本的比值 打出来排序,看拐点在哪 —— 一个没有看过分布的阈值, 要么挡不住,要么误伤一片。
同一个分母下的"看起来等价"的判据,要实测否掉再放弃。 那条 0.006 小节/页 却 coverage 1.00 的记录,是一个单点反例, 它比任何推理都快地杀掉了"每页小节数"这个判据。 为新判据找一个反例,比给旧判据找证据便宜。
比值抓到一次之后,回头看它是不是能抓更早的那几次。 这个项目第三次用比值抓到缺陷时,前两次的缺陷形态完全不同 (排序污染、页码非单调),但都能被某个比值的异常值暴露。 一类工具抓到三次,就该把它做成每个阶段的标准产出, 而不是每次事后再量一遍。
出处¶
~/Tools/booksearch/docs/RAG_PLAN.md §16.6「又一个只有比值能发现的
静默缺陷:TOC 罩不住正文」(725 页 / 8 个标题 / coverage 0.426、
Williamson & Shmoys 的 0.006 对 1.00、第四级 fallback 与 8 → 61 父块);
前两次在 §11.2 与 §14.1。
相关:清洗步骤必须有一道"我是不是清过头了"的自检——它差点删掉一整本书、
一个语料上总结的根因规则,在下一个语料上可能命中 9 条却解释 0 条失败。