跳转至

文档管线(PDF / Word / Markdown / HTML)

Explanation · 原理进阶

一句话理解

文档 = 结构 + 文本 + 图像的混合。核心动作是解析为层次化段落 + OCR 嵌入图 + 智能 chunking。RAG 质量最常见的瓶颈不在 embedding,而在这一步。

TL;DR

  • 别用简单文本抽取,用结构感知解析器(unstructured · llama-parse · MinerU · Marker · Nougat
  • 2024 新路线:VLM-based parsing(GPT-4V / Claude / Qwen-VL 直接读文档)—— 复杂版式 / 公式 / 表格更鲁棒,但成本高
  • PDF 里的图要 OCR;表格要结构化(Markdown / HTML table)
  • Chunking 是关键艺术:按标题层级 > 定长;保留 overlap
  • 每个 chunk 存原文档指针 + 页码 + 坐标,RAG 引用才精确
  • 模型升级不要重写 chunks,只重 embed

完整流水线

flowchart LR
  upload[PDF/Word/HTML/MD] --> store[(对象存储<br/>原文件)]
  store --> parse[结构化解析<br/>unstructured · MinerU · Marker<br/>或 VLM 直解]
  parse --> blocks[元素序列<br/>标题/段落/表/图]
  blocks --> ocr[图像 OCR<br/>PaddleOCR · Surya · VLM]
  blocks --> table[表格保留为<br/>Markdown / HTML]
  ocr --> blocks2[元素序列 2<br/>带文字]
  table --> blocks2
  blocks --> blocks2
  blocks2 --> chunk[智能 Chunking<br/>按标题层级]
  chunk --> embed[Embedding<br/>BGE / Jina · bge-m3]
  embed --> tbl[(Iceberg: doc_chunks)]
  store -.URI.-> tbl

一、解析:不要简单 PDF2TXT

# 错误方式
text = open("doc.pdf").read()   # 完全错
text = pdfminer.extract_text(f)  # 丢了版式、表格、图

推荐:结构感知解析器,保留"标题 / 段落 / 列表 / 表 / 图 / 页眉页脚"元素类型。

主流选项(2024-2026)

解析器 形态 优点 缺点
unstructured 传统 pipeline 开源、元素类型丰富、生态成熟 复杂版式表现中等
llama-parse (LlamaIndex) 云 API(LLM 底层) 质量高、支持表格 / 公式 按页计费
PyMuPDF / pdfplumber 开源、版式精准、快 需自己拼结构逻辑
GROBID 专用模型 学术文献引用 / 结构抽取最佳 仅学术论文
Azure Document Intelligence 云 API 表格结构化最强 API 收费、境外
Nougat (Meta 2023) 端到端 VLM(开源) 学术 PDF → Markdown + LaTeX 公式,一次性 GPU 推理、对非学术版式一般
MinerU (上海 AI Lab 2024) 开源 pipeline + 模型 中文友好、表格 / 公式 / 图混合处理好 依赖模型权重,部署略重
Marker (VikParuchuri 2024) 开源端到端 快、PDF → Markdown 质量优;支持 LaTeX 对扫描版弱于商业方案
Docling (IBM 2024) 开源 pipeline 企业级、结构保留好、可商用 相对年轻
VLM 直接读(GPT-4V / Claude / Qwen-VL) 通用 VLM 复杂版式最鲁棒、公式图表理解好 成本高、结构化输出需额外约束

选型建议:

  • 学术论文 → Nougat 或 GROBID
  • 中文企业文档(年报、合同) → MinerU · Docling · llama-parse
  • 扫描件 / 老旧文档 → Azure DI · PaddleOCR + 结构重建
  • 已有 OSS 基建、通用场景 → Marker · unstructured
  • 预算充足 / 质量优先 / 复杂版式 → VLM 直接读(抽样 + 缓存降成本)

输出格式示例

[
  {"type": "Title",       "text": "第一章 引言"},
  {"type": "NarrativeText","text": "本章介绍……"},
  {"type": "Heading",     "text": "1.1 背景"},
  {"type": "NarrativeText","text": "自 2020 年以来……"},
  {"type": "Table",       "text": "| 年份 | 营收 |\n| 2020 | 100 |"},
  {"type": "Image",       "uri": "extracted/img_001.png"},
  {"type": "Footer",      "text": "Page 1 of 42"}
]

Footer / 页眉要过滤,不要进 chunk。

二、图像 OCR 嵌入

PDF 里的扫描版页面、图表、截图需要 OCR。2024 可选项:

选项 形态 适合
PaddleOCR 开源 中英文通用,成熟
RapidOCR 开源(PaddleOCR ONNX 版) 更轻量、CPU 部署
Surya(2024) 开源端到端 多语言 + 表格检测强
Tesseract 5 开源 英文 / 欧洲语系、老场景
Donut / LayoutLMv3 VLM 端到端 免 OCR:图像 → 结构化输出
VLM(Qwen2-VL / Claude) 通用 VLM 复杂表 / 手写体最鲁棒
from paddleocr import PaddleOCR
ocr = PaddleOCR(use_angle_cls=True, lang='ch')
result = ocr.ocr("extracted/img_001.png", cls=True)
# 返回每行 [bbox, text, confidence]

OCR 结果作为 Image 元素的补充文字写回原位置。

端到端 vs pipeline

传统 pipeline(检测 → 识别 → 后处理)在大规模稳定场景仍是性价比之选。VLM 端到端适合"少量 · 复杂版式 · 结果要结构化"的场景(合同条款、金融报表)。混合路线:先 pipeline 跑大头,低置信度页面回退 VLM。

三、表格处理

表格一定要结构化,别扁平化:

| 产品 | 销量 | 增长率 |
|---|---|---|
| A | 1000 | +20% |
| B | 500  | -5%  |

Markdown 表格对 LLM 最友好。复杂表(合并单元格)可以用 HTML。

三·b、VLM 直接解析(2024 新路线)

传统 pipeline:布局检测 → OCR → 结构重建 → 段落归并 → chunking 步骤多、误差累积。2024 起 VLM(Qwen2-VL · GPT-4V · Claude)能一次把整页图像变成结构化 Markdown

传统 Pipeline vs VLM 直解 · 选型对比

维度 传统 Pipeline VLM 直解
质量 稳定(已验证)· 但复杂版式误差大 高(复杂表格 / 公式 / 混排)
成本 低(开源工具 + 轻量 OCR) 高(API 调用费 或 GPU 推理)
稳定性 高 · 工具链成熟 中 · 模型换代快 · 输出格式可能漂
可调试 每阶段独立可查 黑盒 · 错了难定位
适用 生产保守默认(日均几 M+ 页) 高质量关键业务 + 低频(日均几 K 页)·或传统方案失败的难页fallback

推荐策略两路混合——生产主流走传统 pipeline(成本可预测)· 置信度低或复杂页面 fallback 到 VLM · 不要全量 VLM(除非成本 & 质量都能接受)。

# 伪代码:VLM 直解一页 PDF
page_img = render_page_as_image(pdf, page=3, dpi=200)
prompt = """把这页文档解析为 Markdown。要求:
1. 保留标题层级(#, ##)
2. 表格用 Markdown table
3. 公式用 $...$ / $$...$$
4. 忽略页眉页脚
5. 输出纯 Markdown,不要前后解释"""
md = vlm.call(prompt=prompt, image=page_img)

何时用 VLM 直解

  • 公式 / 化学式 / 电路图多的文档
  • 复杂嵌套表(合并单元格、跨页表)
  • 多栏排版 + 图文混排(期刊、年报)
  • 手写体 / 老扫描件

成本控制

  • 批量文档先用传统 pipeline 扫一遍;置信度低 / 结构混乱的页面再回退到 VLM
  • VLM 输出做缓存(同一份文档只跑一次)
  • 小模型(Qwen2-VL-7B 自部署)替代闭源 API,单页 API 成本可降到 约 ¥0.01-0.02($0.001-0.003)(具体取决于 GPU 租赁 + 图片分辨率 + 输出 token · 不含自部署集群分摊)

四、智能 Chunking

固定长度切分的坑:

  • 切在句子中间 → 语义断层
  • 不考虑标题结构 → chunk 看起来"孤立"
  • 把表格 / 代码块硬切 → 格式崩

推荐策略:层级感知 + 固定长度兜底

def smart_chunk(blocks, max_tokens=512, overlap_tokens=50):
    chunks = []
    current = []
    current_tokens = 0
    current_headings = []    # 最近的标题层级栈

    for block in blocks:
        if block.type in ("Title", "Heading"):
            # 标题:保留到栈,也作为 chunk 边界候选
            current_headings.append(block.text)
            if current_tokens > max_tokens * 0.5:
                chunks.append(make_chunk(current, current_headings))
                current = []
                current_tokens = 0
        else:
            tokens = count_tokens(block.text)
            if current_tokens + tokens > max_tokens:
                chunks.append(make_chunk(current, current_headings))
                # overlap
                current = tail_tokens(current, overlap_tokens)
                current_tokens = overlap_tokens
            current.append(block)
            current_tokens += tokens

    if current:
        chunks.append(make_chunk(current, current_headings))
    return chunks

每个 chunk 带:

  • content — 内容
  • section_path — "第一章 / 1.1 背景"
  • page_range — "3-4"
  • chunk_idx — 同文档内序号
  • doc_id — 原文档

五、Embedding

用文本 embedding 模型(BGE · Jina · bge-m3 · gte-Qwen2),不用 CLIP 这类多模——文档本身主要是文字。

  • 中文 / 混合语言bge-m3(2024,多语言 + 多粒度,稠密+稀疏+ColBERT 三合一)或 gte-Qwen2
  • 英文 → bge-large-en-v1.5 · jina-embeddings-v3 · voyage-3
  • 长文本 → 选 max_tokens ≥ 8K 的模型(bge-m3 为 8192、jina-v3 为 8192)

多模态配合:如果 chunk 包含图 OCR 文字,也可额外存一列 CLIP 向量用于跨模态查询(以图搜文档)。

六、表结构

CREATE TABLE doc_chunks (
  chunk_id         STRING,
  doc_id           STRING,
  doc_uri          STRING,
  chunk_idx        INT,
  content          STRING,
  section_path     STRING,
  page_range       STRING,
  token_count      INT,
  lang             STRING,
  text_vec         VECTOR<FLOAT, 1024>,
  embedding_version STRING,
  owner            STRING,
  visibility       STRING,
  tags             ARRAY<STRING>,
  ts               TIMESTAMP,
  doc_version      INT
) USING iceberg
PARTITIONED BY (bucket(16, doc_id));

七、引用回写(RAG 必须)

RAG 回答时要指明"这条答案来自 doc_uripage_rangesection_path"。这靠表结构里的元数据字段,而不是查询后拼。

八、增量与版本

文档会改:

  • 新版本的 doc 给新 doc_version
  • 不删老版本 chunks(某些在审计 / RAG 回放需要)
  • 只标 doc_version_current = true 给最新版
  • Iceberg Time Travel 让过去版本可查

生产级 Pipeline 设计要点

文档管线的特点:工具链变化快 + VLM 直解成本敏感 + 保守路径 vs 前沿路径差异大——生产级关注:

问题 做法
保守生产默认路径 PyMuPDF + Unstructured + Tesseract OCR 组合——久经考验 · 工具栈稳定 · 成本可预测
高质量前沿路径 VLM 直解(GPT-4o / Claude / Qwen2-VL 等)· 质量高但成本高 + 稳定性弱 · 推荐用于关键业务 / 低频重解析场景
同资产幂等 doc_id(SHA256)+ doc_version 作主键 · 重跑同版本产出一致
解析失败隔离 扫描版识别失败 / 格式不支持 / 损坏 PDF → DLQ 表 + 原因 · 不阻塞主流
异步 worker 分层 解析(CPU 多)/ OCR(CPU 或 GPU)/ embedding(GPU)三类 worker 分离 · 各自 batch 队列
VLM 成本控制 先走保守路径 · 置信度低的页面(如扫描版 + OCR 文本稀疏)路由到 VLM · 不要全量走 VLM
中间产物 解析后的 markdown / 纯文本 / OCR 结果 · 对象存储 · 湖表只存 URL + chunk metadata
不要把所有处理都塞湖管线 中间 preview / 缩略图 / PDF 渲染 不入湖表——在线生成 + 对象存储 cache 更合适

管线韧性 横切主题呼应 · 文档管线对 Schema Evolution(新字段如 vlm_confidence)特别敏感。

陷阱

  • 扫描版 PDF 当原生 PDF 处理 → 文本全空;要有"是扫描版"检测(PyMuPDF 判空 + 采样分辨率)
  • 没保留 section 层级 → RAG 回答时无法"这段出自哪个章节"
  • Chunk 太大(> 1K token) → Prompt 贵、rerank 慢
  • Chunk 太小(< 100 token) → 语义残缺
  • 多语言混排(中英夹杂)tokenizer 不对 → chunk 大小估算错
  • 表格被按行切 → 表头丢失
  • VLM 一把梭不做 fallback → 一次抖动一整页丢失,必须有传统 pipeline 兜底
  • 端到端解析输出无结构约束 → Markdown 飘忽;Prompt 里限定 schema + 用 JSON mode / function call

常见 Chunk 参数

  • max_tokens: 256 – 768(OpenAI Ada 场景常 512;中文 BGE 场景可到 768)
  • overlap_tokens: 10%–20% of max
  • 按段落优先 > 按句子 > 按 token

相关

延伸阅读

传统 pipeline

2024 端到端 / VLM 路线

Chunking / RAG 工程