Langchain学习笔记(五):检索增强生成(RAG)基础原理

:本文是Langchain框架的学习笔记;不是教程!不是教程!内容可能有所疏漏,欢迎交流指正。后续将持续更新学习笔记,分享我的学习心得和实践经验。

一. RAG系统的基本原理与架构

检索增强生成(Retrieval-Augmented Generation, RAG)是一种结合了检索系统和生成式AI的混合架构,旨在解决大语言模型(LLM)的知识时效性和幻觉问题。RAG通过从外部知识库检索相关信息,然后将这些信息作为上下文提供给LLM,从而生成更准确、更可靠的回答。

1. RAG的核心优势

  • 知识时效性:LLM的知识在预训练后就固定了,而RAG可以访问最新信息

  • 减少幻觉:通过提供事实依据,降低模型编造信息的可能性

  • 知识专业性:可以接入专业领域文档,增强模型在特定领域的表现

  • 透明可解释:回答可以追溯到具体的信息来源

  • 降低成本:相比模型微调,RAG实现更为经济高效


2. RAG的基本架构

Langchain学习笔记(五):检索增强生成(RAG)基础原理_第1张图片

RAG系统通常由以下几个核心组件构成:

  • 文档处理系统:负责加载、处理和分块文档

  • 向量化模块:将文本转换为向量表示

  • 向量数据库:存储和索引文本向量

  • 检索器:根据查询检索相关文档

  • 生成模块:结合检索结果和用户查询生成回答


3. RAG的工作流程

索引阶段

  • 加载文档并分割成适当大小的块

  • 使用嵌入模型将每个文本块转换为向量

  • 将向量和原文本存储在向量数据库中

查询阶段

  • 将用户问题转换为向量表示

  • 在向量数据库中检索与问题最相似的文本块

  • 将检索到的文本块与原始问题一起构建提示

  • 将完整提示发送给LLM生成最终回答


二. 文档处理流程(加载、分块、向量化)

1. 文档加载

文档加载是RAG系统的第一步,Langchain提供了丰富的文档加载器(Document Loaders)来处理各种格式的文档。

from langchain_community.document_loaders import PyPDFLoader, CSVLoader, TextLoader

# 加载PDF文档
pdf_loader = PyPDFLoader("knowledge_base/document.pdf")
pdf_docs = pdf_loader.load()

# 加载CSV文件
csv_loader = CSVLoader("knowledge_base/data.csv")
csv_docs = csv_loader.load()

# 加载纯文本文件
text_loader = TextLoader("knowledge_base/article.txt")
text_docs = text_loader.load()

# 合并所有文档
all_docs = pdf_docs + csv_docs + text_docs

2. 文档分块

文档分块是将长文档切分成较小的文本片段,以便于向量化和检索。合理的分块策略对RAG系统的性能至关重要。

from langchain_text_splitters import RecursiveCharacterTextSplitter

# 创建文本分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,        			# 每个块的目标大小(字符数)
    chunk_overlap=200,      			# 相邻块之间的重叠部分,有助于保持上下文连贯性
    length_function=len,    			# 计算文本长度的函数
    separators=["\n\n", "\n", " ", ""]  # 分割文本的分隔符优先级
)

# 分割文档
chunks = text_splitter.split_documents(all_docs)
print(f"文档被分割成 {len(chunks)} 个块")

分块策略的关键考量

  • 块大小:太大会导致检索精度下降,太小会丢失上下文

  • 块重叠:适当重叠可以保持上下文连贯性

  • 分隔符选择:基于文档结构选择合适的分隔符

  • 保留元数据:确保每个块都保留来源信息


3. 向量化处理

向量化是将文本转换为数值向量的过程,这些向量捕捉了文本的语义信息

from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import FAISS

# 初始化嵌入模型
embeddings = OllamaEmbeddings()

# 创建向量数据库
vectorstore = FAISS.from_documents(
    documents=chunks,
    embedding=embeddings
)

# 持久化存储
vectorstore.save_local("./faiss_index")



三. 嵌入模型(Embedding Models)的选择与影响

嵌入模型是RAG系统的关键组件,它决定了文本如何被转换为向量表示,进而影响检索的准确性。

常见嵌入模型对比

模型 维度 优势 劣势 适用场景
text-embedding-3-small 1536 高质量、多语言支持 收费、API依赖 通用场景、多语言
bge-large-zh 1024 中文表现优秀、开源 计算资源需求大 中文内容为主的应用
ollama/nomic-embed-text 768 本地部署、无需联网 精度略低于商业模型 本地应用、隐私敏感场景

2. 在Langchain中使用不同嵌入模型

# OpenAI嵌入
from langchain_community.embeddings import OpenAIEmbeddings
openai_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# HuggingFace嵌入(本地)
from langchain_community.embeddings import HuggingFaceEmbeddings
hf_embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh")

# Ollama嵌入
from langchain_ollama import OllamaEmbeddings
ollama_embeddings = OllamaEmbeddings(model="deepseek-r1:1.5b")

