AI 工程基础体系 · 第 85/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

RAG 文档解析:PDF、Office、HTML、OCR、版面与结构恢复

RAG(Retrieval-Augmented Generation,检索增强生成)不是“把文件转成字符串,再交给大模型”。它至少包含三条因果链:

  1. 原始文件是否被正确解析,决定知识是否进入索引;
  2. 文档是否被正确切分和表示,决定检索能否找回正确证据;
  3. 证据是否保留来源、权限和结构,决定生成结果能否被验证。

RAG 论文将生成过程表示为条件生成:

p(yx)=zDp(zx)p(yx,z)p(y\mid x)=\sum_{z\in \mathcal{D}}p(z\mid x)p(y\mid x,z)

其中,xx 是用户问题,zz 是从知识库中检索到的文档片段,yy 是模型输出,D\mathcal{D} 是文档集合。实际系统不会枚举整个集合,而是先通过检索器选出有限候选片段,再让生成模型基于这些片段回答。

因此,文档解析不是索引前的附属步骤,而是 p(zx)p(z\mid x) 的输入质量控制。解析时丢失的表格列关系、标题层级、脚注归属或访问权限,通常无法由后续向量模型自动恢复。

一、先定义“文档解析”到底要恢复什么

文档解析是把 PDF、Office、HTML、扫描图像等文件,转换为机器可检索、可引用、可审计的结构化表示。这个过程不等于纯文本抽取。

一个适合 RAG 的中间表示通常至少包含:

{
  "document_id": "hr-policy-2024",
  "source_uri": "s3://corp-docs/hr/policy.pdf",
  "page": 3,
  "block_id": "p3-b7",
  "block_type": "table",
  "text": "等级 | 月度补贴\nP1 | 500\nP2 | 800",
  "heading_path": ["员工手册", "福利政策", "交通补贴"],
  "bbox": [72, 310, 520, 490],
  "language": "zh",
  "parser": "pymupdf",
  "parser_version": "1.24.x",
  "acl": ["hr", "manager"],
  "content_hash": "..."
}

这里有几个容易混淆的概念:

  • 字符内容:页面或节点中实际出现的文字。
  • 版面(layout):文字、图片、表格、标题等元素在页面中的空间位置和相对关系。
  • 结构(structure):章节层级、段落边界、列表嵌套、表格行列、脚注引用等语义组织。
  • 结构恢复(structure recovery):当源文件没有直接提供这些结构,或结构在转换中丢失时,根据文本、样式、坐标和上下文重新推断。
  • OCR(Optical Character Recognition,光学字符识别):从图像像素识别出字符及其位置,不是简单的图片转文字。
  • 版面分析:识别页面中的标题、段落、表格、图片、页眉、页脚、公式等区域,并判断其阅读顺序。

例如,PDF 中“标题位于页面上方、字号较大”是版面证据;“该标题下有若干正文段落,直到下一个同级标题”是结构推断。前者不一定能推出后者,尤其是在双栏论文、报告目录和复杂表格中。

二、RAG 文档解析的完整数据流

生产系统通常把解析拆成可重试的阶段,而不是一个不可观测的函数:

flowchart LR
    A[文件接入] --> B[类型识别与安全检查]
    B --> C[原生内容抽取]
    C --> D{内容质量足够?}
    D -- 是 --> E[版面与结构恢复]
    D -- 否 --> F[渲染页面/提取图像]
    F --> G[OCR与版面分析]
    G --> E
    E --> H[统一文档表示]
    H --> I[清洗与结构化切分]
    I --> J[权限与元数据绑定]
    J --> K[嵌入和索引]
    K --> L[检索、重排、引用生成]

每个阶段都应保存输入哈希、输出版本、耗时、警告和错误。原因是文档解析存在部分成功状态:

  • PDF 文本层可以读取,但其中一页是扫描图;
  • DOCX 正文可以读取,但页眉、文本框或批注没有被读取;
  • HTML 主体可读取,但正文被 JavaScript 动态加载;
  • OCR 识别出字符,但表格列顺序错误。

