RAG关键技术要点详解
一、数据分块策略:怎么"切菜"才好吃?
1. 切多大?黄金尺寸怎么定
原则:保持语义完整性 + 匹配LLM能力
太小的问题: 太大的问题:
"猫咪需要..." "从猫咪的起源说起,包括..."
"定期打疫苗..." "生理特征、饲养方法、疾病预防..."
→ 意思不完整 → 信息太杂,LLM记不住
具体尺寸建议:
python
# 不同场景的"黄金尺寸"
场景配置 = {
"技术文档": {
"chunk_size": 800, # 代码+解释需要空间
"chunk_overlap": 100,
"按什么切": "函数/类定义"
},
"法律条文": {
"chunk_size": 500, # 条文要完整
"chunk_overlap": 50,
"按什么切": "法条编号"
},
"对话记录": {
"chunk_size": 300, # 对话短
"chunk_overlap": 30,
"按什么切": "对话轮次"
},
"通用文本": {
"chunk_size": 500, # 大部分场景的最佳平衡
"chunk_overlap": 50,
"按什么切": "段落"
}
}
公式参考:
chunk_size ≈ LLM上下文长度 ÷ 4
例如:GPT-4上下文128K,一次能处理约4000 tokens
检索用块大小建议:4000 ÷ 4 = 1000 tokens ≈ 750汉字
2. 怎么切?四种"刀法"对比
刀法1:固定长度切(最笨但最快)
python
# 就像用尺子量着切
def fixed_length_split(text, chunk_size=500):
chunks = []
for i in range(0, len(text), chunk_size):
chunk = text[i:i+chunk_size]
chunks.append(chunk)
return chunks
优点 :简单快速,适合技术文档
缺点:可能切断句子,破坏语义
刀法2:按句子/段落切(实用首选)
python
# 沿着"天然边界"切
import re
def paragraph_split(text):
# 按双换行切(段落边界)
paragraphs = re.split(r'\n\s*\n', text)
chunks = []
current_chunk = ""
for para in paragraphs:
if len(current_chunk) + len(para) > 500:
if current_chunk:
chunks.append(current_chunk)
current_chunk = para
else:
current_chunk += "\n" + para if current_chunk else para
if current_chunk:
chunks.append(current_chunk)
return chunks
刀法3:语义切分(最智能)
原理:用嵌入模型找出语义边界
python
# 用句子嵌入找出"话题转折点"
from sentence_transformers import SentenceTransformer
def semantic_split(text, model_name='paraphrase-multilingual-MiniLM-L12-v2'):
# 1. 先切成句子
sentences = text.split('。') # 简单示例
# 2. 计算每个句子的向量
model = SentenceTransformer(model_name)
embeddings = model.encode(sentences)
# 3. 找相邻句子差异大的地方(话题转折)
chunks = []
current_chunk = []
for i in range(1, len(sentences)):
# 计算余弦相似度
similarity = cosine_similarity(embeddings[i-1:i+1])
if similarity < 0.7: # 话题明显变化
chunks.append('。'.join(current_chunk) + '。')
current_chunk = [sentences[i]]
else:
current_chunk.append(sentences[i])
return chunks
刀法4:递归切分(适应性最强)
python
# 先试大块,不行再切小
def recursive_split(text, max_size=1000, min_size=200):
# 如果文本已经够小
if len(text) <= max_size:
return [text]
# 先尝试按段落切
paragraphs = text.split('\n\n')
chunks = []
for para in paragraphs:
if len(para) > max_size:
# 段落太大,按句子切
sentences = para.split('。')
temp_chunk = ""
for sentence in sentences:
if len(temp_chunk) + len(sentence) > max_size:
chunks.append(temp_chunk)
temp_chunk = sentence
else:
temp_chunk += sentence + '。'
if temp_chunk:
chunks.append(temp_chunk)
else:
chunks.append(para)
return chunks
3. 元数据:给每块"菜"贴标签
为什么需要元数据?
- 检索时过滤:"只要2024年的政策"
- 回答时引用:"根据《员工手册》第3章"
- 版本管理:"这是最新版的文档"
标准元数据字段:
python
metadata_template = {
"source": "员工手册.pdf", # 来源文件
"page": 25, # 页码
"section": "第三章 休假制度", # 章节
"author": "人力资源部", # 作者
"date": "2024-01-15", # 发布日期
"doc_type": "公司制度", # 文档类型
"version": "2.1", # 版本号
"keywords": ["年假", "请假", "考勤"], # 关键词
}
智能提取元数据:
python
def extract_metadata(text, file_info):
"""从文本中自动提取元数据"""
metadata = {
"source": file_info["filename"],
"size_chars": len(text),
"estimated_tokens": len(text) // 4, # 粗略估算
}
# 提取标题(第一行或最大字号)
lines = text.strip().split('\n')
if lines and len(lines[0]) < 100: # 可能是标题
metadata["title"] = lines[0]
# 提取日期(正则匹配)
import re
date_patterns = [
r'\d{4}年\d{1,2}月\d{1,2}日',
r'\d{4}-\d{2}-\d{2}',
]
for pattern in date_patterns:
dates = re.findall(pattern, text)
if dates:
metadata["mentioned_dates"] = dates
return metadata
二、嵌入模型选择:怎么"翻译"成计算机语言?
1. 质量评估:不是所有模型都适合你
测试方法:
python
def test_embedding_model(model_name, test_pairs):
"""
测试嵌入模型质量
test_pairs = [
("问题1", "相关文档1", "不相关文档1"),
("问题2", "相关文档2", "不相关文档2"),
]
"""
model = load_model(model_name)
scores = []
for question, relevant, irrelevant in test_pairs:
q_vec = model.encode(question)
rel_vec = model.encode(relevant)
irr_vec = model.encode(irrelevant)
# 相关文档应该更相似
rel_sim = cosine_similarity(q_vec, rel_vec)
irr_sim = cosine_similarity(q_vec, irr_vec)
if rel_sim > irr_sim:
scores.append(1) # 模型判断正确
else:
scores.append(0) # 模型判断错误
accuracy = sum(scores) / len(scores)
return accuracy
2. 主流模型对比表
| 模型 | 维度 | 语言 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| OpenAI text-embedding-3 | 1536/3072 | 多语言 | 效果最好,维护好 | 收费,有延迟 | 预算充足的生产环境 |
| BGE (BAAI) | 768 | 中文优化 | 中文效果顶尖,免费 | 英文稍弱 | 中文为主的业务 |
| M3E (国产) | 768 | 中文 | 轻量,速度快 | 专业领域一般 | 快速验证,资源有限 |
| Sentence-BERT | 384 | 多语言 | 平衡,社区活跃 | 需要选对预训练 | 多语言,技术团队强 |
| Voyage AI | 1024 | 多语言 | 长文本优化 | 较新,生态少 | 长文档检索 |
3. 一致性要求:必须用同一把"尺子"
常见错误:
python
# 错误做法:训练和查询用不同模型
train_vectors = model_A.encode(train_docs) # 存的时候用A模型
query_vector = model_B.encode(user_query) # 查的时候用B模型
# → 向量空间不一致,检索会失败
# 正确做法:始终用同一个模型
model = load_model("text-embedding-ada-002")
train_vectors = model.encode(train_docs)
query_vector = model.encode(user_query)
模型版本管理:
python
# 记录模型版本
embedding_config = {
"model_name": "text-embedding-3-small",
"model_version": "2024-01-01",
"dimension": 1536,
"normalized": True, # 是否做了归一化
}
# 存入向量库时保存配置
vector_db.set_metadata({
"embedding_config": embedding_config,
"created_at": "2024-05-20"
})
4. 领域适应:让模型懂你的"行话"
什么时候需要微调?
✓ 你的文档有大量专业术语(医疗、法律、金融)
✓ 通用模型测试效果不好(准确率<80%)
✓ 你有足够的标注数据(>1000对)
✓ 你打算长期使用这个系统
微调方法对比:
python
微调策略 = {
"继续预训练": {
"数据": 大量领域文本(无标注)
"成本": 中等
"效果": 对领域语言理解更好
},
"有监督微调": {
"数据": 标注的(Q,A)对
"成本": 高
"效果": 检索准确率提升明显
},
"适配器微调": {
"数据": 少量标注数据
"成本": 低
"效果": 快速适应,不破坏原模型
}
}
实用建议:
- 先用通用模型测试
- 如果准确率>85%,可能不需要微调
- 微调前准备至少500-1000对标注数据
- 考虑成本:微调一次可能需要几千到几万元
三、检索策略:怎么"大海捞针"?
1. Top-K选择:捞几根针?
动态Top-K策略:
python
def dynamic_top_k(query, default_k=5):
"""
根据查询复杂度动态调整K值
"""
# 简单查询:少返回些
simple_keywords = ["是什么", "谁", "什么时候"]
if any(kw in query for kw in simple_keywords):
return 3 # 简单问题,3个片段足够
# 复杂查询:多返回些
complex_keywords = ["如何", "步骤", "对比", "优缺点"]
if any(kw in query for kw in complex_keywords):
return 8 # 复杂问题,需要更多上下文
# 开放式查询:更多
open_keywords = ["分析", "讨论", "总结"]
if any(kw in query for kw in open_keywords):
return 10
return default_k
根据LLM能力调整:
python
def calculate_max_k(llm_context_size, chunk_size, answer_size):
"""
计算最多能放多少个块
"""
# LLM总容量 = 系统提示词 + 问题 + 上下文 + 答案
available_tokens = llm_context_size - len(system_prompt) - len(query) - answer_size
max_chunks = available_tokens // chunk_size
return max(1, max_chunks - 2) # 留点余量
2. 相似度阈值:多像才算像?
自适应阈值:
python
def adaptive_threshold(query, top_results):
"""
根据检索结果的质量动态调整阈值
"""
scores = [r["score"] for r in top_results]
# 如果结果都很差,降低标准
if max(scores) < 0.5:
return 0.3 # 降低要求
# 如果结果都很好,提高标准
if min(scores) > 0.8:
return 0.7 # 提高要求
# 正常情况:取前几个结果的平均值
avg_score = sum(scores[:3]) / 3
return max(0.5, avg_score - 0.1) # 至少0.5
分档处理:
python
def filter_by_score_bands(results):
"""
按分数分档处理
"""
excellent = [r for r in results if r["score"] > 0.85]
good = [r for r in results if 0.7 <= r["score"] <= 0.85]
fair = [r for r in results if 0.5 <= r["score"] < 0.7]
if excellent:
return excellent[:3] # 有优质结果,只要最好的
elif good:
return good[:5] # 有好结果,多取几个
elif fair:
return fair[:3] # 只有一般结果,少取点
else:
return [] # 没有相关结果
3. 混合检索:向量 + 关键词双保险
实现方案:
python
def hybrid_search(query, vector_db, keyword_db, alpha=0.5):
"""
混合检索:向量相似度 + 关键词匹配
alpha: 向量检索的权重 (0-1)
"""
# 1. 向量检索
query_vector = embedding_model.encode(query)
vector_results = vector_db.search(query_vector, top_k=10)
# 2. 关键词检索 (BM25等)
keyword_results = keyword_db.search(query, top_k=10)
# 3. 结果融合 (加权得分)
all_results = {}
# 处理向量结果
for i, result in enumerate(vector_results):
doc_id = result["id"]
vector_score = result["score"]
rank_score = 1.0 / (i + 1) # 排名分
all_results[doc_id] = {
"doc": result,
"score": alpha * vector_score + (1 - alpha) * rank_score,
"source": "vector"
}
# 处理关键词结果
for i, result in enumerate(keyword_results):
doc_id = result["id"]
keyword_score = result["score"]
rank_score = 1.0 / (i + 1)
if doc_id in all_results:
# 两个检索都找到了,分数相加
all_results[doc_id]["score"] += (1 - alpha) * keyword_score
all_results[doc_id]["source"] = "both"
else:
all_results[doc_id] = {
"doc": result,
"score": (1 - alpha) * keyword_score,
"source": "keyword"
}
# 4. 排序返回
sorted_results = sorted(all_results.values(),
key=lambda x: x["score"],
reverse=True)
return sorted_results[:10]
4. 重排机制:让最相关的排前面
为什么需要重排?
第一次检索(快速但粗糙):
1. 宠物医院地址 (0.85)
2. 猫咪疫苗种类 (0.83)
3. 狗狗训练方法 (0.81) ← 不相关但相似度高
4. 猫咪饮食指南 (0.78)
重排后(精确但慢):
1. 猫咪疫苗种类 (0.92) ✓
2. 宠物医院地址 (0.88)
3. 猫咪饮食指南 (0.85)
4. 狗狗训练方法 (0.30) ✗
Cross-Encoder重排实现:
python
from sentence_transformers import CrossEncoder
def rerank_with_cross_encoder(query, candidates):
"""
用Cross-Encoder对候选结果重排
"""
# 加载预训练的重排模型
model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
# 准备输入对 (query, candidate_text)
pairs = [(query, candidate["text"]) for candidate in candidates]
# 预测相关性分数
scores = model.predict(pairs)
# 更新分数并重排
for candidate, score in zip(candidates, scores):
candidate["rerank_score"] = float(score)
# 可以组合原始分和重排分
candidate["final_score"] = 0.3 * candidate["score"] + 0.7 * score
# 按最终分数排序
reranked = sorted(candidates,
key=lambda x: x["final_score"],
reverse=True)
return reranked
轻量级重排方案:
python
def lightweight_rerank(query, candidates):
"""
如果不想要复杂的Cross-Encoder,可以用这些轻量方法
"""
reranked = []
for candidate in candidates:
score = candidate["score"]
# 1. 关键词重叠加分
query_words = set(query.lower().split())
doc_words = set(candidate["text"].lower().split())
overlap = len(query_words & doc_words) / len(query_words)
score += overlap * 0.1
# 2. 元数据匹配加分
if "metadata" in candidate:
# 如果查询包含日期,文档日期匹配就加分
if "2024" in query and "2024" in str(candidate["metadata"].get("date", "")):
score += 0.05
# 3. 长度惩罚:太短的文档可能信息不全
if len(candidate["text"]) < 100:
score *= 0.9
candidate["adjusted_score"] = score
reranked.append(candidate)
return sorted(reranked, key=lambda x: x["adjusted_score"], reverse=True)
四、实战调优技巧
技巧1:分阶段检索
python
def staged_retrieval(query, vector_db, budget_ms=200):
"""
分阶段检索:先快后准
"""
start_time = time.time()
# 阶段1:快速粗筛 (50ms预算)
fast_results = vector_db.fast_search(query, top_k=20)
if time.time() - start_time > budget_ms * 0.25:
return fast_results[:5] # 超时直接返回
# 阶段2:精准重排 (150ms预算)
if len(fast_results) > 0:
reranked = rerank_with_cross_encoder(query, fast_results[:10])
return reranked[:5]
return []
技巧2:查询扩展
python
def expand_query(query, llm):
"""
用LLM扩展查询,提高召回率
"""
prompt = f"""
用户查询:{query}
请生成3个相关的查询变体,用于文档检索:
1. 同义改写:
2. 更具体的问题:
3. 更宽泛的问题:
"""
response = llm.generate(prompt)
# 解析响应,得到多个查询
queries = [query] + parse_expanded_queries(response)
return queries
技巧3:结果去重
python
def deduplicate_results(results, similarity_threshold=0.9):
"""
去除内容高度相似的结果
"""
unique_results = []
seen_contents = []
for result in results:
content = result["text"]
is_duplicate = False
# 检查是否与已选结果高度相似
for seen in seen_contents:
if similarity(content, seen) > similarity_threshold:
is_duplicate = True
break
if not is_duplicate:
unique_results.append(result)
seen_contents.append(content[:500]) # 只存前500字比较
return unique_results
技巧4:失败回退
python
def robust_retrieval(query, vector_db, backup_strategy="keyword"):
"""
健壮的检索:主策略失败时用备选策略
"""
try:
# 尝试向量检索
results = vector_db.search(query, top_k=5)
# 检查结果质量
if not results or results[0]["score"] < 0.5:
raise ValueError("检索结果质量太低")
return results
except Exception as e:
print(f"向量检索失败: {e}, 切换到备选策略: {backup_strategy}")
if backup_strategy == "keyword":
# 回退到关键词检索
return keyword_search(query, top_k=5)
elif backup_strategy == "hybrid":
# 用混合检索
return hybrid_search(query, vector_db, keyword_db)
else:
# 返回空结果
return []
五、评估和迭代
建立评估体系
python
class RAGEvaluator:
def __init__(self, test_dataset):
self.test_data = test_dataset # 包含(问题, 相关文档列表)
def evaluate_retrieval(self, retrieval_func):
"""评估检索模块"""
scores = []
for question, relevant_docs in self.test_data:
retrieved = retrieval_func(question)
# 计算召回率
retrieved_ids = {r["id"] for r in retrieved}
relevant_ids = {d["id"] for d in relevant_docs}
recall = len(retrieved_ids & relevant_ids) / len(relevant_ids)
# 计算准确率
precision = len(retrieved_ids & relevant_ids) / len(retrieved_ids) if retrieved else 0
scores.append({
"recall": recall,
"precision": precision,
"f1": 2 * recall * precision / (recall + precision) if (recall + precision) > 0 else 0
})
return scores
def evaluate_end_to_end(self, rag_system):
"""端到端评估"""
# 人工或自动评估生成答案的质量
pass
A/B测试框架
python
def ab_test_retrieval(query, strategy_a, strategy_b, traffic_split=0.5):
"""
A/B测试不同检索策略
"""
import random
# 随机分配策略
if random.random() < traffic_split:
strategy = "A"
results = strategy_a(query)
else:
strategy = "B"
results = strategy_b(query)
# 记录实验数据
log_experiment({
"query": query,
"strategy": strategy,
"results_count": len(results),
"top_score": results[0]["score"] if results else 0,
"timestamp": time.time()
})
return results
六、实用建议总结
给新手的建议
- 从简单开始:固定长度分块 + OpenAI嵌入 + 简单检索
- 用现有工具:LangChain/LlamaIndex能解决80%的问题
- 重视评估:没有评估就没有优化方向
给进阶者的建议
- 分块要智能:按语义分割,不要简单按字数
- 检索要混合:向量+关键词往往效果更好
- 模型要合适:中文用BGE,英文用OpenAI
- 结果要重排:Cross-Encoder能显著提升质量
给专家的建议
- 领域微调:重要场景一定要微调嵌入模型
- 多阶段检索:粗筛+精排+重排
- 动态调整:根据查询复杂度调整参数
- 持续优化:建立数据闭环,持续改进
记住 :RAG系统是"三分技术,七分调优"。最好的策略是快速搭建一个基础版本,然后根据真实用户反馈和数据持续优化。不要追求一次性完美,而要追求持续改进。