PageIndex:一种不靠向量检索的长文档 RAG 实现思路

最近在看 PageIndex 这个项目,最大的感受是:它不是在继续优化"切 chunk + 向量召回"这条路,而是换了一个方向,试图让大模型像人读文档一样,先找到"该去哪一章",再决定"该读哪几页"。

这篇文章不做产品介绍,也不铺太多概念,主要讲一下我基于源码和文档理解出来的 PageIndex 实现思路。重点会放在两个问题上:

  1. PageIndex 为什么不走传统 Vector RAG。
  2. 它的树形索引到底是怎么构建出来的。

一、为什么传统 Vector RAG 在长文档里经常不够稳

传统 RAG 的典型流程大概是这样:

  1. 先把文档切成很多 chunk。
  2. 对每个 chunk 生成 embedding。
  3. 查询时把问题向量化,做 Top-K 相似度召回。
  4. 再把召回结果交给大模型生成答案。

这个方案在很多通用场景是成立的,但在金融报告、技术手册、制度文档、合同这类"强结构、强上下文依赖"的长文档里,问题会比较明显:

  1. 语义相似,不等于真正相关。

    两段文字可能用了几乎一样的术语,但讨论的根本不是一件事。

  2. chunk 容易破坏结构。

    原始文档里的"章节关系、上下位关系、页码范围"被切碎之后,模型拿到的是离散片段,不是完整结构。

  3. 很多问题本质上是"导航问题",不是"相似度问题"。

    比如"某个风险披露在什么章节""某个指标定义在正文还是附录",这类问题更像先定位目录,再进入正文。

PageIndex 的核心思路,就是把这个过程改成:

  1. 先给文档建立一棵树。
  2. 再让模型在树上做推理式检索。

也就是说,它先解决"去哪里找",再解决"找到了读什么"。

二、PageIndex 的核心设计:先建树,再检索

PageIndex 可以概括成两步:

  1. 把 PDF 或 Markdown 转成层级化的 tree index。
  2. 查询时让 LLM 基于 tree index 选择相关节点,再回到原文取内容。

这里最关键的是第一步。因为只要树建得足够稳定,后面的检索其实就有点像:

  • 先在"目录树"里选章节
  • 再根据章节映射到页码范围
  • 最后读取对应正文作为上下文

所以 PageIndex 的核心难点,不在"怎么做相似度召回",而在"怎么从一份结构并不总是可靠的 PDF 里,尽量稳定地恢复出树结构"。

三、它的实现主线:从 PDF 到 Tree Index

如果只看主流程,PageIndex 的 PDF 处理链路可以理解成下面这几步:

text 复制代码
输入 PDF
  -> 提取每页文本与 token 数
  -> 检测是否存在目录页
  -> 抽取目录内容
  -> 判断目录是否带页码
  -> 对齐逻辑页码与物理页码
  -> 校验目录项是否靠谱
  -> 修复错误页码
  -> 转成树结构
  -> 补充 node_id / text / summary
  -> 输出 structure.json

下面分开说。

四、第一步:先把 PDF 变成"可计算对象"

源码里最基础的一层,是先把 PDF 逐页解析出来,并统计每一页的 token 数。

这么做有两个直接目的:

  1. 后面做目录检测、章节定位时,LLM 看到的输入是按页组织的。
  2. 后面需要按 token 上限分组,避免一次喂给模型太长的内容。

这一层本身不复杂,但它很重要,因为后面几乎所有步骤都依赖"页"这个最小单位:

  • 哪一页可能是目录页
  • 某个标题应该落在哪一页
  • 某个节点覆盖哪些页

所以 PageIndex 的索引不是建立在 chunk 上,而是先建立在"页面 + 结构关系"上。

五、第二步:先判断文档有没有目录

这是整个流程里很关键的分叉点。

PageIndex 不会默认假设 PDF 一定有可用目录,而是先检测前几页是不是目录页。根据源码里的处理思路,大致会分成三种情况:

  1. 有目录,而且目录带页码。
  2. 有目录,但是目录不带页码。
  3. 没有可用目录。

这三种情况的后续成本差异非常大。

1. 有目录且带页码

这是最理想的情况。

因为这时候模型只需要:

  1. 把目录页提取出来。
  2. 转成结构化 JSON。
  3. 再把目录里的页码和 PDF 的物理页做一次对齐。

这条路径最省,因为不需要扫描整本书去猜每一章从哪一页开始。

2. 有目录但不带页码

