深入LangChain与RAG:从基础管道到生产级优化

深入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_sizechunk_overlap
技术文档/手册递归分割500-100050-100
FAQ/短文本字符分割200-30020-30
长篇小说递归分割1000-1500200
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 关键性能指标

指标定义目标值
召回率@KTop-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月

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI应用开发工程师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值