在上一篇中,我们探讨了 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 性能分析与优化
性能数据对比
| 事实性查询 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 性能与权衡
| 检索精度 (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:
| 索引体积 (每 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博客