如果只保存最终文本,后续无法判断“没有检索到知识”究竟是检索问题,还是解析阶段已经丢失知识。

三、PDF:页面描述格式,不是语义文档格式

PDF 的核心目标是精确呈现页面,而不是表达“这是一个标题”或“这是表格的第三列”。一个 PDF 页面通常由文字绘制指令、字体、图片、路径和坐标组成。即使视觉上是一个段落,文件内部也可能只保存为许多带坐标的独立文字片段。

3.1 PDF 的三种常见内容状态

原生文本 PDF

文字有可提取的字符编码,通常可以获得字符、字体、字号和坐标。法律合同、数字化报告和由文字处理软件导出的 PDF 常属于此类。

扫描 PDF

页面本质是图片,没有可用文字层。普通文本提取可能返回空字符串,或者只返回极少量隐藏文字。

混合 PDF

部分页面有文字层,部分页面是扫描图;也可能正文可复制,但印章、手写批注或表格截图只能通过 OCR 获取。

因此,不能用“文件扩展名是 PDF”决定解析方法。应先检测每页的文字数量、图像数量、字符覆盖率和异常编码。

3.2 用 PyMuPDF 进行原生文本和坐标抽取

下面示例读取每页的词级坐标。它适用于已经包含文字层的 PDF,不会自动解决扫描页 OCR,也不保证表格结构正确。

python -m pip install pymupdf
from pathlib import Path
import fitz  # PyMuPDF

pdf_path = Path("contract.pdf")

with fitz.open(pdf_path) as doc:
    for page_no, page in enumerate(doc, start=1):
        text = page.get_text("text")
        words = page.get_text("words")

        print(f"--- page {page_no} ---")
        print("字符数:", len(text.strip()))
        print("词块数:", len(words))

        # words 中常见形式为:
        # (x0, y0, x1, y1, word, block_no, line_no, word_no)
        for item in words[:5]:
            x0, y0, x1, y1, word = item[:5]
            print({
                "text": word,
                "bbox": [x0, y0, x1, y1]
            })

关键中间结果不是只有 text,还包括:

  • 每个词的边界框;
  • 所属文本块;
  • 行号和词序;
  • 页面尺寸;
  • 字体信息(若解析器能够提供)。

如果抽取结果类似:

政策范围 本政策适用于...
补贴标准 P1 500 P2 800

而视觉页面是两列表格,则纯文本已经丢失了列关系。此时继续切分、嵌入和检索,只会把错误结构扩散到整个 RAG 流程。

3.3 PDF 的阅读顺序不能直接由文件顺序决定

对单栏文档,可以按页面内的 yy 坐标、再按 xx 坐标排序。但双栏页面若直接按全局坐标排序,可能得到:

左栏第一段 右栏第一段 左栏第二段 右栏第二段

正确顺序通常应是:

左栏第一段 左栏第二段 右栏第一段 右栏第二段

因此,阅读顺序恢复通常包括:

  1. 按文字块坐标聚类出栏;
  2. 对每一栏内部按纵向位置排序;
  3. 识别跨栏标题、页眉和页脚;
  4. 对表格区域单独处理,不能与正文排序混合;
  5. 检测页眉页脚的重复模式并标记,而不是盲目删除。

页眉页脚是否删除取决于语义。页码通常是噪声,但“机密等级:内部”可能是权限或合规信息,不能因为重复就删除。

3.4 PDF 表格的难点

表格不是“带很多空格的文本”。至少要恢复:

  • 行与列;
  • 单元格边界;
  • 跨行和跨列;
  • 表头与数据的对应关系;
  • 单元格内换行;
  • 表格标题、脚注和正文引用的关系。

如果只得到:

项目 金额 税率
服务费 1000 6%

还可以通过规则恢复;但遇到跨列表头:

        2024       2025
部门    预算 实际   预算 实际

线性文本可能无法判断每个数字对应哪一年。适合检索的表示应显式保留列路径,例如:

