首页 / 文章

基于文档树进行推理而非搜索向量

PageIndex构建了分层索引,让大语言模型能够直接导航其中,无需经过分块处理和嵌入步骤。其工作原理、在FinanceBench测试中的表现以及适用场景。

发布者:REACTAPP.TOP阅读约 10 分钟2394 词

来源: I Threw Out My Vector Database. RAG Got Way Better With PageIndex

在现场演示过程中,有模型自信地引用200页长年度报告中的错误内容,几乎没有比这更尴尬的事了。接下来总是会出现同样的情节:团队通过更合理的文本分割、更智能的重叠处理以及经过精心调优的嵌入模型来“解决”这个问题,这是每位RAG工程师都会经历的常规升级流程,他们坚信下一次调整就能解决问题。然而系统在回答关于2023年营收预测的问题时,却给出了2021年资本支出计划中的一段文字。由于这两部分内容使用了相似的财务术语,嵌入模型便认为它们之间存在关联,但实际上并非如此。

这类失败现象让人不禁质疑,整个方法的基础是否存在根本性问题。

为何相似度检测不可靠

每一篇关于RAG的教程都遵循相同的流程:将文档分割成500字左右的片段,将其嵌入模型,为每个问题找到最相似的向量,再将这些片段交给大语言模型处理。这听起来很完美,仿佛是将语义搜索与语言生成相结合。

但这种流程忽略了一个关键点:相似性并不等同于相关性。向量数据库找到的只是与问题在语义上相近的文本,而非能够回答该问题的文本。关于某种药物风险因素的段落与关于其疗效的段落在嵌入空间中可能彼此靠近,但只有前者能回应有关该公司财务风险的问题。

分块处理会引发另一个问题。将一份200页的法律文件拆分成500个标记的片段后,作者精心构建的所有内容都会消失:章节划分、交叉引用以及逻辑结构。剩下的只是一堆毫无关联性的碎片,让人无法理解它们的来源及彼此之间的关系。虽然更好的分块方法能稍作缓解,但无法解决根本问题,因为已被丢弃的结构是无法恢复的。

人类分析师绝不会这样阅读。他们会先浏览目录,直接跳到相应章节,在了解前后章内容的基础上进行阅读,并查看脚注。他们是将文档视为一个整体来处理的。

以树结构而非向量结构

VectifyAI团队思考如果RAG系统按这种方式运作会是什么样子,于是开发出了PageIndex。一旦说出口,这个核心理念就显得异常显而易见——无需将文档转换为向量后再通过相似度进行搜索,而是应为文档构建一个分层树状索引,实际上就是一份机器可读的目录,让大语言模型沿着这棵树找到相关内容。

这意味着不需要向量数据库、无需分块处理,也无需相似度得分,仅有结构化索引与智能导航即可。

第一步:构建索引

PageIndex不会拆分所接收的文档。它会分析文档的自然结构,即各个章节、小节、标题以及逻辑分组方式,进而生成一种类似专家组织内容时的层级结构。可以想象成一个自动生成的目录,只不过更为完善。每个节点不仅包含标题,还包含其分支所涵盖内容的概要、它可能回答的问题,以及它与同级节点和父节点之间的关系。

一份100页的技术规范文件可以生成三到四层深的树结构,最顶层是主要功能区域,下层的子节点则对应更具体的主题。文档的逻辑层级会保持作者最初的安排。该处理过程会对每个文档执行一次,且在原始内容保持完整且可访问的同时存储索引。

第二步:遍历树结构

当查询到来时,PageIndex不会将其嵌入或进行最近邻搜索。相反,大型语言模型会遍历索引树。它先读取顶层节点,判断哪个分支最有可能包含答案,然后沿着该分支继续,读取下一层节点并再次进行判断。如此不断向下深入,直到找到相关部分。

这就是改变现状的关键所在。之后,它会从原始文档中完整获取这些部分,不会出现碎片化,再将它们作为生成答案的上下文。

