首页 / 文章

非向量RAG:PageIndex如何在没有嵌入向量的情况下读取文档

带 PageIndex 的无向量 RAG:为何分块向量搜索会在长篇报告中丢失上下文,以及基于推理的树索引如何找到答案(附代码)。

发布者:REACTAPP.TOP阅读约 15 分钟3480 词

来源: I Deleted My Vector Database and My RAG System Got Better

如果你问别人第七章讲了什么,他们不会重读整本书,而是会打开目录,直接跳到第七章阅读那一部分。这样既快捷又准确。

大多数人工智能文档搜索系统并非如此工作。它们会将文本随意分割成片段,把每个片段转换成数字,然后根据问题与最接近的数字进行匹配。有时这没问题,但有时答案会横跨片段边界,而系统却无法识别。一种名为“无向量RAG”的新方法摒弃了数字匹配的方式,让模型像人类一样理解文档的结构。据称,在一项难度较高的金融领域测试中,它的准确率达到了98.7%,而传统方法的成功率仅勉强达到50%。接下来的内容将从基础层面逐步解释这一成果的实现原理。

RAG的功能

RAG即检索增强生成。实际上,它指的是让模型能够访问用户自己的文档,从而使模型的回答源自这些数据,而不仅仅来自训练数据。

这一点很重要,因为像GPT或Claude这样的模型存在知识截止点。它们从未见过任何特定组织的内部报告、法律合同或医疗记录。RAG通过获取相关段落并在模型回答之前将其呈现给模型来填补这一空白。其理念很简单:提出问题,就能得到基于用户材料的答案。但实际情况要复杂得多。

暴力破解法

最直观的设计方式是将整个文档与问题一起放入提示词中,让模型自行处理。无需数据库,也无需复杂的机制。

from openai import OpenAI
import PyPDF2

client = OpenAI(api_key="your-api-key")

# Extract all text from a PDF
def extract_pdf_text(pdf_path):
    reader = PyPDF2.PdfReader(pdf_path)
    full_text = ""
    for page in reader.pages:
        full_text += page.extract_text() + "\n"
    return full_text

document_text = extract_pdf_text("annual_report.pdf")
user_question = "What was the total revenue in 2024?"

response = client.chat.completions.create(
    model="gpt-4.1",
    messages=[
        {"role": "system", "content": "Answer based on the provided document."},
        {"role": "user", "content": f"Document:\n{document_text}\n\nQuestion: {user_question}"}
    ]
)

print(response.choices[0].message.content)

对于五页的PDF来说还行,但200页的年度报告很快就会遇到限制。

  • 上下文限制。模型一次只能处理一定量的文本,200页的文件可能包含30万个标记,这已经超出了最大模型的处理能力。
  • 质量下降。即便所有内容都能被处理,答案也会变得模糊不清。让模型在需要筛选海量信息的情况下寻找单一目标,会导致它失去焦点。
  • 成本问题。服务提供商按标记数量收费,因此每个问题都需要发送30万个标记,即便答案只存在于一页文档中,费用也会迅速累积。

RAG正是为了解决这些限制而诞生的。

向量RAG的工作原理

其思路相当巧妙:先对文档进行处理,将其智能化的表示存储在数据库中,提问时再仅提取相关部分。整个流程包含五个阶段:

  1. 分块处理。将文档拆分成更小的部分,比如每500个单词或每段文字为一个块。
  2. 嵌入转换。专用模型会将每个分块转换为一组数字,即向量,以此体现其含义——含义相似的内容会对应相似的数字。
  3. 存储。这些向量会被存储在专为该目的设计的数据库中,如ChromaDB、Pinecone或Weaviate;Milvus、Qdrant以及PostgreSQL的PGVector扩展也是常见的选择。
  4. 搜索。查询语句也会被嵌入转换,数据库会返回向量与之最接近的分块,这就是相似度搜索。
  5. 生成内容。检索到的分块与查询语句一同输入模型,使其能够在聚焦的上下文中进行工作。

使用ChromaDB和OpenAI实现的完整可行版本如下:

from openai import OpenAI
import chromadb
import PyPDF2

client = OpenAI(api_key="your-api-key")

# Step 1: Extract and chunk the document
def extract_and_chunk(pdf_path, chunk_size=500):
    reader = PyPDF2.PdfReader(pdf_path)
    full_text = ""
    for page in reader.pages:
        full_text += page.extract_text() + "\n"

    words = full_text.split()
    chunks = []
    for i in range(0, len(words), chunk_size):
        chunk = " ".join(words[i:i + chunk_size])
        chunks.append(chunk)
    return chunks