3. 嵌入模型的影响因素

  • 语义理解能力:更强的语义理解能力可以提高检索相关性

  • 维度大小:更高维度通常能捕获更丰富的语义信息

  • 领域适应性:某些模型在特定领域(如医疗、法律)表现更佳

  • 多语言支持:处理多语言内容的能力

  • 计算效率:生成嵌入的速度和资源消耗



四. 检索策略与相关性判断

检索策略决定了如何从向量数据库中找到与用户查询最相关的文档。

1. 基本检索方法

# similarity_search: 简单相似度检索, 参数k: 返回前n个最相似的文档
docs = vectorstore.similarity_search(
    query="什么是检索增强生成?",
    k=3
)

# similarity_search_with_score: 带相似度分数的检索
docs_with_scores = vectorstore.similarity_search_with_score(
    query="什么是检索增强生成?",
    k=3
)

# 基于元数据过滤的检索
filtered_docs = vectorstore.similarity_search(
    query="什么是检索增强生成?",
    k=3,
    filter={"source": "textbook", "year": "2023"}
)

2. 高级检索策略

1. 多查询检索

通过LLM生成多个不同角度的查询,然后合并检索结果,提高召回率。

from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_community.chat_models import ChatZhipuAI

# 初始化LLM
llm = ChatZhipuAI(temperature=0)

# 创建多查询检索器
multi_query_retriever = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(),
    llm=llm
)

# 执行检索
unique_docs = multi_query_retriever.get_relevant_documents(
    query="什么是检索增强生成?"
)

2. 混合检索

结合关键词搜索和语义搜索的优势。

from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever

# 创建BM25检索器(关键词搜索)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3

# 创建向量检索器(语义搜索)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# 创建混合检索器
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5]
)

# 执行检索
hybrid_docs = ensemble_retriever.get_relevant_documents(
    query="什么是检索增强生成?"
)

3. 上下文压缩

先检索较多文档,然后使用LLM筛选最相关部分,减少无关信息

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

# 创建文档压缩器
compressor = LLMChainExtractor.from_llm(llm)

# 创建上下文压缩检索器
compression_retriever = ContextualCompressionRetriever(
    base_retriever=vectorstore.as_retriever(search_kwargs={"k": 8}),
    base_compressor=compressor
)

# 执行检索
compressed_docs = compression_retriever.get_relevant_documents(
    query="什么是检索增强生成?"
)

3. 相关性判断方法

  • 余弦相似度:最常用的向量相似度计算方法

  • 欧氏距离:直接计算向量间的距离

  • 点积:简单但有效的相似度计算

  • 杰卡德相似度:适用于关键词匹配

  • 重排序(Reranking):使用更复杂模型对初步检索结果重新排序



五. RAG vs 微调:场景选择

RAG和模型微调是增强LLM能力的两种主要方法,各有优缺点。

1. RAG的优势

  • 实现成本低:无需大规模计算资源

  • 知识更新容易:只需更新知识库,无需重新训练模型

  • 透明可解释:可以追溯信息来源

  • 适应性强:可以根据不同问题动态检索相关知识


2. 微调的优势

  • 知识内化:知识被编码到模型参数中

  • 推理速度快:无需额外检索步骤

  • 上下文窗口限制小:不受检索文档长度限制

  • 特定任务表现优异:可以针对特定任务优化


3. 场景选择指南

场景特点 推荐方法 原因
知识频繁更新 RAG 只需更新知识库
需要引用来源 RAG 可追溯信息来源
领域知识极专业 RAG 可直接接入专业文档
资源有限 RAG 计算需求低
特定格式输出 微调 可训练特定输出格式
推理速度要求高 微调 无需检索步骤
任务高度特定 微调 可针对性优化
需要深度理解 微调+RAG 结合两者优势



六. RAG的常见挑战与解决思路

挑战1:检索相关性不足

解决思路

  • 优化分块策略,确保语义完整性

  • 使用更高质量的嵌入模型

  • 实施多查询检索策略

  • 添加重排序(Reranking)步骤

  • 结合关键词和语义搜索

# 使用重排序器提高相关性
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CohereRerank

# 初始化Cohere重排序器
compressor = CohereRerank(top_n=3)

# 创建重排序检索器
rerank_retriever = ContextualCompressionRetriever(
    base_retriever=vectorstore.as_retriever(search_kwargs={"k": 10}),
    base_compressor=compressor
)

# 执行检索
reranked_docs = rerank_retriever.get_relevant_documents(
    query="什么是检索增强生成?"
)

挑战2:上下文窗口限制

解决思路

  • 实施文档压缩技术

  • 使用摘要生成减少文本长度

  • 采用层次化检索方法

  • 选择具有更大上下文窗口的模型

# 使用LLM生成检索文档的摘要
from langchain.chains.summarize import load_summarize_chain

# 初始化摘要链
summarize_chain = load_summarize_chain(
    llm,
    chain_type="map_reduce"
)

