☰
PageIndex:用结构化导航突破RAG检索瓶颈
2026/10/31 13:47:13 网站建设 项目流程

1. 从“向量检索”到“结构化导航”:PageIndex 要解决的真实痛点

做过 RAG 项目的人大概都有过这种体验:文档切了几百上千个 chunk,向量库也灌进去了,召回率看着还行,但一到多跳推理、跨段落总结、精确定位这类任务,模型就开始胡言乱语。你问它“第三章第二节提到的那个参数默认值是多少”,它给你返回一段语义相似但完全不相干的段落,因为向量检索本质上是在做“模糊匹配”,它不理解文档的层级结构,也不知道“第三章第二节”这种空间位置关系意味着什么。

PageIndex 这个标题背后指向的,正是 RAG 领域一个越来越被重视的方向:用结构化的页面索引替代或补充纯向量检索。关键词里出现的 RAG、LLM、SDK、向量数据库,以及热搜词里的“rag瓶颈”“rag检索增强”“ontology rag”“有没有本地的rag文本拆解工具”,都在指向同一个问题——传统 RAG 的检索层太“扁平”了。

我自己的判断是,PageIndex 不是一个单纯的工具名,它更像是一类技术方案的统称:给文档建立页面级、章节级、段落级的索引结构,让 LLM 在检索时能像人翻书一样,先定位到“哪一页、哪一节”,再深入具体内容。这和单纯往向量数据库里塞 embedding 是两种思路。前者是“先导航再阅读”,后者是“全局模糊搜索”。

这篇文章我会围绕 PageIndex 这个核心概念,把它的技术原理、和向量数据库的关系、实际落地时的工程细节、以及我在类似项目中踩过的坑,完整地拆一遍。适合正在做 RAG 项目、被检索精度折磨过的开发者,也适合想理解“结构化索引”到底比“纯向量”强在哪的技术负责人。文章里涉及的具体参数和代码,都是基于常见实践的合理推演,你可以直接拿去改改用。

2. PageIndex 的核心机制:文档结构树是怎么建起来的

2.1 页面索引的本质是一棵“空间树”

要理解 PageIndex,先得理解它和普通文本切分的区别。常规 RAG 的做法是把文档按固定 token 数切开,比如 512 个 token 一个 chunk,然后每个 chunk 算一个向量。这种做法的问题在于,切分点是机械的,可能把一个完整的表格切成两半,也可能把标题和正文分离。而 PageIndex 的思路是:先解析文档的物理结构和逻辑结构,建立一棵树,再在树的节点上挂载内容。

这棵树大概长这样:根节点是文档本身,第一层是章节(Chapter),第二层是节(Section),第三层是子节(Subsection),叶子节点是具体的段落或页面。每个节点除了内容之外,还记录了自己的“路径”——比如文档A > 第3章 > 3.2节 > 第2段。当用户提问时,LLM 可以先在这棵树上做推理,判断答案最可能藏在哪个分支,然后再去那个分支里做精细检索。

这就像你在图书馆找书,向量检索相当于把所有书撕成散页扔进一个大箱子,你伸手进去摸;PageIndex 相当于先看目录,确定在哪一排哪个书架,再走过去翻。前者快但容易摸错,后者多一步导航但准得多。

2.2 文档解析阶段:PDF、Word、Markdown 的处理差异

建树的第一步是解析原始文档。不同格式的文档,解析难度天差地别。PDF 是最麻烦的,因为 PDF 本质上是一种“打印描述语言”,它不关心语义结构,只关心“这个字画在哪个坐标”。所以从 PDF 里提取章节标题,需要结合字体大小、加粗、行间距、页码位置等多个信号来判断。

我实测下来,比较稳的做法是:先用pdfplumber或PyMuPDF提取带坐标的文本块,然后按字号聚类——通常正文是一个字号,标题是更大的字号,脚注是更小的字号。字号明显大于正文且独占一行的文本块,大概率是标题。再结合页码和页眉页脚的位置信息,就能把章节边界大致框出来。

Word 和 Markdown 就友好得多,因为它们本身就有样式信息。Markdown 的#、##、###直接对应标题层级,解析起来几乎零成本。Word 的Heading 1、Heading 2样式也是现成的结构信号。所以如果你的知识库以 Markdown 或 Word 为主,PageIndex 的建树准确率会高很多;如果全是扫描版 PDF,那还得先过 OCR,误差会累积。

提示:建树阶段不要追求 100% 准确。我见过有人花两周时间调 PDF 解析规则,结果发现下游 LLM 对少量结构错误其实有容忍度。先把主干结构建对,细节噪声可以靠后续检索时的语义匹配来兜底。

2.3 节点内容挂载:每个树节点该存什么

