别把整份 PDF 都喂给 VLM:按页路由的文档解析架构

本文只讨论通用的工程方法与架构取舍,不涉及任何具体业务、代码、数据与效果指标。文中出现的阈值均为示意性经验值,需按自己的语料重新标定。

一、问题:文档进 RAG 的第一道坎

RAG 系统的效果,很大程度上不取决于检索算法,而取决于入库前那份文本有多完整。而现实里的语料大量是 PDF 和 PPT——尤其是 PPT,它是所有文档格式里对纯文本提取最不友好的一种:一页幻灯片上真正有信息量的部分,往往是一张柱状图、一个流程图、一组只有数字没有说明的 KPI 卡片。

处理这类文档,有两条极端路线:

路线 A:纯文本提取。 用 PyMuPDF、python-pptx 把文字抠出来。优点是快、免费、确定性强。缺点是图表里的信息全部丢失——提取出来的可能只有一个标题和三个孤零零的数字,下游检索到这一页也没用。

路线 B:全页 VLM。 每一页渲染成图片,丢给多模态大模型,让它输出结构化描述。优点是"什么都能看懂"。缺点有三个,且都很硬:

  1. 成本:一份 80 页的材料就是 80 次多模态调用,其中大部分页面是纯文字的目录页、章节封面、结束页。
  2. 延迟:单页视觉理解 8–20 秒,串行不可接受,并发又要考虑配额。
  3. 幻觉:VLM 读图表时会编数字。这是最要命的一点——错误的数字进了 RAG 库,比没有这一页更糟。

所以真正该问的问题不是"用不用 VLM",而是:哪些页值得用 VLM,哪些页不值得。

PDF / PPT格式检测Track 1 · 确定性提取按页路由渲染为图片Track 2 · VLM 并发处理MergeRAG 语料 skip:Track 1 已足够需要视觉理解的页增强结果回填

双轨结构:确定性轨永远跑,模型轨只处理被选中的页面

二、核心思路:先用免费的信息做决策

这套架构的关键洞察其实只有一句:

判断"这一页值不值得调用 VLM"所需要的信息,在你做纯文本提取的时候,已经免费拿到了。

用 PyMuPDF 遍历一页 PDF 时,除了文字,你还顺手知道了:这页有多少个字符、多少个矢量绘图对象、多少张嵌入图片、有没有标题、文本行的长度分布。这些数字的获取成本是(本来就要遍历一遍),但它们对"这一页的信息是不是藏在图里"这个问题的判别力相当强:

  • 字符数 8、矢量对象 60 → 几乎确定是一张图表;
  • 字符数 1200、矢量对象 0、图片 0 → 纯文字页,VLM 看了也说不出更多东西;
  • 字符数 0、矢量对象 0、图片 1 → 一张整页贴图,元数据完全失语,需要另想办法。

于是架构自然分成两轨:

  • Track 1(确定性轨):纯 CPU、无模型,逐页提取文字、标题、KPI,同时产出上面那组元数据。这一轨的输出是基线,任何时候都存在。
  • Track 2(模型轨):只对被路由选中的页面调用 VLM,产出增强信息。

两轨在最后合并。Track 2 失败或超时不影响交付——最差情况就是退化成路线 A。

三、路由:一个三级漏斗

路由是这套架构里唯一真正有设计含量的部分。它对每一页做三级判断:

每一页① 复杂度分 < 阈值?② 纯图片页?③ 图表分 ≥ 阈值?skip · 不调用模型probe · 运行时探测低成本 OCR 探测(2–5s)启发式判定内容类型OCR 专用模型(3B)通用多模态模型(20B+) 按判定结果二选一

三级漏斗:每一级都在回答"还需不需要更贵的判断"

第一级:复杂度评分——先把不值得看的页筛掉

累加一组启发式信号,得到一个复杂度分数:

