LangChain 第二篇:RAG从文档加载到检索增强生成

RAG(Retrieval-Augmented Generation,检索增强生成)是当前大模型应用中最常见、也最容易落地的一种架构。

简单来说:

在模型回答问题之前,先从你的知识库中找出相关内容,再把这些内容一并交给模型生成答案。

这一篇将基于一个最小可用、但工程上完整的 RAG 流程,从零梳理:

  • 文档是如何被加载为 LangChain 能理解的 Document
  • 文档是如何被切分
  • 向量是如何存储与检索的
  • 检索结果如何被压缩 / 重排序
  • 最终如何接入到 LLM 调用中

1. RAG 检索整体流程

一个最基本的 RAG 系统,至少包含以下 5 个步骤:

js 复制代码
原始文档
↓ 加载
Document
↓ 切分
Chunks
↓ 向量化
VectorStore
↓ 检索
Relevant Docs
↓ 送给 LLM
Answer

我们可以使用LangChain,将上面的步骤拆成一组可组合、可替换的组件,下面我们按这个顺序一步一步来。

2. 文档加载器(Document Loader)

文档加载器作用:

把各种格式的文件,统一转成 Document 对象。

Document 是什么

LangChain 中所有 RAG 的基础数据结构都是:

python 复制代码
from langchain_core.documents import Document


Document(
    page_content="正文内容",
    metadata={"page": 1, "source": "xxx.pdf"}
)
  • page_content:参与向量化和生成的文本
  • metadata:不参与 embedding,但可用于过滤、追溯来源

后面的切分、存储、检索,本质上都是在操作 List[Document]

常见文档加载器

PDF:PyPDFLoader

python 复制代码
from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("example.pdf")

# 按页加载
docs = loader.load()

Word:Docx2txtLoader

python 复制代码
from langchain_community.document_loaders import Docx2txtLoader

loader = Docx2txtLoader("example.docx")
docs = loader.load()

Markdown:UnstructuredMarkdownLoader

python 复制代码
from langchain_community.document_loaders import UnstructuredMarkdownLoader

loader = UnstructuredMarkdownLoader("README.md")
docs = loader.load()

TXT / 日志类文本:TextLoader

python 复制代码
from langchain_community.document_loaders import TextLoader


loader = TextLoader("data.txt", encoding="utf-8")
docs = loader.load()

3. 文本切分器(TextSplitter)

大模型并不适合直接吃整篇文档:

  • 上下文长度有限
  • 向量召回精度会下降

切分策略,直接决定 RAG 的上限。

CharacterTextSplitter

基于固定字符长度切分,简单、可控:

python 复制代码
from langchain.text_splitter import CharacterTextSplitter


text_splitter = CharacterTextSplitter(
    separator="\n\n", # 使用两个换行符作为分隔符
    chunk_size=1000, # 每个文本块的最大字符数
    chunk_overlap=200 # 相邻块之间的重叠字符数
)

split_docs = text_splitter.split_documents(docs)

适合结构简单、段落清晰的文本。

RecursiveCharacterTextSplitter (推荐)

按设定的分隔符列表递归尝试切分文本,直到每个块的大小不超过设定的 chunk_size,并保留一定的 chunk_overlap 以减少上下文丢失。这种方式能最大限度保持段落、句子、单词的完整性,非常适合长文本处理、向量化检索和 RAG 场景。

python 复制代码
from langchain.text_splitter import RecursiveCharacterTextSplitter


text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500, # 每个文本块的最大字符数
    chunk_overlap=50, # 相邻块之间的重叠字符数
    separators=["\n\n", "\n", "。", " ", ""], # 递归尝试切分文本的标识
    keep_separator=True # 支持保持分隔符
)


split_docs = text_splitter.split_documents(docs)

这是 RAG 中最常用的切分方式。

其他切分方式

  • TokenTextSplitter:按 token 数切(对齐模型上下文)
  • MarkdownHeaderTextSplitter:按 Markdown 标题层级切
  • HTMLHeaderTextSplitter:按 HTML 结构切