# 对检索文档进行摘要
summary = summarize_chain.run(docs)

# 使用摘要构建最终提示
final_prompt = f"基于以下信息回答问题:\n\n{summary}\n\n问题: 什么是检索增强生成?"

挑战3:幻觉问题

解决思路

  • 指导LLM仅基于检索内容回答

  • 实施事实验证机制

  • 添加不确定性表达

  • 使用结构化输出格式

# 构建防幻觉提示模板
from langchain.prompts import ChatPromptTemplate

template = """你是一个诚实的AI助手。请基于以下检索到的信息回答用户问题。
如果检索的信息不足以回答问题,请直接说"我没有足够的信息来回答这个问题",不要编造答案。

检索到的信息:
{context}

用户问题: {question}

回答:"""

prompt = ChatPromptTemplate.from_template(template)

挑战4:多语言支持

解决思路

  • 使用多语言嵌入模型

  • 实施跨语言检索策略

  • 添加翻译步骤

  • 构建语言特定的知识库

# 使用多语言嵌入模型
from langchain_community.embeddings import HuggingFaceEmbeddings

# 初始化多语言嵌入模型
multilingual_embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/paraphrase-multilingual-mpnet-base-v2"
)

# 创建多语言向量数据库
multilingual_vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=multilingual_embeddings,
    persist_directory="./multilingual_db"
)



七. 实际应用:构建一个简单的RAG原型系统

下面我们将构建一个完整的RAG系统原型,包括文档处理、向量存储、检索和生成等全流程。

步骤1:环境准备

# 安装必要的包
# pip install langchain langchain_community langchain_core pypdf

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate

# 设置ollama服务
OLLAMA_BASE_URL = "http://localhost:11434"
MODEL_NAME = "deepseek-r1:1.5b"

步骤2:文档处理流程

# 加载PDF文档
loader = PyPDFLoader("knowledge_base/ai_textbook.pdf")
documents = loader.load()

# 文本分割
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ".", " ", ""]
)
chunks = text_splitter.split_documents(documents)

# 初始化嵌入模型
embeddings = OllamaEmbeddings(
    base_url=OLLAMA_BASE_URL,
    model=MODEL_NAME
)

# 创建向量数据库
vectorstore = FAISS.from_documents(
    documents=chunks,
    embedding=embeddings,
    distance_strategy="COSINE"  # 指定使用余弦相似度
)

步骤3:构建检索器

# 创建检索器
retriever = vectorstore.as_retriever()

# 测试检索效果
retrieved_docs = retriever.invoke("什么是检索增强生成?")
print(f"检索到 {len(retrieved_docs)} 个文档")

步骤4:构建RAG链

# 初始化聊天模型
chat_model = ChatOllama(
    base_url=OLLAMA_BASE_URL,
    model=MODEL_NAME,
    temperature=0.1  # 降低温度以获得更确定性的回答
)

# 聊天提示模板(ChatPromptTemplate)
chat_prompt = ChatPromptTemplate.from_messages([
    ("system", """你是一个只能回答基于提供文档的问题的助手。
    请遵循以下规则:
        1. 只使用提供的文档中的信息回答问题
        2. 如果文档中没有相关信息,请回答"文档中没有提供这个信息"
        3. 不需要添加自己的知识、观点或分析
        4. 保持回答简洁、准确, 不包含任何额外的信息或过程内容

    以下是参考文档内容:
    {context}"""),
    ("human", "{question}")
])

# 创建 RAG 链
qa_chain = RetrievalQA.from_chain_type(
    llm=chat_model,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True,
    chain_type_kwargs={
        "prompt": chat_prompt
    }
)

步骤5:测试RAG系统

# 测试问题
questions = [
    "什么是检索增强生成?",
    "RAG系统的主要组成部分是什么?",
    "RAG和模型微调有什么区别?"
]

# 执行测试
for question in questions:
    answer = qa_chain.invoke(question)
    print(f"回答: {answer}")

步骤6:评估与改进

# 简单评估函数
def evaluate_rag(question, expected_keywords):
    answer = qa_chain.invoke(question)
    score = sum([1 for keyword in expected_keywords if keyword.lower() in answer.lower()])
    return {
        "question": question,
        "answer": answer,
        "score": score,
        "max_score": len(expected_keywords)
    }

# 评估测试集
eval_questions = [
    {
        "question": "RAG系统如何减少幻觉问题?",
        "expected_keywords": ["检索", "外部知识", "事实依据", "验证"]
    },
    {
        "question": "什么场景适合使用RAG而非微调?",
        "expected_keywords": ["知识更新", "透明度", "资源限制", "专业领域"]
    }
]

# 执行评估
results = [evaluate_rag(**q) for q in eval_questions]

# 输出评估结果
for result in results:
    print(f"\n问题: {result['question']}")
    print(f"得分: {result['score']}/{result['max_score']}")
    print("-" * 50)
    print(f"回答: {result['answer']}")
    print("=" * 80)

你可能感兴趣的:(LangChain,langchain,学习,笔记)