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