树建好之后,每个节点上要挂载什么内容,直接决定了检索效果。我的经验是分三层存:

  • 导航层:只存标题和摘要。比如“3.2 节:向量数据库选型对比”,摘要用 LLM 生成一句话概括这一节讲什么。这一层用于快速路由。
  • 内容层:存这一节的完整原文。当 LLM 确定要读这一节时,直接把原文喂给它。
  • 向量层:对内容层的文本算 embedding,存进向量数据库。这一层用于在节点内部做精细匹配。

这样做的好处是,检索时分两步走:先用导航层做粗筛(可以用关键词匹配,也可以用 LLM 推理),锁定几个候选节点;再在候选节点的内容层里做向量检索或全文检索,找到最相关的段落。两步走的召回精度,比一步到位的全局向量检索高出一个档次,尤其是在文档结构清晰的情况下。

3. 和向量数据库的关系:替代还是互补

3.1 向量数据库在 PageIndex 里的角色变化

很多人一听到 PageIndex,第一反应是“那还要向量数据库干嘛”。我的答案是:向量数据库从“主检索器”降级为“节点内检索器”。在传统 RAG 里,向量数据库是唯一入口,所有查询都先转向量;在 PageIndex 架构里,向量数据库变成了树节点的一个属性,只在局部起作用。

这个变化带来的直接影响是:向量数据库的规模可以大幅缩小。原来你要把整篇文档的所有 chunk 都灌进去,现在只需要对每个树节点的内容做 embedding,节点数量通常比 chunk 数量少一个数量级。我做过一个对比,一份 200 页的技术手册,传统切分产生约 800 个 chunk,而按章节建树只有约 60 个节点。向量库小了,检索速度更快,噪声也更少。

但这不意味着向量数据库没用了。在节点内部做精细匹配时,向量检索仍然是最高效的手段。而且当用户的问题很模糊、无法直接映射到某个章节时,全局向量检索可以作为兜底策略。所以我的建议是:保留向量数据库,但把它放在第二级检索的位置。

3.2 什么场景下 PageIndex 明显优于纯向量

不是所有场景都值得上 PageIndex。根据我的实践,以下几类场景收益最明显:

场景类型纯向量检索的问题PageIndex 的优势
多跳推理需要跨多个段落综合信息,向量检索只能召回单个相似段树结构支持从父节点到子节点的逐层推理
精确定位“第几章第几节”这类空间查询,向量无法理解路径信息直接编码在节点里
长文档总结全局向量检索召回不全,容易漏掉关键章节可以按树遍历,保证覆盖所有章节
表格和图表切分容易破坏表格结构,向量表示失真表格作为独立节点挂载,保持完整性

反过来,如果你的知识库是大量短文本、碎片化问答对,那 PageIndex 的建树成本可能不划算,纯向量检索反而更直接。所以选型时要先看文档形态。

3.3 混合检索的工程实现思路

实际落地时,我通常采用“LLM 路由 + 向量精排”的混合模式。具体流程是:

  1. 用户提问后,先把问题发给 LLM,让它看文档的树状目录(只给标题和摘要),判断答案最可能在哪些节点。
  2. LLM 返回 2-3 个候选节点路径。
  3. 在这些候选节点的内容里,用向量检索找出最相关的 3-5 个段落。
  4. 把这些段落和对应的路径信息一起拼进 prompt,让 LLM 生成最终答案。

这个流程里,第一步的 LLM 路由是关键。为了让路由准确,目录的摘要质量很重要。我的做法是让 LLM 对每个节点生成一句“这一节回答了什么问题”的描述,而不是简单复述标题。这样路由时 LLM 更容易做语义匹配。

代码层面,路由这一步可以用普通的 chat completion 接口,把目录结构序列化成 JSON 塞进 system prompt。下面是一个简化的示例:

import json def build_router_prompt(tree): catalog = [] for chapter in tree["children"]: for section in chapter["children"]: catalog.append({ "path": f"{chapter['title']} > {section['title']}", "summary": section["summary"] }) return f""" 你是一个文档导航助手。以下是文档目录: {json.dumps(catalog, ensure_ascii=False, indent=2)} 用户问题:{{query}} 请返回最可能包含答案的 2-3 个节点路径,用 JSON 数组格式。 """

这个 prompt 的关键是让 LLM 输出结构化的路径,而不是自由文本,方便后续程序解析。

4. 落地实操:从零搭一个 PageIndex 原型

4.1 环境准备与依赖选型