这时候目录只能提供"章节层级",不能直接提供"位置"。

所以系统需要再去正文里扫描,逐步判断每个标题最可能出现在哪一页。也就是说,目录能帮你确定树,但不能帮你确定节点边界。

3. 没有目录

这是最贵的一条路径。

系统只能把全文按 token 分组,然后让 LLM 从正文里归纳出章节结构,再逐步续写整个目录树。这个过程本质上是在"从正文反推目录"。

所以 PageIndex 的一个很现实的工程特征是:

文档本身越规整,尤其是目录越清晰,它的构建成本就越低,稳定性也越高。

六、第三步:把目录内容转成结构化 JSON

拿到目录页之后,接下来不是直接建树,而是先把目录文本转成结构化列表。

这里的难点有两个:

  1. PDF 里的目录文本经常很脏。

    比如换行错乱、页码和标题混在一起、层级缩进不稳定。

  2. 目录可能非常长。

    一次生成不完整,就得让模型继续往后续写。

所以 PageIndex 在这里做的不是一次性抽取,而更像一个"生成 + 检查 + 续写"的循环:

  1. 先让模型把目录转成 JSON。
  2. 再检查这次转换是否完整。
  3. 如果不完整,继续补全。
  4. 直到拿到一个足够完整的结构列表。

从实现思路上看,这一步非常像"半结构化信息抽取",而不是普通摘要。

它的目标不是让模型理解目录,而是让模型输出一个后续可计算、可修复、可验证的中间结果。

七、第四步:页码对齐,是整套方案里最容易被低估的一环

很多人第一次看这类方案,都会觉得"目录里不是已经有页码了吗,直接用不就行了"。

但 PDF 里经常存在两套页码:

  1. 文档正文里的逻辑页码
  2. PDF 文件自身的物理页序号

比如封面、版权页、前言、目录这些页面,往往会导致两者出现偏移。

所以 PageIndex 不会直接相信目录页码,而是会做一次"页码对齐":

  1. 先估算目录页码和物理页码之间的 offset。
  2. 再把目录里的章节页码映射到真实 PDF 页序。
  3. 最后得到每个节点的真实起始位置。

如果目录不带页码,那就更进一步,直接去正文中定位标题首次出现的页面。

这一层做得好不好,决定了后面节点边界准不准。因为一旦某一章的起始页错了,整棵树往后都会发生连锁偏移。

八、第五步:不是抽出来就完了,还要校验和修复

这是 PageIndex 很工程化的一点。

很多文档解析方案到"LLM 输出结构化结果"就结束了,但 PageIndex 还多做了一层验证。

它会抽查目录项,去对应页里验证:

  • 这个标题是不是真的出现在这里
  • 它是不是出现在比较合理的位置

如果发现某些条目不对,就进入修复流程,再重新定位这些章节的页码。

这个设计说明它默认接受一个事实:

LLM 在目录抽取和页码对齐上并不总是可靠,所以必须允许"先生成,再纠错"。

这套思路很像传统数据工程里的 ETL:

  1. 先抽取
  2. 再校验
  3. 最后修复异常

只是这里的"抽取器"和"修复器"都换成了大模型。

九、第六步:从平铺目录转成真正的树

当前面拿到的是一个平铺的目录列表之后,系统才会做真正的树构建。

这一步主要完成三件事:

  1. 根据标题层级关系,把线性目录转成父子嵌套结构。
  2. 为每个节点计算 start_indexend_index
  3. 对过大的节点继续递归拆分。

这里的 start_index / end_index 很重要,因为它们决定了后面检索时到底取哪些页。

比如:

  • 一级标题可能覆盖第 20 到 60 页
  • 它下面的二级标题再把 20 到 60 页继续细分

这样一来,整棵树不仅有"层级信息",也有"范围信息"。

为什么还要递归拆分大节点

因为即便目录存在,某些章节也可能非常长。

如果一个节点动不动覆盖几十页甚至上百页,那么检索时即使定位到了这个节点,也还是太粗了。于是 PageIndex 会对过大的节点继续深入处理,把大章节再细分出更深一层的子节点。

这一步体现了它和普通目录抽取工具的区别:

它不是只想把目录抄出来,而是想得到一棵"足够适合后续检索"的树。

十、第七步:给树补充可检索字段

树构建完成后,系统还会按配置补一些增强信息,常见的包括:

  1. node_id

    给每个节点分配唯一 ID,方便检索阶段引用。

  2. text

    把节点覆盖页的原文挂到节点上,后面可以直接取。

  3. summary

    给节点生成摘要,方便后续做更轻量的检索或展示。

  4. doc_description

    给整篇文档生成一句话描述。