chunks = extract_and_chunk("annual_report.pdf")

# Step 2 and 3: Embed and store in ChromaDB
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("my_documents")

collection.add(
    documents=chunks,
    ids=[f"chunk_{i}" for i in range(len(chunks))]
)

# Step 4: Search for relevant chunks
user_question = "What was the total revenue in 2024?"

results = collection.query(
    query_texts=[user_question],
    n_results=5
)

relevant_chunks = "\n\n".join(results["documents"][0])

# Step 5: Generate answer with focused context
response = client.chat.completions.create(
    model="gpt-4.1",
    messages=[
        {"role": "system", "content": "Answer based only on the provided context."},
        {"role": "user", "content": f"Context:\n{relevant_chunks}\n\nQuestion: {user_question}"}
    ]
)

print(response.choices[0].message.content)

如今大多数公司都在使用类似系统。它能够很好地处理关于简单文档的直接问题,但一旦文档变得复杂,就会出现缺陷。

其失败的三种方式

将完整信息割裂的片段

想象一下报告中的一段文字,内容是第三季度营业收入达到42亿美元,同比增长12%,这主要得益于云服务带来的28亿美元经常性收入(详情见附录C)。如果500字的显示范围恰好结束在该句的中间,那么一个片段会保留数字但丢失解释,另一个片段则会保留解释但丢失数字。这两种情况都无法呈现完整信息。

这种缺陷很容易重现:

paragraph = """The company's Q3 operating income was $4.2 billion,
representing a 12% increase over the prior year period. This growth
was primarily driven by the expansion of cloud services, which
contributed $2.8 billion in recurring revenue as detailed in the
segment breakdown in Appendix C."""

# Simulating a chunk boundary at word 20
words = paragraph.split()
chunk_1 = " ".join(words[:20])
chunk_2 = " ".join(words[20:])

print("Chunk 1:", chunk_1)
print("---")
print("Chunk 2:", chunk_2)

若询问是什么推动了第三季度营业收入的增长,搜索结果可能会返回仅提及云服务但未附数据的部分,或者附有数据但未说明原因的部分。无论哪种情况,答案都不完整。

按段落拆分也无法解决这个问题。有些段落只有两句话,而有些则占半页篇幅,甚至连续三段文字才能构成一个只有合在一起阅读才有意义的完整故事。

毫无用处的参考资料

合同和财务文件中常常出现诸如“第4.2.1节对此有规定”或“详细数据见附录G”之类的指引。如果有人询问第12页中提及的第87页的数据,搜索结果很可能会停留在第12页,因为关键词匹配,但向量搜索系统不会跟随这些指引去查找包含数据的第87页。

简单的模拟就能显示出这个问题:

# Simulating a cross-reference failure
documents = {
    "page_12": "The company's debt-to-equity ratio improved significantly in 2024. For a full breakdown of long-term obligations, see Appendix G on page 87.",
    "page_45": "Marketing expenses increased by 15% due to new campaigns.",
    "page_87": "Appendix G: Long-term debt stands at $12.4B. Senior notes: $8.1B. Credit facility: $4.3B. Maturity schedule: 2026-2034."
}

# Vector RAG would likely return page_12 for a debt question
# but miss page_87 where the actual numbers live
# because "debt-to-equity ratio" is more similar to the query
# than "Senior notes" and "Credit facility"

这种情况在合同、监管文件和学术论文中很常见。答案往往同时存在于两个地方,而无法将它们关联起来的系统总会得到不完整的结果。

术语不匹配

向量相似度在很大程度上取决于用户的用词与文档用词是否相似。如果报告中使用的是“非GAAP调整后的EBITDA”,而用户询问的是“调整后的收益”,那么嵌入向量可能无法足够精准地对齐,从而无法提取出正确的内容片段。人们并不需要掌握文档中的精确术语,也不应该需要。

在技术领域情况更为糟糕。医生在搜索心脏病发作治疗方案时,可能会错过那些以“心肌梗死管理路径”表述的内容片段。虽然含义相同,但向量之间的差异可能很大,导致正确的内容片段根本无法被识别出来。

这些并非罕见的边缘情况。在实际应用中,对于篇幅较长且结构复杂的文档,这类情况屡见不鲜。

无向量RAG的出现

2025年9月,VectifyAI的一个团队发布了一个名为PageIndex的框架,采用了不同的处理方式。它不会将文档切分后再匹配编号,而是为文档构建结构化映射,让人工智能模型能够据此进行推理。

