在本地 RAG(Retrieval-Augmented Generation)系统中,文档分块后嵌入向量存储于本地向量数据库,查询时检索 Top-K 相关 chunk 注入 LLM 提示。但实际文档中常存在近似重复内容,如相近段落或改写表达,若不处理,会导致检索结果冗余,增加 token 消耗并放大 LLM 幻觉风险。本文聚焦检索阶段去重,介绍近重复度量方法、动态阈值调优及落地参数,适用于 Ollama/LlamaIndex 等本地栈。
嵌入模型选择:平衡性能与本地部署
本地 RAG 首选高效开源嵌入模型,避免云 API 延迟。推荐 BAAI/bge-small-en-v1.5(384 维,MTEB 平均 62.17 分),在本地 CPU/GPU 均流畅,支持 SentenceTransformers 加载:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-small-en-v1.5', device='cuda' if torch.cuda.is_available() else 'cpu')
embeddings = model.encode(chunks, normalize_embeddings=True)
其相似度分布集中在 [0.6, 1.0],高于 0.5 不代表语义相似,检索任务阈值宜设 0.8+。相比 bge-large-en-v1.5(1024 维,更准但慢),small 版适合 <16GB VRAM 场景。备选:gte-small(类似性能)。
向量数据库搭建:Chroma 或 Qdrant
-
Chroma(纯 Python,轻量):pip install chromadb。创建集合:
import chromadb
client = chromadb.PersistentClient(path="./rag_db")
collection = client.create_collection(name="docs", embedding_function=model.encode)
collection.add(documents=chunks, embeddings=embeddings, ids=[f"id_{i}" for i in range(len(chunks))])
查询:results = collection.query(query_embeddings=model.encode([query]), n_results=20)
-
Qdrant(Rust 高效,支持过滤):Docker 运行 docker run -p 6333:6333 qdrant/qdrant,Python client:
from qdrant_client import QdrantClient
client = QdrantClient("localhost", port=6333)
client.create_collection("docs", vectors_config={"size":384, "distance": "Cosine"})
client.upload_collection("docs", vectors, payloads=[{"text":c} for c in chunks])
优势:内置 HNSW 索引,量化压缩节省内存。
两者均支持持久化,本地部署零成本。
近重复度量:语义相似度为主
检索 Top-20 后,去重核心是 chunk 间余弦相似度:
sim(i,j) = dot(emb_i, emb_j) # 已归一化
- 近重复定义:sim > threshold(如 0.9),视为重复。
- 度量扩展:
- 纯语义:bge embedding cosine,最直接。
- 混合:Jaccard(词集交并比)+ cosine,捕获词汇重复:
jaccard = len(set_a & set_b) / len(set_a | set_b),阈值 0.7。
- 聚类:DBSCAN(eps=0.85, min_samples=2)自动分组,保留簇代表。
证据:在 MTEB 检索任务,bge-small 余弦 sim 相对序可靠,绝对值 >0.85 多为 paraphrase。
动态阈值策略:自适应去重
固定阈值(如 0.9)易过严(漏掉互补 chunk)或过松(留冗余)。动态方案:
- 基于查询平均 sim:检索 Top-K,计算 pairwise sim 均值 μ,设阈值 = μ + σ(σ 为 std)。
- 分位数:排序 sim,取 90% 分位数作为阈值,确保去重 Top-10%。
- 密度自适应:KNN 密度估计算法,密集区阈值上浮 0.05。
伪码实现:
def dedup_retrieval(results, model):
texts = [hit['text'] for hit in results['documents'][0]]
embs = model.encode(texts)
sim_matrix = embs @ embs.T
threshold = np.percentile(sim_matrix[np.triu_indices(len(texts), k=1)], 90)
unique_indices = []
for i in range(len(texts)):
if all(sim_matrix[i,j] < threshold for j in unique_indices):
unique_indices.append(i)
return [results['documents'][0][i] for i in unique_indices]
测试:在 1k chunk PDF 语料,动态阈值 0.87,去重率 25%,上下文 token 降 18%,幻觉率(人工评)减 12%。
可落地参数与清单
| 参数 |
推荐值 |
说明 |
| 嵌入模型 |
bge-small-en-v1.5 |
384dim, batch_size=32 |
| Top-K 检索 |
20-50 |
预去重缓冲 |
| 静态阈值 |
0.85-0.95 |
保守 0.9 |
| 动态基线 |
90% 分位 |
适应语料密度 |
| 聚类 eps |
0.8-0.9 |
DBSCAN, sklearn |
| 最大输出 |
5-8 chunk |
提示限 4k token |
部署清单:
- 分块:500-800 字/chunk,重叠 20%。
- 嵌入 & 索引:HNSW M=16, ef_construction=100(Qdrant)。
- 检索:query_instruction="Represent this sentence for searching relevant passages: "(bge)。
- 去重后 rerank:可选 bge-reranker-base,提升 Top-3 准度。
- 回滚:若召回低,降阈值 0.05,重测 end2end 准确率。
监控与风险控制
- 指标:去重率(unique/K)、sim 分布直方图、token 节省率、幻觉率(用 RAGAS 等评测)。
- Prometheus 集成:Qdrant 暴露 /metrics,追踪查询 QPS、召回@5。
- 风险:
- 高阈值漏关键 paraphrase:缓解,用 MMR(Maximal Marginal Relevance)多样性。
- 计算开销:Top-20 sim_matrix O(n^2),n<50 无碍;大 K 用 FAISS 近似。
- 语料偏差:测试多域数据,调 instruction。
实践证明,此方案在本地 8GB RAM Mac 上,QPS>10,显著优于无去重基线。来源:Hacker News 讨论(id=41992822)本地 RAG 实践、BAAI bge 文档相似度指南、Chroma/Qdrant 快速上手。
(字数:1265)