欢迎光临
我们一直在努力

RAG索引生成优化篇(下):RAPTOR 与 ColBERT

在上一篇中,我们探讨了 Multi-representation Indexing —— 通过"摘要匹配 + 原文返回"的双路策略突破检索精度瓶颈。本篇将深入另外两种更前沿的索引优化方案:RAPTOR(递归抽象处理树)和 ColBERT(Token 级交互检索),它们分别从层次化索引和细粒度交互两个维度重新定义了 RAG 的检索范式。


三、RAPTOR(递归抽象处理树)

3.1 核心思想与动机

RAPTOR 全称是 Recursive Abstractive Processing for Tree-Organized Retrieval(面向树组织检索的递归抽象处理),由 Stanford 提出。

传统 RAG 面临一个根本矛盾:

如果 Chunk 太大 → Embedding 无法聚焦关键信息,检索精度下降; 如果 Chunk 太小 → 丢失上下文,无法回答需要综合多个片段的全局性问题。

RAPTOR 的核心思路是:既然不同粒度各有优势,那就把所有粒度都建上索引。它通过递归聚类 + 摘要生成,将文档构建成一棵多层语义树:

  • 叶子节点 = 细粒度 Chunk(原始文本)

  • 中间节点 = 中粒度摘要(聚类后生成)

  • 根节点 = 粗粒度摘要(全文主题)

这样,无论用户的查询是具体的事实性问题还是抽象的主题性问题,RAPTOR 都能在对应的语义层级上找到最匹配的节点。

3.2 RAPTOR 架构详解

RAPTOR 的整体架构由离线构建和在线检索两部分组成:

关键组件:

  • Embedding 模型:将文本转为向量(与标准 RAG 相同)

  • 聚类算法:GMM(高斯混合模型)或层次聚类,对语义相近的 Chunk 进行分组

  • LLM 摘要器:为每个集群生成一句"代表该集群核心内容"的摘要

  • 树结构存储:每一层的所有节点(包括摘要和原始 Chunk)都存入向量库

3.3 RAPTOR 核心算法流程

算法步骤 ①:文档切分与向量化

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
import numpy as np

# 1. 切分文档
splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=100)
chunks = splitter.split_documents(documents)

# 2. 向量化
embedding_model = OpenAIEmbeddings(model="text-embedding-3-large")
embeddings = np.array([
   embedding_model.embed_query(chunk.page_content)
   for chunk in chunks
])

print(f"生成了 {len(chunks)} 个 Chunk,向量维度: {embeddings.shape[1]}")

算法步骤 ②:聚类(使用 GMM 自动确定簇数)

与 K-Means 需要预设 K 值不同,RAPTOR 使用 GMM(高斯混合模型) 来自动发现最优簇数:

from sklearn.mixture import GaussianMixture

def cluster_embeddings(embeddings: np.ndarray, max_clusters: int = 10) -> np.ndarray:
   """使用 GMM 进行聚类,自动选择最优簇数"""
   best_bic = float('inf')
   best_labels = None
   best_n = 2

   for n_components in range(2, min(max_clusters + 1, len(embeddings))):
       gmm = GaussianMixture(
           n_components=n_components,
           covariance_type='tied',
           random_state=42
      )
       gmm.fit(embeddings)
       bic = gmm.bic(embeddings)  # BIC 越低越好

       if bic < best_bic:
           best_bic = bic
           best_labels = gmm.predict(embeddings)
           best_n = n_components

   print(f"最优簇数: {best_n}, BIC: {best_bic:.2f}")
   return best_labels

# 执行聚类
labels = cluster_embeddings(embeddings)

算法步骤 ③:生成集群摘要

from sklearn.mixture import GaussianMixture

def cluster_embeddings(embeddings: np.ndarray, max_clusters: int = 10) -> np.ndarray:
   """使用 GMM 进行聚类,自动选择最优簇数"""
   best_bic = float('inf')
   best_labels = None
   best_n = 2

   for n_components in range(2, min(max_clusters + 1, len(embeddings))):
       gmm = GaussianMixture(
           n_components=n_components,
           covariance_type='tied',
           random_state=42
      )
       gmm.fit(embeddings)
       bic = gmm.bic(embeddings)  # BIC 越低越好

       if bic < best_bic:
           best_bic = bic
           best_labels = gmm.predict(embeddings)
           best_n = n_components

   print(f"最优簇数: {best_n}, BIC: {best_bic:.2f}")
   return best_labels

