数据库制作(不用向量数据库也能做 RAG,看看这个思路)

数据库制作(不用向量数据库也能做 RAG,看看这个思路)
不用向量数据库也能做 RAG,看看这个思路

大多数 RAG 系统都依赖向量数据库。你把文档切成块,算出嵌入向量,存进向量数据库,然后查询时做相似度搜索。

但有没有想过:不用向量,也能做 RAG?

这篇文章介绍了一个完全不同的思路:PageIndex——一个基于分层页面索引的"无向量"RAG 系统。不使用嵌入,不使用相似度搜索,而是让 LLM 像人类翻书一样"导航"文档。

核心思想:像翻书一样检索

想想你平时怎么在教科书里找信息。

你不会从头读到尾。你会:

  1. 打开目录
  2. 找到相关章节
  3. 看章节里的小节
  4. 直接跳到需要的那一页

PageIndex 做的事完全一样

  1. 把文档转成一棵"树"(目录结构)
  2. 每个分支是一个章节
  3. 每个叶子节点是实际文本
  4. 查询时,LLM 逐层导航这棵树

没有向量,没有相似度搜索,只有结构化导航。

系统架构

数据库制作(不用向量数据库也能做 RAG,看看这个思路)

PageIndex 的工作分为两个阶段:

索引阶段(只运行一次)

查询阶段(每次提问运行)

完整实现步骤

步骤 1:定义节点结构

每个文档章节变成一个 PageNode:

@dataclassclass PageNode:    title: str           # 章节标题    content: str         # 原始文本(叶子节点填充)    summary: str         # LLM 生成的摘要    depth: int           # 深度:0=根, 1=章节, 2=子节    children: list       # 子节点    parent: PageNode     # 父节点

步骤 2:解析文档为树

核心逻辑

  1. 把整个文档发给 LLM,分割成顶层章节
  2. 对于超过 300 词的长章节,再发给 LLM 分割成子节
  3. 短章节直接作为叶子节点
def parse_document(text: str) -> PageNode:    root = PageNode(title="root", ...)        for item in _segment(text):  # LLM 分段        node = PageNode(title=item["title"], ...)                if len(content.split()) > 300:  # 长章节            subsections = _segment(content)  # 再分割            for sub in subsections:                node.children.append(PageNode(...))        else:            node.content = content  # 短章节作为叶子                    root.children.append(node)        return root

关键阈值:SUBSECTION_THRESHOLD = 300 词

步骤 3:生成摘要(自底向上)

后序遍历:先处理子节点,再处理父节点

def build_summaries(node: PageNode):    # 先处理所有子节点    for child in node.children:        build_summaries(child)        if node.is_leaf():        # 叶子:总结自己的内容        node.summary = _summarize(node.content)    else:        # 内部节点:总结子节点的摘要        children_text = "\n".join(f"[{c.title}]: {c.summary}" for c in node.children)        node.summary = _summarize(children_text)

为什么后序遍历? 保证每个子节点都有摘要后,父节点才能基于子节点摘要生成自己的摘要。

步骤 4:树搜索检索

核心流程:从根开始,逐层选择分支,直到叶子

def retrieve(query: str, root: PageNode) -> str:    node = root    while not node.is_leaf():        node = _pick_child(query, node)  # LLM 选择分支    return node.content

LLM 选择分支的逻辑

def _pick_child(query: str, node: PageNode) -> PageNode:    # 展示所有子节点的摘要    options = "\n".join(        f"{i+1}. [{c.title}]: {c.summary}"        for i, c in enumerate(node.children)    )        prompt = f"""你正在导航文档树来回答问题。    当前章节: "{node.title}"问题: {query}子节点:{options}哪个子节点最可能包含答案?只回复数字。"""        response = client.chat.completions.create(...)    index = int(response.choices[0].message.content) - 1    return node.children[index]

步骤 5:保存和加载索引

def save(node: PageNode, path: str):    # 递归序列化为 JSON    def load(path: str) -> PageNode:    # 从 JSON 反序列化为树

索引只需构建一次,之后反复使用。

索引长什么样?

运行 build_index 后,index.json 大致如下:

{  "title": "root",  "summary": "文档涵盖退货、运输选项和账户设置。",  "children": [    {      "title": "退货和退款",      "summary": "退款在收到退货后 14 天内处理。",      "content": "我们接受 30 天内的退货...",      "children": []    },    {      "title": "运输选项",      "summary": "涵盖国内(3-5天)和国际运输(7-14天)。",      "children": [        {          "title": "国内运输",          "summary": "标准配送通过 USPS 需 3-5 个工作日。",          "content": "我们通过 USPS 进行国内运输...",        },        {          "title": "国际运输",          "summary": "国际订单通过 DHL 运输,7-14 天送达。",          "content": "国际运输覆盖 50+ 个国家...",        }      ]    }  ]}

注意

  • 短章节("退货和退款")作为深度 1 的叶子
  • 长章节("运输选项")成为内部节点,有深度 2 的子节点

与传统向量 RAG 的对比

维度

向量 RAG

PageIndex

存储

向量数据库

JSON 文件

检索方式

相似度搜索

树导航

嵌入模型

必需

不需要

可解释性

黑盒

路径可追溯

更新成本

增删向量

重建树

适用场景

大规模文档

结构化文档

常见问题与解决

问题 1:LLM 总选错分支

原因:摘要太模糊

解决:用更强的模型生成摘要,或在提示词中要求更多细节

问题 2:LLM 把章节切在不合适的位置

原因:长文档分段时切断了语义

解决

  • 增加 max_tokens
  • 或预先把文档切成 ~3000 词的块再分段

问题 3:叶子内容太长

原因:SUBSECTION_THRESHOLD 设置太高

解决:降低阈值,让更多章节被分割成子节


完整代码结构

pageindex-rag/├── pageindex/│   ├── __init__.py│   ├── node.py       # PageNode 定义│   ├── parser.py     # 文档解析│   ├── indexer.py    # 摘要生成│   ├── retriever.py  # 树搜索检索│   └── storage.py    # JSON 序列化├── main.py           # 入口└── document.md       # 示例文档

结语

PageIndex 展示了一个重要的思路:不是所有问题都需要向量搜索

对于结构化文档(手册、FAQ、政策文档),"导航式检索"可能比"相似度搜索"更自然:

  1. 可解释性强:你能看到 LLM 为什么选择某个分支
  2. 无需向量基础设施:一个 JSON 文件就够了
  3. 查询成本低:不需要计算查询向量

但局限性也存在

  • 非结构化文档(新闻、对话记录)可能不适合
  • 文档更新需要重建整个树
  • 大规模文档可能需要更深的树,增加导航步数

不得不感叹一句:有时候最简单的方案,就是最有效的方案。

参考

  • GitHub - pageindex-rag

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

最新文章

热门文章

本栏目文章