首页 / 文章

页面索引:基于推理的无向量RAG与树形索引

PageIndex 放弃了向量数据库,转而使用由大语言模型驱动的树形索引。本文将介绍这种基于推理的 RAG 工作原理,并提供 Python 代码、MCP 接口示例以及基准测试数据。

发布者:REACTAPP.TOP阅读约 14 分钟3118 词

来源: 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财年相比如何?标准的处理流程如下:

  1. 将文档分割成固定大小的块,通常为300到500个标记。
  2. 使用如text-embedding-3-large这样的模型对每个块进行嵌入处理。
  3. 将生成的向量存储在Pinecone、Weaviate、Milvus、Chroma、pgvector或类似系统中。
  4. 在查询时,对问题进行嵌入处理,然后通过余弦相似度找出最接近的k个向量。
  5. 将这些块传递给大语言模型以生成答案。

对于博客文章、支持工单和产品文档这类简短的非结构化文本,这种方法是有效的。但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。系统随后会获取这些节点的实际内容。

检索过程是一个循环:

  1. 浏览目录,了解整体结构并找出可能的候选部分。
  2. 选择最有可能包含所需信息的部分。
  3. 阅读该部分,获取该节点中的原始文本。
  4. 检查是否足够。如果足够,则写出答案;如果不足够,则返回第一步,尝试其他部分。

这与向量搜索的思路不同。向量数据库会并行地对每个数据块计算余弦相似度,虽然速度快,但理解深度有限。而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篇短文档则是向量数据库的强项,为每篇文档分别构建树结构并进行推理并不现实。
  • 结构至关重要。格式规范的SEC申报文件能生成结构清晰的层级结构;而格式混乱、缺乏逻辑分区的扫描文件则会导致目录质量下降,进而影响后续所有内容。虽然有针对此类情况的基于视觉的模式,但结构化文档依然是最佳选择。
  • 大语言模型的推理也不完美。向量RAG技术会出错,因为嵌入向量仅能近似表达含义;当推理过程也仅是近似时,PageIndex也会出问题:模型可能会跳转到错误的部分、误读摘要或遗漏相关子节点。这些故障虽然更易于理解,也可能更简单调试,但依然会发生。
  • 一个基准标准。FinanceBench用于评估结构规范的SEC申报文件的质量。对于那些格式混乱、涉及多个领域、表述模糊且来自不同类型文档的查询,其结果尚未经过同等程度的独立验证。98.7%这一数值是真实的,但了解它所衡量的是什么非常重要。
  • 何时使用它

    在以下情况下应选择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/上。

    相关阅读

  • 将 pgvector 或 SQL Server 2025 用作 RAG 向量存储:配置、查询及注意事项 —— 利用 PostgreSQL 的 pgvector 功能与 SQL Server 2025 的原生向量功能构建 RAG 系统:系统配置、余弦相似度检索、与业务数据的关联查询,以及六个需避免的 SQL Server 预览版问题。
  • 通过父子分块与层次索引提升 RAG 检索效果 —— 为何过小的数据块虽能提高匹配率却无法获得准确答案,以及如何通过父子分块、去重扩展和层次索引在 RAG 检索中恢复上下文信息。
  • RAG的运作原理:嵌入、向量数据库与分块 —— 跟踪一段文本从分词到池化再到余弦相似度搜索的全过程,了解为何分块方式以及嵌入模型的选择决定了RAG的质量。
  • 基于PageIndex的无向量RAG:以推理替代向量进行检索 —— PageIndex如何用由大语言模型引导的文档树取代嵌入和分块技术,附有逐步的Python实现示例,以及说明在何种情况下向量RAG仍更优的指导。
  • 无需框架的RAG:分块、评分、预算与提示词 —— 将RAG解释为简单的标记选择过程:包含词重叠评分功能的简短Python代码、标记预算限制机制,以及嵌入模型和向量存储真正带来的改进。