4. 文本向量化(Embedding)

在前面的步骤中,我们已经拿到了List[Document]

但向量数据库并不能直接存文本,它真正存的是使用向量模型向量化后的高维浮点数组。 这个过程,就叫 Embedding(向量化)。

Embedding 的作用:

把"文本的语义",映射成一个可以计算距离的向量。

之后所有的检索,都是在:

"问题的向量,和哪一段文档的向量最接近?"

使用 HuggingFace Embedding 运行本地向量模型

bge-base-zh 为例(中文效果很好):

python 复制代码
from langchain_community.embeddings import HuggingFaceEmbeddings

def get_embeddings():
    return HuggingFaceEmbeddings(
        model_name="BAAI/bge-large-zh-v1.5",
        model_kwargs={"device": "cuda"},  # 或 cpu
        encode_kwargs={"normalize_embeddings": True} # 配合 cosine 相似度效果更稳定
    )

在使用 cosine 相似度时,建议开启 normalize_embeddings=True,可以让不同长度文本的向量分布更稳定。
首次加载需要在HuggingFace中下载模型,如果网络问题下载不下来可以使用阿里魔塔等镜像网站下载到本地,然后将BAAI/ 改成本地路径

通义千问 DashScope Embedding (云端模型)

python 复制代码
from langchain_community.embeddings import DashScopeEmbeddings

def get_embeddings():
    return DashScopeEmbeddings(
        model="text-embedding-v3",
        dashscope_api_key="sk-xxx"
    )

5 向量数据库:PGVector

这里使用 PostgreSQL + pgvector 作为示例。

你可能注意到了一个细节:我们从来没有手动调用 embedding.embed(),这是因为: LangChainembedding 的调用,隐藏在 VectorStore 内部了

python 复制代码
from langchain_postgres import PGVector

vectorstore = PGVector(
    embeddings=get_embeddings(), # 使用定义好的嵌入模型进行向量检索和入库
    collection_name="knowledge_base",
    connection="postgresql+psycopg://user:password@host:5432/db"
)

# 写入向量库
vectorstore.add_documents(split_docs)

⚠️ 注意:向量入库和向量检索必须使用完全相同的 Embedding 模型和参数,否则即使文档存在,也几乎无法被检索到。

6 检索器(Retriever)

python 复制代码
retriever = vectorstore.as_retriever(
    search_type="similarity", #按照分值,取最高的
    search_kwargs={
        "k": 20, #检索前20个相似的
        "filter": {"tag": "主角"} #ai或人工标注的标签
    }
)


results = retriever.invoke("孙悟空是谁")

常见的检索方式还有:

  • search_type="mmr":在相似度和多样性之间做平衡
  • search_type="similarity_score_threshold":按阈值过滤

常见filter写法

python 复制代码
# 等于
{"filter": {"uuid": "123"}}


# 或
{"filter": {"$or": [{"type": "1"}, {"type": "2"}]}}


# 范围
{"filter": {"year": {"$gte": 2020, "$lte": 2024}}}

7 重排序与上下文压缩

在 RAG 中,最常见的一步是 向量相似度检索: 把问题和文档都映射成向量,然后找"最相似"的文本片段。

但在真实工程中,你很快就会发现一个问题: 向量召回到的,是"语义相似"的内容,但不一定是"最相关、最可用"的内容。

向量检索(Embedding Search)的本质是:

  • 把文本映射到高维空间
  • 用余弦距离 / 内积衡量"语义接近程度"

这意味着它擅长的是:

  • 同义表达
  • 语义模糊匹配
  • 大概方向相近的内容

但它 并不理解任务目标,比如:哪一段更适合回答"定义型问题" 哪一段是结论,哪一段只是背景,哪一段虽然语义相似,但其实是反例 / 否定 / 无关描述

举个例子:

问题:"孙悟空是谁?"