# 执行聚类
labels = cluster_embeddings(embeddings)

算法步骤 ④:递归构建树

from sklearn.mixture import GaussianMixture

def cluster_embeddings(embeddings: np.ndarray, max_clusters: int = 10) -> np.ndarray:
   """使用 GMM 进行聚类,自动选择最优簇数"""
   best_bic = float('inf')
   best_labels = None
   best_n = 2

   for n_components in range(2, min(max_clusters + 1, len(embeddings))):
       gmm = GaussianMixture(
           n_components=n_components,
           covariance_type='tied',
           random_state=42
      )
       gmm.fit(embeddings)
       bic = gmm.bic(embeddings)  # BIC 越低越好

       if bic < best_bic:
           best_bic = bic
           best_labels = gmm.predict(embeddings)
           best_n = n_components

   print(f"最优簇数: {best_n}, BIC: {best_bic:.2f}")
   return best_labels

# 执行聚类
labels = cluster_embeddings(embeddings)

3.4 RAPTOR 检索策略

RAPTOR 提供了两种检索策略,适用于不同的查询类型:

策略 1:树遍历检索(Tree Traversal)

适合具体的事实性查询,逐层向下深入定位。

def tree_traversal_search(query, tree, top_k=2):
"""从根节点逐层向下检索"""
query_emb = embedding_model.embed_query(query)
current_nodes = tree["level_2"]["summaries"] # 从最顶层开始
current_embs = tree["level_2"]["embeddings"]

for level in range(2, 0, -1): # 从上到下
# 计算当前层所有节点与查询的相似度
scores = cosine_similarity([query_emb], current_embs)[0]
top_indices = np.argsort(scores)[-top_k:]

if level > 1:
# 非叶子层:扩展子节点
next_nodes = []
for idx in top_indices:
cluster_mask = tree[f"level_{level-1}"]["clusters"] == idx
next_nodes.extend(…)
current_nodes = next_nodes
else:
# 叶子层:返回结果
return [current_nodes[i] for i in top_indices]

策略 2:扁平化检索(Collapsed Tree)

适合抽象的主题性查询,把所有层的所有节点展平,统一检索。

def collapsed_tree_search(query, tree, top_k=5):
"""将所有层节点展平,统一检索"""
all_nodes = []
all_embeddings = []

# 收集所有层的节点
for level_key, level_data in tree.items():
if level_key == "level":
continue
all_nodes.extend(level_data["summaries"])
all_embeddings.extend(level_data["embeddings"])

# 统一计算相似度
all_embeddings = np.array(all_embeddings)
query_emb = embedding_model.embed_query(query)
scores = cosine_similarity([query_emb], all_embeddings)[0]
top_indices = np.argsort(scores)[-top_k:]

return [all_nodes[i] for i in reversed(top_indices)]

# 使用场景判定
def search(query, tree):
"""根据查询类型自动选择策略"""
if is_specific_query(query): # "X 函数的参数是什么?"
return tree_traversal_search(query, tree) # 树遍历
else: # "系统的整体架构是怎样的?"
return collapsed_tree_search(query, tree) # 扁平化

3.5 RAPTOR 性能分析与优化

性能数据对比
指标标准 RAGRAPTOR(树遍历)RAPTOR(扁平化)提升
事实性查询 Recall@5 73.1% 84.2% 79.8% +15.2%
主题性查询 Recall@5 68.5% 72.1% 83.6% +22.0%
多跳推理准确率 51.3% 67.8% 61.2% +32.2%
平均检索延迟 45ms 78ms 52ms -15%~73%

注:数据基于论文公开基准,实际效果因数据集不同有所差异。

优化建议

1. 聚类算法选择:

