为什么不用向量?
传统的向量 RAG 为什么在专业文档上频频翻车?PageIndex 提出了一条全新路线——用"推理"取代"相似度"。
想象你在一个图书馆找答案
传统 RAG 的工作方式像一个图书管理员,你问一个问题,他根据"关键词相似度"去书架上找最像的段落。问题是:相似不等于相关。
比如你问"苹果公司的净利润",向量搜索可能返回一段写着"果园苹果丰收"的文字——因为"苹果"这个词很像。这种"字面相似但语义无关"的灾难在专业文档中尤其常见。
向量搜索本质上是一种"模式匹配"——它测量的是两段文字在数学空间中的距离。但真正的信息检索需要推理能力:理解上下文、多步判断、知道去哪里找。PageIndex 的哲学就是:让 LLM 像人类专家一样"思考着检索"。
两种 RAG 路线:向量 vs 推理
传统向量 RAG
把文档切成碎片 → 转成数学向量 → 按距离排序取 Top-K → 喂给 LLM。像是在图书馆里根据关键词标签盲目翻找。
PageIndex 推理 RAG
把文档组织成层级树索引 → LLM 像翻目录一样逐层推理 → 精确定位目标章节 → 取出完整上下文。像专家先看目录,再翻到具体章节。
# PageIndex 核心入口:一行调用即可生成树索引
from pageindex import page_index
result = page_index(
doc="annual_report.pdf",
model="gpt-4o",
if_add_node_summary='yes',
if_add_node_id='yes'
)
# result 返回层级树结构,包含每个节点的
# title, node_id, start_index, end_index, summary
从 pageindex 包导入核心函数
调用 page_index,传入 PDF 文件路径
指定用 GPT-4o 模型来理解和分析文档
让每个节点都带上摘要——方便后续推理检索
给每个节点分配唯一 ID——方便精确定位
返回结果是一个嵌套的树形结构
每个节点有:标题、ID、起止页码、摘要
PageIndex 的四大核心优势
省去了 Embedding 转换、向量存储、Chunking 分块等一整套基础设施
LLM 看到目录结构后会"思考"该去哪个分支找答案,像人类专家一样逐步定位
返回的是完整的章节内容(带页码和段落位置),不是被切碎的片段。每个结果都能追溯到原文的具体位置
检索结果会根据你的完整对话历史和领域知识动态调整——不像向量搜索只看当前问题的 Query
真实效果:FinanceBench 基准测试
PageIndex 驱动的 Mafin 2.5 系统在金融文档问答基准测试 FinanceBench 上达到了 98.7% 的准确率,显著超越所有传统向量 RAG 方案。
98.7%
FinanceBench 准确率——在真实 SEC 财报上的问答正确率
30K+ ⭐
GitHub 星标——开发者社区对无向量 RAG 方案的认可
0 个向量
完全不使用向量数据库,无需 Embedding 模型,无需分块策略
传统 RAG vs PageIndex:架构对比
点击每个组件了解它的角色。
传统向量 RAG
PageIndex 推理 RAG
你正在为一家投资公司搭建财报分析系统,文档都是 200 页以上的 SEC 文件。传统向量 RAG 的回答经常"驴唇不对马嘴"。PageIndex 的核心改进是什么?
为什么向量搜索在专业文档上容易出错?
树索引的诞生
PageIndex 如何把一份 200 页的 PDF 变成一棵可推理的层级树?从检测目录到递归细分,每一步都是 LLM 在"思考"。
什么是"树索引"?
想象一本教科书:最外面是"章",里面是"节",再里面是"小节"。PageIndex 做的事情就是让 LLM 把一份文档自动整理成这样的层级结构——就像一张增强版的目录。
PageIndex 的灵感来自 AlphaGo 的蒙特卡洛树搜索——不是暴力遍历所有可能,而是在树结构上"有方向地探索"。LLM 看到目录后推理"答案最可能在哪个分支",然后只深入那个分支。
{
"title": "Financial Stability",
"node_id": "0006",
"start_index": 21,
"end_index": 22,
"summary": "The Federal Reserve ...",
"nodes": [
{
"title": "Monitoring Vulnerabilities",
"node_id": "0007",
"start_index": 22,
"end_index": 28
}
]
}
节标题——"金融稳定"
节点 ID——全局唯一编号
起始页码——该节从第 21 页开始
结束页码——该节到第 22 页结束
AI 生成的摘要——概括该节内容
子节点列表——该节下的更细分区
子节点同样有标题、ID 和页码范围
这就是树结构的精髓:层层嵌套,精确定位
从 PDF 到树:五步流程
用 PyPDF2/PyMuPDF 把每一页的文字和 token 数量提取出来,为后续处理做准备
LLM 逐页扫描前 20 页,判断"这一页有没有目录?"。如果有,直接提取并结构化;如果没有,进入"无目录模式"
meta_processor 根据文档特征自动选择:有目录+有页码 → 直接对齐;有目录+无页码 → 逐页定位;无目录 → 从零构建
对结果抽样验证,让 LLM 检查"这个标题真的出现在这一页吗?"。准确率低于 100% 的部分自动重试修复(最多 3 轮)
如果某个节点超过 10 页或 20000 token,自动对该节点执行同样的树构建流程——直到每个节点都足够精细
树索引生成:数据在流动
点击"下一步",看数据如何从 PDF 文件一步步变成一棵完整的树索引。
关键代码:递归处理大节点
当某个节点太大了(超过 10 页或 20000 token),PageIndex 会自动对该节点执行完整的树构建流程——这就是"递归"的威力。
async def process_large_node_recursively(
node, page_list, opt=None, logger=None):
node_page_list = page_list[
node['start_index']-1:node['end_index']]
if (node['end_index'] - node['start_index']
> opt.max_page_num_each_node):
node_toc = await meta_processor(
node_page_list,
mode='process_no_toc',
start_index=node['start_index'],
opt=opt, logger=logger)
node['nodes'] = post_processing(
node_toc, node['end_index'])
if 'nodes' in node and node['nodes']:
tasks = [
process_large_node_recursively(
child, page_list, opt, logger)
for child in node['nodes']
]
await asyncio.gather(*tasks)
定义递归函数,接收一个节点和页面列表
先取出该节点对应的页面子集
判断:如果这个节点跨度超过限制页数
对这个子集重新执行完整的树构建流程
使用"无目录模式"——从头构建层级结构
把构建结果挂到当前节点的 nodes 字段
递归入口:如果子节点存在且不为空
为每个子节点创建一个异步任务
所有子任务并行执行——提高速度
组件之间的"对话"
树索引的生成过程就像多个角色在协作。点击消息,看它们如何配合。
一份 150 页的文档,前 5 页有目录但目录中没有页码。meta_processor 会选择哪条处理路径?
验证环节是如何工作的?
推理式检索
树索引建好了,接下来怎么用?LLM 像人类专家一样"翻目录"——逐层推理,精确定位,取出完整上下文。
人类专家怎么查文档?
想象你是一个金融分析师,需要在一本 300 页的年报里找到"2024 年 Q3 的研发支出"。你会:
翻到目录
看到"财务报表"章节
推理:研发支出在"利润表"下
翻到 47-48 页,找到答案
PageIndex 让 LLM 执行完全相同的推理过程。它不是"搜索",而是在树上导航。
向量搜索是"一次性的":把问题变成向量,计算距离,返回结果。推理式检索是"多步骤的":LLM 先看顶层目录,判断该去哪个分支,然后深入子节点,逐步缩小范围——每一步都在推理。
Agent 的三个工具
在 Agent 模式下,AI 有三个核心工具来完成检索:
get_document()
获取文档元信息——名称、描述、页数、处理状态。就像先看看这本书的封面和简介。
get_document_structure()
获取完整的树索引(不含正文)。Agent 看到目录后推理"答案在哪个分支"。这是推理式检索的核心。
get_page_content(pages)
取出指定页码的正文内容。Agent 推理到具体页码范围后,用这个工具获取原始文本。
Agent 的 System Prompt
这段系统提示词定义了 Agent 的行为规范——它告诉 AI "先看结构,再取内容,用最少的页面"。
AGENT_SYSTEM_PROMPT = """
You are PageIndex, a document QA assistant.
TOOL USE:
- Call get_document() first to confirm
status and page/line count.
- Call get_document_structure() to identify
relevant page ranges.
- Call get_page_content(pages="5-7")
with tight ranges; never fetch
the whole document.
Answer based only on tool output.
"""
定义 Agent 的身份:PageIndex 文档问答助手
第一步:调用 get_document() 确认文档状态和页数
第二步:调用 get_document_structure() 获取树结构
根据目录推理出相关页码范围
第三步:用精确的页码范围获取内容
关键规则:永远不要一次获取整个文档
回答只能基于工具返回的内容,不能瞎编
推理检索:一步步发生什么
用户问一个问题,Agent 自动推理、调用工具、找到答案。点击"下一步"看完整流程。
PageIndex 系统架构
点击每个组件,了解它在系统中的角色。
用户层
Agent 层
工具层
数据层
找 Bug 挑战
下面这段代码是 PageIndex Agent 的工具注册代码,但藏着一个错误。点击你认为有 Bug 的那一行。
找出这段代码中的问题:
@function_tool
def get_page_content(pages: str) -> str:
"""Get text of specific pages."""
return client.get_page_content(doc_id, pages)
agent = Agent(
tools=[get_document, get_page_content],
如果用户问"这份财报的净利润是多少?",Agent 最合理的工具调用顺序是什么?
为什么 Agent 被要求"永远不要一次获取整个文档"?
从索引到对话
PageIndexClient 把索引、存储和检索整合成一个优雅的 API。从一行代码索引文档到 Agent 自动问答,全流程代码解读。
一个 Client,三个角色
PageIndexClient 是整个系统的门面。它同时扮演三个角色:
调用 client.index("report.pdf"),自动检测文件类型(PDF/Markdown),生成树索引,存入 workspace
调用 client.get_document_structure(doc_id) 和 client.get_page_content(doc_id, "5-7"),按需检索
索引结果自动保存到文件系统(Workspace),重启后自动加载历史索引
核心代码:index() 方法
这是 PageIndexClient 最核心的方法。一行调用,背后自动完成:文件类型检测 → 树索引生成 → 页面文本提取 → 持久化存储。
def index(self, file_path: str,
mode: str = "auto") -> str:
doc_id = str(uuid.uuid4())
ext = os.path.splitext(file_path)[1].lower()
if mode == "pdf" or (mode == "auto"
and ext == '.pdf'):
result = page_index(
doc=file_path,
if_add_node_summary='yes',
if_add_node_text='yes',
if_add_node_id='yes',
if_add_doc_description='yes')
# Extract per-page text
pages = []
with open(file_path, 'rb') as f:
pdf_reader = PyPDF2.PdfReader(f)
for i, page in enumerate(
pdf_reader.pages, 1):
pages.append({'page': i,
'content': page.extract_text()})
# ... save to workspace
return doc_id
index 方法:传入文件路径,返回文档 ID
生成唯一标识符 UUID
根据文件扩展名自动判断类型
如果是 PDF(auto 模式自动检测)
调用 page_index 生成树索引
开启摘要——每个节点带 AI 摘要
开启文本——每个节点带原始文本
开启节点 ID——方便精确定位
开启文档描述——一句话概括整份文档
额外提取每页文本,供后续查询使用
这样后续检索就不需要原始 PDF 了
保存到 workspace 并返回文档 ID
检索工具:灵活的页码查询
get_page_content 支持多种页码格式,让 Agent 能精确描述它需要的内容范围。
def _parse_pages(pages: str) -> list:
result = []
for part in pages.split(','):
part = part.strip()
if '-' in part:
start = int(part.split('-',1)[0])
end = int(part.split('-',1)[1])
result.extend(
range(start, end + 1))
else:
result.append(int(part))
return sorted(set(result))
页码解析函数,支持多种格式
先用逗号分割,处理多个页码段
如果包含连字符,说明是范围
例如 "5-7" → [5, 6, 7]
展开为连续的页码列表
否则是单个页码,直接转整数
最终去重并排序返回
"5-7"
范围查询——取第 5、6、7 页的内容
"3,8"
列表查询——取第 3 页和第 8 页的内容
"12"
单页查询——只取第 12 页
"5-7,12,20-22"
混合查询——所有格式可以自由组合
PageIndex 项目结构
点击看每个文件的职责。
概念匹配挑战
把左侧的概念拖到右侧正确的描述位置。测试你对 PageIndex 核心概念的理解。
文档的层级目录结构,每个节点有 title、node_id、start_index、end_index、summary
中枢调度器,根据文档特征自动选择最优处理路径,失败时自动降级
本地文件存储目录,索引结果以 JSON 持久化,重启后自动加载复用
LLM 抽样检查章节标题是否出现在标注页码,不准的自动重试最多 3 轮
超过 10 页或 20000 token 的节点自动执行完整树构建流程,直到足够精细
PageIndexClient 支持哪些文件格式?
你有一个应用需要索引 50 份 PDF 并长期使用。PageIndexClient 的 Workspace 是怎么工作的?
实战与设计哲学
从配置参数到部署方式,从设计理念到扩展方向。掌握 PageIndex 背后的工程哲学,才能在实际项目中用好它。
config.yaml:调节索引质量的旋钮
PageIndex 的行为由配置文件控制。理解这些参数,就能根据文档特点调出最佳效果。
# pageindex/config.yaml
model: "gpt-4o-2024-11-20"
retrieve_model: "gpt-5.4"
toc_check_page_num: 20
max_page_num_each_node: 10
max_token_num_each_node: 20000
if_add_node_id: "yes"
if_add_node_summary: "yes"
if_add_doc_description: "no"
if_add_node_text: "no"
索引生成用的 LLM 模型——推荐 GPT-4o
检索推理用的模型(默认和索引模型相同)
扫描前 20 页检测目录——大多数文档目录在前 20 页
每个节点最多 10 页——超过就递归细分
每个节点最多 20000 token——防止超出上下文窗口
给每个节点分配唯一 ID
为每个节点生成 AI 摘要——推理检索的关键
是否生成文档级别的一句话描述
是否在节点中包含原始文本(默认关闭节省空间)
PageIndex 的五大设计原则
推理优于统计
向量搜索本质是统计学方法——计算数学距离。但真正的信息检索需要推理:理解问题含义、判断相关性、多步定位。用"思考"替代"计算"。
结构优于碎片
传统 RAG 把文档切成固定大小的碎片,破坏了原始结构。PageIndex 保留文档的自然层级——章、节、小节——让 LLM 像翻书一样导航。
自我验证与修复
不盲目信任 LLM 的第一次输出。每个定位结果都要验证——"标题真的在这一页吗?"不准的自动重试,最多 3 轮。准确率从 ~85% 提升到 ~99%。
优雅降级
meta_processor 有三条路径,从最高效到最可靠。如果快速路径(有目录+有页码)准确率不够,自动降级到更稳健的方式。最坏情况也能从零构建。
并发加速
树索引生成过程中,验证、修复、摘要生成都是 asyncio.gather 并发执行的。25 个节点的摘要生成,并发只需要一次调用的时间。
并发之美:同时生成所有摘要
传统方式是逐个节点串行生成摘要,25 个节点要调用 25 次 LLM。PageIndex 用 asyncio.gather 并发执行,时间从 N 次降低到 1 次。
async def generate_summaries_for_structure(
structure, model=None):
nodes = structure_to_list(structure)
tasks = [
generate_node_summary(
node, model=model)
for node in nodes
]
summaries = await asyncio.gather(*tasks)
for node, summary in zip(
nodes, summaries):
node['summary'] = summary
return structure
接收树结构,为每个节点生成摘要
先把嵌套树展平为节点列表
为每个节点创建一个异步摘要任务
generate_node_summary 让 LLM 总结节点内容
关键:asyncio.gather 并发执行所有任务
所有任务同时进行,而不是排队等待
把生成的摘要写回对应节点
返回带摘要的完整树结构
三种部署方式
直接用这个开源仓库。标准 PDF 解析,适合简单文档。免费,但复杂 PDF 可能需要额外 OCR 处理。
通过 MCP 或 API 调用增强版管线(更好的 OCR + 树构建)。即开即用,适合生产环境。
私有化部署,数据不出内网。支持自定义模型(如本地部署的 LLM)、大规模并发处理、定制化索引策略。
从单文档到百万文档
单份文档的树索引解决了"精确检索"的问题。但如果你有 100 万份文档怎么办?
PageIndex File System 是一个文件级的树层——它在单文档树索引之上又加了一层"跨文档"的索引结构。让 LLM 先推理"该看哪份文档",再深入那份文档的树索引。两轮推理,百万文档也不怕。
用户提问
跨文档树推理
选定目标文档
文档内树推理
取出精确内容
完整实战:从索引到问答
这段代码展示了从创建客户端到 Agent 自动问答的完整流程。只需几行代码就能跑起来。
# 1. 创建客户端
client = PageIndexClient(
workspace="./my_workspace")
# 2. 索引文档
doc_id = client.index(
"annual_report.pdf")
# 3. 查看树结构
structure = json.loads(
client.get_document_structure(doc_id))
utils.print_tree(structure)
# 4. Agent 自动问答
question = "研发支出是多少?"
query_agent(client, doc_id, question)
# Agent 自动:
# → get_document()
# → get_document_structure()
# → get_page_content("47-48")
# → 生成回答
初始化客户端,指定本地存储目录
调用 index 方法,一行搞定文档索引
返回文档 ID,后续所有操作都用它
查看生成的树索引结构
打印树形目录,直观查看层级
提问——Agent 自动完成整个检索流程
Agent 自动调用 get_document 确认状态
Agent 自动调用 get_structure 推理定位
Agent 自动调用 get_page_content 取内容
Agent 基于内容生成最终回答
设计者的"对话"
如果把 PageIndex 的设计哲学拟人化,它们之间的对话会是什么样?点击消息看看。
你的团队正在为一个法律事务所搭建文档分析系统,需要处理上千份合同和法律文件。每份文件 50-300 页不等,需要精确引用条款和页码。你会怎么设计?
综合前面学到的所有内容,以下哪种方案最合适?
如果 PageIndex 要处理一份有 25 个一级节点的文档,哪个设计决策对性能影响最大?
PageIndex 用"推理"取代"相似度",用"层级树"取代"碎片块",用"自我验证"确保可靠性。它不是一个更好的向量搜索,而是一种全新的检索范式——让 AI 像人类专家一样思考着找答案。记住核心公式:树索引 + LLM 推理 = 精确检索。