部门=研发 | 年份=2024 | 指标=预算 | 值=100
部门=研发 | 年份=2024 | 指标=实际 | 值=92

这类规范化适合问答和过滤,但不应替代原始表格快照。原始单元格坐标仍需保留,以便引用和人工核验。

四、Office:格式内部有结构,但结构不等于页面布局

4.1 DOCX 是 ZIP 加 XML,不是简单文本文件

DOCX 通常是一个 ZIP 容器,内部包含正文 XML、样式、关系、图片、页眉页脚、脚注等。正文段落和表格可以通过库直接访问,但文本框、绘图对象、批注、域代码和复杂分页信息可能需要额外处理。

使用 python-docx 读取普通段落和表格:

python -m pip install python-docx
from docx import Document

doc = Document("manual.docx")

blocks = []

for p in doc.paragraphs:
    text = p.text.strip()
    if text:
        blocks.append({
            "type": "paragraph",
            "style": p.style.name,
            "text": text
        })

for table_index, table in enumerate(doc.tables):
    rows = []
    for row in table.rows:
        rows.append([cell.text.strip() for cell in row.cells])

    blocks.append({
        "type": "table",
        "table_index": table_index,
        "rows": rows
    })

for block in blocks:
    print(block)

这段代码能正确处理普通正文和文档主体中的表格,但有明确边界:

  • 它不等价于“按页面阅读顺序读取所有可见内容”;
  • 文本框中的文字可能不在 doc.paragraphs 中;
  • 页眉页脚需要通过 section 单独读取;
  • 文档中的图片需要另行提取和 OCR;
  • 表格可能包含嵌套表格、合并单元格或重复单元格引用;
  • Word 的分页结果依赖渲染引擎,不能仅由 XML 稳定推断。

4.2 XLSX 与 PPTX 的结构差异

Excel 的核心不是段落,而是二维或多维单元格结构。解析时应保留:

  • 工作表名称;
  • 单元格坐标;
  • 公式与计算结果;
  • 合并单元格;
  • 隐藏行列;
  • 单元格格式;
  • 表头范围和数据范围。

将整个工作表拼接成一段文本会破坏行列语义。更适合的单位可能是“表格区域”“按行记录”或“按业务主键聚合的记录”。

PowerPoint 的信息分布在幻灯片、文本框、形状、图表、演讲者备注和图片中。视觉阅读顺序通常需要按形状坐标恢复。一个标题和三个并列文本框,不应仅按 XML 出现顺序拼接。

4.3 Office 渲染与语义解析的取舍

Office 文件可以直接解析出段落和表格,也可以先转换为 PDF 再进行版面分析。两条路径各有损失:

  • 直接解析更容易保留逻辑结构、样式和表格对象;
  • 渲染后更接近用户看到的分页、位置和视觉层次;
  • 转 PDF 可能改变字体、分页、公式、动画或隐藏元素;
  • 只读 XML 可能忽略文本框、浮动对象和最终布局。

实际系统常同时保存“逻辑解析结果”和“渲染版面结果”,并通过块 ID 建立关联,而不是强迫一种表示覆盖所有用途。

五、HTML:DOM 结构丰富,但正文和视觉结构可能分离

HTML 至少包含三种结构:

  1. DOM 结构:标签嵌套关系;
  2. 样式结构:CSS 决定显示、隐藏、字号和布局;
  3. 运行时结构:JavaScript 执行后动态生成的内容。

静态抓取只能得到初始 HTML。若正文由 JavaScript 请求接口后插入,解析器可能只看到导航栏和一个空容器。

5.1 静态 HTML 的安全解析

python -m pip install beautifulsoup4 lxml
from bs4 import BeautifulSoup

html = """
<html>
  <body>
    <nav>首页 | 登录</nav>
    <main>
      <h1>报销制度</h1>
      <h2>交通费用</h2>
      <p>市内交通每人每月上限为 500 元。</p>
      <table>
        <tr><th>等级</th><th>上限</th></tr>
        <tr><td>P1</td><td>500</td></tr>
      </table>
    </main>
  </body>
</html>
"""

