01

为什么不用向量?

传统的向量 RAG 为什么在专业文档上频频翻车?PageIndex 提出了一条全新路线——用"推理"取代"相似度"。

想象你在一个图书馆找答案

传统 RAG 的工作方式像一个图书管理员,你问一个问题,他根据"关键词相似度"去书架上找最像的段落。问题是:相似不等于相关

比如你问"苹果公司的净利润",向量搜索可能返回一段写着"果园苹果丰收"的文字——因为"苹果"这个词很像。这种"字面相似但语义无关"的灾难在专业文档中尤其常见。

💡
核心洞察

向量搜索本质上是一种"模式匹配"——它测量的是两段文字在数学空间中的距离。但真正的信息检索需要推理能力:理解上下文、多步判断、知道去哪里找。PageIndex 的哲学就是:让 LLM 像人类专家一样"思考着检索"。

两种 RAG 路线:向量 vs 推理

📌

传统向量 RAG

把文档切成碎片 → 转成数学向量 → 按距离排序取 Top-K → 喂给 LLM。像是在图书馆里根据关键词标签盲目翻找。

🌲

PageIndex 推理 RAG

把文档组织成层级树索引 → LLM 像翻目录一样逐层推理 → 精确定位目标章节 → 取出完整上下文。像专家先看目录,再翻到具体章节。

CODE

# 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

✂️
Chunking
🔢
Embedding
🗄️
Vector DB
📊
Top-K 搜索

PageIndex 推理 RAG

🌲
树索引
🧠
推理式检索
📍
精确上下文
点击任意组件查看详细说明

你正在为一家投资公司搭建财报分析系统,文档都是 200 页以上的 SEC 文件。传统向量 RAG 的回答经常"驴唇不对马嘴"。PageIndex 的核心改进是什么?

为什么向量搜索在专业文档上容易出错?

02

树索引的诞生

PageIndex 如何把一份 200 页的 PDF 变成一棵可推理的层级树?从检测目录到递归细分,每一步都是 LLM 在"思考"。

什么是"树索引"?

想象一本教科书:最外面是"章",里面是"节",再里面是"小节"。PageIndex 做的事情就是让 LLM 把一份文档自动整理成这样的层级结构——就像一张增强版的目录

🌲
灵感来源:AlphaGo

PageIndex 的灵感来自 AlphaGo 的蒙特卡洛树搜索——不是暴力遍历所有可能,而是在树结构上"有方向地探索"。LLM 看到目录后推理"答案最可能在哪个分支",然后只深入那个分支。

CODE