算法优势劣势适用场景
GMM 自动确定簇数,软分配 计算量较大 数据量 < 1 万 Chunk
K-Means 速度快,实现简单 需预设 K,硬分配 数据量 > 1 万 Chunk
层次聚类 天然适配树结构 时间复杂度 O(n³) 小规模、高质量要求

2. 摘要成本控制:

  • 用 gpt-4o-mini / Claude Haiku 替代大模型生成摘要,成本可降低 80%

  • 缓存策略:相同语义集群的摘要可跨文档复用

  • 低频更新场景下可完全离线预计算

3. 混合检索策略:

  • 简单查询直接用扁平化(延迟低)

  • 复杂多跳查询用树遍历(精度高)

  • 结合规则:查询词数 > 15 或包含"和"、"以及"等连词 → 自动切换树遍历


四、ColBERT(Token 级别上下文交互检索)

4.1 核心思想

传统 RAG 的检索方式可以概括为 "单向量表示":

整个查询 → 一个向量;整个文档 Chunk → 一个向量;两者做点积。

这带来的问题是:查询中的每个 Token 被迫和文档 Chunk 中所有 Token 的信息混在一起。比如查询 "苹果的市值","苹果" 需要匹配财务文档,但如果文档里也有 "苹果公司发布了新手机",仅靠一个向量很难区分"苹果"是水果还是公司。

ColBERT(Contextualized Late Interaction over BERT)的核心创新是 Late Interaction(延迟交互):

不对查询和文档做"早融合"(各压缩成一个向量),而是保留每个 Token 的独立表示,在检索的最后一步才做细粒度的 Token-to-Token 交互。

一句话总结:ColBERT = 双塔模型的高效 + Cross-Encoder 的精度。

4.2 ColBERT 详细架构

关键设计:

  • 查询和文档各自独立通过 BERT 编码,互不依赖(双塔架构)→ 文档向量可离线预计算

  • 每个 Token 保留独立的 Embedding,不做池化压缩

  • 相似度计算在最后一步(Late Interaction),用 MaxSim 实现 Token 级别的匹配

4.3 MaxSim 算法详解

MaxSim 是 ColBERT 的灵魂。给定查询 Token 矩阵 $E_Q \\in \\mathbb{R}^{n \\times d}$ 和文档 Token 矩阵 $E_D \\in \\mathbb{R}^{m \\times d}$:

$$ \\text{ColBERT}(Q, D) = \\sum_{i=1}^{n} \\max_{j=1}^{m} E_{Q_i} \\cdot E_{D_j}^{T} $$

通俗解释:

  • 查询中的每个 Token 独立地去文档中找最相似的 Token

  • 把所有 Token 的最佳匹配分数求和,得到最终相似度

  • 这样 "苹果" 能匹配到 "Apple Inc","市值" 能匹配到 "市值 3.2 万亿",互不干扰

4.4 ColBERT 实战代码

第一步:安装和初始化

# 安装 RAGatouille(ColBERT 的易用封装)
# !pip install ragatouille

from ragatouille import RAGPretrainedModel

# 加载预训练模型(自动下载)
RAG = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")

print("ColBERT v2 模型加载完成!")

第二步:索引文档

# 准备文档
documents = [
"苹果公司(Apple Inc.)是一家美国科技公司,总部位于加州库比蒂诺。",
"苹果公司的市值在 2024 年突破 3 万亿美元,成为全球市值最高的公司之一。",
"iPhone 是苹果公司最重要的产品线,贡献了约 50% 的营收。",
"蒂姆·库克于 2011 年接替史蒂夫·乔布斯担任苹果公司 CEO。",
"苹果公司的 M 系列芯片基于 ARM 架构,在性能和能效方面表现出色。",
]

# 创建索引(离线过程,耗时较长,但只需执行一次)
index_path = RAG.index(
collection=documents,
index_name="apple_docs",
max_document_length=512, # 每个文档最大 Token 数
split_documents=True, # 自动切分长文档
)

print(f"索引已创建: {index_path}")

第三步:搜索

# 执行检索
query = "苹果公司目前的市值和 CEO 是谁?"
results = RAG.search(query, k=3)