soup = BeautifulSoup(html, "lxml")

for node in soup.select("script, style, nav, footer"):
    node.decompose()

main = soup.select_one("main") or soup
print(main.get_text("\n", strip=True))

这里删除 navscriptfooter 是示例策略,不是普遍规范。某些页面的页脚包含法律条款、版本号或生效日期,删除会造成事实缺失。清洗规则应按站点或内容类型测试,并记录规则版本。

5.2 HTML 的结构恢复

HTML 通常比 PDF 更容易获得标题层级,但仍有三个问题:

  • 页面使用 <div> 模拟标题,缺少语义标签;
  • 目录、推荐内容和正文混在同一 DOM 中;
  • CSS 网格或多栏布局改变了视觉顺序。

因此,正文抽取应结合:

  • 标签语义;
  • DOM 深度和父子关系;
  • 文本密度;
  • 链接密度;
  • 重复模板识别;
  • CSS 可见性;
  • 页面渲染后的视觉位置。

对动态页面,浏览器自动化是常见实现,但它会引入更高成本和安全风险:脚本执行可能访问内部网络、触发下载或暴露凭据。生产环境应使用隔离容器、网络白名单、资源限制和超时,而不是在主应用进程中直接执行任意页面脚本。

六、OCR:从像素识别文字,错误会改变检索事实

OCR 的输入是图像,输出通常包括:

  • 识别文本;
  • 字符或词的置信度;
  • 边界框;
  • 行和段落分组;
  • 版面类别;
  • 有时还包括表格和公式区域。

“识别率高”不能只看字符准确率。RAG 更关注语义关键位置是否正确。例如把“不得超过 1000 元”识别为“不得超过 10000 元”,只错一个字符,却改变了政策含义。

6.1 一个可运行的 Tesseract 示例

前置条件:

  • 安装 Tesseract OCR 引擎;
  • 安装中文语言包,例如 chi_sim
  • Python 安装 pytesseractPillow
python -m pip install pillow pytesseract
from PIL import Image
import pytesseract

image = Image.open("scan-page.png")

data = pytesseract.image_to_data(
    image,
    lang="chi_sim+eng",
    output_type=pytesseract.Output.DICT
)

items = []
for i, text in enumerate(data["text"]):
    text = text.strip()
    if not text:
        continue

    items.append({
        "text": text,
        "confidence": float(data["conf"][i]),
        "bbox": [
            data["left"][i],
            data["top"][i],
            data["width"][i],
            data["height"][i]
        ]
    })

for item in items[:10]:
    print(item)

预期输出类似:

{'text': '报销制度', 'confidence': 96.2, 'bbox': [80, 42, 210, 38]}

此示例只完成文字识别和词框输出,不会自动恢复表格或标题层级。常见预处理包括去噪、倾斜校正、分辨率调整、二值化和版面裁剪,但预处理过度会损伤小字体、印章和低对比度文字。

6.2 OCR 的质量控制

可以为每页建立质量信号:

Q=αC+βD+γSδAQ = \alpha C + \beta D + \gamma S - \delta A

其中:

  • CC 是 OCR 平均置信度;
  • DD 是文字密度或有效字符比例;
  • SS 是与词典、语言模型或原生文本的一致性;
  • AA 是异常指标,例如大量不可识别字符、重复字符或数字格式错误;
  • α,β,γ,δ\alpha,\beta,\gamma,\delta 是按业务设定的权重。

这不是通用质量标准,而是路由信号。低 QQ 的页面可以:

  • 重新渲染更高分辨率;
  • 切分为表格、正文和图片区域后分别识别;
  • 使用另一 OCR 引擎交叉比较;
  • 标记为“需要人工复核”;
  • 禁止直接作为高风险答案的唯一证据。

OCR 置信度也不是事实正确性的概率。引擎可能对错误的“1”和“7”都给出高置信度,因此还要检查金额、日期、百分比、合同编号等领域格式。

七、版面恢复:先恢复区域,再恢复阅读顺序