该检索机制在设计上具有可解释性。可以清楚地看到大型语言模型遍历了哪些节点,以及最终停留在原始文档的哪几页。没有不可见的余弦相似度得分,推理过程一目了然。

尝试使用

设置很简单。首先克隆该仓库并安装其依赖项:

git clone https://github.com/VectifyAI/PageIndex.git
cd PageIndex
pip3 install --upgrade -r requirements.txt

然后在根目录中创建一个包含 API 密钥的 .env 文件:

CHATGPT_API_KEY=your_openai_api_key_here

要为 PDF 建立索引,只需将脚本指向该文件。财务报告是个很好的例子,因为这正是该工具表现最出色的领域:

python3 run_pageindex.py --pdf_path /path/to/annual_report.pdf

Markdown文档使用不同的标记:

python3 run_pageindex.py --md_path /path/to/technical_spec.md

这样就完成了设置。无需配置向量数据库,无需选择嵌入模型,也无需纠结分块参数。

处理文档后会生成类似这样的索引结构,此处已简化展示:

{
  "document": "Apple Inc. Annual Report 2023",
  "index": {
    "title": "Apple Inc. Annual Report 2023",
    "summary": "Comprehensive financial and operational report covering revenue, product segments, risks, and strategic outlook",
    "children": [
      {
        "title": "Business Overview",
        "summary": "Company description, product lines, and market position",
        "pages": [1, 8],
        "children": [...]
      },
      {
        "title": "Financial Results",
        "summary": "Revenue, operating income, EPS, and segment performance for fiscal 2023",
        "pages": [45, 72],
        "children": [
          {
            "title": "Revenue by Product Category",
            "summary": "iPhone, Mac, iPad, Wearables, and Services revenue breakdown",
            "pages": [46, 52]
          },
          {
            "title": "Geographic Revenue Distribution",
            "summary": "Americas, Europe, Greater China, Japan, Rest of Asia Pacific",
            "pages": [53, 58]
          }
        ]
      },
      {
        "title": "Risk Factors",
        "summary": "Operational, market, regulatory, and competitive risks",
        "pages": [89, 110]
      }
    ]
  }
}

想象一下,有分析师询问2023财年的iPhone营收情况。大型语言模型无需搜索类似的文本片段,只需查看索引,即可判断“财务业绩 > 按产品类别划分的营收”是所需路径,随后直接前往该路径并完整获取第46页至第52页的内容。答案会附带精确的页码引用,而且由于模型从未看过“风险因素”部分,因此不会有任何干扰信息影响结果。

基于推理的检索简化后的代码实现如下:

import json
from openai import OpenAI

client = OpenAI()
def navigate_index(query: str, index_node: dict, depth: int = 0) -> list[dict]:
    """
    Recursively navigate the document index using LLM reasoning.
    Returns list of relevant leaf nodes with page references.
    """
    children = index_node.get("children", [])
    if not children:
        # Leaf node: return this section as relevant
        return [index_node]
    # Ask the LLM which branches are relevant to the query
    children_summary = "\n".join([
        f"[{i}] {child['title']}: {child['summary']}"
        for i, child in enumerate(children)
    ])
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    "You are navigating a document index to find sections relevant "
                    "to a query. Select the index numbers of sections that are likely "
                    "to contain the answer. Return a JSON array of selected indices."
                )
            },
            {
                "role": "user",
                "content": (
                    f"Query: {query}\n\n"
                    f"Available sections:\n{children_summary}\n\n"
                    f"Which sections should I look into? Return JSON array of indices only."
                )
            }
        ],
        temperature=0,
        response_format={"type": "json_object"}
    )
    selected = json.loads(response.choices[0].message.content).get("indices", [])
    relevant_nodes = []
    for idx in selected:
        if idx             # Recurse into selected branches
            relevant_nodes.extend(
                navigate_index(query, children[idx], depth + 1)
            )
    return relevant_nodes