这一设计的灵感来源于专家们的实际工作方式。当分析师交给某人一份200页的报告,而对方只需要一个数据时,他不会通读每一页。而是会查看目录,挑选最有可能包含答案的部分并仔细阅读。如果目录中提示“参见附录G”,他就会直接查看附录G。

PageIndex能让模型以相同方式运作。关键在于,最相似的文本并不总是最相关的文本。向量数据库会返回与查询最相似的内容,但答案有时会出现在与之毫无相似之处的部分。我们需要的是对答案应位于何处的推理,而非模式匹配。

该系统分两步运行。

第一步:树形索引

无需进行分块和嵌入处理,文档会被交给一个推理模型,由其将其构建成树形结构。可以想象一名出色的实习生阅读整份报告后,会起草一份多层级的目录,每个条目包含标题、简短摘要、页码范围以及指向其子条目的链接。一份财务报告的目录可能如下所示:

Root: Annual Report 2024
  |-- Executive Summary (pages 1-3)
  |     Summary: "Overview of company performance, key metrics..."
  |-- Financial Statements (pages 15-45)
  |     |-- Income Statement (pages 15-20)
  |     |-- Balance Sheet (pages 21-30)
  |     +-- Cash Flow Statement (pages 31-45)
  |-- Risk Factors (pages 46-60)
  +-- Appendices (pages 80-120)
        |-- Appendix A: Segment Data (pages 80-95)
        +-- Appendix G: Detailed Tables (pages 96-120)

没有向量,也没有嵌入模型,仅有与文档结构相匹配的逻辑框架。该模型通过推理来识别章节分隔、主题转换和新板块等自然边界,而非依据固定的字数进行分割。

第二步:基于推理的搜索

在查询时,模型仅能看到该结构树,而看不到整个文档。它会浏览标题和摘要,运用自身的领域知识,选出最有可能包含答案的节点。

以探讨营业利润率的同比变化为例。该模型的推理过程大致如下:利润率属于财务指标,因此利润表很重要,但进行同比比较时可能还需要更早时期的数据,或许可以在附录中找到。系统会返回这两个部分的节点ID,随后仅提取这两个部分的原始文本,最终答案便基于这些有限的信息生成。

要演示这一概念,只需使用普通的OpenAI接口即可:

import json
from openai import OpenAI
import PyPDF2

client = OpenAI(api_key="your-api-key")

# Extract text page by page
def extract_pages(pdf_path):
    reader = PyPDF2.PdfReader(pdf_path)
    pages = {}
    for i, page in enumerate(reader.pages):
        pages[i + 1] = page.extract_text()
    return pages

pages = extract_pages("annual_report.pdf")
all_text = "\n".join([f"--- Page {k} ---\n{v}" for k, v in pages.items()])

# Step 1: Build the tree index using AI reasoning
tree_prompt = f"""You are a document analyst. Read this document and create
a hierarchical table of contents as a JSON tree. Each node should have:
- "title": section name
- "summary": 2-3 sentence description of what this section covers
- "pages": [start_page, end_page]
- "children": array of child nodes (or empty array)

Document:
{all_text[:80000]}

Return ONLY valid JSON. No other text."""

tree_response = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": tree_prompt}],
    response_format={"type": "json_object"}
)

document_tree = json.loads(tree_response.choices[0].message.content)
print("Document tree built successfully!")
print(json.dumps(document_tree, indent=2)[:500])

# Step 2: Reasoning-based search over the tree
user_question = "What was the year-over-year change in operating margin?"

search_prompt = f"""You are a retrieval expert. Given a user question and
a document's table of contents tree, identify which sections are most
likely to contain the answer.

Think step by step:
1. What kind of information does the question ask for?
2. Which sections would a human expert check first?
3. Are there sections that might cross-reference each other?

Document tree:
{json.dumps(document_tree, indent=2)}

Question: {user_question}

Return JSON with:
- "reasoning": your step-by-step thought process
- "relevant_pages": list of page numbers to retrieve"""

search_response = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": search_prompt}],
    response_format={"type": "json_object"}
)

search_result = json.loads(search_response.choices[0].message.content)
print("Reasoning:", search_result["reasoning"])

# Step 3: Fetch only relevant pages and generate answer
relevant_text = ""
for page_num in search_result["relevant_pages"]:
    if page_num in pages:
        relevant_text += f"\n--- Page {page_num} ---\n{pages[page_num]}"