版面恢复的目标是回答“页面上哪些内容属于什么区域,以及这些区域如何阅读”。它通常包括:

  1. 页面分割:正文、标题、表格、图片、页眉、页脚;
  2. 区域分类:判断每个区域的类型;
  3. 区域排序:恢复阅读顺序;
  4. 关联分析:把图注、脚注、标题和主体关联起来;
  5. 坐标保存:支持引用、裁剪和人工核验。

7.1 为什么坐标是重要的一等数据

只保存文本会导致以下问题:

  • 无法在原文中高亮证据;
  • 无法定位 OCR 错误;
  • 无法判断两段文字是否来自不同栏;
  • 无法重新构造表格;
  • 无法将检索结果展示为“第几页、第几行”。

因此,每个块最好保存归一化坐标:

x^=xW,y^=yH\hat{x}=\frac{x}{W}, \quad \hat{y}=\frac{y}{H}

其中 x,yx,y 是页面坐标,W,HW,H 是页面宽高。归一化后,跨分辨率渲染仍能定位相同区域。但不同 PDF 库的坐标原点和单位可能不同,必须在适配层统一,并通过已知页面测试验证。

7.2 标题层级不能只看字号

标题推断可以综合:

  • 字号和字体粗细;
  • 与前后块的垂直间距;
  • 编号模式,如“1”“1.1”“第一章”;
  • 是否处于目录或正文;
  • 同级标题的样式一致性;
  • 下方是否跟随正文;
  • 标题文本长度和标点模式。

例如,“第一章 总则”通常是一级标题;“1.1 适用范围”可能是二级标题;但“重要提示”也可能只是加粗的正文。单一规则会误判。

恢复出的 heading_path 对检索非常重要。相同句子放在“退款政策”和“隐私政策”下,含义可能不同。切片时应把路径加入元数据或上下文:

文档:员工手册
章节:福利政策 > 交通补贴
内容:市内交通每人每月上限为 500 元。

八、结构恢复:从块到章节、列表、表格和引用关系

结构恢复的对象不仅是标题。一个可用的文档树可能如下:

文档
├── 1 总则
│   ├── 1.1 目的
│   └── 1.2 适用范围
├── 2 报销标准
│   ├── 2.1 交通费
│   └── 表 2-1 交通补贴
└── 附录 A 术语

恢复过程可以分为三步。

第一步:生成原子块

原子块是最小可定位单位,例如:

  • 段落;
  • 标题;
  • 列表项;
  • 表格单元格;
  • 图片及其说明;
  • 脚注;
  • 公式区域。

原子块不应过早拼接,因为后续可能需要重新排序或重新关联。

第二步:推断块关系

常见关系包括:

  • parent_of:标题属于哪个章节;
  • next:阅读顺序中的下一个块;
  • caption_of:图注属于哪张图片;
  • footnote_of:脚注属于哪个正文引用;
  • row_ofcolumn_of:单元格属于哪一行或哪一列;
  • continues_from:跨页表格或段落的延续关系。

第三步:构造多种视图

同一原始结构可以产生不同索引视图:

  • 面向语义检索的段落视图;
  • 面向精确过滤的字段视图;
  • 面向表格问答的行列视图;
  • 面向引用展示的页面坐标视图;
  • 面向权限过滤的 ACL 视图。

把所有内容强行变成一种纯文本,是结构丢失的主要来源之一。

九、切分不是按字符数截断

切分(chunking)是将结构化文档划分为可检索片段。它同时受两个约束:

  1. 片段需要足够完整,能独立表达一个事实;
  2. 片段不能过大,否则向量表示和上下文窗口中会混入过多无关内容。

设一个候选片段包含事实集合 F(c)F(c),用户问题需要事实集合 F(q)F(q)。理想片段应满足:

F(q)F(c)F(q)\subseteq F(c)

同时希望片段长度 L(c)L(c) 不超过预算 BB。但这通常不可同时满足,因为一个定义可能跨越多个段落,表格的表头和数据也可能距离很远。