条件 分数 含义
文本 < 30 字符 +3.0 文字极少,信息大概率在图里
矢量对象 > 15 +min(n/12, 3.0) 矢量元素多,可能是图表
图片数 ≥ 2 +min(n×0.8, 3.0) 多图嵌入
无标题 +2.5 缺少文字说明
短行占比 > 60% +2.0 碎片化标签,典型的图表坐标轴
存在孤立 KPI +0.5/个 有数字但没有上下文
是章节分隔页 −3.0 封面页,看了也没用

分数低于阈值(示意值 3.0)→ skip,直接沿用 Track 1 的结果,一次模型调用都不发。

这里最值得说的是那个 −3.0。大部分启发式规则都在做加法——"这页看起来很复杂,快调用模型"。但章节封面页恰恰是"看起来很复杂"(文字极少、无标题、可能有一张装饰大图)却完全不值得看的页面。没有这条负权重,漏斗会在最没价值的页面上漏掉一大堆算力。一个好的成本控制规则,必须同时包含否决项。

第二级:纯图片页检测——元数据失语的情况

条件很简单:文本 = 0、矢量 = 0、图片 ≥ 1。

这类页面的元数据什么都说明不了:它可能是一张扫描的财务报表,也可能是一张全屏配图。元数据的判别力在这里归零,所以不能再猜,只能进入 probe(运行时探测)路径——后面单独讲。

第三级:分流到哪个模型

剩下的页面还要再判一次"图表分":

条件 分数
矢量对象 > 30 +0.4
图片 ≥ 2 且文本 < 80 +0.3
文本 < 30 +0.2
短行密集 +0.2
KPI 数量 ≥ 4 −0.2

超过阈值(示意值 0.6)走 OCR 路径,否则走 语义路径

为什么要分两条路径?因为这两类页面需要的能力根本不同:

路径 模型 适合的页面 要的是什么
OCR 3B 级 OCR 专用模型 表格、数据密集图表 精确:数字一个都不能错,还要空间定位
语义 20B+ 级通用多模态模型 概念图、图文混排 理解:这张图在讲什么逻辑

用大模型去读一张密密麻麻的表格,既贵又容易在数字上出错;用 OCR 专用模型去读一张架构示意图,它只会吐出一堆零散的框内文字,读不出关系。模型分层和页面分类是同一件事的两面——这比"用一个最强的模型处理所有页面"要工程化得多。

注意 KPI 数量那条 −0.2:KPI 多说明这一页有清晰的"数字 + 标签"结构,语义模型反而能理解得更好。又一条否决项。

probe:花小钱买信息

纯图片页没法靠元数据判断,那就先花一点点成本去看一眼

  1. Phase 1:用 OCR 模型跑一次低成本快速模式(2–5 秒),拿到粗糙的文本输出;
  2. 决策:对这段输出做启发式分析(耗时 < 1ms)——表格竖线多、数字密度高、短行碎片多 → 判为图表/表格;否则判为连续文本;
  3. Phase 2:按判定结果,走完整 OCR(含定位标注)或语义视觉理解。

这里有个反直觉但正确的设计:Phase 2 不复用 Phase 1 的输出。快速模式的结果没有定位标注、精度也不够,只配用来做分类,不配作为最终结果。

看起来浪费了一次调用,但账要这么算:快速探测 2–5 秒,而路由错误的代价是一次 8–20 秒的错误路径调用加上一份质量不达标的结果。用确定的小成本,换掉不确定的大成本,这是合算的。

四、模型轨的工程细节

路由决定了"调用哪些页、用哪个模型",剩下的是把调用跑稳。几个值得记录的点:

自适应 DPI。 渲染图片时按页面类型选分辨率:图表/数据密集页用 200 DPI(数字要清晰),纯文字页 96 DPI(省内存),混合页 144 DPI。一份材料的渲染内存占用可能因此差出几倍——而且高 DPI 对纯文字页的识别率没有任何提升。

三层超时协作。 单靠一个总超时是不够的,需要三层配合:

