RAG系统优化全攻略:从召回率提升到幻觉抑制的7个技巧

做RAG(检索增强生成)系统快两年了,踩过的坑比走过的路还多。最开始以为搭个向量数据库、接个大模型就完事了,结果生产环境一跑,召回率感人,幻觉满天飞。今天把这套实战中沉淀下来的7个优化技巧分享出来,每个都附带具体操作和踩坑记录,希望能帮你少走弯路。
技巧一:文档分块策略——别再用固定大小切分
刚开始做RAG时,我傻傻地用固定512 token切分文档,结果一个完整的概念被硬生生砍成两半,检索时永远找不到关键信息。后来改用语义分块,效果立竿见影。
操作步骤:
- 安装LangChain的语义分块器:
pip install langchain-experimental - 设置分块参数:
chunk_size=1024,chunk_overlap=200 - 关键:根据文档类型调整——技术文档用
markdown分隔符,PDF用段落分隔
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
text_splitter = SemanticChunker(
embeddings=OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=0.8
)
chunks = text_splitter.split_documents(documents)
print(f"分成了 {len(chunks)} 个块")
踩坑提示:语义分块对中文支持不太好,建议先做jieba分词预处理,否则分块边界会莫名其妙。
技巧二:混合检索——向量+关键词双保险
纯向量检索在专业术语和缩写词上表现极差。比如搜索”CNN”,向量检索可能给你返回”卷积神经网络”或者”新闻网络”,但用户想要的是”美国有线电视新闻网”。加入BM25关键词检索后,召回率从65%飙升到89%。
操作步骤:
- 安装Elasticsearch或Meilisearch做关键词引擎
- 配置混合检索权重:
vector_weight=0.7,keyword_weight=0.3 - 对特殊查询(如代码、缩写)自动切换为关键词优先
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
keyword_retriever = BM25Retriever.from_documents(documents, k=3)
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, keyword_retriever],
weights=[0.7, 0.3]
)
results = ensemble_retriever.invoke("什么是CNN在医疗领域的应用?")
踩坑提示:权重别设死,线上跑AB测试时发现,0.6 + 0.4的组合在技术问答场景下表现最好,但新闻场景需要0.4 + 0.6。
技巧三:查询重写——让用户的问题更易检索
用户经常问”这个怎么用”、”那个是什么”,这种模糊查询直接检索效果极差。我们加了一个查询重写模块,用大模型把模糊问题转换成具体查询。
操作步骤:
- 写一个重写Prompt,要求模型补充缺失信息
- 设置重写策略:补充上下文、同义词扩展、问题拆解
- 缓存重写结果,避免重复调用大模型
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
rewrite_prompt = PromptTemplate.from_template("""
将用户的问题改写成更具体、更适合检索的形式。
原始问题: {question}
历史对话: {history}
改写后的查询:
""")
llm = ChatOpenAI(model="gpt-4", temperature=0)
rewrite_chain = rewrite_prompt | llm
rewritten_query = rewrite_chain.invoke({"question": "这个怎么用", "history": "我们在讨论Docker部署"})
print(rewritten_query) # 输出: "Docker部署的基本步骤和常用命令"
踩坑提示:别让重写模型太”聪明”,设置temperature=0,否则它会自由发挥导致检索跑偏。
技巧四:RAPTOR——给检索加个”思维导图”
普通RAG只能检索到叶子节点,但很多知识需要从高层视角理解。RAPTOR技术通过递归摘要构建多层索引,让检索能”看到森林”。
操作步骤:
- 安装RAPTOR库:
pip install raptor-rag - 设置聚类参数:
max_cluster_size=100,summary_model="gpt-4" - 构建树形索引:每个节点包含原始文本+子节点摘要
# 使用RAPTOR构建索引
python -m raptor build_index
--input_dir ./documents
--output_dir ./raptor_index
--max_cluster_size 100
--summary_model gpt-4
踩坑提示:RAPTOR的构建成本很高,建议只对核心知识库使用,普通文档用传统分块就行。我踩过坑:对10万份文档全量构建,API账单直接爆炸。
技巧五:幻觉抑制——给生成加个”安检门”
幻觉是RAG最头疼的问题。我们的方案是:在生成后加一个”事实核查”模块,让另一个模型验证输出是否基于检索结果。
操作步骤:
- 在Prompt中要求模型输出”引用来源”
- 用NLI(自然语言推理)模型检查生成与检索结果的一致性
- 设置置信度阈值:低于0.7的生成内容自动标记为”低置信度”
from transformers import pipeline
nli_model = pipeline("text-classification", model="roberta-large-mnli")
def check_hallucination(generated_text, retrieved_docs):
for doc in retrieved_docs:
result = nli_model(f"{doc.page_content} [SEP] {generated_text}")
if result['label'] == 'CONTRADICTION':
return False, f"与文档矛盾: {doc.page_content[:50]}..."
return True, "通过核查"
# 使用示例
is_valid, message = check_hallucination("Docker是2013年发布的", retrieved_docs)
print(message)
踩坑提示:NLI模型对长文本支持有限,建议把generated_text切成句子逐句检查,否则会被截断导致误判。
技巧六:上下文压缩——别让大模型”撑死”
给大模型喂太多不相关上下文,反而会降低生成质量。上下文压缩技术可以智能过滤掉无关信息。
操作步骤:
- 使用LLMChainExtractor提取关键信息
- 设置压缩率:
max_tokens=2000 - 对压缩后的内容做二次相关性排序
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=ensemble_retriever
)
compressed_docs = compression_retriever.invoke("Docker和Kubernetes的区别")
print(f"压缩前: {len(original_docs)} 个文档, 压缩后: {len(compressed_docs)} 个文档")
踩坑提示:压缩器会消耗额外token,如果上下文本来就少(<2000 tokens),直接跳过压缩步骤,否则得不偿失。
技巧七:多轮对话——给RAG加上”记忆”
用户问完”Docker怎么安装”,接着问”怎么配置网络”,如果系统不记得上下文,第二个问题就会跑偏。我们用ConversationBufferWindowMemory维护最近5轮对话。
操作步骤:
- 初始化内存:
memory = ConversationBufferWindowMemory(k=5) - 在检索时传入历史对话
- 对历史对话做压缩,避免token超限
from langchain.memory import ConversationBufferWindowMemory
from langchain.chains import ConversationalRetrievalChain
memory = ConversationBufferWindowMemory(
memory_key="chat_history",
return_messages=True,
k=5
)
qa_chain = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=ensemble_retriever,
memory=memory,
verbose=True
)
# 多轮对话示例
response1 = qa_chain.invoke({"question": "Docker怎么安装?"})
response2 = qa_chain.invoke({"question": "那网络怎么配置?"}) # 自动理解是Docker的网络配置
print(response2['answer'])
踩坑提示:别把全部历史对话都塞进去,k=3在大多数场景下就够用了。k值过大反而会引入噪声,让模型分不清当前问题的重点。
这7个技巧不是银弹,但组合使用能让RAG系统从”能用”变成”好用”。建议按这个顺序逐步优化:先解决召回率(技巧1-3),再提升检索效率(技巧4),最后处理生成质量(技巧5-7)。每个技巧上线前记得做AB测试,毕竟你的用户数据才是最真实的评判标准。

这个分块坑我也踩过,512硬切真挺离谱。