9.1 结构优先的切分算法

一个可靠的基本流程是:

  1. 先按文档结构切分:章节、段落、列表、表格;
  2. 如果块超过长度预算,在句子或列表项边界切分;
  3. 给子块继承完整标题路径;
  4. 对跨块定义保留有限重叠;
  5. 表格单独序列化,不与相邻正文盲目拼接;
  6. 为每个片段记录页码、块 ID 和原文偏移。

例如原文:

2 报销标准
2.1 交通费
员工市内交通费用按月报销。P1 等级上限为 500 元,P2 等级上限为 800 元。

不应只切成:

员工市内交通费用按月报销。
P1 等级上限为 500 元,P2 等级上限为 800 元。

而应保留:

章节:报销标准 > 交通费
内容:员工市内交通费用按月报销。P1 等级上限为 500 元,P2 等级上限为 800 元。

否则用户问“P2 等级交通补贴是多少”,检索结果可能缺乏“交通费”的限定。

9.2 按固定窗口切分的反例

原文:

本政策适用于正式员工。外包人员不适用。出差交通费按以下标准执行:
高铁:二等座;
飞机:经济舱。

若每 30 个字符截断,可能产生:

片段 A:本政策适用于正式员工。外包人员不适用。出差交通费按
片段 B:以下标准执行:高铁:二等座;飞机:经济舱。

问题“外包人员能否报销飞机经济舱”需要同时检索适用范围和费用标准。固定窗口虽然可能依靠重叠解决,但重叠不是结构理解;重叠过小会丢语义,过大则增加索引冗余和检索噪声。

十、表格、图片、公式和脚注应走不同表示

表格

表格适合同时保存:

  • 原始二维单元格;
  • 带表头路径的行记录;
  • 可读文本;
  • 页面坐标。

例如:

表名:交通补贴标准
等级=P1;项目=市内交通;上限=500 元
等级=P2;项目=市内交通;上限=800 元

这比简单拼接单元格更适合问答,但对于需要精确计算的场景,还应保存数值类型和单位。

图片

图片本身可能包含流程图、架构图、扫描签章或图表。应保存图片区域、图注和 OCR/视觉模型生成的描述,并明确描述是推断结果还是原文文字。模型生成的图片摘要不能当作原始事实的无条件替代。

公式

公式中的符号、上下标和单位不能简单去掉。例如:

总价=单价×数量\text{总价}=\text{单价}\times\text{数量}

如果解析为“总价单价数量”,检索可能找到关键词,却无法用于推理。可以同时保存 LaTeX、线性文本和渲染图片,但要标记来源与转换方式。

脚注和引用

脚注可能改变正文含义,例如“仅限试点地区”。将脚注全部移到文档末尾会使正文片段失去限定条件。更安全的做法是建立 footnote_of 关系,并在生成检索片段时将相关脚注附加到正文。

十一、解析质量必须用“事实任务”评测

只评估“抽取了多少字符”是不够的。建议分别评估以下层次。

字符和词级准确性

适合 OCR 或编码问题:

  • 字符错误率;
  • 词错误率;
  • 数字、日期、金额、编号的精确率。

块级准确性

判断段落、标题、表格和图片区域是否识别正确:

  • 区域检测的 precision、recall、IoU;
  • 块类型分类准确率;
  • 阅读顺序错误率。

结构级准确性

检查:

  • 标题层级是否正确;
  • 表头与数据是否对应;
  • 脚注是否绑定;
  • 列表嵌套是否保持;
  • 跨页内容是否正确合并。

RAG 任务级准确性

最终要测:

  • 相关片段召回率;
  • 证据是否包含完整限定条件;
  • 引用页码和坐标是否正确;
  • 答案是否忠实于证据;
  • 解析错误是否导致高风险错误答案。

一个完整算例是:测试集包含 100 个问题,其中 20 个依赖表格,15 个依赖脚注,10 个来自扫描页。若总体检索 Recall@5 为 90%,但表格问题只有 40%,则不能认为解析系统质量良好;总体平均值掩盖了结构性失败。