向量检索可能会召回:

  • 《西游记》的背景介绍
  • 关于"齐天大圣"称号的来历
  • 描述花果山的段落
  • 一段提到孙悟空名字但在讲别的角色的内容

这些内容都"像"孙悟空,但真正适合直接喂给 LLM 的,可能只有其中 1~2 段。如果全部召回,并喂给大模型又会因为噪音过大,幻觉概率变高

所以我们需要重排序(Rerank)拿到真正需要的内容

它是「问题 × 文档」成对判断,而不是各自向量化后比距离。

Rerank 是精匹配 ,不是近似匹配,以 CrossEncoder / DashScopeRerank 为代表的 rerank 模型。他会把问题 + 文档片段拼在一起,直接判断:这段内容,对回答这个问题有没有帮助

这意味着它能识别:

  • 这段是不是在"回答问题",还是只是"提到了关键词"
  • 是定义、结论,还是铺垫背景
  • 是否存在否定、反转、条件限制

所以一个成熟、稳定的检索流程通常是 使用向量召回快速找"可能相关的候选集"然后使用重排序从候选集中找top(N) "最有用的内容"

HuggingFace CrossEncoder

使用HuggingFace 本地模型进行重排序,这里我们使用 bge-reranker-base

python 复制代码
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.document_compressors.cross_encoder_rerank import CrossEncoderReranker

reranker = CrossEncoderReranker(
    model=HuggingFaceCrossEncoder("bge-reranker-base"),
    top_n=5
)

通义千问 Rerank

也可以使用云端服务提供的重排序,比如阿里的gte-rerank-v2

python 复制代码
from langchain_community.document_compressors.dashscope_rerank import DashScopeRerank

reranker = DashScopeRerank(
    model="gte-rerank-v2",
    dashscope_api_key="sk-xxx",
    top_n=5
)

ContextualCompressionRetriever

把 召回 + 重排序 封装成一个整体:

python 复制代码
from langchain.retrievers import ContextualCompressionRetriever
from langchain_core.runnables import RunnableLambda


compression_retriever = ContextualCompressionRetriever(
    base_retriever=retriever,
    base_compressor=reranker
)


results = compression_retriever.invoke("孙悟空是谁")

8 接入 LLM,完成 RAG

python 复制代码
chain = compression_retriever | prompt | llm | StrOutputParser()

chain.invoke("孙悟空是谁")

9. 小结

这一篇从真实工程路径出发,梳理了一个完整的 LangChain RAG 流程:

  • 文档加载(多格式)
  • 文本切分(多策略)
  • 向量存储与检索
  • 重排序与上下文压缩
  • 最终接入 LLM 生成答案

理解这些之后,每一步都可以独立替换和优化

后续无论是:

  • 多知识库 RAG
  • Agent + RAG

本质上,都只是这些 Runnable 的不同组合形式。

相关推荐
淡酒交魂6 小时前
「LangChain」ChatPromptTemplate学习笔记
机器学习·langchain
淡酒交魂7 小时前
「LangChain学习」ChatPromptTemplate学习笔记
机器学习·langchain
Biehmltym7 小时前
【AI】04AI Aent:十分钟跑通LangGraph项目:调用llm+agent开发+langSmith使用
java·人工智能·langchain·langgraph
大模型真好玩8 小时前
LangGraph智能体开发设计模式(二)——协调器-工作者模式、评估器-优化器模式
人工智能·langchain·agent
kimi-22220 小时前
LangChain 将数据加载到 Chroma 向量数据库
数据库·langchain
学Linux的语莫1 天前
Milvus向量数据库的操作(基于Langchain)
数据库·langchain·milvus
社恐的下水道蟑螂1 天前
LangChain:AI 应用开发框架的深度解析与实践指南
前端·langchain·ai编程
白兰地空瓶1 天前
别再把大模型只当聊天机器人:LangChain Tool 才是 AI 应用的分水岭
langchain
香蕉君1 天前
第一品——LangChain核心基础
人工智能·langchain