带 PageIndex 的无向量 RAG:通过推理而非向量进行检索
PageIndex通过交换嵌入与分块方式,为能够遍历文档树的LLM提供支持。附有一段简短的Python代码示例,以及向量RAG仍更优的适用场景。
来源: **I Stopped Using Vector Databases for RAG : PageIndex Vectorless RAG**
任何从事RAG系统开发一段时间的人都会体会到这种烦恼。你花费数小时处理嵌入向量、调整分块大小、选择向量数据库,系统却依然给出错误答案。它找到的可能是类似的内容,但“类似”与“相关”并非同一概念。
财务报告生动地展现了这一问题。以一份120页的10-K文件为例,向量RAG系统总是从执行摘要中提取内容,而真正答案却在第87页的脚注中。这两段文字虽有关键词和相似含义,但只有后者包含你需要的数据。
PageIndex正是为解决这一问题而设计的。
什么是PageIndex
PageIndex 是 VectifyAI 团队开发的开源 RAG 框架。它的核心理念简单且与众不同——并非将文档转换为向量后再进行最近邻搜索,而是从文档中构建分层树结构,让大语言模型通过推理找到答案。
它完全省去了嵌入、向量存储和分块处理等步骤。
该项目将此技术称为“无向量 RAG”,这一名称相当准确。它于 2025 年 9 月出现便迅速引起了关注,主要得益于一项测试数据:在 FinanceBench 测试集上的准确率高达 98.7%,而传统向量 RAG 在同一测试集上的准确率仅约为 50%。虽然需要对厂商公布的测试数据保持谨慎,并在自己的文档上进行测试,但即便只有部分优势,这也已属于完全不同的层级。
向量 RAG 的局限性
向量RAG的核心假设是,与查询在语义上最相似的文本也最相关。但实际上这一假设总是不成立。
如果询问“该公司2023年的总债务是多少?”,得分最高的文本片段可能是:
- CEO关于债务管理策略的信件,
- 关于债务契约的风险说明部分,
- 定义长期债务的术语表条目。
这三者都与查询在含义上较为接近,但都没有给出答案,即第64页资产负债表上的具体数字。
其他问题也随之出现:
- 分块会破坏上下文关系。将一份100页的文档拆分成500个token的片段后,其结构就会被破坏。像“如表3.2所示”这样的句子,一旦与表3.2被分到不同的片段中,就会与表3.2脱节,文档的逻辑和行文顺序也会被忽略。
- 向量模型无法追踪交叉引用。文档中常出现“参见附录G”或“参考第4节中的往年数据”这样的表述,而相似度搜索无法追踪这些引用指向。它们只是没有与目标内容关联的文本片段而已。
- 查询表达的是意图而非内容。用户描述的是自己想要了解的信息,而非期望得到的答案形式。答案可能使用不同的词汇,而向量检索因为需要将问题文本与答案文本进行匹配,所以很难准确找到答案。
基于推理的检索
思维方式的转变在于:PageIndex不再询问哪些内容片段与查询相似,而是思考人类专家会在文档的哪个位置查找答案。这是一种不同的检索策略,更适用于结构化且复杂的文档。
工作原理
整个过程分为两个阶段。
第一阶段:构建树形索引。面对一份文档,PageIndex不会将其分割成片段,而是直接读取并构建出一个反映文档自然结构的层次结构,类似于智能的深度嵌套目录。每个节点代表一个章节或子章节,包含标题及其内容摘要。大型语言模型会在索引过程中撰写这些摘要,它们随后会作为检索信号使用。
对于10-K格式的文件,其树形结构可能如下所示:
Annual Report 2023
├── Business Overview
│ ├── Products and Services
│ └── Market Position
├── Risk Factors
│ ├── Financial Risks
│ └── Operational Risks
├── Financial Statements
│ ├── Balance Sheet
│ │ ├── Assets
│ │ └── Liabilities
│ └── Income Statement
└── Notes to Financial Statements
├── Note 1: Accounting Policies
└── Note 12: Long-term Debt
该文档不再是扁平的,它具有结构,大语言模型可以像人翻阅书籍一样在其中浏览。
第二阶段:基于推理的树形搜索。当有查询进入时,不会先查询向量数据库。大语言模型会收到查询以及树结构的顶层结构,然后需要推理出答案可能所在的位置。它可能会判断关于长期债务的问题应归类在财务报表部分,很可能是在附注中,进而返回一个节点ID。
PageIndex随后会展开该节点,显示更详细的下一层级内容,大语言模型会持续深入搜索,直到找到能回答问题的内容。
人类分析师也是以同样的方式工作。没人会从头到尾阅读200页的报告,而是先查看目录,找到相应章节直接阅读。PageIndex则能以机器的速度完成这一过程。
除了最后一次内容获取之外,每个阶段都是模型的推理过程。没有通过向量进行匹配,而是由大语言模型自行判断该去何处查找。
一个简化的实现示例
以下示例用 Python 构建了这个概念的简化版本,其中以 Gemini 作为大语言模型。
步骤 1:解析文档
首先逐页提取原始文本。PyMuPDF 能够高效地处理 PDF 文件。
import fitz # pip install pymupdf
def parse_pdf(pdf_path: str) -> list[dict]:
doc = fitz.open(pdf_path)
pages = []
for i, page in enumerate(doc):
text = page.get_text().strip()
if text:
pages.append({"page_num": i + 1, "text": text})
doc.close()
return pages
然后将连续的页面分组为若干部分。与随意分割不同,这种方法会尊重页面边界,将相关内容放在一起。
def group_pages_into_sections(pages, per_section=3):
sections = []
for i in range(0, len(pages), per_section):
batch = pages[i : i + per_section]
section_id = f"S{str(i // per_section + 1).zfill(3)}"
combined_text = "\n\n".join(p["text"] for p in batch)
sections.append({
"section_id": section_id,
"start_page": batch[0]["page_num"],
"end_page": batch[-1]["page_num"],
"text": combined_text,
})
return sections
步骤2:使用大语言模型构建树形索引
这正是该方法的核心所在。无需使用嵌入向量,模型会读取每个部分的预览内容并生成结构化的摘要。最终得到的是一个嵌套的JSON树,每个节点都包含标题、简短摘要以及关键主题列表。
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
def index_section(section: dict) -> dict:
preview = section["text"][:1500]
prompt = f"""Read this section from a document and summarize it.
Section pages: {section['start_page']} to {section['end_page']}
Text:
{preview}
Respond with ONLY valid JSON:
{{
"title": "short descriptive title (5-8 words)",
"summary": "2-3 sentence summary of what this section covers",
"key_topics": ["topic1", "topic2", "topic3"]
}}"""
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=prompt,
config=types.GenerateContentConfig(temperature=0.0),
)
parsed = json.loads(response.text.strip())
return {
"node_id": section["section_id"],
"title": parsed["title"],
"pages": f"{section['start_page']}-{section['end_page']}",
"summary": parsed["summary"],
"key_topics": parsed["key_topics"],
}
对所有部分执行该操作后,完整的树结构将被保存为 JSON 文件。
def build_tree_index(sections):
nodes = [index_section(s) for s in sections]
return {
"title": "Your Document Title",
"total_sections": len(nodes),
"nodes": nodes,
}
# Save for reuse
with open("tree.json", "w") as f:
json.dump(tree, f, indent=2)
步骤3:基于推理的树形搜索
此时,该方法与向量RAG完全分道扬镳。当有问题出现时,无需搜索数据库,而是将整个树结构提供给大语言模型,让其判断哪些部分最有可能包含答案。它的行为就像一名分析师在浏览目录一样。
def retrieve_sections(tree: dict, query: str) -> dict:
# Build a compact text representation of the tree
tree_text = f"Document: {tree['title']}\n\n"
for node in tree["nodes"]:
tree_text += f"[{node['node_id']}] Pages {node['pages']} | {node['title']}\n"
tree_text += f" Summary: {node['summary']}\n"
tree_text += f" Topics: {', '.join(node['key_topics'])}\n\n"
prompt = f"""You are a document retrieval expert.
Given this document tree, identify which sections most likely answer the question.
Think step by step about where a human expert would look.
{tree_text}
QUESTION: {query}
Respond with ONLY valid JSON:
{{
"reasoning": "your step-by-step reasoning about where to look",
"selected_ids": ["S001", "S004"],
"confidence": "high/medium/low"
}}"""
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=prompt,
config=types.GenerateContentConfig(temperature=0.0),
)
return json.loads(response.text.strip())
响应中的推理字段并非仅用于装饰,它记录了模型为何选择那些内容片段,这正是此处所说的可解释性。你可以查看检索决策过程,这是向量搜索所无法提供的功能。
第4步:获取内容
拿到选定的内容片段ID后,即可提取这些片段的原始文本。无需进行向量查找或嵌入计算,只需在已解析的内容片段字典中进行查找即可。
def retrieve_content(selected_ids: list, sections: list) -> str:
section_map = {s["section_id"]: s for s in sections}
context_parts = []
for sid in selected_ids:
if sid in section_map:
sec = section_map[sid]
context_parts.append(
f"--- Pages {sec['start_page']}-{sec['end_page']} ---\n"
+ sec["text"][:3000]
)
return "\n\n".join(context_parts)
第5步:生成答案
检索到的文本会与原始问题一同发送给Gemini,模型会结合页面级引用给出精确的答案。
def generate_answer(query: str, context: str) -> str:
prompt = f"""Answer the question using only the provided context.
Be specific. Include exact numbers, technical terms, and cite page numbers.CONTEXT:
{context}
QUESTION: {query}
ANSWER:"""
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=prompt,
config=types.GenerateContentConfig(temperature=0.1),
)
return response.text.strip()
PageIndex与向量RAG的对比
这两种方法在多个方面存在差异。向量RAG处理速度快,能够处理大规模数据集,且需要嵌入模型和索引。而PageIndex不需要这些,但每次查询会发起多次连续的LLM调用,作为回报,它在处理结构化文档时能提供更高的准确性,并保留完整的推理过程。
何时选择PageIndex
它并非向量RAG的通用替代品,而是用于不同任务的工具。
以下情况适合选择PageIndex:
- 您正在处理单份长度较长、结构化的文档,且准确性比速度更重要:财务报告、法律合同、监管文件、技术手册、学术论文。任何具有明确层级结构、不能仅满足“大致准确”要求的文档都适用。
- 你需要可解释性,因为用户或利益相关方必须清楚知晓答案的来源。你可以获得包含章节引用和页码的推理轨迹。
- 你需要为金融、法律和医疗保健等受监管行业构建系统,因为错误的答案会带来严重后果,而报告的准确率至关重要。
以下情况请选择向量RAG:
- 当你需要在海量数据集中搜索并确定哪些文档相关时,向量RAG在这方面表现优异,而PageIndex仅能深入分析单份文档,无法从上千份文档中选出合适的那一份。
- 当吞吐量和延迟对你而言很重要时,每次查询都需要多次调用大语言模型,随着流量增加,成本和速度都会上升。
- 当资料内容简短、结构松散或以对话形式呈现时,向量RAG的优势在于能够处理层级结构,而缺乏层级结构时其优势便不复存在。
将二者结合使用。先利用向量搜索在文档集合中找到合适的文档,再通过PageIndex从中提取精确的答案。这种混合方式能够兼收两者的优势。
更深层的理念
PageIndex的真正做法是将检索视为推理问题而非相似性问题。这一转变的意义不仅体现在文档处理上。有报告指出,Claude Code已不再依赖嵌入搜索来查找代码,而是转而采用基于推理的查找方式。这一趋势愈发明显:当任务需要理解结构并遵循逻辑时,推理比相似性搜索更有效。
向量数据库不会消失。它们依然具备高速、可扩展的特性,非常适合对大量数据集进行语义搜索。但对于在复杂文档中精准查找所需信息的难题,以推理优先的方法似乎是更优的选择。
PageIndex是当前实现这一理念的较好方案之一。它是开源的,拥有简洁的API,该项目还提供了云服务及MCP服务器,无需从零编写检索代码即可将其集成到Claude或其他智能工作流中。官方提供的示例是个不错的起点,大约半小时内就能搭建起可运行的系统。如果你的用户需要针对200页的报告提出精确的问题,那么尝试一下这个方案是值得的。
相关阅读
- RAG的工作原理:嵌入、向量数据库与分块 —— 跟踪一段文本从分词到池化再到余弦相似度搜索的全过程,了解为何分块方式以及嵌入模型决定了RAG的质量。
- PageIndex:基于树索引的无向量、推理型RAG框架 —— PageIndex用由大语言模型驱动的树索引替代了向量数据库。本文将介绍其工作原理、可用示例代码、基准测试结果以及适用场景。