answer_response = client.chat.completions.create(
    model="gpt-4.1",
    messages=[
        {"role": "system", "content": "Answer precisely based on the document context provided."},
        {"role": "user", "content": f"Context:{relevant_text}\n\nQuestion: {user_question}"}
    ]
)

print("\nAnswer:", answer_response.choices[0].message.content)

所以整个技巧可以用一句话概括:跳过向量存储,跳过嵌入向量,跳过分块大小调整,让模型自行分析大纲,确定答案所在位置,然后仅获取相关部分。

使用 PageIndex 框架

自行实现的版本适合学习使用。对于实际项目,PageIndex 能承担更多工作:它能生成优化过的树结构,支持更高级的搜索策略,并且提供云服务或自托管选项。基本使用方式如下:

from pageindex import PageIndexClient
import time

# Initialize the client
pi_client = PageIndexClient(api_key="your-pageindex-api-key")

# Upload and process your document
result = pi_client.submit_document("./annual_report.pdf")
doc_id = result["doc_id"]

# Wait for processing to complete
while True:
    status = pi_client.get_document(doc_id)["status"]
    if status == "completed":
        print("Document processed!")
        break
    time.sleep(5)

# Inspect the generated tree structure
tree_result = pi_client.get_tree(doc_id)
if tree_result.get("status") == "completed":
    tree = tree_result["result"]
    for node in tree:
        print(f"[{node['node_id']}] {node['title']} - Page {node['page_index']}")

# Ask questions using the Chat API
response = pi_client.chat_completions(
    messages=[{"role": "user", "content": "What was total revenue in FY2024 vs FY2023?"}],
    doc_id=doc_id
)

print(response["choices"][0]["message"]["content"])

生成的树形结构如下:

{
  "title": "Financial Stability",
  "node_id": "0006",
  "page_index": 21,
  "text": "The Federal Reserve maintains financial stability...",
  "nodes": [
    {
      "title": "Monitoring Financial Vulnerabilities",
      "node_id": "0007",
      "page_index": 22,
      "text": "The Federal Reserve's monitoring focuses on..."
    },
    {
      "title": "Domestic and International Cooperation",
      "node_id": "0008",
      "page_index": 28,
      "text": "In 2023, the Federal Reserve collaborated..."
    }
  ]
}

每个节点都有一个唯一标识,指向原始文档,还有标题、文本内容以及用于子章节的子节点。该模型通过读取摘要来遍历这棵结构树,并决定进一步展开哪些分支。

在单次查询中可以对比多份文档:

response = pi_client.chat_completions(
    messages=[{"role": "user", "content": "Compare the results across these two reports."}],
    doc_id=["pi-abc123def456", "pi-abc123ghi789"]
)

使用 LangChain、CrewAI 或 OpenAI Agents SDK 开发智能体的开发者可以通过模型上下文协议(MCP)连接 PageIndex,这样智能体无需编写自定义集成代码即可将其作为检索工具使用。

该宣称背后的基准测试

FinanceBench 是让无向量 RAG 技术受到关注的基准测试工具。其问题均来自真实的 SEC 文件(10-K 和 10-Q 报告),并要求精确的答案:需从表格中提取数据、进行多步骤计算,还要在不同部分之间进行交叉引用。这绝非简单的测试。

传统的向量 RAG 方法在该测试中的得分大约在 30% 到 50% 之间。而依托 Mafin 2.5 推理引擎的 PageIndex 则取得了 98.7% 的高分,这已是完全不同的水平,提升幅度极为显著。人们通常会提到三个原因:

  • 跟随交叉引用。当某个条目标注“参见附录G”时,推理模型会沿着结构树找到附录G并获取相应数据。而向量存储则不知道需要跟随该指针。
  • 保持布局完整。财务表格包含表头、嵌套表头、脚注以及单元格间的链接。树形结构能将这些元素在清晰的边界内保持在一起,而固定大小的分块处理则会将它们分开。
  • 多跳推理。要计算营业利润率的同比变化,需要来自两个部分的数值以及相应的计算公式。推理模型会确定需要这两个部分的内容并同时获取,而相似度搜索则很可能只获取与问题描述最接近的那部分数据。

需要说明的一点是:截至2026年初,PageIndex团队尚未公布详细的速度或处理能力数据。由于每次查询都需要进行多次连续的模型调用来进行遍历和推理,因此其每条查询的处理速度较慢,成本也高于单次向量查找。对于高吞吐量、低延迟的应用场景,这种权衡可能并不适用。

如何选择

非向量式RAG并非向量数据库的通用替代品,而是一种专用工具。