层级 机制 管什么
总时限 wall-clock 检查 整体不超时
停止信号 线程 Event 通知正在跑的线程优雅退出
取消 Future cancel 取消还在排队的任务

只有 Event 没有 cancel,队列里的任务还会被启动;只有 cancel 没有 Event,已经在跑的调用会一直挂到底。两者管的是不同状态的任务。

增量保存。 每完成一页就落一次盘,重跑时跳过已完成的页面。文档解析是典型的"长耗时 + 部分可用"任务,全或无的语义在这里是错的。

单页失败不影响全局。 某一页超时或解析失败,记一个错误标记继续跑,那一页退化成 Track 1 的结果。

五、双轨架构的隐藏红利:交叉验证

这是我认为整套设计里最漂亮的一点。

VLM 从图表里读出一个 KPI 数字,怎么知道它有没有编?——拿去和 Track 1 提取的原始文本比对。因为 Track 1 是确定性提取,它拿到的字符是文件里真实存在的字节,不存在幻觉。

于是每个 KPI 都带一个标记:这个数字能不能在原始文本里找到。找不到的不丢弃(图片里的数字本来就不在文本层),但会被标记出来,下游可以选择降权或提示。

一开始我以为 Track 1 只是个兜底方案,做完才发现它同时是幻觉检测的基准线。这类"一个组件顺带解决了另一个问题"的结构,通常说明架构的切分方向是对的。

六、几个设计决策

设计点 选择 原因
配置读取 每次请求重新拉,不缓存 配置变更需实时生效,这点开销换来运维自由度
格式检测 扩展名优先 + magic bytes 兜底 对象存储路径可能没有扩展名,或带 query 参数
probe 两阶段 探测输出只用于分类,不复用 快速模式精度不够,不配当最终结果
并发模型 线程池 I/O 密集型 RPC 调用,GIL 影响可忽略,不必上协程或多进程
临时文件 落盘而非内存 解析库只接受文件路径;用 finally 保证清理
失败语义 降级而非报错 任何一环失败都退回上一轨的结果

七、延伸思考:这套评分函数应该被学出来

写到这里必须说一句诚实的话:上面那两张评分表,本质上是一个手写权重的线性分类器。

它现在能工作,是因为特征选得准(字符数、矢量数、图片数确实和"信息在不在图里"强相关)。但那些 +3.0、−0.2、0.6 的具体数值,是人拍出来的。这带来两个问题:换一批语料(比如从商业 PPT 换成学术论文)阈值就要重调;而调整时没有客观标准,只能靠人肉抽样看 case。

更长期正确的做法是:把线上跑过的页面攒起来,用"这一页最终是否真的从 VLM 调用中获益"作为标签,训练一个极轻量的分类器——逻辑回归或者几十棵树的 GBDT 就够,推理开销依然是微秒级。人工评分表退化成它的初始化和兜底。

同样,probe 这条路径也有更便宜的解法:与其花 2–5 秒跑一次 OCR 来分类,不如把页面缩略图(比如 64×64)过一个几百 KB 的小型图像分类器,几毫秒就能区分"表格 / 图表 / 文字 / 照片"。之所以现在没这么做,是因为那需要标注数据和一次训练——这正是"短期简单"和"长期正确"之间那个典型的取舍点。当前版本选了短期简单,但应该清楚地知道自己欠了这笔账。

八、最后

抽掉所有实现细节,这套架构做的事情可以概括成一句话:

用零成本的信号,决定高成本的计算花在哪里。

这个模式在计算机系统里到处都是:CPU 用分支预测决定要不要预取,数据库用统计信息决定走不走索引,CDN 用访问频次决定缓存什么。它们的共同结构是——有一个廉价的、不完美的判别器,去调度一个昂贵的、高质量的执行器。

大模型时代这个模式只会更重要,因为"昂贵的执行器"变得前所未有地昂贵。当调用一次模型的成本是解析一页文本的上千倍时,决定"调不调"的那个判断,本身就值得被当作一个正经的系统组件来设计。

关键词: LLM RAG 架构