页面索引:基于推理的无向量RAG与树形索引
PageIndex 放弃了向量数据库,转而使用由大语言模型驱动的树形索引。本文将介绍这种基于推理的 RAG 工作原理,并提供 Python 代码、MCP 接口示例以及基准测试数据。
来源: PageIndex: The RAG Framework That Threw Out Vector Databases and Still Hit 98.7% Accuracy
想象这样一个RAG框架:它完全摒弃了向量数据库,却能取得任何基于嵌入的系统都无法企及的基准测试成绩。这就是VectifyAI开发的开源项目PageIndex。在用于金融文档问答的标准基准测试FinanceBench中,普通的基于向量的RAG系统得分仅在30%到50%左右,而PageIndex的得分则高达98.7%。它并非依靠更优秀的嵌入模型达到这一成绩,而是通过放弃嵌入技术实现的。
这一成绩值得仔细研究,因为针对SEC备案文件的金融问答属于生产环境中最苛刻的检索测试之一。这类任务需要多步骤推理,还要在不同章节及精确数字之间进行切换。目前表现最好的系统也并未使用向量、分块技术或向量数据库。
接下来将介绍PageIndex的功能、实现方式、可用的Python代码,以及它的优势与局限性。
长文档面临的难题
任何基于超过博客文章长度的文本构建RAG系统的人都会遇到这个问题。有用户询问基于200页年度报告构建的系统:2024财年的净收入总额与2023财年相比如何?标准的处理流程如下:
- 将文档分割成固定大小的块,通常为300到500个标记。
- 使用如
text-embedding-3-large这样的模型对每个块进行嵌入处理。 - 将生成的向量存储在Pinecone、Weaviate、Milvus、Chroma、pgvector或类似系统中。
- 在查询时,对问题进行嵌入处理,然后通过余弦相似度找出最接近的k个向量。
- 将这些块传递给大语言模型以生成答案。
对于博客文章、支持工单和产品文档这类简短的非结构化文本,这种方法是有效的。但10-K格式的文件则具有可预测的分割方式,会给处理带来困难。
查询内容似乎并非答案。向量检索假定与查询最相似的文本才是最相关的。但查询表达的是意图而非内容。关于收入趋势的查询并不会直接显示出第47页上的那行数据,而是会列出所有涉及收入的段落。
一个术语,六十处出现。在冗长的年度报告中,“营业收入”会在不同语境下出现数十次。相似度算法会将它们大致同等排序,而真正能回答问题的那段文字可能永远无法进入前三名。
分块处理会破坏表格结构。表格标题位于第14个分块中,而所需的数据行却在第15个分块里。单独来看这两者都缺乏意义。重叠窗口、语义分块以及了解表格结构的解析器虽能有所帮助,但都无法解决根本问题。
不会跨轮次保留记忆。如果前一条消息询问的是资产相关内容,而下一条消息又问“负债呢?”,检索系统会将其视为完全新的查询,因为它无法判断用户指的是同一份报告中的同一部分内容。
交叉引用信息会被隐藏。假设第12页引导读者查看附录G,而该附录实际位于第87页。由于附录内容与原始问题在语义上没有关联,向量RAG系统永远不会跟随这些引用提示。虽然可以在数据块中加入交叉引用元数据作为变通方法,但这不仅不够稳定,也无法实现规模化应用。
这些并非罕见的边缘情况,而是源于该系统的架构设计。正如VectifyAI的联合创始人张明天在项目公告中所说的,检索系统真正需要的是相关性,而要实现相关性就必须依靠推理能力。
PageIndex是什么
PageIndex是由VectifyAI在2025年9月发布的无向量、基于推理的RAG框架,由张唐和余唐共同创建。该框架采用MIT许可证,其代码仓库发展迅速,到2026年3月底已获得23,000个星标和近2,000次分支。
它的核心理念源自AlphaGo。与通过向量相似度逐个对比文本片段的方式不同,PageIndex采用经过训练的策略来智能地导航信息空间。该框架将这一思路应用于文档处理,具有以下三个显著特点:
- 无需向量数据库。文档结构与大型语言模型的推理能力取代了相似度搜索功能,因此不需要Pinecone、Chroma或任何嵌入模型。
- 无需文本分块。文档仍以自然的章节、段落和表格形式存在,而非随意划分的500个标记的窗口。
- 类人类导航方式。其运作方式如同人工检索文档:先查看目录,找到可能包含答案的章节,然后进行阅读。
该方法分为两个步骤。
第一步:层次化树形索引
导入文档时不会生成任何嵌入向量。PageIndex会分析文档结构并生成一棵层次化树结构,本质上就是适合大语言模型使用的目录。每个节点包含标题、该章节的简短摘要、页面范围,以及用于子章节的子节点,嵌套深度可根据需要调整。
一份50页的SEC申报文件可能会生成包含30到50个节点的树结构,这些节点以一个JSON对象的形式存储。所有数据都存储在JSON中,而非向量数据库中。整个对象可完全容纳在单个大语言模型的上下文窗口内,用户可以打开任意节点并阅读其内容。一个节点的结构如下:
{
"node_id": "0006",
"title": "Financial Stability",
"start_index": 21,
"end_index": 22,
"summary": "Covers the Federal Reserve's financial stability oversight...",
"sub_nodes": [
{
"node_id": "0007",
"title": "Monitoring Financial Vulnerabilities",
"start_index": 22,
"end_index": 28,
"summary": "Describes the Fed's vulnerability monitoring framework..."
},
{
"node_id": "0008",
"title": "Domestic and International Cooperation",
"start_index": 28,
"end_index": 31,
"summary": "Federal Reserve collaboration with international bodies..."
}
]
}
每个 node_id 都指向该部分的原始内容:文本、表格和图片。当需要时,大语言模型可以请求获取任意节点的内容。
这一设计的巧妙之处在于索引的存储位置。向量数据库会在模型外部维护一个独立的、静态的嵌入向量索引。而这里的 JSON 树结构则存在于推理过程中大语言模型的活跃上下文中。VectifyAI 将其称为上下文内索引,模型不仅会查询它,还会实时对其进行分析推理。
步骤 2:基于推理的树形搜索
当有问题出现时,PageIndex会向大语言模型提供仅包含标题和摘要的树结构,而不包含原始文本,从而实际上是在询问基于该结构与问题应从何处查找答案。模型会读取节点的标题和摘要,判断哪些部分可能包含答案,然后返回一组节点ID。系统随后会获取这些节点的实际内容。
检索过程是一个循环:
- 浏览目录,了解整体结构并找出可能的候选部分。
- 选择最有可能包含所需信息的部分。
- 阅读该部分,获取该节点中的原始文本。
- 检查是否足够。如果足够,则写出答案;如果不足够,则返回第一步,尝试其他部分。
这与向量搜索的思路不同。向量数据库会并行地对每个数据块计算余弦相似度,虽然速度快,但理解深度有限。而PageIndex则让大语言模型自行推理答案可能所在的位置。在推理过程中,模型可以追踪交叉引用,识别出关于附录数据的问题应归属于对应的附录节点,也能明白多部分构成的问题需要来自不同章节的内容。每一步都会留下推理轨迹,因此你可以清楚地看到访问了哪些节点以及原因。这样一来,信息检索就不再是个黑箱了。
附录G示例
一个基于 VectifyAI MCP 集成的演示展示了这一点的重要性。有人询问美联储年度报告中递延资产的总额。报告正文第75页至第82页讨论了递延资产的变化,但并未给出总数。不过第77页有一句话指出,表5.3汇总了收入、支出和分配情况,而附录G则提供了更详细的信息。
Vector RAG 在这里就卡住了。附录G只是一张数字表格,与关于递延资产的问题没有语义关联,因此数据库会忽略它。PageIndex 的处理方式则不同。大语言模型会读取结构树,找到财务部分,发现对附录G的引用,通过结构树追踪该引用,获取正确的表格,并返回精确的数值,同时通过推理轨迹展示每一步的查找过程。
Python,端到端
下面的代码改编自官方的 PageIndex 教程,附有注释说明执行流程。首先从安装开始:
pip install pageindex openai
导入内容:
import os
import json
import asyncio
from pageindex import PageIndexClient
from openai import AsyncOpenAI
接着是凭证和客户端。API密钥来自PageIndex控制面板:
# Grab an API key from https://dash.pageindex.ai/api-keys
PAGEINDEX_API_KEY = os.environ["PAGEINDEX_API_KEY"]
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]pi_client = PageIndexClient(api_key=PAGEINDEX_API_KEY)
openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
构建树结构
引入相关工具:
import pageindex.utils as utils
上传PDF文件,让PageIndex生成树结构。索引生成需要一些时间,因此脚本会持续查询直至准备就绪,然后输出结果:
# Upload a PDF; PageIndex handles the tree generation
doc = pi_client.upload("annual_report_2024.pdf")
doc_id = doc["doc_id"]# Tree generation takes a bit, so we poll
while not pi_client.is_retrieval_ready(doc_id):
print("Still indexing...")
import time; time.sleep(5)# Grab the tree and take a look
tree = pi_client.get_tree(doc_id, node_summary=True)["result"]
print("Document Tree:")
utils.print_tree(tree)
打印输出会显示每一个章节、子章节以及页面范围,这样就能立刻判断该结构是否正确反映了整体布局。
让大语言模型搜索该结构
这是核心步骤。该函数会接收该结构以及一个查询条件:
async def find_relevant_nodes(tree: dict, query: str) -> list:
"""LLM reasons over tree structure to identify relevant nodes."""
在处理过程中,原始文本会被去除以节省令牌,因为模型只需要标题和摘要。提示词将模型设定为文档检索专家,要求其遵循交叉引用,并输出包含推理过程及节点ID列表的JSON格式结果:
# Strip raw text to save tokens; the LLM only needs titles and summaries
tree_without_text = utils.remove_fields(
tree.copy(), fields=["text"]
) search_prompt = f"""
You are a document retrieval expert. Given a question and
a hierarchical tree structure of a document, identify all
nodes likely to contain the answer. Each node has a node_id, title, and summary.
Follow cross-references if a section mentions another. Question: {query} Document tree structure:
{json.dumps(tree_without_text, indent=2)} Reply in this JSON format only:
{{
"thinking": "<reasoning about which nodes are relevant>",
"node_list": ["node_id_1", "node_id_2"]
}}
""" response = await openai_client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": search_prompt}],
temperature=0,
response_format={"type": "json_object"},
) result = json.loads(response.choices[0].message.content)
print(f"LLM reasoning: {result['thinking']}")
return result["node_list"]
获取内容并回复
下一个函数会收集所选节点的原始文本:
def collect_node_content(tree: dict, node_ids: list) -> str:
"""Pull raw text from the nodes the LLM selected."""
all_nodes = utils.flatten_tree(tree)
context_parts = []
接着循环体会构建上下文,每个部分都会标注标题与页码范围,而整个流程则负责统筹这一切:选择节点、获取内容、生成带有引用信息的答案,并将答案与所使用的节点一同返回。
for node in all_nodes:
if node["node_id"] in node_ids:
title = node.get("title", "Untitled")
pages = f"pages {node.get('start_index', '?')}-{node.get('end_index', '?')}"
text = node.get("text", "")
context_parts.append(
f"[{title} | {pages}]\n{text}"
)
return "\n\n---\n\n".join(context_parts)
async def answer_query(tree: dict, query: str) -> dict:
"""Full vectorless RAG pipeline: tree search + answer generation.""" # Step 1: LLM picks the nodes
node_ids = await find_relevant_nodes(tree, query) # Step 2: Fetch content from those nodes
context = collect_node_content(tree, node_ids) # Step 3: Generate answer with citations
answer_prompt = f"""
Answer the question using only the provided context.
Cite specific pages and sections in your answer. Context:
{context} Question: {query}
""" response = await openai_client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": answer_prompt}],
temperature=0,
) return {
"answer": response.choices[0].message.content,
"retrieved_nodes": node_ids,
"context_length": len(context),
}
# Run it
query = "What was the total value of deferred assets in 2023?"
result = asyncio.run(answer_query(tree, query))
print(result["answer"])
print(f"Nodes used: {result['retrieved_nodes']}")
将其接入 MCP
对于 Claude、Cursor 或任何支持 MCP 的智能体,PageIndex 可通过托管服务器直接连接:
{
"mcpServers": {
"pageindex": {
"type": "http",
"url": "https://api.pageindex.ai/mcp",
"headers": {
"Authorization": "Bearer your_api_key"
}
}
}
}
还有一个用于从磁盘上传文件的本地MCP服务器,它需要Node.js 18或更高版本:
{
"mcpServers": {
"pageindex": {
"command": "npx",
"args": ["-y", "@pageindex/mcp"]
}
}
}
数据背景
该核心数据来自FinanceBench,它依据真实的SEC备案文件,包括10-K、10-Q和8-K报告,并且只接受精确的答案。基于PageIndex构建的VectifyAI金融分析工具Mafin 2.5被用于对全部数据集进行测试。其准确率如下:
| 系统 | 准确率 | 基准覆盖范围 |
|---|---|---|
| Mafin 2.5 (PageIndex) | 98.7% | 100% |
| 传统向量RAG | 约50% | 各不相同 |
| 无RAG的GPT-4o | 约31% | 100% |
| Perplexity | 约45% | 各不相同 |
相比传统向量RAG高出约49个百分点的准确率绝非微小的改进,这属于完全不同层次的结果。有三个因素反复被提及:
- 跟随交叉引用。文档内的引用如“参见附录G”可通过树结构进行导航,而相似度搜索则无法识别这类引用。
- 保持结构。表格具有表头、脚注以及单元格间的关联关系,PageIndex会将这些以树节点的形式保留下来,而分块处理则会破坏这些结构。
- 多步骤推理。对于那些在文档不同部分提及、需要比较2023财年与2024财年的问题,迭代循环能够自然地处理。
其不足之处
尽管在特定领域表现出色,但PageIndex并不能在所有情况下替代向量RAG。
- 速度更慢。 每次检索都需要多次调用大型语言模型:读取索引、分析节点、获取内容、检查是否足够,可能还会循环处理。向量RAG的响应时间为毫秒级,而PageIndex则为秒级。联合创始人指出,由于检索是在生成过程中同步进行的,因此生成第一个token的时间可能与普通的大型语言模型调用相当,但总体延迟仍然更高。
- 成本更高。 每次查询需要多次推理调用,而非仅一次嵌入查找。在处理大量数据时,其成本会变得非常可观。
- 它更适合深度处理而非广度处理。 它擅长从单篇长文档中深度提取信息。而同时搜索10,000篇短文档则是向量数据库的强项,为每篇文档分别构建树结构并进行推理并不现实。
何时使用它
在以下情况下应选择PageIndex:
- 文档篇幅较长、结构清晰且具有专业性:年度报告、合同、监管申报文件、技术手册
- 准确性比速度更重要,例如错误答案会导致经济损失或合规风险
- 查询需要多步骤推理或在不同章节之间切换
- 您需要审计追踪功能,因为每个答案都会附带推理过程和节点引用,这正是受监管行业所看重的
在以下情况下则应继续使用向量RAG:
- 在查询量较大的情况下需要亚秒级的响应速度
- 需要在大量短文档中进行搜索
- 这些文档为非结构化或对话式格式
- 90%的准确率已经足够
- 每次查询的成本是主要限制因素
混合式方法可能尚未得到充分探索。可以先用向量搜索找出大量文档中重要的那篇,再将其交给PageIndex进行深度提取。
自发布以来的新进展
自2025年9月以来,该项目发展迅速。
智能无向量 RAG。最新版本将自托管的 PageIndex 集成到 OpenAI Agents SDK 中。该智能体被赋予三种工具(get_document()、get_document_structure() 和 get_page_content()),能够自行在结构树上进行推理,自动调用合适的工具并获取内容,无需手动选择节点。这是构建智能体工作流的理想起点:
pip install openai-agents
python3 examples/agentic_vectorless_rag_demo.py
基于视觉、无需OCR的模式。这种专为视觉处理设计的流程不再需要OCR步骤。页面图像会直接传递给视觉模型,而文档结构树则由该模型对页面内容的识别结果生成。对于资产负债表这类布局与文字同样重要的财务文件而言,这一点至关重要。由于所有内容都不会先被转换为文本,因此图表、合并单元格表格及脚注都能保持完整。
TypeScript SDK pageindex-js-sdk于2026年初推出,可用于Node.js集成及基于网页的文档处理工具。一键式.mcpb桌面扩展程序可通过自动处理的OAuth机制将PageIndex安装到Claude Desktop中。自2026年1月起推出的独立项目ChatIndex则将同样的树状索引理念应用于长篇对话记录的解析。
更广泛的变革
PageIndex代表了从被动检索——即获取任何看似相似的内容——向主动式、智能型检索的转变,后者会思考应在何处查找信息。这一趋势在代码工具中已有所体现。Claude Code和Cursor已不再依赖基于向量的RAG技术来处理代码,转而主动探索代码库,PageIndex团队也希望文档能遵循这一方向。对于结构化文档而言,这一预测似乎合情合理。
向量数据库并不会消失。它们速度快、成本低,足以应对各种问题。但在需要确保准确性、透明度以及深入理解文档的场景中,比如财务审计、法律调查、医疗记录处理和合规检查等,基于推理的检索方式显然更为合适。
相关代码和文档位于https://github.com/VectifyAI/PageIndex,文档说明则在https://docs.pageindex.ai/上。
相关阅读
- 利用 Foundry Agent Service 和 Agent Framework 将智能体投入生产环境 —— 讲述销售准备智能体如何从本地代码转变为托管式、基于身份识别、处于沙箱环境且受监控的部署形式,文章发表于 Microsoft 365,内容基于业务数据。
- 对 RAG 检索中的过时数据块、偏差及噪声上下文进行审计 —— 一份用于 RAG 检索的 Python 审计脚本:包含黄金查询、余弦相似度相关性筛选、Kolmogorov-Smirnov 偏差检测以及上下文效率分析,同时还介绍了如何阅读该脚本生成的报告。