for i, result in enumerate(results):
print(f"=== 结果 {i+1} (Score: {result['score']:.3f}) ===")
print(f"内容: {result['content']}")
print(f"排名: {result['rank']}")
print()

高级用法:Reranking

ColBERT 除了作为独立检索器,还能作为 Reranker 工作:先由传统向量检索粗筛 Top-100,再用 ColBERT 精排 Top-10。

# 使用 ColBERT 作为 Reranker
from langchain.retrievers import ContextualCompressionRetriever
from langchain_community.document_compressors import CohereRerank # 示例
# 实际使用 ragatouille 的 Reranker:
# RAG.rerank(query, candidate_docs, k=10)

# 粗筛:BM25 / 向量检索
coarse_results = vector_store.similarity_search(query, k=100)

# 精排:ColBERT Rerank
fine_results = RAG.rerank(
query=query,
documents=[doc.page_content for doc in coarse_results],
k=10
)

4.5 ColBERT 性能与权衡

维度标准 RAG(单向量)ColBERT(Late Interaction)Cross-Encoder(全注意力)
检索精度 (NDCG@10) 0.68 0.82 0.86
索引速度 中等(多 Token 向量) 不可索引
检索延迟 ~10ms ~50ms ~500ms
存储开销 1×(每 Chunk 1 向量) 128×(每 Chunk 128 Token 向量) 0(无索引)
可扩展性 ✅ 百万级 ⚠️ 十万级(受存储限制) ❌ 仅少量候选

核心权衡:精度 vs 存储。ColBERT 用 128 倍的存储换来了接近 Cross-Encoder 的精度,但远低于 Cross-Encoder 的计算开销。

4.6 ColBERT v2 的优化

ColBERT v2 在原始版本的基础上引入了两项关键改进:

残差压缩(Residual Compression)

原始 ColBERT 存储所有 Token 的 Embedding,存储开销巨大。ColBERT v2 使用残差压缩技术:

去噪训练

ColBERT v2 引入跨批次负采样(Cross-batch Negatives)和困难负样本挖掘(Hard Negative Mining),大幅提升了模型对"表面相关但实质无关"文档的辨别能力。

# ColBERT v2 的使用方式与 v1 完全一致,仅模型路径不同
RAG_v2 = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")

# v2 在相同检索质量下,索引体积减少约 90%
# 这使得 ColBERT 真正具备了百万级文档的扩展能力

ColBERT v1 vs v2:

指标ColBERT v1ColBERT v2提升
索引体积 (每 1M 文档) ~2TB ~150GB -92%
检索延迟 50ms 35ms -30%
MRR@10 (MS MARCO) 0.360 0.397 +10.3%
最大可扩展文档量 ~100K ~10M 100×

总结:三种索引优化方案对比

选型建议:

你的场景推荐方案原因
快速落地,成本敏感 Multi-representation 概念简单,改动小,ROI 高
长文档、多层次语义 RAPTOR 天然处理不同粒度的查询
精度要求极高,存储可接受 ColBERT 接近 Cross-Encoder 的精度
超大规模(百万级) Multi-representation + ColBERT 精排 粗筛 + 精排的经典组合
追求极致效果 RAPTOR(分层) + ColBERT(匹配) 索引层 + 检索层双优化

在 RAG 系统的演进中,索引优化是"四两拨千斤"的发力点。从简单的单向量检索,到摘要代理、层次化语义树、最终到 Token 级交互,每一层优化都在缩小"查询意图"和"文档表达"之间的语义 Gap。

【RAG技术从小白到深入理解】一文搞懂 RAG:索引、检索、生成与评估全流程-CSDN博客

【RAG技术从小白到深入理解】RAG 查询优化策略:从多查询到 HyDE 的完整指南-CSDN博客

【RAG技术从小白到深入理解】路由优化与查询构建策略:RAG 系统的智能调度与精准检索-CSDN博客

RAG索引生成优化篇(上):Multi-representation Indexing(多表征索引)-CSDN博客

赞(0)
未经允许不得转载:171主机测评 » RAG索引生成优化篇(下):RAPTOR 与 ColBERT
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址