解析评测还应包含反例集:

  • 双栏 PDF;
  • 旋转页面;
  • 中英文混排;
  • 跨页表格;
  • 合并单元格;
  • 低分辨率扫描;
  • 页眉中含关键合规信息;
  • 动态渲染 HTML;
  • 同一文件的新旧版本。

十二、检索、权限和引用必须在解析阶段建立联系

OpenAI 的 Retrieval 指南强调,检索系统通常会对文件内容进行分块、嵌入和索引;具体分块策略和检索配置会影响结果。无论使用托管检索还是自建向量库,都不能把权限当作生成阶段的提示词。

正确的顺序是:

用户身份可访问文档/块过滤检索重排生成\text{用户身份} \rightarrow \text{可访问文档/块过滤} \rightarrow \text{检索} \rightarrow \text{重排} \rightarrow \text{生成}

而不是先从所有文档检索,再要求模型“不要使用无权限内容”。后者既可能泄露信息,也无法保证模型不引用已经看见的内容。

ACL 应绑定到文档块或可继承的结构节点:

{
  "chunk_id": "doc-1:p3:b7",
  "acl": {
    "groups": ["finance"],
    "users": [],
    "classification": "internal"
  }
}

当章节继承文档权限时,切片过程中必须保留继承关系。文档更新、权限变更和删除也要进入索引生命周期:

  • 内容哈希变化:重新解析受影响版本;
  • 权限变化:立即更新过滤索引;
  • 删除请求:删除原文、解析缓存、嵌入和检索索引;
  • 版本变化:保留生效日期,避免新旧政策混答。

引用应回到原始可核验位置:

来源:员工手册.pdf,第 3 页,表 2-1,坐标 [72, 310, 520, 490]

“来源是某个向量块”不够,用户无法判断该块是否漏掉了表头、脚注或否定条件。

十三、并发、失败路径与可恢复性

大批量解析通常是异步任务。可以把每个文档建模为状态机:

stateDiagram-v2
    [*] --> RECEIVED
    RECEIVED --> VALIDATING
    VALIDATING --> REJECTED: 类型/安全检查失败
    VALIDATING --> EXTRACTING
    EXTRACTING --> OCR_REQUIRED: 文本不足或页面为图像
    EXTRACTING --> STRUCTURING: 原生内容可用
    OCR_REQUIRED --> OCR_RUNNING
    OCR_RUNNING --> OCR_FAILED: 超时/资源不足
    OCR_RUNNING --> STRUCTURING
    STRUCTURING --> CHUNKING
    CHUNKING --> INDEXING
    INDEXING --> READY
    READY --> SUPERSEDED: 新版本到达
    OCR_FAILED --> RETRYING
    RETRYING --> OCR_RUNNING

每次状态转换应具备幂等性。常见做法是用:

job_key=(content_hash,parser_version,config_hash)\text{job\_key}=(\text{content\_hash},\text{parser\_version},\text{config\_hash})

作为任务键。相同文件、解析器版本和配置重复提交时,不应重复产生不一致结果。

典型故障与处理方式包括:

  • 文件伪装扩展名:通过 MIME、魔数和容器结构共同检测;
  • 损坏 PDF:隔离原文件,记录页级错误,不把空结果当成功;
  • OCR 超时:设置页数、像素和 CPU 限制,支持页级重试;
  • HTML 无限跳转:限制重定向次数、总下载大小和执行时间;
  • 恶意压缩包:限制解压文件数、展开大小和目录深度,防止 Zip Bomb;
  • 重复索引:使用文档版本和内容哈希去重;
  • 部分页面失败:结果标记为 partial,禁止伪装成完整解析;
  • 索引成功但元数据失败:采用事务性发布,只有文本、嵌入、权限和来源都准备好后才进入可检索状态。

十四、成本和模型选择是同一个生产决策

解析成本不只来自 OCR 或大模型,还包括:

  • 文件下载和对象存储;
  • PDF 渲染;
  • OCR 的每页计算;
  • 表格和版面模型推理;
  • 文本嵌入;
  • 向量存储;
  • 重排和生成;
  • 重新解析与版本并存。