def answer_with_pageindex(query: str, index: dict, document_pages: dict) -> str:
    """
    Full PageIndex retrieval and answer generation.
    """
    # Navigate the index to find relevant sections
    relevant_nodes = navigate_index(query, index)
    # Retrieve full text from identified pages
    context_parts = []
    citations = []
    for node in relevant_nodes:
        pages = node.get("pages", [])
        if pages:
            page_start, page_end = pages[0], pages[1]
            for page_num in range(page_start, page_end + 1):
                if page_num in document_pages:
                    context_parts.append(document_pages[page_num])
                    citations.append(f"p.{page_num}")
    context = "\n\n".join(context_parts)
    # Generate answer with full, unchunked context
    answer_response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    "Answer the question based on the provided document sections. "
                    "Be precise. If the answer involves numbers or dates, quote them exactly."
                )
            },
            {
                "role": "user",
                "content": f"Document sections:\n{context}\n\nQuestion: {query}"
            }
        ],
        temperature=0
    )
    answer = answer_response.choices[0].message.content
    citation_str = ", ".join(set(citations))
    return f"{answer}\n\n**Source:** {citation_str}"

看看缺少了什么:嵌入模型、余弦相似度、向量数据库以及分块逻辑。基于文档索引的结构化推理取代了整个检索流程。

金融领域基准测试的佐证

FinanceBench收集的是关于真实金融文件的问题,如SEC备案文件、年度报告和业绩电话会议记录。答案必须具备数值精度,各部分内容需精确互引,脚注也需仔细阅读。基于向量的RAG系统在此类任务中往往表现不佳,因为金融文件中充斥着不同主题但表述相似的文字。

Mafin 2.5基于PageIndex构建,在FinanceBench测试中的准确率达到了98.7%。而同类金融基准测试中,典型的向量RAG系统的准确率则根据配置不同在65%到80%之间。这一差距绝非微不足道,它将真正值得投资分析师信赖的工具与那些只会输出需要逐行核验的虚假内容的工具区分开来。

结构是其中的关键原因。金融文件恰恰是最需要层级结构的领域。合并资产负债表、报表附注以及管理层讨论与分析报告都遵循着清晰且有序的布局,分析师们一直依赖这种结构。PageIndex能够保留并利用这种布局,而向量分块技术则会破坏它。

何时选择哪种技术

PageIndex并非能全面取代向量RAG,最佳选择取决于具体项目需求。

它在以下情况下表现优异:

  • 文档具有清晰的层级结构,如报告、手册、合同和规格书等。
  • 准确性及精确引用是不可或缺的。
  • 应用领域较为专业,比如金融、法律、医疗或监管工作。
  • 问题解答需要参考同一文档中的多个部分。
  • 可解释性很重要,系统必须能够说明为何会给出特定结果。

在以下情况下,传统的向量RAG仍然适用:

  • 语料库由数千份简短且独立的文档组成,例如支持工单或产品评价。
  • 文档没有统一的结构。
  • 目标是在庞大语料库中进行近似语义搜索。
  • 延迟是主要考量因素,且分块处理已得到优化。

在大多数企业文档应用场景中,系统需要真正实用,而不仅仅在演示时看起来出色,这类场景更符合第一种情况而非第二种。

为了在没有基础设施负担的情况下实现生产环境使用,PageIndex 还提供了 REST API 和 MCP 集成。开发者的配置方式如下:

import requests

PAGEINDEX_API_KEY = "your_api_key"
BASE_URL = "https://api.pageindex.ai/v1"
def upload_document(file_path: str) -> str:
    """Upload a document and get back a document_id."""
    with open(file_path, "rb") as f:
        response = requests.post(
            f"{BASE_URL}/documents",
            headers={"Authorization": f"Bearer {PAGEINDEX_API_KEY}"},
            files={"file": f}
        )
    return response.json()["document_id"]