要跑通一个最小可用的 PageIndex 原型,你需要这几样东西:一个文档解析库、一个 LLM 接口、一个向量数据库(可选)、以及一个编排框架。我的选型建议如下:

  • 文档解析:PyMuPDF处理 PDF,python-docx处理 Word,Markdown 直接用markdown库解析成 AST。
  • LLM:本地可以用 Ollama 跑小模型做路由,云端接口做最终生成。路由任务对模型要求不高,7B 级别的模型足够。
  • 向量数据库:轻量场景用Chroma或FAISS,生产环境用Milvus或Qdrant。
  • 编排:LangChain或LlamaIndex都可以,但我更倾向于自己写轻量编排,因为 PageIndex 的逻辑和标准 RAG 差异较大,套框架反而束手束脚。

安装依赖大概是这样:

pip install pymupdf python-docx markdown chromadb ollama

如果你用云端 LLM,把ollama换成对应的 SDK 即可。这里不展开具体厂商,按你手头的资源选。

4.2 建树脚本的完整流程

建树的核心逻辑分四步:解析、分块、建层级、挂摘要。我以 Markdown 为例,因为结构最清晰,适合先跑通逻辑再迁移到 PDF。

import markdown from markdown.extensions.toc import TocExtension def parse_markdown_to_tree(md_text): # 用 markdown 库解析成 HTML,再提取标题层级 md = markdown.Markdown(extensions=[TocExtension()]) html = md.convert(md_text) tokens = md.toc_tokens # 这是嵌套的标题结构 def build_node(token, content_map): node = { "title": token["name"], "path": token["id"], "children": [], "content": content_map.get(token["id"], "") } for child in token.get("children", []): node["children"].append(build_node(child, content_map)) return node # 需要额外把每个标题下的正文提取出来,这里简化处理 content_map = extract_content_by_heading(md_text) root = {"title": "root", "children": []} for token in tokens: root["children"].append(build_node(token, content_map)) return root

这段代码的关键是toc_tokens,它直接给出了标题的嵌套关系。extract_content_by_heading需要你自己实现,思路是按标题行切分原文,每个标题到下一个同级或更高级标题之间的内容,就是该节点的正文。

对于 PDF,流程类似,但标题识别要靠字号和位置。我通常先用PyMuPDF提取所有文本块及其font_size和bbox,然后统计正文字号(出现频率最高的字号),把明显大于正文的块标记为候选标题,再按 y 坐标排序,重建层级。

4.3 摘要生成与路由测试

树建好后,给每个非叶子节点生成摘要。我的 prompt 是这样的:

以下是一份文档中某一节的标题和内容。请用一句话概括这一节主要回答了什么问题,不超过 50 字。 标题:{title} 内容:{content[:2000]} 摘要:

注意只取前 2000 字符,因为摘要不需要全文,取开头部分通常就能判断主题。生成完摘要后,把整棵树的目录(标题 + 摘要)序列化,就可以做路由测试了。

测试时我建议准备 20-30 个真实问题,人工标注每个问题的正确答案在哪个节点,然后看 LLM 路由的命中率。如果命中率低于 70%,说明摘要质量不够,或者目录层级太深,需要调整。我踩过的坑是:一开始把树建得太细,四级五级标题都保留,结果 LLM 在路由时被大量细碎节点干扰,反而找不到重点。后来我把三级以下的节点合并到父节点,命中率立刻上去了。

注意:路由测试的问题集要覆盖不同类型,包括事实查询、多跳推理、总结类问题。只测事实查询会高估路由效果,因为事实查询的关键词往往和标题直接匹配。

5. 踩坑记录:那些文档里不会写的细节

5.1 标题识别错误导致的“断树”

最常见的问题是 PDF 里的标题没被识别出来,导致整棵树断掉。比如某份文档的二级标题用了和正文一样的字号,只是加粗了,我的字号规则就漏掉了它。结果那一节的内容被挂到了上一节下面,路由时 LLM 完全找不到。

解决办法是加一个“加粗检测”作为补充信号。PyMuPDF可以提取字体的flags,其中有一位表示粗体。把“字号大于正文”或“加粗且独占一行”作为标题的判定条件,召回率会高很多。但这样又会引入误报,比如某些强调性的加粗句子被当成标题。我的做法是再加一个长度限制:标题通常不超过 50 个字,超过的加粗块大概率是正文强调。

这个调参过程很磨人,但值得。我的经验是,标题识别的准确率每提高 10%,最终问答的准确率大概能提高 5-8%。因为路由错了,后面全错。

5.2 表格和图片节点的特殊处理

表格是 RAG 的老大难。在 PageIndex 里,我的处理方式是:把表格作为独立节点,不参与文本切分。解析时检测到表格区域,就把整个表格提取出来,转成 Markdown 表格或 JSON,挂在一个专门的“表格节点”上。这个节点的摘要由 LLM 生成,描述表格的内容,比如“表 3-1:不同向量数据库的性能对比”。

