最近在看 PageIndex 这个项目,最大的感受是:它不是在继续优化"切 chunk + 向量召回"这条路,而是换了一个方向,试图让大模型像人读文档一样,先找到"该去哪一章",再决定"该读哪几页"。
这篇文章不做产品介绍,也不铺太多概念,主要讲一下我基于源码和文档理解出来的 PageIndex 实现思路。重点会放在两个问题上:
PageIndex为什么不走传统 Vector RAG。- 它的树形索引到底是怎么构建出来的。
一、为什么传统 Vector RAG 在长文档里经常不够稳
传统 RAG 的典型流程大概是这样:
- 先把文档切成很多 chunk。
- 对每个 chunk 生成 embedding。
- 查询时把问题向量化,做 Top-K 相似度召回。
- 再把召回结果交给大模型生成答案。
这个方案在很多通用场景是成立的,但在金融报告、技术手册、制度文档、合同这类"强结构、强上下文依赖"的长文档里,问题会比较明显:
-
语义相似,不等于真正相关。
两段文字可能用了几乎一样的术语,但讨论的根本不是一件事。
-
chunk 容易破坏结构。
原始文档里的"章节关系、上下位关系、页码范围"被切碎之后,模型拿到的是离散片段,不是完整结构。
-
很多问题本质上是"导航问题",不是"相似度问题"。
比如"某个风险披露在什么章节""某个指标定义在正文还是附录",这类问题更像先定位目录,再进入正文。
PageIndex 的核心思路,就是把这个过程改成:
- 先给文档建立一棵树。
- 再让模型在树上做推理式检索。
也就是说,它先解决"去哪里找",再解决"找到了读什么"。
二、PageIndex 的核心设计:先建树,再检索
PageIndex 可以概括成两步:
- 把 PDF 或 Markdown 转成层级化的 tree index。
- 查询时让 LLM 基于 tree index 选择相关节点,再回到原文取内容。
这里最关键的是第一步。因为只要树建得足够稳定,后面的检索其实就有点像:
- 先在"目录树"里选章节
- 再根据章节映射到页码范围
- 最后读取对应正文作为上下文
所以 PageIndex 的核心难点,不在"怎么做相似度召回",而在"怎么从一份结构并不总是可靠的 PDF 里,尽量稳定地恢复出树结构"。
三、它的实现主线:从 PDF 到 Tree Index
如果只看主流程,PageIndex 的 PDF 处理链路可以理解成下面这几步:
text
输入 PDF
-> 提取每页文本与 token 数
-> 检测是否存在目录页
-> 抽取目录内容
-> 判断目录是否带页码
-> 对齐逻辑页码与物理页码
-> 校验目录项是否靠谱
-> 修复错误页码
-> 转成树结构
-> 补充 node_id / text / summary
-> 输出 structure.json
下面分开说。
四、第一步:先把 PDF 变成"可计算对象"
源码里最基础的一层,是先把 PDF 逐页解析出来,并统计每一页的 token 数。
这么做有两个直接目的:
- 后面做目录检测、章节定位时,LLM 看到的输入是按页组织的。
- 后面需要按 token 上限分组,避免一次喂给模型太长的内容。
这一层本身不复杂,但它很重要,因为后面几乎所有步骤都依赖"页"这个最小单位:
- 哪一页可能是目录页
- 某个标题应该落在哪一页
- 某个节点覆盖哪些页
所以 PageIndex 的索引不是建立在 chunk 上,而是先建立在"页面 + 结构关系"上。
五、第二步:先判断文档有没有目录
这是整个流程里很关键的分叉点。
PageIndex 不会默认假设 PDF 一定有可用目录,而是先检测前几页是不是目录页。根据源码里的处理思路,大致会分成三种情况:
- 有目录,而且目录带页码。
- 有目录,但是目录不带页码。
- 没有可用目录。
这三种情况的后续成本差异非常大。
1. 有目录且带页码
这是最理想的情况。
因为这时候模型只需要:
- 把目录页提取出来。
- 转成结构化 JSON。
- 再把目录里的页码和 PDF 的物理页做一次对齐。
这条路径最省,因为不需要扫描整本书去猜每一章从哪一页开始。
2. 有目录但不带页码
这时候目录只能提供"章节层级",不能直接提供"位置"。
所以系统需要再去正文里扫描,逐步判断每个标题最可能出现在哪一页。也就是说,目录能帮你确定树,但不能帮你确定节点边界。
3. 没有目录
这是最贵的一条路径。
系统只能把全文按 token 分组,然后让 LLM 从正文里归纳出章节结构,再逐步续写整个目录树。这个过程本质上是在"从正文反推目录"。
所以 PageIndex 的一个很现实的工程特征是:
文档本身越规整,尤其是目录越清晰,它的构建成本就越低,稳定性也越高。
六、第三步:把目录内容转成结构化 JSON
拿到目录页之后,接下来不是直接建树,而是先把目录文本转成结构化列表。
这里的难点有两个:
-
PDF 里的目录文本经常很脏。
比如换行错乱、页码和标题混在一起、层级缩进不稳定。
-
目录可能非常长。
一次生成不完整,就得让模型继续往后续写。
所以 PageIndex 在这里做的不是一次性抽取,而更像一个"生成 + 检查 + 续写"的循环:
- 先让模型把目录转成 JSON。
- 再检查这次转换是否完整。
- 如果不完整,继续补全。
- 直到拿到一个足够完整的结构列表。
从实现思路上看,这一步非常像"半结构化信息抽取",而不是普通摘要。
它的目标不是让模型理解目录,而是让模型输出一个后续可计算、可修复、可验证的中间结果。
七、第四步:页码对齐,是整套方案里最容易被低估的一环
很多人第一次看这类方案,都会觉得"目录里不是已经有页码了吗,直接用不就行了"。
但 PDF 里经常存在两套页码:
- 文档正文里的逻辑页码
- PDF 文件自身的物理页序号
比如封面、版权页、前言、目录这些页面,往往会导致两者出现偏移。
所以 PageIndex 不会直接相信目录页码,而是会做一次"页码对齐":
- 先估算目录页码和物理页码之间的 offset。
- 再把目录里的章节页码映射到真实 PDF 页序。
- 最后得到每个节点的真实起始位置。
如果目录不带页码,那就更进一步,直接去正文中定位标题首次出现的页面。
这一层做得好不好,决定了后面节点边界准不准。因为一旦某一章的起始页错了,整棵树往后都会发生连锁偏移。
八、第五步:不是抽出来就完了,还要校验和修复
这是 PageIndex 很工程化的一点。
很多文档解析方案到"LLM 输出结构化结果"就结束了,但 PageIndex 还多做了一层验证。
它会抽查目录项,去对应页里验证:
- 这个标题是不是真的出现在这里
- 它是不是出现在比较合理的位置
如果发现某些条目不对,就进入修复流程,再重新定位这些章节的页码。
这个设计说明它默认接受一个事实:
LLM 在目录抽取和页码对齐上并不总是可靠,所以必须允许"先生成,再纠错"。
这套思路很像传统数据工程里的 ETL:
- 先抽取
- 再校验
- 最后修复异常
只是这里的"抽取器"和"修复器"都换成了大模型。
九、第六步:从平铺目录转成真正的树
当前面拿到的是一个平铺的目录列表之后,系统才会做真正的树构建。
这一步主要完成三件事:
- 根据标题层级关系,把线性目录转成父子嵌套结构。
- 为每个节点计算
start_index和end_index。 - 对过大的节点继续递归拆分。
这里的 start_index / end_index 很重要,因为它们决定了后面检索时到底取哪些页。
比如:
- 一级标题可能覆盖第 20 到 60 页
- 它下面的二级标题再把 20 到 60 页继续细分
这样一来,整棵树不仅有"层级信息",也有"范围信息"。
为什么还要递归拆分大节点
因为即便目录存在,某些章节也可能非常长。
如果一个节点动不动覆盖几十页甚至上百页,那么检索时即使定位到了这个节点,也还是太粗了。于是 PageIndex 会对过大的节点继续深入处理,把大章节再细分出更深一层的子节点。
这一步体现了它和普通目录抽取工具的区别:
它不是只想把目录抄出来,而是想得到一棵"足够适合后续检索"的树。
十、第七步:给树补充可检索字段
树构建完成后,系统还会按配置补一些增强信息,常见的包括:
-
node_id给每个节点分配唯一 ID,方便检索阶段引用。
-
text把节点覆盖页的原文挂到节点上,后面可以直接取。
-
summary给节点生成摘要,方便后续做更轻量的检索或展示。
-
doc_description给整篇文档生成一句话描述。
这一步说明 PageIndex 输出的不是一个简单目录,而是一个可继续加工的"中间索引层"。
你可以把它理解成:
- 目录是骨架
- 页码范围是定位能力
- text 和 summary 是检索材料
- node_id 是引用锚点
十一、检索阶段其实很像"在树上做导航"
当 tree index 已经建好后,查询流程就比较顺了。
大致可以理解成下面这样:
text
用户问题
-> 把问题和树结构一起交给 LLM
-> LLM 选择相关 node_id
-> 根据 node_id 找到对应页码范围
-> 读取节点正文
-> 再交给 LLM 组织最终答案
这和传统向量召回最大的区别是:
它不是直接问"哪几个 chunk 最像这个问题",而是先问"这个问题最该去文档的哪一层、哪几个章节里找"。
这就是为什么我觉得 PageIndex 更像一种"结构化导航式 RAG"。
十二、这种方案为什么在专业长文档里有价值
我觉得 PageIndex 真正有价值的地方,不在于"完全替代向量检索",而在于它抓住了长文档检索里一个经常被忽略的事实:
很多问题的正确答案,依赖的不是相似度,而是文档结构。
尤其在下面这些场景里,这种思路会更有优势:
- 金融报告、招股书、监管文件
- 技术标准、产品说明书、制度文档
- 教材、论文、法律文本
因为这些文档往往天然具备清晰章节层级,用户的问题也常常和"某一节讨论什么、某一章是否提到某个点"强相关。
这时候,如果先用树结构做导航,再读具体内容,通常比直接做 chunk 相似度召回更稳。
十三、它的代价也很明显
PageIndex 并不是没有成本,反而它的代价很清楚:
-
索引构建高度依赖 LLM。
目录提取、页码补齐、校验修复、摘要生成,都会消耗调用次数和 token。
-
文档越不规整,成本越高。
没有目录、目录混乱、页码体系不一致时,处理链路会明显变长。
-
超大目录和超长文档会带来上下文压力。
一旦目录特别长,结构抽取和续写的稳定性就会下降。
-
它更适合"高价值文档",不一定适合"海量碎片文档"。
如果你的数据天然就是短文本、FAQ、评论、工单,树索引未必比向量检索更划算。
所以 PageIndex 更像是对传统 RAG 的补位,而不是无条件替代。
十四、如果让我一句话总结它的实现思路
我的理解是:
PageIndex 本质上是在用 LLM 做一套"文档结构恢复系统"。
它先把 PDF 还原成一棵带页码范围的章节树,再把检索问题转化为树上的节点选择问题。这样做的目的,不是提高"相似度召回精度",而是提升"在长文档中定位正确信息位置"的能力。
这也是它最值得关注的地方:
它没有继续围绕 chunk 做优化,而是把重点放在"结构"上。
十五、最后
如果你当前做的是:
- 年报、研报、制度文档、说明书这类长文档问答
- 对引用位置、章节边界、可解释性有要求
- 觉得传统向量召回经常"看起来相关,实际上答非所问"
那 PageIndex 这类思路很值得研究。