无向量RAG:你的RAG流水线根本不需要向量数据库

4 阅读12分钟

基于推理的检索如何在结构化文档上击败相似度搜索,以及如何使用 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的开源库,只做两件事:

  1. 生成文档树:

    读取PDF,构建层级化文档树

  2. 文档树检索:

    提供基于推理的导航工具

它是实现方案,不是概念本身。本文用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:**一次导航推理,一次生成答案。


投入使用前必须了解的局限

  1. 延迟更高每次查询至少多一次LLM调用做导航,对话场景下延迟可感知。
  2. 大规模成本更高向量检索边际成本几乎为零;无向量RAG每次查询都要跑LLM,量大后成本显著更高。
  3. 严重依赖文档结构只对结构清晰的PDF友好:报告、合同、论文、手册。 扫描件、无格式PDF、导出PPT效果很差。
  4. 多文档扩展仍在开发单文档问答极强;跨大量文档检索时,单文档树的开销会快速上升,PageIndex已标明这是已知限制。
  5. 检索质量取决于模型导航决策的质量 = 大模型推理能力。 想用小模型本地部署,必须严格测试。

无向量RAG vs 传统向量RAG:如何选择?

选择无向量RAG,如果:

  • 文档结构清晰:财报、合同、论文、手册、政策文件

  • 准确性是第一优先级,错误答案会带来实际风险

  • 需要可审计、可追溯的检索链路:章节、页码、推理依据

  • 场景以单文档精问答为主,而非跨库海量检索

选择传统向量RAG,如果:

  • 有成千上万份文档,需要跨库全局搜索

  • 问题偏宽泛语义:“找出所有关于X的内容”

  • 查询量巨大、延迟要求严格

  • 文档格式混乱、无清晰结构

**最实用的测试方法:**从真实场景抽20~30个问题,两种方案都跑一遍,对比准确率。 一天的评估,胜过数月的架构争论。


总结

**传统向量RAG:**搜索“看起来像问题”的文本。**无向量RAG:**推理“答案在文档哪里”。

对大多数海量通用文档场景,向量RAG仍是合理起点。

但对结构化专业文档,在准确性至关重要、错误代价高昂的场景: 构建文档树 + 基于推理的检索,才是更贴合文档天然组织方式的根本方案。

PageIndex只是这一模式的开源实现之一。 而模式本身——构建结构化文档地图 → 让LLM推理读哪一章 → 提取该章节全文——你可以用任何大模型复现。 本文代码就是完整参考实现。

-------------------------------------------------------------

微信公众号:算子之心