基于推理的检索如何在结构化文档上击败相似度搜索,以及如何使用 PageIndex 构建它
你向AI助手询问一份200页合同的问题,它自信满满地给出回答,结果却是错的。它提取了正确主题的文本,却选错了条款,而模型从头到尾都没有察觉。
这不是模型问题,而是检索问题。之所以会发生,是因为传统向量RAG的核心假设——文本看起来相似就等于内容相关——在任何有结构的文档上都会失效。
现在有一种全新思路:无向量RAG(Vectorless RAG)。
不需要嵌入向量,不需要向量数据库,不需要分块流水线。系统不再搜索相似文本,而是像人类专家一样,推理出答案在文档中的位置。
本文将解释这一概念、它在哪些场景优于传统RAG,以及如何使用开源库 PageIndex 构建它——大约只需50行Python代码。
什么是传统RAG?
RAG,即检索增强生成(Retrieval-Augmented Generation)。
它只解决一个问题:AI模型不知道你的私有文档。它们在公开数据上训练并固定参数。RAG通过在提问时从你的文档中提取相关信息并交给模型,解决了这一问题。
两阶段流程:
阶段一:构建索引(仅执行一次)
-
文档被切分为300~500词的文本块
-
每个块被转换为一组数字,称为嵌入向量(embedding)——代表文本语义的数学“指纹”
-
所有向量存入向量数据库(如Pinecone、Weaviate、pgvector),专门用于快速查找相似向量
阶段二:查询(每次提问执行)
-
你的问题被嵌入为相同格式的向量
-
数据库返回与问题向量最接近的文本块
-
这些文本块 + 问题一起送入大模型,生成最终答案
就是这样。整个流水线只有两步。对通用知识的宽泛问题效果很好,但在结构化、专业文档上直接崩溃。
它到底在哪里失效?
问题不在于分块大小或嵌入质量,而是更底层的缺陷。
向量搜索优化的是相似度,而非真实性。当你问“是什么推动了第三季度收入增长?”时,它会拉出所有提到收入的块,而不是真正解释原因的块。
**分块会让问题更糟:**语义被切断,定义在一个块,依赖关系在另一个块,交叉引用失效。模型最终只能瞎猜,因为上下文是残缺的。
而且一旦失败,你完全无法追溯。你得到的只有相似度分数,而不是推理过程——没有任何解释说明为什么检索到这段内容。 在金融、法律等领域,这是致命缺陷。
什么是无向量RAG?
无向量RAG 是一种检索方案,它用**大模型驱动的结构化文档推理,**完全替代了“嵌入—搜索—分块”整套流水线。
核心理念:不再把文档转成向量并搜索最接近的匹配,而是让大模型阅读文档的结构化地图,自主决定打开哪一部分。
模型像人类翻阅复杂报告一样**推理文档层级结构:**看结构、定位答案可能所在的章节、直接阅读该部分。
它去掉了什么:
-
无需选择和维护嵌入模型
-
无需部署和查询向量数据库
-
无需调试分块策略
-
不会出现“相似度很高但内容完全错误”的无声检索失败
它增加了什么:
-
文档树:层级化JSON结构,每个节点是章节,包含标题、页码范围、大模型生成的摘要
-
推理步骤:大模型阅读文档树,根据问题决定检索哪些节点
-
可追溯答案:你能清楚看到检索了哪一章、为什么检索
无向量RAG并非适用于所有场景。它专为答案位于特定可识别章节的结构化文档设计: 财务报告、法律合同、技术手册、学术论文。
对这类文档,它生成的答案基于**具体页码,**而不是从松散相关的文本碎片中拼凑。
PageIndex 是什么?
PageIndex(github.com/VectifyAI/PageIndex)是实现无向量RAG的开源库,只做两件事:
-
生成文档树:
读取PDF,构建层级化文档树
-
文档树检索:
提供基于推理的导航工具
它是实现方案,不是概念本身。本文用PageIndex演示,但“构建树—推理—提取正确章节”的模式可以搭配任何大模型和树生成方法使用。
PageIndex 如何实现无向量RAG
想象你给一位研究员一份200页的年报并提问。研究员不会逐页阅读,而是先看目录,定位最可能包含答案的章节,翻到对应页码,阅读并回答。
**PageIndex 完全复刻这一过程,**只不过“研究员”是大模型,“目录”是从文档生成的结构化JSON树。
两步流程:
步骤1:构建文档树索引PageIndex读取文档,创建层级树,每个节点代表一个章节,包含:
-
章节标题
-
唯一节点ID
-
页码范围(起始页—结束页)
-
大模型生成的章节内容摘要
-
子节点(子章节)
文档保留天然结构:4.2节知道自己属于第4章,引言与结论保持关联,没有任何关系被切断。
节点示例:
{
"title":"Financial Stability",
"node_id":"0006",
"start_index":21,
"end_index":22,
"summary":"美联储监控金融脆弱性...",
"nodes":[
{
"title":"Monitoring Financial Vulnerabilities",
"node_id":"0007",
"start_index":22,
"end_index":28,
"summary":"美联储的监控方法包括..."
},
{
"title":"Domestic and International Cooperation",
"node_id":"0008",
"start_index":28,
"end_index":31,
"summary":"2023年,美联储合作..."
}
]
}
步骤2:基于文档树推理当问题到来时,大模型只阅读文档树(标题+摘要,不读全文),输出:
-
thinking:大模型一步步导航的自然语言推理过程
-
node_list:决定检索的具体节点ID
然后只提取这些节点的完整文本。没有余弦相似度,只有可审计、可阅读、可验证的显式决策。
完整代码教程:从零构建
我们将对DeepSeek-R1论文建立索引,并回答关于其结论的问题。无向量数据库,无嵌入模型。
你需要:
-
PageIndex API Key:dash.pageindex.ai/api-keys(提供免费额度)
-
OpenAI API Key
-
Python 3.8+
步骤0:安装与连接
%pip install -q --upgrade pageindex
from pageindex import PageIndexClient
import pageindex.utils as utils
PAGEINDEX_API_KEY = "YOUR_PAGEINDEX_API_KEY"
pi_client = PageIndexClient(api_key=PAGEINDEX_API_KEY)
PageIndexClient 是与服务的连接入口:传入PDF,生成树;传入文档ID,返回树用于查询。
import openai
OPENAI_API_KEY = "YOUR_OPENAI_API_KEY"
asyncdefcall_llm(prompt, model="gpt-4o", temperature=0):
client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY)
response = await client.chat.completions.create(
model=model,
messages=[{"role":"user", "content": prompt}],
temperature=temperature
)
return response.choices[0].message.content.strip()
关键点:
-
async def:异步函数,多LLM调用可并行不阻塞
-
temperature=0:结果确定性,相同树+相同问题=相同决策,检索需要稳定而非创意
-
同一函数同时处理**树搜索(推理去哪)和最终回答(生成答案),**接口统一
步骤1:下载并索引文档
import os, requests, json
pdf_url = "https://arxiv.org/pdf/2501.12948.pdf"# DeepSeek-R1 论文
pdf_path = os.path.join("../data", pdf_url.split('/')[-1])
os.makedirs(os.path.dirname(pdf_path), exist_ok=True)
response = requests.get(pdf_url)
withopen(pdf_path, "wb") as f:
f.write(response.content)
print(f"已下载 {pdf_url}")
doc_id = pi_client.submit_document(pdf_path)["doc_id"]
print("文档已提交:", doc_id)
代码说明:
-
split('/')[-1]提取文件名
-
os.makedirs(..., exist_ok=True)安全创建文件夹
-
"wb"二进制写入,避免损坏PDF
-
submit_document上传并返回文档ID,树在服务端异步构建
输出示例:
已下载https://arxiv.org/pdf/2501.12948.pdf
文档已提交:pi-cmeseq08w00vt0bo3u6tr244g
步骤1.2:获取文档树
if pi_client.is_retrieval_ready(doc_id):
tree = pi_client.get_tree(doc_id, node_summary=True)['result']
print('文档简化树结构:')
utils.print_tree(tree)
else:
print("文档处理中,请稍后重试...")
-
is_retrieval_ready:检查树是否生成完成
-
node_summary=True:包含大模型生成的章节摘要,LLM靠它判断去哪
-
['result']:从响应中取出真实树结构
返回结果示例:
[{'title': 'DeepSeek-R1: 激励推理能力...',
'node_id': '0000',
'nodes':[
{'title': '摘要', 'node_id': '0001',
'summary': '介绍两种推理模型...'},
{'title': '1. 引言','node_id': '0003',
'nodes':[
{'title': '1.1. 贡献', 'node_id': '0004'},
{'title': '1.2. 评估结果', 'node_id': '0005'}
]},
{'title': '2. 方法', 'node_id': '0006', ...},
{'title': '5. 结论、局限与未来工作',
'node_id': '0019',
'summary': '展示关于DeepSeek-R1性能的结论...'}
]}]
这就是**可导航的文档地图。**模型像人一样看目录:看结构、找候选、直接跳转。
步骤2:文档树搜索(推理发生的地方)
query = "这份文档的结论是什么?"
# 移除全文,LLM只需要标题和摘要做决策
tree_without_text = utils.remove_fields(tree.copy(), fields=['text'])
search_prompt = f"""
你将收到一个问题和一份文档的树结构。
每个节点包含节点ID、标题和对应摘要。
你的任务是找出所有可能包含答案的节点。
问题:{query}
文档树结构:
{json.dumps(tree_without_text, indent=2)}
请按以下JSON格式返回:
{{
"thinking": "<你一步步判断哪些节点相关的推理过程>",
"node_list": ["node_id_1", "node_id_2", ...]
}}
直接返回最终JSON结构,不要输出任何其他内容。
"""
tree_search_result = await call_llm(search_prompt)
提示词设计逻辑:
-
LLM只看标题+摘要,Prompt更轻量、便宜、快速
-
先输出
thinking再输出node_list:强制思维链,决策更稳 -
要求纯JSON输出,避免多余文字破坏后续解析
步骤2.2:打印推理过程与检索节点
node_map = utils.create_node_mapping(tree)
tree_search_result_json = json.loads(tree_search_result)
print('推理过程:')
utils.print_wrapped(tree_search_result_json['thinking'])
print('\n检索到的节点:')
for node_id in tree_search_result_json["node_list"]:
node = node_map[node_id]
print(f"节点ID: {node['node_id']}\t 页码: {node['page_index']}\t 标题: {node['title']}")
-
create_node_mapping:把嵌套树打平为ID字典,快速查节点
-
json.loads:解析LLM输出
-
循环打印:页码、标题、节点ID,检索链路完全可追溯
实际输出示例:
推理过程:
问题询问结论。通常结论出现在标题明确为“结论”的章节。
本文档中节点0019《5.结论、局限与未来工作》最相关。
摘要(0001)可能有高层总结,但不太可能包含完整结论。
讨论部分(0018)讲影响,但不是明确结论。
因此主要检索节点0019。
检索到的节点:
节点ID:0019页码:16标题:5.结论、局限与未来工作
这段推理清晰、可审计、可验证。 这是传统向量搜索永远做不到的。
步骤3:获取上下文并生成答案
node_list = json.loads(tree_search_result)["node_list"]
# 获取每个节点的完整文本并拼接
relevant_content = "\n\n".join(node_map[node_id]["text"] for node_id in node_list)
answer_prompt = f"""
根据上下文回答问题:
问题:{query}
上下文:{relevant_content}
只根据提供的上下文给出清晰简洁的答案。
"""
answer = await call_llm(answer_prompt)
utils.print_wrapped(answer)
生成答案示例:
文档中的结论包括:
-DeepSeek-R1-Zero(纯强化学习,无冷启动数据)在各类任务上表现强劲
-DeepSeek-R1(冷启动数据+迭代强化学习微调)达到与OpenAI-o1-1217相当的性能
-将DeepSeek-R1的推理能力蒸馏到小模型前景广阔:
DeepSeek-R1-Distill-Qwen-1.5B在数学基准上超过GPT-4o和Claude-3.5-Sonnet
答案**完全正确,**锚定在第16页,每一句话都能追溯到节点0019。 如果答案出错,你能立刻定位问题:是树搜索选错了节点,还是摘要有误。
整条流水线从问题到答案**只调用两次LLM:**一次导航推理,一次生成答案。
投入使用前必须了解的局限
- 延迟更高每次查询至少多一次LLM调用做导航,对话场景下延迟可感知。
- 大规模成本更高向量检索边际成本几乎为零;无向量RAG每次查询都要跑LLM,量大后成本显著更高。
- 严重依赖文档结构只对结构清晰的PDF友好:报告、合同、论文、手册。 扫描件、无格式PDF、导出PPT效果很差。
- 多文档扩展仍在开发单文档问答极强;跨大量文档检索时,单文档树的开销会快速上升,PageIndex已标明这是已知限制。
- 检索质量取决于模型导航决策的质量 = 大模型推理能力。 想用小模型本地部署,必须严格测试。
无向量RAG vs 传统向量RAG:如何选择?
选择无向量RAG,如果:
-
文档结构清晰:财报、合同、论文、手册、政策文件
-
准确性是第一优先级,错误答案会带来实际风险
-
需要可审计、可追溯的检索链路:章节、页码、推理依据
-
场景以单文档精问答为主,而非跨库海量检索
选择传统向量RAG,如果:
-
有成千上万份文档,需要跨库全局搜索
-
问题偏宽泛语义:“找出所有关于X的内容”
-
查询量巨大、延迟要求严格
-
文档格式混乱、无清晰结构
**最实用的测试方法:**从真实场景抽20~30个问题,两种方案都跑一遍,对比准确率。 一天的评估,胜过数月的架构争论。
总结
**传统向量RAG:**搜索“看起来像问题”的文本。**无向量RAG:**推理“答案在文档哪里”。
对大多数海量通用文档场景,向量RAG仍是合理起点。
但对结构化专业文档,在准确性至关重要、错误代价高昂的场景: 构建文档树 + 基于推理的检索,才是更贴合文档天然组织方式的根本方案。
PageIndex只是这一模式的开源实现之一。 而模式本身——构建结构化文档地图 → 让LLM推理读哪一章 → 提取该章节全文——你可以用任何大模型复现。 本文代码就是完整参考实现。
-------------------------------------------------------------