Files
rag/core/mmr.py
lacerate551 100d1a06eb init: RAG 知识库服务初始提交
- 后端 API(Flask + Gunicorn)
- RAG 引擎(混合检索 + 云端 Reranker + 引用溯源)
- 文档解析(MinerU + 多格式支持)
- Docker 生产部署配置
- 排除前端项目、敏感配置、模型文件
2026-06-04 17:35:27 +08:00

224 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
MMRMax Marginal Relevance去重模块
功能:
- 平衡相关性和多样性
- 避免重复内容占据 top_k 结果
- 前置到 rerank 之前,减少 rerank 输入量
使用场景:
召回 100 个 → MMR 去重取 30 个 → rerank 取 top_k
"""
import numpy as np
from typing import List, Dict, Tuple, Optional
import logging
logger = logging.getLogger(__name__)
def cosine_similarity(vec1: np.ndarray, vec2: np.ndarray) -> float:
"""计算两个向量的余弦相似度"""
norm1 = np.linalg.norm(vec1)
norm2 = np.linalg.norm(vec2)
if norm1 == 0 or norm2 == 0:
return 0.0
return float(np.dot(vec1, vec2) / (norm1 * norm2))
def mmr_rerank(
query_emb: np.ndarray,
candidates: List[Dict],
top_k: int = 30,
lambda_param: float = 0.5,
emb_key: str = 'embedding'
) -> List[Dict]:
"""
Max Marginal Relevance 去重
公式: MMR = λ * Relevance - (1-λ) * Max_Similarity
Args:
query_emb: 查询向量
candidates: 候选文档列表,每个文档需包含 embedding
top_k: 返回数量
lambda_param: 相关性/多样性权衡参数 (0-1)
- 1.0: 只考虑相关性
- 0.5: 平衡相关性和多样性
- 0.0: 只考虑多样性
emb_key: embedding 在候选文档中的 key
Returns:
去重后的候选文档列表
"""
if not candidates:
return []
if len(candidates) <= top_k:
return candidates
selected = []
remaining = candidates.copy()
while len(selected) < top_k and remaining:
mmr_scores = []
for i, cand in enumerate(remaining):
# 获取候选文档的 embedding
cand_emb = cand.get(emb_key)
if cand_emb is None:
# 没有 embedding跳过或使用默认分数
mmr_scores.append(-float('inf'))
continue
cand_emb = np.array(cand_emb)
# 1. 相关性:与查询的相似度
relevance = cosine_similarity(query_emb, cand_emb)
# 2. 冗余度:与已选文档的最大相似度
if selected:
# 过滤出有 embedding 的已选文档
selected_with_emb = [s for s in selected if s.get(emb_key) is not None]
if selected_with_emb:
max_sim = max(
cosine_similarity(cand_emb, np.array(s.get(emb_key)))
for s in selected_with_emb
)
else:
max_sim = 0.0
else:
max_sim = 0.0
# 3. MMR 分数 = λ * 相关性 - (1-λ) * 冗余度
mmr_score = lambda_param * relevance - (1 - lambda_param) * max_sim
mmr_scores.append(mmr_score)
# 选择 MMR 分数最高的
if mmr_scores:
best_idx = np.argmax(mmr_scores)
if mmr_scores[best_idx] > -float('inf'):
selected.append(remaining.pop(best_idx))
else:
# 所有候选都没有 embedding直接取前 top_k
selected.extend(remaining[:top_k - len(selected)])
break
return selected
def mmr_filter_by_content(
candidates: List[Dict],
top_k: int = 30,
similarity_threshold: float = 0.9
) -> List[Dict]:
"""
基于内容相似度的去重(简化版,不需要 embedding
适用于:
- 没有 embedding 的情况
- 快速去重场景
Args:
candidates: 候选文档列表
top_k: 返回数量
similarity_threshold: 相似度阈值,超过则视为重复
Returns:
去重后的候选文档列表
"""
if not candidates:
return []
if len(candidates) <= top_k:
return candidates
selected = []
remaining = candidates.copy()
while len(selected) < top_k and remaining:
current = remaining.pop(0)
# 检查是否与已选内容重复
is_duplicate = False
current_content = current.get('content', current.get('document', ''))[:200]
for s in selected:
s_content = s.get('content', s.get('document', ''))[:200]
# 简单的 Jaccard 相似度
words1 = set(current_content)
words2 = set(s_content)
if words1 and words2:
intersection = len(words1 & words2)
union = len(words1 | words2)
similarity = intersection / union if union > 0 else 0
if similarity > similarity_threshold:
is_duplicate = True
break
if not is_duplicate:
selected.append(current)
return selected
# ==================== 测试 ====================
if __name__ == "__main__":
import sys
if sys.platform == 'win32':
sys.stdout.reconfigure(encoding='utf-8')
print("=" * 60)
print("MMR 去重测试")
print("=" * 60)
# 模拟候选文档
np.random.seed(42)
def random_embedding():
emb = np.random.randn(768)
return emb / np.linalg.norm(emb)
query_emb = random_embedding()
# 创建 10 个候选,前 5 个相似
base_emb = random_embedding()
candidates = []
for i in range(10):
if i < 5:
# 前 5 个与 query 相似
emb = query_emb + np.random.randn(768) * 0.1
else:
# 后 5 个与 query 不太相似
emb = random_embedding()
candidates.append({
'id': f'doc_{i}',
'content': f'文档内容 {i}',
'embedding': emb / np.linalg.norm(emb)
})
print(f"\n候选数量: {len(candidates)}")
# MMR 去重
selected = mmr_rerank(query_emb, candidates, top_k=5, lambda_param=0.5)
print(f"MMR 选择数量: {len(selected)}")
print(f"选择的文档 ID: {[c['id'] for c in selected]}")
# 计算多样性
embs = [c['embedding'] for c in selected]
diversity_scores = []
for i in range(len(embs)):
for j in range(i + 1, len(embs)):
sim = cosine_similarity(embs[i], embs[j])
diversity_scores.append(sim)
avg_similarity = np.mean(diversity_scores) if diversity_scores else 0
print(f"平均相似度(越低多样性越高): {avg_similarity:.3f}")