{
  "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 到树:五步流程

1
解析 PDF,提取每页文本

PyPDF2/PyMuPDF 把每一页的文字和 token 数量提取出来,为后续处理做准备

2
检测是否有原生目录

LLM 逐页扫描前 20 页,判断"这一页有没有目录?"。如果有,直接提取并结构化;如果没有,进入"无目录模式"

3
三种处理路径(自动选择)

meta_processor 根据文档特征自动选择:有目录+有页码 → 直接对齐;有目录+无页码 → 逐页定位;无目录 → 从零构建

4
验证 + 自动修复

对结果抽样验证,让 LLM 检查"这个标题真的出现在这一页吗?"。准确率低于 100% 的部分自动重试修复(最多 3 轮)

5
递归细分大节点

如果某个节点超过 10 页或 20000 token,自动对该节点执行同样的树构建流程——直到每个节点都足够精细

树索引生成:数据在流动

点击"下一步",看数据如何从 PDF 文件一步步变成一棵完整的树索引。

📄
PDF 文件
📖
页面提取器
🧠
LLM 分析器
验证修复器
🌲
递归构建器
点击"下一步"开始

关键代码:递归处理大节点

当某个节点太大了(超过 10 页或 20000 token),PageIndex 会自动对该节点执行完整的树构建流程——这就是"递归"的威力。

CODE

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 会选择哪条处理路径?

回顾一下树索引生成流程的第 4 步:验证环节。PageIndex 不是盲目信任 LLM 的第一次输出,而是让 LLM 再检查一遍自己结果的准确性。">

验证环节是如何工作的?

03

推理式检索

树索引建好了,接下来怎么用?LLM 像人类专家一样"翻目录"——逐层推理,精确定位,取出完整上下文。

人类专家怎么查文档?

想象你是一个金融分析师,需要在一本 300 页的年报里找到"2024 年 Q3 的研发支出"。你会:

1

翻到目录

2

看到"财务报表"章节

3

推理:研发支出在"利润表"下

4

翻到 47-48 页,找到答案

PageIndex 让 LLM 执行完全相同的推理过程。它不是"搜索",而是在树上导航

💡
关键区别

向量搜索是"一次性的":把问题变成向量,计算距离,返回结果。推理式检索是"多步骤的":LLM 先看顶层目录,判断该去哪个分支,然后深入子节点,逐步缩小范围——每一步都在推理。

Agent 的三个工具

Agent 模式下,AI 有三个核心工具来完成检索:

📋

get_document()

获取文档元信息——名称、描述、页数、处理状态。就像先看看这本书的封面和简介。

🌲

get_document_structure()

获取完整的树索引(不含正文)。Agent 看到目录后推理"答案在哪个分支"。这是推理式检索的核心。

📄

get_page_content(pages)

取出指定页码的正文内容。Agent 推理到具体页码范围后,用这个工具获取原始文本。

Agent 的 System Prompt

这段系统提示词定义了 Agent 的行为规范——它告诉 AI "先看结构,再取内容,用最少的页面"。

CODE

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 自动推理、调用工具、找到答案。点击"下一步"看完整流程。

👤
用户
📋
get_document
🌲
get_structure
📄
get_pages
💬
生成回答
点击"下一步"开始

PageIndex 系统架构

点击每个组件,了解它在系统中的角色。

用户层

💬
用户提问

Agent 层

🤖
PageIndex Agent

工具层

📋
get_document()
🌲
get_structure()
📄
get_page_content()

数据层

🗄️
Workspace 持久化
点击任意组件查看详细说明

找 Bug 挑战

下面这段代码是 PageIndex Agent 的工具注册代码,但藏着一个错误。点击你认为有 Bug 的那一行。

找出这段代码中的问题:

1 @function_tool
2 def get_page_content(pages: str) -> str:
3 """Get text of specific pages."""
4 return client.get_page_content(doc_id, pages)
5
6 agent = Agent(
7 tools=[get_document, get_page_content],

如果用户问"这份财报的净利润是多少?",Agent 最合理的工具调用顺序是什么?

为什么 Agent 被要求"永远不要一次获取整个文档"?

04

从索引到对话

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 最核心的方法。一行调用,背后自动完成:文件类型检测 → 树索引生成 → 页面文本提取 → 持久化存储。

CODE

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 能精确描述它需要的内容范围。

CODE

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/ 核心 Python 包
page_index.py 树索引生成引擎——目录检测、结构提取、验证修复、递归构建
page_index_md.py Markdown 文档支持——按标题层级 (#/##/###) 构建树
retrieve.py 检索工具函数——get_document、get_structure、get_page_content
client.py PageIndexClient——统一的索引+查询+持久化接口
utils.py 工具集——LLM 调用、token 计算、PDF 解析、JSON 处理、配置加载
config.yaml 默认配置——模型选择、页数限制、节点粒度控制
examples/ 示例代码和测试文档
run_pageindex.py 命令行入口——直接对 PDF/Markdown 生成树索引

概念匹配挑战

把左侧的概念拖到右侧正确的描述位置。测试你对 PageIndex 核心概念的理解。

树索引
meta_processor
Workspace
验证修复
递归细分

文档的层级目录结构,每个节点有 title、node_id、start_index、end_index、summary

拖到这里

中枢调度器,根据文档特征自动选择最优处理路径,失败时自动降级

拖到这里

本地文件存储目录,索引结果以 JSON 持久化,重启后自动加载复用

拖到这里

LLM 抽样检查章节标题是否出现在标注页码,不准的自动重试最多 3 轮

拖到这里

超过 10 页或 20000 token 的节点自动执行完整树构建流程,直到足够精细

拖到这里
PageIndexClient 的 index 方法不仅支持 PDF。想想它还支持什么格式?在代码中可以看到 ext 判断逻辑。">

PageIndexClient 支持哪些文件格式?

你有一个应用需要索引 50 份 PDF 并长期使用。PageIndexClient 的 Workspace 是怎么工作的?

05

实战与设计哲学

从配置参数到部署方式,从设计理念到扩展方向。掌握 PageIndex 背后的工程哲学,才能在实际项目中用好它。

config.yaml:调节索引质量的旋钮

PageIndex 的行为由配置文件控制。理解这些参数,就能根据文档特点调出最佳效果。

CODE

# 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 次。

CODE

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 并发执行所有任务

所有任务同时进行,而不是排队等待

把生成的摘要写回对应节点

返回带摘要的完整树结构

三种部署方式

🏠
自托管(Self-hosted)

直接用这个开源仓库。标准 PDF 解析,适合简单文档。免费,但复杂 PDF 可能需要额外 OCR 处理。

☁️
云服务(Cloud)

通过 MCPAPI 调用增强版管线(更好的 OCR + 树构建)。即开即用,适合生产环境。

🏢
企业版(Enterprise)

私有化部署,数据不出内网。支持自定义模型(如本地部署的 LLM)、大规模并发处理、定制化索引策略。

从单文档到百万文档

单份文档的树索引解决了"精确检索"的问题。但如果你有 100 万份文档怎么办?

🌐
PageIndex File System

PageIndex File System 是一个文件级的树层——它在单文档树索引之上又加了一层"跨文档"的索引结构。让 LLM 先推理"该看哪份文档",再深入那份文档的树索引。两轮推理,百万文档也不怕。

1

用户提问

2

跨文档树推理

3

选定目标文档

4

文档内树推理

5

取出精确内容

完整实战:从索引到问答

这段代码展示了从创建客户端到 Agent 自动问答的完整流程。只需几行代码就能跑起来。

CODE

# 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 推理 = 精确检索