这一步说明 PageIndex 输出的不是一个简单目录,而是一个可继续加工的"中间索引层"。

你可以把它理解成:

  • 目录是骨架
  • 页码范围是定位能力
  • text 和 summary 是检索材料
  • node_id 是引用锚点

十一、检索阶段其实很像"在树上做导航"

当 tree index 已经建好后,查询流程就比较顺了。

大致可以理解成下面这样:

text 复制代码
用户问题
  -> 把问题和树结构一起交给 LLM
  -> LLM 选择相关 node_id
  -> 根据 node_id 找到对应页码范围
  -> 读取节点正文
  -> 再交给 LLM 组织最终答案

这和传统向量召回最大的区别是:

它不是直接问"哪几个 chunk 最像这个问题",而是先问"这个问题最该去文档的哪一层、哪几个章节里找"。

这就是为什么我觉得 PageIndex 更像一种"结构化导航式 RAG"。

十二、这种方案为什么在专业长文档里有价值

我觉得 PageIndex 真正有价值的地方,不在于"完全替代向量检索",而在于它抓住了长文档检索里一个经常被忽略的事实:

很多问题的正确答案,依赖的不是相似度,而是文档结构。

尤其在下面这些场景里,这种思路会更有优势:

  1. 金融报告、招股书、监管文件
  2. 技术标准、产品说明书、制度文档
  3. 教材、论文、法律文本

因为这些文档往往天然具备清晰章节层级,用户的问题也常常和"某一节讨论什么、某一章是否提到某个点"强相关。

这时候,如果先用树结构做导航,再读具体内容,通常比直接做 chunk 相似度召回更稳。

十三、它的代价也很明显

PageIndex 并不是没有成本,反而它的代价很清楚:

  1. 索引构建高度依赖 LLM。

    目录提取、页码补齐、校验修复、摘要生成,都会消耗调用次数和 token。

  2. 文档越不规整,成本越高。

    没有目录、目录混乱、页码体系不一致时,处理链路会明显变长。

  3. 超大目录和超长文档会带来上下文压力。

    一旦目录特别长,结构抽取和续写的稳定性就会下降。

  4. 它更适合"高价值文档",不一定适合"海量碎片文档"。

    如果你的数据天然就是短文本、FAQ、评论、工单,树索引未必比向量检索更划算。

所以 PageIndex 更像是对传统 RAG 的补位,而不是无条件替代。

十四、如果让我一句话总结它的实现思路

我的理解是:

PageIndex 本质上是在用 LLM 做一套"文档结构恢复系统"。

它先把 PDF 还原成一棵带页码范围的章节树,再把检索问题转化为树上的节点选择问题。这样做的目的,不是提高"相似度召回精度",而是提升"在长文档中定位正确信息位置"的能力。

这也是它最值得关注的地方:

它没有继续围绕 chunk 做优化,而是把重点放在"结构"上。

十五、最后

如果你当前做的是:

  • 年报、研报、制度文档、说明书这类长文档问答
  • 对引用位置、章节边界、可解释性有要求
  • 觉得传统向量召回经常"看起来相关,实际上答非所问"

PageIndex 这类思路很值得研究。

相关推荐
Later2 小时前
Apache Doris 深度讲解:从核心概念到实战项目
后端
攒了一袋星辰2 小时前
SequenceGenerator高并发有序顺序号生成中间件 - 架构设计文档
java·后端·spring·中间件·架构·kafka·maven
码农刚子2 小时前
字符串拼接用“+”还是 StringBuilder?别再凭感觉写了
后端·代码规范
茶杯梦轩2 小时前
面试常问:DNS,CDN,Cookie,Session和Token详解及实战避坑指南
后端·网络协议·面试
Memory_荒年2 小时前
TiDB 单机部署与监控完整指南
运维·数据库·后端
犯困的饭团2 小时前
3_【自动化引擎Ansible Runner】深入功能模块 - 不止于 Playbook
后端
写Cpp的小黑黑2 小时前
WHEP 拉流技术详解(基于一个 html/js demo)
后端
GetcharZp2 小时前
告别 Selenium!这款 Go 语言神器,让网页自动化与爬虫快到飞起!
后端
天下无贼2 小时前
【Python】2026版——FastAPI 框架快速搭建后端服务
后端·python·aigc