从基础RAG到高级RAG,检索增强生成的最佳实践
大模型的训练数据是有截止日期的,而且对于企业私有知识一无所知。RAG(检索增强生成) 是目前解决这个问题最主流、最实用的方案。
过去半年,我先后在3个项目上落地了RAG技术,从最基础的"向量检索+LLM"到多路召回+重排序+查询重写的复杂方案,踩过的坑都成了宝贵的经验。
# 这是最基础的RAG实现
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.chains import RetrievalQA
# 加载文档
documents = load_documents()
# 切分
text_splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50)
chunks = text_splitter.split_documents(documents)
# 向量化存储
vectordb = Chroma.from_documents(chunks, HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5"))
# 检索问答
qa_chain = RetrievalQA.from_chain_type(llm=llm, retriever=vectordb.as_retriever())
result = qa_chain.run("什么是微服务架构?")这个基础方案能解决60%的问题,但剩下的40%需要更精细的优化。
切分是RAG中影响最大的因素之一。我试过多种策略:
切分策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
固定长度切分 | 任意场景 | 简单可控 | 容易切碎语义 |
递归字符切分 | 通用 | 保留段落完整 | 长段落差不可控 |
语义切分 | 长文档 | 保留完整语义 | 速度慢 |
文档结构切分 | Markdown/HTML | 结构完整 | 格式依赖 |
最佳实践:对于技术文档,优先按文档结构切分(标题、段落、列表),再配合语义切分做补充。
from langchain_text_splitters import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "header1"),
("##", "header2"),
("###", "header3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
docs = markdown_splitter.split_text(markdown_content)纯向量检索的最大问题:语义相似 ≠ 信息相关。
例如用户问"MySQL怎么建索引",向量检索可能召回一堆"索引的原理"而不是"建索引的命令"。
混合检索结合了向量检索的语义能力和关键词检索的精确匹配能力:
class HybridRetriever:
def __init__(self, vector_store, bm25_index):
self.vector_store = vector_store
self.bm25_index = bm25_index
def retrieve(self, query, top_k=5):
# 向量检索
vector_results = self.vector_store.similarity_search(query, k=top_k*2)
# BM25关键词检索
bm25_results = self.bm25_index.search(query, k=top_k*2)
# RRF融合
return self.reciprocal_rank_fusion(vector_results, bm25_results, top_k)效果:混合检索相比纯向量检索,Top-3准确率从78%提升到89%。
用户的原始问题往往太口语化或太模糊,直接检索效果不好:
用户问题: "那个报警的事处理了没?"
改写后: "系统报警事件的处理状态和后续方案"def rewrite_query(original_query, chat_history):
prompt = f"""
基于对话上下文,将用户的问题改写成更适合知识库检索的查询。
历史对话:{chat_history[-3:]}
用户问题:{original_query}
改写要求:
- 提取关键词,去掉口语化的表达
- 补充必要的上下文信息
- 保持问题的完整性
改写结果:
"""
return llm.generate(prompt)初步检索可能返回10-20个片段,但真正相关的只有少数几个。重排序器(Reranker)能对候选片段重新排序,把最相关的放在前面。
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
def rerank(query, candidates, top_k=3):
pairs = [[query, doc.page_content] for doc in candidates]
scores = reranker.predict(pairs)
scored = list(zip(candidates, scores))
scored.sort(key=lambda x: x[1], reverse=True)
return [doc for doc, score in scored[:top_k]]为什么需要重排序? 向量检索找到的是"语义相似"的片段,但语义相似不一定是最匹配问答需求的片段。Reranker能做更精细的相关性判断。
检索到的文档块中只有一部分是真正相关的,把全文扔给LLM既浪费token又容易引入噪声:
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=base_retriever
)
compressed_docs = compression_retriever.get_relevant_documents(query)除了向量检索,还可以增加多路召回通道,提升召回覆盖率:
class MultiSourceRetriever:
def __init__(self):
self.retrievers = {
"vector": VectorRetriever(),
"keyword": BM25Retriever(),
"sql": SQLRetriever(), # 从结构化数据中检索
"graph": KnowledgeGraphRetriever(), # 知识图谱检索
}
def retrieve(self, query, top_k=5):
all_results = []
for name, retriever in self.retrievers.items():
results = retriever.retrieve(query, top_k=top_k)
for r in results:
r.source = name
all_results.append(r)
# RRF融合排序
return self.rrf_merge(all_results, top_k)我们需要为一个拥有5000+篇技术文档的知识库搭建问答系统。
class AdvancedRAGSystem:
def __init__(self):
self.query_rewriter = QueryRewriter(llm)
self.retriever = MultiSourceRetriever()
self.reranker = Reranker()
self.response_generator = ResponseGenerator(llm)
def answer(self, question, history=[]):
# 1. 查询重写
rewritten = self.query_rewriter.rewrite(question, history)
# 2. 多路召回
candidates = self.retriever.retrieve(rewritten, top_k=20)
# 3. 重排序
top_docs = self.reranker.rerank(rewritten, candidates, top_k=5)
# 4. 生成回答
answer = self.response_generator.generate(question, top_docs)
return answer维度 | 基础RAG | 优化后RAG | 提升幅度 |
|---|---|---|---|
回答准确率 | 72% | 91% | +19% |
检索召回率 | 68% | 85% | +17% |
引用准确率 | 60% | 88% | +28% |
幻觉率 | 15% | 5% | -10% |
平均响应时间 | 1.8s | 2.5s | 稍慢但可接受 |
RAG系统的评估需要从多个维度:
RAG评估矩阵
├── 检索质量
│ ├── 命中率(Hit Rate):相关文档是否被召回
│ └── MRR(Mean Reciprocal Rank):相关文档的排名
├── 生成质量
│ ├── 忠实度(Faithfulness):是否基于检索结果
│ ├── 相关性(Relevance):是否回答了问题
│ └── 完整性(Completeness):回答是否全面
└── 用户体验
├── 响应速度
└── 引用准确性from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
# 使用RAGAS框架做自动化评估
result = evaluate(
dataset=test_dataset,
metrics=[faithfulness, answer_relevancy, context_precision],
llm=eval_llm,
embeddings=eval_embeddings
)原因:文档切分不合理、向量模型不匹配、查询语义偏移
解决:
原因:检索到的上下文不完整、LLM过度泛化、prompt设计不合理
解决:
原因:检索链路太长、LLM推理慢、文档太大
解决:
RAG技术还在快速演进:
RAG是当前大模型落地最实用的技术路线之一。从基础RAG到优化RAG,每一步优化都能带来可衡量的效果提升。
核心要点:
你在做RAG落地时踩过什么坑?有什么好的优化经验?欢迎分享!