深入LangChain与RAG:从基础管道到生产级优化
穿透LangChain封装,理解RAG每一环,构建可控、可优化的智能问答系统
前言
在"手写LoRA"的思想指导下,我们同样需要深入理解RAG(检索增强生成)的原理,而不是仅仅调用现成API。LangChain作为构建LLM应用的事实标准框架,提供了从文档加载到检索生成的完整工具链。但真正掌握RAG,需要理解:
- 文档分块策略对检索质量的根本影响
- 向量检索的数学原理与性能权衡
- 检索-生成之间的信息瓶颈如何突破
- 生产环境下的优化手段与评估体系
本文将构建一个完整的RAG系统,并深入剖析每一层的设计决策。
第一部分:RAG技术原理与LangChain架构
1.1 为什么需要RAG?
LLM存在三个致命缺陷:
| 问题 | 表现 | 后果 |
|---|---|---|
| 幻觉 | 生成事实不正确的信息 | 不可用于专业领域 |
| 知识滞后 | 训练数据截止日期后的事件无法知晓 | 无法回答最新问题 |
| 无权限访问私有数据 | 无法读取企业内部文档 | 无法个性化回答 |
RAG通过检索外部知识库来解决这三个问题:查询时先从知识库中检索相关文档,再将文档作为上下文注入LLM的生成过程。这种"检索-增强-生成"的闭环,让LLM的回答有据可依。
1.2 LangChain的核心组件
LangChain的模块化设计让RAG开发效率大幅提升:
┌─────────────────────────────────────────────────────────────┐
│ LangChain RAG 组件栈 │
├─────────────────────────────────────────────────────────────┤
│ Document Loaders → 加载 PDF/网页/数据库等15+格式 │
│ Text Splitters → 按语义/固定长度/标题切分文档 │
│ Embeddings → 将文本转为向量 (OpenAI/HuggingFace) │
│ Vector Stores → FAISS/Chroma/Pinecone 向量存储 │
│ Retrievers → 语义检索/混合检索/多查询检索 │
│ Chains → 串联检索与生成形成端到端流程 │
└─────────────────────────────────────────────────────────────┘
第二部分:手写RAG核心Pipeline
2.1 环境准备
# 安装核心依赖
# pip install langchain langchain-community langchain-openai
# pip install faiss-cpu chromadb tiktoken
# pip install pypdf beautifulsoup4
import os
from typing import List, Dict, Any, Optional
import numpy as np
2.2 文档加载器:多源数据接入
LangChain的Document对象是流转的核心数据单元,包含page_content(文本)和metadata(元数据)。
from langchain_community.document_loaders import (
PyPDFLoader,
WebBaseLoader,
DirectoryLoader,
TextLoader
)
from langchain.schema import Document
class DocumentLoaderFactory:
"""支持多格式文档加载的工厂类"""
@staticmethod
def load_pdf(file_path: str) -> List[Document]:
"""加载PDF文档"""
loader = PyPDFLoader(file_path)
return loader.load()
@staticmethod
def load_web(url: str) -> List[Document]:
"""加载网页内容"""
loader = WebBaseLoader(url)
return loader.load()
@staticmethod
def load_directory(dir_path: str, glob_pattern: str = "**/*.txt") -> List[Document]:
"""批量加载目录下所有文档"""
loader = DirectoryLoader(
dir_path,
glob=glob_pattern,
loader_cls=TextLoader,
show_progress=True
)
return loader.load()
# 使用示例
docs = DocumentLoaderFactory.load_pdf("policy_manual.pdf")
print(f"加载了 {len(docs)} 个页面")
2.3 文本分割器:分块策略的深度控制
分块是RAG中最关键但也最容易被忽视的环节。块太小会丢失上下文,块太大会导致检索精度下降。典型参数为chunk_size=500, chunk_overlap=50。
from langchain.text_splitter import (
RecursiveCharacterTextSplitter,
CharacterTextSplitter,
MarkdownHeaderTextSplitter
)
class SmartTextSplitter:
"""智能分块器 - 支持多种策略"""
@staticmethod
def recursive_split(
documents: List[Document],
chunk_size: int = 500,
chunk_overlap: int = 50
) -> List[Document]:
"""
递归字符分割器(推荐)
按段落 → 句子 → 词 的层次递归切分,最大化语义完整性
"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=["\n\n", "\n", "。", "!", "?", ". ", " ", ""],
length_function=len,
)
return splitter.split_documents(documents)
@staticmethod
def markdown_split(documents: List[Document]) -> List[Document]:
"""
按Markdown标题结构分块
适合技术文档、README等结构化内容
"""
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
# 注意:此方法直接处理文本,需要先提取page_content
return splitter.split_text(documents[0].page_content)
@staticmethod
def semantic_split(
documents: List[Document],
chunk_size: int = 1000,
chunk_overlap: int = 200
) -> List[Document]:
"""
语义感知分块(通过段落边界)
先按段落分割,再合并不超过chunk_size的段落
"""
# 先按段落分割
paragraph_splitter = CharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separator="\n\n",
keep_separator=False
)
return paragraph_splitter.split_documents(documents)
# 使用推荐配置
splitter = SmartTextSplitter()
chunks = splitter.recursive_split(docs, chunk_size=500, chunk_overlap=50)
print(f"生成了 {len(chunks)} 个分块")
分块策略选择指南:
| 文档类型 | 推荐策略 | chunk_size | chunk_overlap |
|---|---|---|---|
| 技术文档/手册 | 递归分割 | 500-1000 | 50-100 |
| FAQ/短文本 | 字符分割 | 200-300 | 20-30 |
| 长篇小说 | 递归分割 | 1000-1500 | 200 |
| Markdown文档 | 标题结构分割 | - | - |
2.4 向量存储:从FAISS到混合检索
向量检索的核心是相似度计算。大多数方案使用余弦相似度或内积来度量Query与文档块的距离。
基础方案:FAISS + HuggingFace Embeddings
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.vectorstores.utils import DistanceStrategy
class VectorStoreBuilder:
"""向量存储构建器 - 支持多种后端"""
@staticmethod
def build_faiss_index(
chunks: List[Document],
model_name: str = "BAAI/bge-small-en-v1.5"
) -> FAISS:
"""
构建FAISS向量索引
bge-small在CPU上可达到800 tokens/s的处理速度
"""
embeddings = HuggingFaceEmbeddings(
model_name=model_name,
encode_kwargs={'normalize_embeddings': True} # 归一化用于余弦相似度
)
vectorstore = FAISS.from_documents(
chunks,
embeddings,
distance_strategy=DistanceStrategy.COSINE # 余弦相似度
)
return vectorstore
@staticmethod
def save_index(vectorstore: FAISS, path: str = "faiss_index"):
"""持久化索引"""
vectorstore.save_local(path)
@staticmethod
def load_index(path: str = "faiss_index") -> FAISS:
"""加载持久化的索引"""
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-en-v1.5"
)
return FAISS.load_local(
path,
embeddings,
allow_dangerous_deserialization=True
)
# 构建索引
vectorstore = VectorStoreBuilder.build_faiss_index(chunks)
VectorStoreBuilder.save_index(vectorstore)
检索器配置
def create_retriever(
vectorstore: FAISS,
search_type: str = "similarity",
k: int = 3
):
"""
创建检索器
Args:
search_type: "similarity" 或 "mmr" (最大边际相关性)
k: 返回文档数量
"""
return vectorstore.as_retriever(
search_type=search_type,
search_kwargs={"k": k}
)
第三部分:超越基础RAG——三大优化技术
只靠基础向量检索,面对真实业务场景往往力不从心。下面三个优化手段能显著提升效果。
3.1 混合检索:BM25 + 向量检索
痛点:纯向量检索擅长"语义"但不擅长"关键词"。当用户搜索专有名词(如"ModelArts")时,向量检索可能返回语义相关但不包含该词的内容。
解决方案:让BM25(关键词检索)和向量检索联手,实现1+1 > 2的效果。
from langchain.retrievers import BM25Retriever, EnsembleRetriever
def create_hybrid_retriever(
chunks: List[Document],
vectorstore: FAISS,
weights: List[float] = [0.3, 0.7] # BM25权重, 向量权重
):
"""
创建混合检索器
BM25负责关键词匹配,向量负责语义匹配
"""
# 1. 创建BM25检索器
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
# 2. 创建向量检索器
vector_retriever = vectorstore.as_retriever(
search_kwargs={"k": 3}
)
# 3. 集成两个检索器
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=weights # 可调整权重
)
return ensemble_retriever
3.2 查询改写:让LLM当"翻译官"
痛点:多轮对话中,用户常问"它怎么样?"“具体说说第二个”。这种依赖上下文的查询直接扔给检索系统,必然失效。
解决方案:在检索之前,先让LLM把口语化、有歧义的查询改写为独立的、信息完整的检索语句。
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
class QueryRewriter:
"""查询改写器 - 将用户问题转化为检索友好格式"""
def __init__(self, llm=None):
self.llm = llm or ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
self.prompt = ChatPromptTemplate.from_messages([
("system", """你是一个信息检索专家。请将用户问题改写为独立的、对检索系统友好的查询。
要求:
1. 将代词(它、这个、那个)替换为明确的名词
2. 补充对话历史中缺失的上下文
3. 只输出改写后的查询,不要其他内容"""),
("user", "对话历史:\n{chat_history}\n\n用户问题: {question}")
])
self.chain = self.prompt | self.llm
def rewrite(self, question: str, chat_history: str = "") -> str:
"""改写查询"""
if not chat_history:
return question
result = self.chain.invoke({
"chat_history": chat_history,
"question": question
})
return result.content.strip()
# 使用示例
rewriter = QueryRewriter()
query = "它主要用在哪些场景?"
chat_history = "用户: 介绍下Text2SQL技术。\nAI: Text2SQL能将自然语言转化为SQL。"
rewritten = rewriter.rewrite(query, chat_history)
print(f"原始: {query}\n改写: {rewritten}")
# 输出: "Text2SQL技术主要应用在哪些业务场景?"
3.3 结果重排:优中选优的精排裁判
痛点:混合检索保证了召回"广度",但返回的5个文档里可能只有2个真正相关。如果相关文档排在后面,会影响LLM生成质量。
解决方案:用Cross-Encoder重排模型对初步召回结果进行二次打分和排序,把最相关的文档顶到最前面。
from sentence_transformers import CrossEncoder
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
class RerankerRetriever:
"""带重排序功能的检索器"""
def __init__(self, base_retriever, model_name: str = "BAAI/bge-reranker-large"):
"""
Args:
base_retriever: 基础检索器(如混合检索器)
model_name: 重排序模型
"""
# 加载Cross-Encoder模型
self.cross_encoder = CrossEncoder(model_name)
# 包装为LangChain重排序器
self.compressor = CrossEncoderReranker(
model=self.cross_encoder,
top_n=3 # 精排后只取前3
)
# 创建压缩检索器
self.retriever = ContextualCompressionRetriever(
base_compressor=self.compressor,
base_retriever=base_retriever
)
def get_relevant_documents(self, query: str) -> List[Document]:
"""执行检索+重排"""
return self.retriever.invoke(query)
第四部分:生成链与完整RAG Pipeline
4.1 提示词模板设计
提示词是RAG生成质量的决定性因素。必须明确要求模型"仅基于上下文回答",并在信息不足时承认不知道。
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
# 标准RAG提示模板
RAG_PROMPT_TEMPLATE = """你是一个基于知识库的问答助手。请**仅使用**以下提供的上下文来回答用户问题。
如果上下文中没有足够的信息,请直接说"根据现有文档,我无法回答这个问题",不要编造答案。
上下文:
{context}
用户问题: {question}
回答:"""
def format_docs_for_prompt(docs: List[Document]) -> str:
"""将检索结果格式化为提示词中的上下文"""
return "\n\n".join([
f"[来源: {doc.metadata.get('source', 'unknown')}]\n{doc.page_content}"
for doc in docs
])
4.2 完整的RAG Chain
from langchain_core.runnables import RunnableLambda
def build_rag_chain(retriever, llm):
"""
使用LangChain的LCEL语法构建RAG链
LCEL (LangChain Expression Language) 支持可组合、可流式的chain构建
"""
prompt = ChatPromptTemplate.from_template(RAG_PROMPT_TEMPLATE)
# 构建RAG链: 检索 → 格式化 → 生成
rag_chain = (
{
"context": retriever | RunnableLambda(format_docs_for_prompt),
"question": RunnablePassthrough()
}
| prompt
| llm
| StrOutputParser()
)
return rag_chain
# 使用示例
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1)
retriever = create_hybrid_retriever(chunks, vectorstore)
rag_chain = build_rag_chain(retriever, llm)
question = "公司的年假政策是什么?"
answer = rag_chain.invoke(question)
print(answer)
第五部分:生产级部署与评估
5.1 关键性能指标
| 指标 | 定义 | 目标值 |
|---|---|---|
| 召回率@K | Top-K结果中包含正确答案的比例 | > 85% |
| MRR | 正确答案排名倒数的均值 | > 0.7 |
| 忠实度 | 回答是否基于检索上下文 | > 90% |
| 回答延迟 | 从查询到返回答案的耗时 | < 2秒 |
5.2 缓存策略
高频查询可使用Redis缓存检索结果,避免重复计算。
import hashlib
import redis
class CachedRetriever:
"""带缓存的检索器"""
def __init__(self, retriever, cache_ttl: int = 3600):
self.retriever = retriever
self.redis = redis.Redis(host='localhost', port=6379, db=0)
self.cache_ttl = cache_ttl
def get_relevant_documents(self, query: str) -> List[Document]:
# 生成缓存键
cache_key = hashlib.md5(query.encode()).hexdigest()
# 尝试从缓存读取
cached = self.redis.get(cache_key)
if cached:
# 反序列化并返回
return self._deserialize(cached)
# 执行检索
docs = self.retriever.get_relevant_documents(query)
# 写入缓存
self.redis.setex(cache_key, self.cache_ttl, self._serialize(docs))
return docs
5.3 评估框架示例
def evaluate_rag_system(rag_chain, test_questions: List[Dict[str, str]]):
"""
评估RAG系统效果
test_questions格式: [{"question": "...", "expected_answer": "..."}]
"""
results = []
for item in test_questions:
question = item["question"]
expected = item["expected_answer"]
# 执行查询
generated = rag_chain.invoke(question)
# 使用LLM Judge评估
judge_prompt = f"""请评估以下回答是否准确回答了问题。
问题: {question}
预期答案: {expected}
实际回答: {generated}
请给出评分: 0-10分,并说明原因。"""
results.append({
"question": question,
"generated": generated,
"expected": expected,
"score": ... # 调用LLM Judge打分
})
# 计算平均分
avg_score = sum(r["score"] for r in results) / len(results)
return results, avg_score
总结:RAG优化的层次框架
| 层次 | 优化手段 | 关键组件 |
|---|---|---|
| L1:基础架构 | 文档加载、分块、向量存储、检索生成 | FAISS, LangChain Chain |
| L2:检索增强 | 混合检索、查询改写、结果重排 | BM25, Query Rewriter, Cross-Encoder |
| L3:系统优化 | 缓存、监控、评估 | Redis, Prometheus, LLM Judge |
关键洞察:RAG系统的性能瓶颈通常不在LLM,而在检索质量。正如一位实践者所说:“问题往往不出在LLM身上,而是出在’检索’这一环”。把分块策略、混合检索、重排序这三步做好,就能解决大部分RAG应用的问题。
原创声明:本文为CSDN博主原创文章,基于RAG论文与LangChain工业实践深度总结。如有问题,欢迎评论区交流讨论!
最后更新:2026年6月

392

被折叠的 条评论
为什么被折叠?



