RAG 索引进阶笔记:不是只存进去,而是要存得聪明

这篇主要讲什么

这一份 PDF 对应 Part 12 ~ Part 14,主题是索引升级。

如果说基础 RAG 解决的是“先把资料放进库里”,那这一篇关注的是:

怎么存,后面才更容易找对。

这其实是个很现实的问题。很多系统不是搜不到,而是存得太粗糙,导致检索阶段天然吃亏。

1. Multi-representation Indexing:一份内容,存多个表示

原文先讲的是多表示索引。

简单理解就是:

同一份原始文档,不只存原文,还可以存它的摘要、标题、关键片段,甚至其他形式的描述。

这样做的好处很明显:

用户提问时,有时更容易和“摘要版”对上,而不是和“原始长文”直接对上。

文中的做法是:

  • 原文保留在存储层
  • 用摘要去建立向量索引
  • 检索时先靠摘要命中
  • 命中后再回到原文取完整内容

我觉得这个思路很实用,因为它兼顾了两件事:

  • 检索时更轻、更聚焦
  • 回答时还能拿到完整原文

这比“全部直接按大块原文建索引”要聪明很多。

2. RAPTOR:先做层级摘要,再逐层检索

这一部分原文更多是在给论文和代码入口,但核心思想其实很好懂。

RAPTOR 可以理解成:

先把文档分块,再对这些块做摘要,然后把摘要继续往上归纳,形成一个树状结构。

这样检索时,不只是从底层小块里硬搜,还可以先从更高层的摘要节点入手。

它像什么?

很像你读一本书时,不会每次都从某一页某一段开始翻,而是会先看目录、看章节摘要,再决定往哪里钻。

我对 RAPTOR 的理解是:

它特别适合长文档、结构复杂的资料库。因为这类资料如果只靠平铺的小块检索,很容易只看到局部,看不到全局。

3. ColBERT:不是整段比,而是细到词级别去匹配

基础向量检索,通常是一整段文本压成一个向量,再去比较相似度。

ColBERT 的思路更细:

不是整段只给一个表示,而是让文本里的每个 token 都有自己的向量表示,然后再做更细粒度的匹配。

通俗点说,它不是问:

“这一整段和问题像不像?”

而是更像在问:

“问题里的每个关键点,能不能在文档里找到很像的局部对应?”

我觉得这类方法的优势是:

对一些细节命中要求高的场景,会比普通向量检索更强。

但代价也很现实:

  • 更复杂
  • 更重
  • 部署和维护成本更高

所以它不一定适合所有项目,尤其不适合刚起步就上来堆复杂方案。

这份内容的核心思想

我觉得这一篇的核心思想是:

索引不是仓库,它更像检索系统对资料的“预加工”。

你怎么存,几乎决定了你后面能不能找得准、找得快、找得全。

我自己的理解

这一篇让我更明确了一件事:

RAG 的上限,很大程度上不是生成模型决定的,而是索引策略决定的。

因为模型回答之前,已经被“喂了什么内容”限制住了。

如果检索阶段只能拿到粗糙、片面、缺上下文的内容,那后面回答再流畅,也只是把不完整的信息说顺而已。

我自己的偏好是:

  • 小型项目,先把基础切分和摘要索引做好
  • 中型项目,再考虑多表示索引
  • 文档真的很长、层次很复杂时,再看 RAPTOR
  • 对精确匹配要求很高且资源够用时,再考虑 ColBERT

也就是说,先把简单但有效的招数吃透,再谈更重的方案。

这一篇最值得带走的经验

  • 原文不一定是最适合检索的表示
  • 摘要、标题、结构化信息都可以成为索引材料
  • 长文档不能只平铺切块,层级信息很重要
  • 更细粒度的匹配更强,但也更贵

一句总结

这一篇讲透了一个常被忽视的问题:知识库不是把资料“塞进去”就结束了,真正重要的是把资料变成“容易被找对”的形式。索引做得聪明,后面的检索和回答才有底气。

核心代码实现

方法 1:Multi-representation Indexing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 原文
docs = load_documents()

# 先给每篇文档生成摘要
summaries = []
for doc in docs:
summary = llm.invoke(f"请总结这篇文档:\n{doc.page_content}")
summaries.append(summary)

# 用摘要建索引
summary_docs = []
for i, summary in enumerate(summaries):
summary_docs.append(Document(
page_content=summary,
metadata={"doc_id": docs[i].id}
))

vectorstore.add_documents(summary_docs)
docstore.save_raw_documents(docs)
1
2
3
4
def retrieve_with_multi_representation(question):
hits = vectorstore.similarity_search(question, k=3)
raw_docs = [docstore.get(doc.metadata["doc_id"]) for doc in hits]
return raw_docs

方法 2:RAPTOR

1
2
3
4
5
6
7
8
9
10
11
def build_raptor_tree(chunks):
level = chunks
tree = [level]

while len(level) > 1:
grouped = group_similar_chunks(level)
summaries = [summarize(group) for group in grouped]
level = summaries
tree.append(level)

return tree
1
2
3
4
def raptor_retrieve(question, tree):
high_level_hits = retrieve_from_top_levels(question, tree)
fine_grained_hits = drill_down(question, high_level_hits, tree)
return fine_grained_hits

方法 3:ColBERT 思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def colbert_score(query_tokens, doc_tokens):
score = 0
for q in query_tokens:
# 对 query 中每个 token,找 doc 里最像的 token
best_match = max(similarity(q, d) for d in doc_tokens)
score += best_match
return score

def colbert_retrieve(question, docs):
q_tokens = embed_each_token(question)
scored = []
for doc in docs:
d_tokens = embed_each_token(doc.page_content)
scored.append((doc, colbert_score(q_tokens, d_tokens)))
return sorted(scored, key=lambda x: x[1], reverse=True)