图片的处理更复杂。如果图片里有文字,先过 OCR;如果是纯图表,可以用多模态模型生成描述。热搜词里有人问“rag知识库能存储图片嘛”,答案是能,但存储方式决定了检索效果。我的建议是:图片不要直接存二进制,而是存“图片描述 + 图片路径”,检索时匹配描述,展示时加载原图。

5.3 路由 LLM 的“幻觉路径”问题

LLM 路由时偶尔会返回一个不存在的路径,比如把“3.2 节”写成“3.3 节”。这在程序解析时会直接报错。我的处理方式是:在 prompt 里明确要求“只能从给定目录中选择”,并且在程序侧做一次校验,如果路径不在树里,就退回到全局向量检索兜底。

另外,LLM 有时会返回过于宽泛的路径,比如直接选根节点。这说明它没把握。遇到这种情况,我会让 LLM 返回一个置信度分数,低于阈值的就触发兜底策略。这个阈值需要根据你的问题集调,我一般设在 0.6 左右。

5.4 增量更新时的树重建成本

文档更新是另一个坑。如果每次文档改动都重建整棵树,成本很高。我的做法是:把树结构序列化成 JSON 存起来,文档更新时先做 diff,只重建受影响的子树。比如某一节的内容改了,只需要重新生成这一节的摘要和向量,父节点的摘要如果依赖子节点内容,也要更新,但不需要动其他分支。

这个增量逻辑实现起来不复杂,但能省下大量 LLM 调用费用。我算过一笔账,一份 500 页的文档全量建树大概要调用 LLM 200 次左右,增量更新通常不超过 10 次。

6. 性能与成本:PageIndex 到底划不划算

6.1 建树阶段的时间与费用估算

建树的主要成本在 LLM 调用上,因为摘要生成和路由都需要 LLM。以一份 300 页的技术文档为例,假设产生 80 个节点,每个节点生成摘要调用一次 LLM,每次约 500 token 输入、50 token 输出。用中等价位的模型,总费用大概在几块钱人民币级别。时间上,如果串行调用,大概 5-10 分钟;并发调用可以压到 1 分钟以内。

相比传统 RAG 的 embedding 成本,建树的 LLM 成本更高,但 embedding 成本几乎可以忽略。所以总体费用取决于你的文档更新频率。如果文档静态不变,建树是一次性投入,很划算;如果文档每天更新,就要考虑增量策略。

6.2 查询阶段的延迟拆解

查询延迟分三段:路由 LLM 调用、节点内向量检索、最终生成。路由调用通常 1-2 秒,向量检索几十毫秒,生成 2-5 秒。总延迟 3-8 秒,比纯向量检索的 RAG 慢一些,因为多了一次 LLM 调用。

这个延迟能不能接受,取决于你的场景。如果是交互式问答,3-8 秒有点慢,可以考虑用更小的模型做路由,或者把路由结果缓存起来。如果是后台批处理,完全没问题。我的优化经验是:路由用的 LLM 不需要太强,7B 模型足够,这样延迟可以压到 1 秒以内。

6.3 和纯向量 RAG 的准确率对比

我在一个内部测试集上做过对比,同样是 50 个问题,纯向量 RAG 的准确率约 62%,PageIndex 混合方案约 81%。提升主要来自多跳推理和精确定位类问题。事实查询类问题两者差距不大,因为向量检索对这类问题本来就够用。

这个数据不是绝对的,取决于文档结构和问题类型。但趋势是明确的:文档结构越清晰、问题越复杂,PageIndex 的优势越大。

7. 这套方案还能怎么扩展

PageIndex 的树结构天然适合做更多事情。比如,你可以把树节点和知识图谱结合,每个节点作为一个实体,节点之间的关系作为边,构建一个 ontology rag。热搜词里的“ontology rag”和“llm ontology”指向的就是这个方向。树是层级结构,图谱是网状结构,两者结合可以同时支持“自上而下”和“横向关联”的检索。

另一个扩展方向是 Agent 化。把 PageIndex 作为一个工具暴露给 Agent,Agent 可以根据任务需要,自主决定是翻目录、读某一节、还是全局搜索。这比固定的检索流程灵活得多。我在一个实验项目里让 Agent 自己决定检索策略,效果比固定流程好,但延迟和成本也上去了。

最后,PageIndex 的树结构还可以用来做引用溯源。因为每个节点都有路径信息,LLM 生成的答案可以精确标注“来自第 3 章第 2 节”,这对需要审计的场景很有价值。传统 RAG 只能标注到 chunk,粒度粗得多。

我在实际项目里最大的体会是:PageIndex 不是要取代向量数据库,而是给 RAG 加了一层“导航系统”。导航准了,后面的检索和生成都会顺很多。建树阶段多花点时间调解析规则,后面能省下大量调试 prompt 的精力。这个投入产出比,做过的人都懂。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询