def query_document(document_id: str, question: str) -> dict:
    """Query an indexed document and get a cited answer."""
    response = requests.post(
        f"{BASE_URL}/query",
        headers={
            "Authorization": f"Bearer {PAGEINDEX_API_KEY}",
            "Content-Type": "application/json"
        },
        json={
            "document_id": document_id,
            "question": question
        }
    )
    return response.json()

# Example usage
doc_id = upload_document("q3_earnings_report.pdf")
result = query_document(doc_id, "What was total revenue in Q3?")
print(result["answer"])
print(f"Sources: {result['citations']}")

上传后,API会自动构建索引;当有查询时,它会返回带有可追溯页面引用的结构化答案。

更深层的理念

除了基准分数之外,还有更复杂的情况在发生。传统的向量RAG将文档视为一组相关的文本片段,而忽略作者所设计的结构。每一个标题、分段标记和交叉引用都是指引信号,是作者告诉读者该去何处的内容,但传统方法在试图从零重建相关性之前就抛弃了所有这些信息。PageIndex则将文档视为其本来面目:一种为具备能力的读者提供导航功能的结构化作品。

撰写年度报告的高级分析师不会从第一页开始阅读。他们会先查看目录,直接跳到感兴趣的部分,通过附注核对相关数据,然后再回到管理层讨论与分析部分。PageIndex中的大型语言模型也会做同样的事情。

这也是它能够应用于专业领域的原因。药品试验报告的结构与法律合同或技术规范不同,但在每种文档中,作者都遵循了某种内在逻辑,而专家读者会利用这一逻辑。PageIndex能够捕捉到这种逻辑,并据此进行信息检索。

不同的诊断方式

令人清醒的结论是,多年的努力可能针对了错误的问题。人们专注于分块策略,而真正的缺陷从一开始就在破坏文档结构。尽管语义相似度从来都不是正确的检索信号,人们仍不断调整嵌入模型。当检索方法本身就是瓶颈时,人们又强行添加重排器来提升检索质量。

RAG生态系统构建了复杂的机制来弥补分块策略的固有缺陷。PageIndex提出了另一种方案:无需分块、无需嵌入、无需相似度搜索。只需构建一个反映文档真实结构的索引,然后通过推理方式在其中进行检索。

它并不能解决所有问题。在处理数百万份简短且非结构化的文档时,向量搜索在语义搜索方面依然更具优势。然而,对于涉及长篇结构化文档的各类问题——这涵盖了大多数企业的需求——分层树结构值得重点关注。重新运行那次两次失败的演示,询问2023年的营收预测,系统现在会直接跳转到相应板块,调出正确的页面,并给出精确的引用信息。没有错误的年份,没有错误的板块,也没有过度的自信。

PageIndex是开源项目,可在GitHub上自行托管,同时还提供托管的聊天界面以及包含REST和MCP参考资料的API文档,方便您在自己的文档上尝试使用。

相关阅读

  • Spring AI:利用Lucene和RRF通过混合搜索获得更好的RAG结果 — 在Spring AI检索器中将向量搜索与Lucene BM25结合,并通过互反排名融合技术整合排序结果,从而使RAG既能匹配语义内容又能匹配精确术语。
  • OpenAI的Agents API如何改变AI工作的成本结构 — Agent会自动处理模型推理、工具调用、沙箱环境、重试操作以及人工审核,因此不再需要通过token数量来计算成本,应改为衡量实际被接受的任务数量。
  • RAG的混合检索方案:BM25、排名融合与重排技术优于向量搜索 —— 为何纯向量搜索在RAG中无法准确匹配关键词,以及如何通过BM25结合语义搜索、互斥排名融合与交叉编码器重排技术将检索准确率提升至91%。
  • Pinecone、Weaviate、Milvus、Qdrant还是Chroma:如何选择向量数据库 —— 用通俗语言解析各类向量数据库的特点,涵盖无服务器与混合检索、Rust语言及GPU索引技术等,同时分析各自的优缺点及最佳适用场景。