Document-Parsing-RAG

双栏 PDF、扫描件、表和图注一旦阅读顺序错了,切分再精细,向量也在对齐一段语义颠倒的文本。根因是版面结构没进检索单元。本方法先还原块类型与 reading order,再交给切分。它不选 chunk_size(Chunking-centric)。

本文属于 RAG 工程框架中的「1 数据接入与文档切分」环节,聚焦「Document Parsing」方法。

定位

维度 内容
角色 切分之前的版面还原器
输入 → 输出 PDF/扫描件 → blocks[](类型、页码、阅读顺序)+ 切分边界提示
默认组合 数字 PDF 用 Docling;生信双栏+公式用 MinerU
何时不用 已是规整 Markdown/纯文本,阅读顺序正确率接近饱和

核心机制

把二维版面变成一维可读磁带。阅读顺序正确率以金标准相邻块有序对为分母:

$$
\mathrm{ROA}=\frac{|{(i,i+1):;\mathrm{pred_order}(b_i)<\mathrm{pred_order}(b_{i+1})}|}{N-1}
$$

表格必须作为 table_row / 单元格关系留下,禁止当连续段落。下游切分只允许在 prefer_boundaries 上开口。

段末注释:阅读顺序(reading order) = 人类扫页的先后;ROA 上面公式是相邻块是否仍保持金标准先后。

图 1 双栏乱序把方法与图注绞在一起;先框类型再按列阅读,表格整块保留

实现路径与心智:页面(或 PDF 对象)先出框与类型(标题/段落/表/图),再按人类阅读顺序把二维版面串成一维 text_blocks,表格走单元格关系而不是当连续正文。底层心智:解析是在重建「阅读磁带」;下游切分和嵌入都把这条磁带当成真相,顺序错了就是系统性语义颠倒,不是召回模型不够好。

优缺点

  • 优点:脏 PDF 上把召回上限从「不可救」拉回到可切分;元数据(页码、章节路径)可追溯。
  • 缺点:多格式适配成本高;解析失败会级联到切分与检索。

契约与走通样例

本阶段输出停在 blocks + reading_order,不切窗口、不生成答案。

输入

1
2
3
4
5
6
{
"doc_id": "P-GAPDH-01",
"type": "pdf",
"pages": 1,
"note": "双栏:左栏 Methods,右栏 Figure 1 caption;表 1 为 siRNA 剂量"
}

中间量

错误顺序 [表1, 图注, Methods] 时,问句「GAPDH siRNA 处理多久」与混搭块余弦 $0.41$。修正为 [heading, methods_para, table_row] 后同一表行块升到 $0.78$。

1
2
3
4
5
6
7
8
{
"reading_order": ["B1", "B2", "B3"],
"blocks": [
{"block_id": "B1", "kind": "heading", "text": "Methods", "page": 1},
{"block_id": "B2", "kind": "paragraph", "text": "Treat HEK293T with 20 nM siRNA targeting GAPDH for 48 h.", "page": 1},
{"block_id": "B3", "kind": "table_row", "text": "siRNA | 20 nM | 48 h", "page": 1, "table_id": "T1"}
]
}

输出(给切分)

1
2
3
4
5
{
"doc_id": "P-GAPDH-01",
"reading_order": ["B1", "B2", "B3"],
"chunk_hints": {"prefer_boundaries": ["B1", "B2", "B3"], "merge_table": true}
}

社区实现

数字 PDF 默认 Docling;生信双栏、公式、跨页表默认 MinerU;多格式兜底 Unstructured。

  • 场景:期刊 PDF、扫描补充材料。
  • 接法:解析 → 标准化 blocks → 再交给 Chunking-centric。
  • 风险:换解析器会改 block_id,必须做文档版本化;MinerU 更吃 GPU。

工程落地

最小可运行示例

复制为 .py 后直接运行(仅标准库)。本例用已检测框演示阅读顺序与按块边界切分;生产检测器用 Docling / MinerU。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
"""版面框 → 按栏阅读顺序 → 禁止跨 table_id 切。"""
from __future__ import annotations

from dataclasses import dataclass


@dataclass
class Block:
"""版面单元。生产对应 Docling 的 layout item。"""

block_id: str
text: str
x: float
y: float
col: int
table_id: str | None = None


def restore_reading_order(blocks: list[Block]) -> list[Block]:
"""左栏再右栏,栏内从上到下。"""
return sorted(blocks, key=lambda b: (b.col, b.y, b.x))


def split_with_block_boundaries(blocks: list[Block]) -> list[str]:
"""同一 table_id 必须整块保留。"""
chunks, buf, tid = [], [], None
for b in blocks:
if b.table_id is not None:
if buf:
chunks.append(" ".join(buf))
buf = []
if tid == b.table_id and chunks:
chunks[-1] = chunks[-1] + " | " + b.text
else:
chunks.append(b.text)
tid = b.table_id
continue
tid = None
buf.append(b.text)
if buf:
chunks.append(" ".join(buf))
return chunks


if __name__ == "__main__":
blocks = [
Block("b2", "48 h before RNA extraction", 20, 10, col=1),
Block("b1", "20 nM siRNA targeting GAPDH", 2, 10, col=0),
Block("t1", "GAPDH", 2, 40, col=0, table_id="T1"),
Block("t2", "20 nM", 20, 40, col=1, table_id="T1"),
]
ordered = restore_reading_order(blocks)
print([b.block_id for b in ordered])
print(split_with_block_boundaries(ordered))

参数

参数 起点 影响
layout_engine 数字 PDF:Docling;论文双栏:MinerU 换引擎等于换 reading_order
ocr_min_confidence 0.82 过低留下 2OOO 这类错字,词项与数值双失败
table_merge true false 时剂量与品类错配
drop_headers_footers true 页眉「Methods」重复污染每一页的块
reading_order 左→右、上→下(按栏) 双栏若当单栏,ROA 会塌

失效—信号—螺丝

  • 双栏乱序:chunk 语义前后颠倒。信号:ROA 低、同页问句余弦异常低。螺丝:换栏感知引擎,抽检 reading_order。
  • 表格当正文切:品类与数值错配。信号:表行问句 Recall 低。螺丝:table_merge: true,禁止跨 table_id 切。
  • OCR 把 20 nM 认成 2O nM:关键词与数值双失败。信号:置信度低于阈值仍入库。螺丝:提高 ocr_min_confidence,低置信块人工/二次 OCR。

规模(100 篇生物学 PDF)

假设约 100 篇、10–15 页/篇、双栏插图多。内存 16–32 GB;临时盘 10–25 GB;可选 GPU 8 GB。解析+OCR+表格约 3–10 CPU·h1–4 GPU·h。在线不跑解析。

-------------本文结束感谢您的阅读-------------