可以将单文档成本近似写成:

Cdoc=Cdownload+Cnative+Crender+Cocr+Cembed+CstorageC_{\text{doc}}= C_{\text{download}}+ C_{\text{native}}+ C_{\text{render}}+ C_{\text{ocr}}+ C_{\text{embed}}+ C_{\text{storage}}

其中 OCR 和视觉模型通常应按需触发,而不是对所有页面执行。一个常见路由是:

  1. 先尝试原生抽取;
  2. 只有文字密度低或结构异常的页面才渲染;
  3. 只有疑似扫描或关键图像区域才 OCR;
  4. 只有表格、公式或低置信度区域才升级到更昂贵模型。

但不能只按成本路由。若合同金额、医疗剂量或安全规程对错误极其敏感,应使用更严格的质量阈值和人工复核,即使单页成本更高。

十五、常见误解与诊断方法

误解一:PDF 能复制文字,就说明解析正确

复制功能只说明存在某种文字层,不说明阅读顺序、表格结构和字体编码正确。诊断时应对比页面截图、词坐标、块顺序和关键数字。

误解二:OCR 置信度高,就可以直接入库

置信度不是事实保证。应对金额、日期、否定词、单位和编号做规则检查,并抽样比对原图。

误解三:增加 chunk overlap 就能修复结构丢失

重叠只能缓解边界截断,不能恢复列关系、标题归属或脚注语义。遇到表格和双栏问题,应先修复版面与结构。

误解四:把所有内容转成 Markdown 就完成了结构化

Markdown 对标题、列表和简单表格有帮助,但它不能完整表达坐标、合并单元格、脚注关联、隐藏内容、版本和权限。Markdown 可以是展示格式,不应成为唯一规范数据模型。

误解五:检索不到答案一定是向量模型问题

排查顺序应先看:

  1. 原文是否真的被解析出来;
  2. 关键事实是否落在同一结构片段中;
  3. 片段是否带有标题、表头和脚注;
  4. ACL 是否误过滤;
  5. 关键词检索和向量检索是否都失败;
  6. 重排器是否把正确证据降权;
  7. 生成模型是否忽略了已召回证据。

一个简单有效的诊断方法是建立“原文—解析块—切片—检索结果—最终引用”的链路追踪。只展示最终答案而不展示这条链路,无法判断系统在哪一层犯错。

十六、一个可执行的最小落地方案

对普通企业文档,可以先实现以下最小闭环:

  1. 对上传文件做类型、大小、魔数和恶意内容检查;
  2. PDF 使用原生文本抽取,并按页检测文本密度;
  3. 对低密度页面渲染后 OCR,保留词框和置信度;
  4. DOCX 分别读取段落、表格、页眉页脚和图片;
  5. HTML 清理模板节点,同时保留标题层级和 URL;
  6. 所有结果转换为统一块模型,保存 document_id、页码或 DOM 路径、坐标、类型和版本;
  7. 先按章节和语义边界切分,再按 token 预算限制大小;
  8. 表格同时保存二维结构和带表头路径的文本;
  9. 切片继承 ACL、标题路径、文档版本和生效时间;
  10. 通过关键词检索、向量检索和引用定位共同验证;
  11. 对扫描页、表格、脚注和双栏文档建立专门评测集。

最终目标不是得到“看起来通顺的文本”,而是得到一组可以追溯的证据单元:

原始文件页面/DOM版面块结构关系检索片段引用答案\text{原始文件} \rightarrow \text{页面/DOM} \rightarrow \text{版面块} \rightarrow \text{结构关系} \rightarrow \text{检索片段} \rightarrow \text{引用答案}

其中任一箭头断裂,RAG 就可能出现“模型回答流畅,但证据错误”的结果。PDF、Office、HTML 和 OCR 只是不同输入形态;版面恢复与结构恢复才是把这些形态转换为可靠知识系统的核心。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。