当响应需要在不到一秒的时间内生成,当数据集由大量短文档组成(如常见问题库或文章集),当每条查询的成本高于追求完美准确度的需求,或者当消费者级聊天机器人能够接受足够好的答案时,向量式RAG更为合适。

无向量RAG适用于长度较长、结构化的专业文档,如年度报告、合同、监管文件和技术手册。它也适合那些准确性至关重要的场景,即查询必须基于交叉引用,或者需要清晰的审计轨迹来显示答案的各个部分来源。

在实际应用中,可以考虑采用混合方案。轻量级的元数据或关键词过滤器可用于筛选相关文档,随后通过基于树的推理方法找到文档中的具体章节。这样既能实现首次查询时的传统搜索速度,又能保证二次推理的准确性。

成本与性能之间确实存在权衡。推理模型每次调用的成本高于嵌入模型,而且树形遍历需要多次连续调用,从而导致更高的延迟。人们正在用速度和价格来换取准确性和可解释性。在金融、法律和医疗等对错误答案后果严重的受监管领域,这种权衡通常能带来好处。

发展前景

从基于相似度的检索向基于推理的检索转变虽然还处于初期阶段,但发展方向十分明确。PageIndex已经支持一种基于视觉的变体,可直接处理PDF页面图像,无需进行文本提取或OCR处理。只需输入页面图像,模型即可对其内容进行推理。这对于文本提取难以处理的复杂布局、扫描页面以及嵌入的图表非常有帮助。

基于智能代理的检索技术也在研发中。该模型不会只进行一次检索,而是持续探索:它会追踪参考信息、对比不同部分、向自身提出后续问题,并通过多轮推理不断优化答案,这正是快速浏览文档与深入研究文档之间的区别。

任何想要进行实验的人都可以使用GitHub上的开源PageIndex仓库,其中包含了标准流程和基于视觉的流程对应的可运行Jupyter笔记本。该项目在2025年9月发布后获得了20,000个星标,其Discord频道也十分活跃。

向量数据库依然存在,且能很好地处理大量工作负载。然而,那些需要深度解析冗长复杂文档的应用程序正在不断增加,而将文本切割成固定大小的片段后再通过相似度搜索来找到正确片段的办法已逐渐显得力不从心。合同、申报文件和技术规格书正是企业最为看重的文档,也正是基于推理的检索方式能发挥最大作用的领域。

判断的方法是先在已经非常熟悉的文档上运行自行实现的版本,然后再对同一文件使用PageIndex,看看树形结构是如何保留文档原有的结构划分的。最好的检索系统并非拥有最先进技术的系统,而是能够找到正确答案的系统。

尝试方法

  • 向量RAG,适合理解基础原理:安装chromadb和openai,先用短文档运行ChromaDB示例,再尝试使用长文档,观察答案在何处出现偏差。
  • 自行实现非向量版本,有助于掌握概念:对同一文档运行OpenAI的树状推理示例,然后将结果与向量版本进行对比。
  • 生产环境下的非向量方案:从文档网站获取免费的PageIndex API密钥,安装pageindex Python包并按照SDK示例操作。上传真实文档后提出已知答案的问题,这是判断该方法适用性的最快方式。

所有示例均使用Python 3.10或更高版本,依赖OpenAI SDK、ChromaDB和PyPDF2,可通过pip install openai chromadb PyPDF2 pageindex进行安装。除OpenAI API密钥外,PageIndex示例还需PageIndex密钥。

相关阅读

  • RAG中的元数据增强:过滤器、引用、新鲜度与访问控制 —— 为何RAG系统会返回错误地区的策略,以及如何在数据导入时通过添加来源、机构、时间和安全相关的元数据来改善检索效果。
  • 30道RAG与向量数据库面试题及简短答案 —— 30道关于RAG和向量搜索的面试题及简短解答,涵盖混合检索、HNSW与IVF的对比、数据分块、重排序、评估指标以及多租户安全等内容。
  • 基于文档树的推理而非向量搜索 — PageIndex构建了分层索引,让大语言模型能够直接在其中导航,无需进行分块处理或嵌入操作。本文将介绍其工作原理、在FinanceBench测试中的表现以及适用场景。
  • GraphRAG与Vector RAG:为何向量搜索在大规模场景下会失效 — Vector RAG在分块隔离、跨文档推理以及处理大规模数据方面存在缺陷。本文将说明GraphRAG如何通过构建社区和摘要来解决这些问题,以及何时选择哪种技术。