Skip to content

一、RAG 怎么让AI知道私人数据?检索增强生成

当大语言模型(LLM)面临 “知识覆盖不足”(如小众领域、未训练数据)或 “实时性缺失” 问题时,检索增强生成(Retrieval-Augmented Generation,简称 RAG) 成为关键解决方案。它通过 “连接外部知识库” 让模型基于实时、专属数据生成准确回答,以下从核心问题、实现流程、优劣势对比三方面展开整理:

1、RAG 的核心应用场景:解决 LLM 的固有局限

LLM 的知识依赖训练数据,存在天然短板,RAG 正是为弥补这些短板而生:

  1. 小众 / 专业领域知识不足:若训练数据中某领域文本覆盖少(如特定行业技术文档、学术研究),LLM 无法生成精准回答。
  2. 私有数据无法访问:企业内部数据(如员工手册、客户档案)、个人私密文档(如日记、医疗记录)不会纳入公开 LLM 的训练数据,LLM 无法直接回答相关问题。
  3. 知识时效性缺失:LLM 的训练数据有 “截止日期”(如 GPT-4 截止到 2023 年 10 月),无法回答训练后出现的新信息(如 2024 年新政策、新事件)。

RAG 的核心价值:让 LLM “实时访问外部知识库”,无需重新训练模型,即可基于专属 / 最新数据生成回答,典型应用包括:

  • 企业知识库问答(如员工查询内部制度);
  • 个人文档问答(如基于 PDF 简历回答职业经历);
  • 行业专业工具(如基于医疗文献回答病症问题)。

2、RAG 的实现流程:三步完成 “检索 - 增强 - 生成”

RAG 的完整流程分为 “数据准备”“相似检索”“结合生成” 三大核心步骤,环环相扣确保模型获取有效外部信息:

步骤 1:数据准备(离线阶段)—— 构建可检索的向量数据库

此阶段为后续检索打基础,核心是将 “非结构化外部文档” 转化为 “结构化向量数据”:

  1. 文档加载与分割:
    • 加载外部文档(如 PDF、TXT、Word),由于 LLM 上下文窗口有限(无法处理超长文本),需将文档切分成短文本块(如每块 200-500 字符),避免信息超出窗口或分割过细导致语义断裂。
  2. 文本向量化(嵌入):
    • 通过 “嵌入模型(Embedding Model)” 将每个文本块转化为固定长度的向量(如 1536 维数字串)。
    • 关键特性:向量需保留文本的 “语义 / 语法关联”—— 相似文本的向量在 “向量空间” 中的距离更近(如 “猫抓老鼠” 和 “猫咪捕捉老鼠” 向量距离近),无关文本距离远(如 “猫抓老鼠” 和 “行星运行” 向量距离远),为后续相似检索提供数学依据。
  3. 向量存储:
    • 将所有文本块的向量存入 “向量数据库”(如 Pinecone、Chroma、FAISS),向量数据库专门优化 “相似性查询” 效率,可快速找到与目标向量最接近的结果。

步骤 2:相似检索(在线阶段)—— 匹配用户问题与知识库

当用户提出问题时,需从向量数据库中找到最相关的文本块:

  1. 问题向量化:将用户的问题(如 “这份文档中提到的产品定价策略是什么?”)通过同一嵌入模型转化为向量。
  2. 相似性查询:向量数据库计算 “问题向量” 与 “所有文本块向量” 的距离,筛选出距离最近的 Top N 个文本块(如 Top 3)—— 这些文本块是知识库中与用户问题最相关的内容。

步骤 3:结合生成(在线阶段)—— LLM 基于检索结果生成回答

将 “用户问题 + 相关文本块” 合并为提示,传给 LLM 生成精准回答:

  1. 构建提示:按 “问题 + 上下文” 格式组合内容,示例:

    “基于以下上下文回答问题: 【上下文】文档中提到,2024 年产品定价策略为:基础版 99 元 / 月,专业版 299 元 / 月,企业版按用户数计费(10 人以内 1999 元 / 月,每增加 10 人加 1000 元)。 【问题】这份文档中提到的产品定价策略是什么?”

  2. LLM 生成回答:LLM 以 “相关文本块” 为依据,避免依赖自身固有知识,生成与知识库内容一致的准确回答。

3、RAG 与 “直接传入全文” 的对比:何时该用 RAG?

随着 LLM 上下文窗口增大(如 GPT-4 Turbo 支持 128k Token),部分场景可 “直接将全文 + 问题传给模型”,但 RAG 在特定场景下仍不可替代,二者对比如下:

维度直接传入全文(无 RAG)检索增强生成(RAG)
适用场景知识库规模小(全文可容纳进 LLM 上下文窗口),对细节准确度要求极高知识库规模大(超窗口上限)、私有 / 实时数据、需控制成本
优点1. 无文本分割导致的信息损失,回答准确度可能更高;2. 实现逻辑简单(无需向量数据库)1. 支持超大规模知识库;2. 响应速度快(仅传相关文本);3. 成本低(少消耗 Token,可用小窗口模型)
缺点1. 响应慢(模型需处理全文);2. 成本高(大窗口模型收费贵 + 多消耗 Token);3. 无法支持超窗口数据1. 文本分割可能丢失跨块语义(需优化分割策略);2. 需额外维护向量数据库,实现复杂度高

4、RAG 的核心价值总结

  1. 无需训练,快速适配新领域:无需对 LLM 进行微调(Fine-tuning),仅需更新知识库,即可让模型处理新领域问题,降低技术门槛与成本。
  2. 知识可控且可追溯:回答基于明确的外部文档,可追溯信息来源(如 “回答来自文档第 3 章”),避免 LLM “幻觉”(生成虚假信息)。
  3. 灵活支持私有 / 实时数据:企业可将内部数据构建为私有知识库,个人可接入私密文档,且能通过更新知识库实现 “知识实时迭代”(如新增 2024 年数据)。

通过 RAG,可轻松构建 “专属领域 AI 工具”(如法律文档问答、医疗文献解读),或实现 “PDF/Word 文档问答” 等实用功能,是 LLM 落地行业场景的关键技术之一。

二、Document Loader 把外部文档加载进来

在 RAG(检索增强生成)流程中,“文档加载器(Document Loader)” 是数据准备的第一步,负责将不同来源、不同格式的内容(如本地文本、PDF、网络资源)统一加载为 LangChain 可处理的Document对象(含文本内容与元数据)。LangChain 社区提供了丰富的加载器,覆盖主流文档格式与资源类型,以下从核心概念、常用加载器实战、扩展类型三方面展开整理:

1、文档加载器的核心概念

1.1. 核心作用

将非结构化 / 结构化数据(如 TXT、PDF、网页内容)转化为标准化的Document对象列表,每个Document包含两个关键属性:

  • page_content:文本内容本身(字符串),是后续分割、嵌入的核心数据;
  • metadata:元数据(字典),记录文本的来源信息(如文档路径、页码、URL、语言等),用于追溯数据来源或筛选内容。

1.2. 设计优势

  • 格式统一:无论原始数据是 TXT、PDF 还是网页,加载后均为Document对象,后续分割、嵌入步骤无需适配不同格式,降低开发成本;
  • 来源广泛:支持本地文件、网络资源、数据库等多种数据源,满足不同场景的知识库构建需求。

2、常用文档加载器实战

2.1. TextLoader:加载纯文本文件(TXT)

纯文本文件(如.txt)无格式干扰,是最基础的数据源,TextLoader可直接读取其内容。

使用步骤

python
# 1. 导入TextLoader
from langchain_community.document_loaders import TextLoader

# 2. 初始化加载器:传入TXT文件路径
loader = TextLoader("demo.txt")  # 替换为你的TXT文件路径

# 3. 执行加载:返回Document对象列表
documents = loader.load()

# 4. 查看加载结果
print(f"加载的Document数量:{len(documents)}")
print(f"\n第一个Document元素的文本内容:\n{documents[0].page_content}")  # 查看第一个Document元素的文本内容
print(f"\n第一个Document的内容:\n{documents[0].page_content[:200]}...")  # 打印前200字符
print(f"\n第一个Document的元数据:\n{documents[0].metadata}")  # 元数据(含文件路径等)

关键说明:

  • 若 TXT 文件较大(如万字以上),load()会返回一个包含 1 个Document的列表(整文件为一个文本块),后续需通过文本分割器切分为更小的块;
  • 元数据默认包含source(文件路径),可用于追溯文本来源。

2.2. PyPDFLoader:加载 PDF 文件

PDF 文件含格式信息(如页码、字体、排版),需依赖PyPDF库解析文本,PyPDFLoader会按页码拆分文本,每一页对应一个Document对象。

安装PyPDF依赖(解析 PDF 的核心库):

sh
pip install pypdf

使用步骤

python
# 1. 导入PyPDFLoader
from langchain_community.document_loaders import PyPDFLoader

# 2. 初始化加载器:传入PDF文件路径
loader = PyPDFLoader("report.pdf")  # 替换为你的PDF文件路径

# 3. 执行加载:返回按页码拆分的Document列表(一页一个Document)
documents = loader.load()

# 4. 查看加载结果
print(f"PDF总页数(Document数量):{len(documents)}")
print(f"\n第1页Document元素的文本内容:\n{documents[0].page_content}")  # 查看第一个Document元素的文本内容
print(f"\n第1页Document的内容:\n{documents[0].page_content[:200]}...")
print(f"\n第1页Document的元数据:\n{documents[0].metadata}")  # 元数据含"page"(页码)、"source"(路径)

关键说明

  • 加载结果中,每个Document对应 PDF 的一页,元数据的page字段标记页码,便于后续定位内容来源;
  • 若 PDF 含扫描图(非文字内容),PyPDFLoader无法提取文本,需先通过 OCR 工具(如 Tesseract)将图片转为文字,再用TextLoader加载。

2.3. WikipediaLoader:加载维基百科词条内容

支持直接从维基百科加载指定词条的内容,无需手动复制粘贴,适合构建含权威信息的知识库。

安装wikipedia依赖(调用维基百科 API 的库):

sh
pip install wikipedia

使用步骤

python
# 1. 导入WikipediaLoader
from langchain_community.document_loaders import WikipediaLoader

# 2. 初始化加载器:配置词条、语言、加载数量
loader = WikipediaLoader(
    query="人工智能",  # 维基百科词条名(中文/英文均可)
    lang="zh",         # 语言:"zh"(中文)、"en"(英文)等
    load_max_docs=2    # 最多加载的相关文档数量(避免内容过多)
)

# 3. 执行加载:返回相关词条的Document列表
documents = loader.load()

# 4. 查看加载结果
print(f"加载的维基百科文档数量:{len(documents)}")
print(f"\n第1个Document元素的文本内容:\n{documents[0].page_content}")  # 查看第一个Document元素的文本内容

for i, doc in enumerate(documents, 1):
    print(f"\n【文档{i}】标题:{doc.metadata['title']}")
    print(f"内容预览:\n{doc.page_content[:300]}...")
    print(f"来源URL:{doc.metadata['source']}")  # 元数据含词条URL

关键参数说明

  • query:必填,维基百科词条名(如 “牛顿第二定律”“ChatGPT”);
  • lang:可选,默认 “en”(英文),需指定 “zh” 获取中文词条;
  • load_max_docs:可选,默认加载所有相关词条,建议设较小值(如 2-5)避免内容冗余。

3、LangChain 支持的其他文档加载器(扩展类型)

除上述三种,LangChain 社区还支持数十种文档加载器,覆盖主流格式与资源,部分常用类型如下:

加载器类型适用场景依赖库 / 注意事项
CSVLoader加载 CSV 表格文件(如 Excel 导出的结构化数据)无需额外依赖,支持指定列提取文本
Docx2txtLoader加载 Word 文档(.docx 格式)需安装docx2txt
UnstructuredPowerPointLoader加载 PPT 文档(.pptx 格式)需安装unstructured库,提取幻灯片文本
WebBaseLoader加载网页内容(通过 URL)需安装beautifulsoup4库,支持解析 HTML
YouTubeLoader加载 YouTube 视频的字幕内容需安装youtube-transcript-api库,支持多语言字幕
GitHubLoader加载 GitHub 仓库中的代码 / 文档需配置 GitHub Token,支持指定仓库 / 文件路径

查看所有加载器

可访问 LangChain 官方文档的Document Loaders 列表,按 “格式”“来源” 筛选所需加载器,每个加载器均有详细使用示例。

4、文档加载的通用注意事项

  1. 编码问题:加载 TXT 文件时,若遇编码错误(如中文乱码),可在TextLoader中指定编码格式,示例:

    python
    loader = TextLoader("demo.txt", encoding="utf-8")  # 常用编码:utf-8、gbk
  2. 大文件处理:加载超大文件(如 100MB 以上的 TXT/PDF)时,建议先分割文件或使用 “流式加载”(部分加载器支持load_incrementally=True),避免内存溢出;

  3. 元数据利用:加载后可通过元数据筛选内容(如仅保留 PDF 的第 1-10 页),示例:

    python
    # 筛选PDF的第1-5页Document
    filtered_docs = [doc for doc in documents if 1 <= doc.metadata["page"] <= 5]

三、Text Splitter 上下文窗口有限?文本切成块

在 RAG(检索增强生成)流程中,“文本分割” 是衔接 “文档加载” 与 “向量嵌入” 的关键步骤 —— 由于大语言模型(LLM)上下文窗口有限,需将超长文档切分为 “语义完整、长度可控” 的文本块,为后续精准检索和嵌入奠定基础。以下从核心原理、工具选择、参数配置及中文适配展开整理:

1、文本分割的核心意义与挑战

1. 核心意义

  • 适配 LLM 窗口限制:若文档长度远超模型上下文窗口(如一本 10 万字的书),无法直接传入模型,需分割为短文本块(如每块 500 字符),确保后续能被模型处理。
  • 保障语义完整性:分割后的文本块需是 “可理解的最小单元”(如完整句子、段落),避免切分在半句话、关键概念中间,导致 AI 无法理解文本含义。

2. 核心挑战

  • 避免语义断裂:若分割符号选择不当(如在 “牛顿第二定律” 中间切分),会破坏文本逻辑,后续检索和生成都会出错。
  • 平衡长度与连贯性:文本块过长可能仍超窗口,过短则丢失上下文关联(如仅切分单个短句,无法体现段落逻辑)。

2、LangChain 核心分割器:RecursiveCharacterTextSplitter(字符递归分割器)

LangChain 中最常用、适配性最强的分割器是RecursiveCharacterTextSplitter(字符递归分割器),其核心逻辑是 “按优先级使用分割符,递归切分直到文本块符合长度要求”,尤其适合处理多格式、多语言文档。

1. 前期准备:安装与导入

  • 安装依赖:该分割器属于langchain-text-splitters库,需先安装:

    sh
    pip install langchain-text-splitters
  • 导入模块

    python
    from langchain_text_splitters import RecursiveCharacterTextSplitter

2. 关键参数解析

创建分割器实例时,需配置 4 个核心参数,直接影响分割效果:

参数名作用示例值
chunk_size单个文本块的最大长度(单位:字符,部分场景可设为 Token 数)500
chunk_overlap相邻文本块的重叠长度(用于保持上下文连贯性,避免分割处信息丢失)50
separators分割符列表(按优先级排序,优先用靠前的分割符切分,失败则尝试下一个)中文适配列表
length_function计算文本长度的函数(默认len,即字符数;可自定义为 Token 计数器)len
(1)chunk_sizechunk_overlap:平衡长度与连贯性
  • chunk_size:需根据模型上下文窗口调整(如 GPT-3.5 支持 4k Token≈3000 字符,可设chunk_size=2000),演示时可设较小值(如 500)方便测试。
  • chunk_overlap:通常设为chunk_size的 10%-20%(如chunk_size=500时设overlap=50),确保相邻块在分割处有重叠(如 “第 1 块结尾 50 字符 = 第 2 块开头 50 字符”),避免关键信息断裂。
(2)separators:适配中文的分割符配置

默认separators为英文场景设计(含空格、英文标点),中文文档需自定义分割符列表,按 “从大到小” 的语义单元排序(优先按段落、再按句子、最后按短句),示例:

python
# 中文适配的分割符列表(优先级从高到低)
chinese_separators = [
    "\n\n",  # 段落分隔(优先按段落切分)
    "\n",    # 换行分隔(段落内按换行切分)
    "。",    # 中文句号(句子结束)
    "!",    # 感叹号
    "?",    # 问号
    ",",    # 逗号(短句分隔,尽量避免,仅在必要时使用)
    "、",    # 顿号
    ""       # 空字符串(最后兜底,任意位置切分,避免无法分割)
]
  • 逻辑:先尝试用 “段落分隔(\n\n)” 切分,若单个段落仍超chunk_size,再用 “换行(\n)”,以此类推,最后用空字符串兜底,确保所有文本都能被分割。

3. 文本分割实战

以 “加载后的中文文档” 为例,完整分割流程如下

python
#!pip install langchain_text_splitters

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 加载一份中文产品文档
loader = TextLoader("./demo.txt")
docs = loader.load()
# 创建中文适配的字符递归分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,  # 单个文本块最大500字符
    chunk_overlap=40,  # 相邻块重叠40字符
    # separators=chinese_separators,  # 中文分割符列表
    # length_function=len      # 按字符数计算长度
    separators=["\n\n", "\n", "。", "!", "?", ",", "、", ""]
)
# 执行分割(输入Documents列表,输出分割后的Documents列表)
texts = text_splitter.split_documents(docs)
texts

# 验证结果(查看分割后的文本块数量与长度)
# # print(f"分割前文档数:{len(docs)}")
# print(f"分割后文本块数:{len(texts)}")
# print(f"第一个文本块长度:{len(texts[0].page_content)}")  # 应≤500
sh
[Document(page_content='罗浮宫(法语:Musée du Louvre,英语 /ˈluːv(rə)/ ),正式名称为罗浮博物馆,位于法国巴黎市中心的塞纳河边,原是建于12世纪末至13世纪初的王宫,现在是一所综合博物馆,亦是世界上最大的艺术博物馆之一,以及参观人数最多的博物馆,是巴黎中心最知名的地标。\n\n罗浮宫的建筑物始建于1190年左右,并在近代曾多次进行扩建,今天所见的模样则一个巨大的翼楼和亭阁建筑群,主要组成部分的总面积则超过60,600平方公尺(652,000平方英尺),馆内永久收藏则包括雕塑、绘画、美术工艺及古代东方、古代埃及和古希腊罗马等7个分类,主要收藏1860年以前的艺术作品与考古文物,罗浮宫博物馆在1793年8月10日开幕起正式对公众开放,平均每天有15,000名游客到此参观,其中65%是外国游客。\n\n位置\n\n罗浮宫与杜乐丽花园的卫星照片\n罗浮宫博物馆位于巴黎市中心的卢浮宫内,位于塞纳河右岸,毗邻杜乐丽花园。最近的两个地铁站是皇家宫-罗浮宫站和卢浮-里沃利站,前者有直达地下购物中心 Carrousel du Louvre 的地下通道。', metadata={'source': './demo.txt'}),
 Document(page_content='在1980年代末和1990年代大改建之前,罗浮宫共有好几个街道入口,目前大部分入口已经永久关闭。自1993年以来,博物馆的正门位置位于拿破仑广场金字塔底下的地下空间,游客可以从金字塔本身、旋转阶梯处连接到博物馆的通道。\n\n博物馆的参观时间随著时代的推移而变化。自18世纪开放以来,只有艺术家和来自外国的观光游客享有特权参观,这项特权后在1850年代才消失。当博物馆从1793年首次开放时,新历法法国共和历规定了“十天周”(法语:décades),其中前六天为艺术家和外国人访问,后三天为将军访问,民众仅能在最后一天参观,后在在1800年代初期在恢复七天周后,民众在每周只有4小时的时间能在罗浮宫参观,周六和周日则是缩减至下午2点至下午4点期间参观。\n\n从1824年开始的一项新规定允许公众在星期日和节假日时参观,然而其他日子只对艺术家和外国游客开放,这种情况到1855年才发生了变化,博物馆更改成除了周一外全天向公众免费开放,直到1922年才开始收费。\n\n当前自1946年开始,罗浮宫除了在周二公休和特殊假日外,通常向游客全面开放参观,内部允许使用照相机和录像机,但禁止使用闪光灯。', metadata={'source': './demo.txt'})]
python
print(texts[0].page_content)
sh
罗浮宫(法语:Musée du Louvre,英语 /ˈluːv()/ ),正式名称为罗浮博物馆,位于法国巴黎市中心的塞纳河边,原是建于12世纪末至13世纪初的王宫,现在是一所综合博物馆,亦是世界上最大的艺术博物馆之一,以及参观人数最多的博物馆,是巴黎中心最知名的地标。

罗浮宫的建筑物始建于1190年左右,并在近代曾多次进行扩建,今天所见的模样则一个巨大的翼楼和亭阁建筑群,主要组成部分的总面积则超过60,600平方公尺(652,000平方英尺),馆内永久收藏则包括雕塑、绘画、美术工艺及古代东方、古代埃及和古希腊罗马等7个分类,主要收藏1860年以前的艺术作品与考古文物,罗浮宫博物馆在1793年8月10日开幕起正式对公众开放,平均每天有15,000名游客到此参观,其中65%是外国游客。

位置

罗浮宫与杜乐丽花园的卫星照片
罗浮宫博物馆位于巴黎市中心的卢浮宫内,位于塞纳河右岸,毗邻杜乐丽花园。最近的两个地铁站是皇家宫-罗浮宫站和卢浮-里沃利站,前者有直达地下购物中心 Carrousel du Louvre 的地下通道。

4. 分割效果验证

  • 长度合规:所有分割后的文本块page_content长度均≤chunk_size,无超窗口风险。
  • 语义完整:文本块以 “段落、句子” 结尾(如 “。”“!”),无半句话、断裂概念(如不会出现 “牛顿第二定” 这样的不完整短语)。

3、文本分割的注意事项

① 根据文档类型调整参数:

  • 长文档(如书籍):chunk_size可设大(如 1000 字符),overlap设 100-200 字符,确保段落逻辑连贯。
  • 短文档(如新闻稿):chunk_size可设小(如 300 字符),overlap设 30-50 字符,避免单块过长。

② 中文与英文分割符区分:

  • 英文文档可用默认separators["\n\n", "\n", ". ", " ", ""]),依赖空格和英文句号;
  • 中文文档必须自定义separators,避免用空格(中文无空格分隔习惯),优先用中文标点和换行。

③ 长度计算方式选择:

  • 若需精准匹配模型 Token 限制(如 GPT-4 的 Token 窗口),可将length_function设为 Token 计数器(如tiktoken.encoding_for_model("gpt-4").encode),确保chunk_size按 Token 数计算,避免字符数与 Token 数差异导致超窗口。

4、后续流程衔接

文本分割完成后,下一步将进入 “向量嵌入” 阶段 —— 通过嵌入模型(如 OpenAI Embeddings、Sentence-BERT)将每个split_documents中的文本块转化为向量,最终存入向量数据库,为后续 RAG 的 “相似检索” 做准备。

四、Text Embedding 文本变数字?神奇的嵌入向量

在 RAG(检索增强生成)流程中,“文本嵌入(Embedding)” 是将 “分割后的文本块” 转化为 “机器可理解的向量” 的核心步骤 —— 通过嵌入模型捕捉文本的语义与语法关系,为后续 “相似性检索” 提供数学基础。以下从核心原理、工具选择、OpenAI Embeddings 实战三方面展开整理:

1、文本嵌入的核心原理与价值

1.1、什么是文本嵌入?

文本嵌入是通过嵌入模型将非结构化文本(如句子、段落)转化为固定长度的数值向量(如 1536 维、3072 维数字串)的过程。

  • 关键特性:向量需保留文本的 “语义关联性”——
    • 相似文本(如 “猫抓老鼠” 和 “猫咪捕捉老鼠”)的向量在 “向量空间” 中的距离更近;
    • 无关文本(如 “猫抓老鼠” 和 “行星运行轨道”)的向量距离更远。
  • 核心价值:将 “文本语义匹配” 转化为 “向量数学计算”(如计算余弦相似度),让向量数据库能快速找到与用户问题最相关的文本块。

1.2、LangChain 的嵌入逻辑

LangChain 本身不直接实现嵌入功能,而是通过集成第三方嵌入模型(如 OpenAI、百度文心一言、Sentence-BERT)提供统一接口,开发者无需关注模型底层细节,只需调用封装好的方法即可完成嵌入。

2、主流嵌入模型与工具选择

常用的嵌入模型分为 “商业模型” 和 “开源模型” 两类,需根据场景选择:

类型代表模型特点适用场景
商业模型OpenAI Embeddings(如 text-embedding-3-large)语义捕捉精准,API 调用便捷,需付费企业级应用、对精度要求高的场景
开源模型Sentence-BERT(如 all-MiniLM-L6-v2)免费,可本地部署,精度略低于商业模型个人项目、预算有限、数据隐私敏感

OpenAI Embeddings为例(最常用的商业嵌入模型),讲解具体实现流程。

3、OpenAI Embeddings 实战步骤

3.1、前期准备:依赖安装与 API 密钥配置

(1)安装依赖

需安装openai库(用于调用 OpenAI API)和 LangChain 相关模块:

sh
pip install openai langchain-openai
(2)配置 API 密钥
  • 方式 1:将 API 密钥存入环境变量(推荐,避免硬编码):
    • Windows:set OPENAI_API_KEY="你的API密钥"
    • macOS/Linux:export OPENAI_API_KEY="你的API密钥"
  • 方式 2:在代码中直接传入密钥(仅用于测试,不推荐生产环境)。

3.2、初始化 OpenAI Embeddings 实例

langchain-openai导入OpenAIEmbeddings,并配置模型参数:

python
from langchain_openai import OpenAIEmbeddings

# 初始化嵌入模型实例
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-large",  # 指定嵌入模型(可替换为text-embedding-3-small等)
    openai_api_key="你的API密钥"      # 若已设环境变量,可省略该参数(自动读取)
)
关键参数说明:
  • model:嵌入模型名称,OpenAI 官方支持的模型包括:
    • text-embedding-3-large:高维度(默认 3072 维),精度高,适合复杂语义匹配;
    • text-embedding-3-small:低维度(默认 1536 维),速度快、成本低,适合简单场景;
    • 旧模型(如text-embedding-ada-002):仍可用,但推荐优先使用 v3 系列。
  • dimensions(可选):自定义向量维度(仅 v3 系列支持),如dimensions=1024—— 可在 “精度” 和 “存储 / 计算成本” 间平衡(维度越小,向量数据库存储压力越小,检索速度越快)。

3.3、文本嵌入核心操作

(1)单文本 / 多文本嵌入

通过embed_query(单文本,常用于用户问题嵌入)或embed_documents(多文本,常用于文本块嵌入)方法实现:

python
#!pip install openai
from langchain_openai import OpenAIEmbeddings

embeddings_model = OpenAIEmbeddings(model="text-embedding-3-large")

# 使用其他API,则需要提供额外参数
# embeddings_model = OpenAIEmbeddings(model="text-embedding-3-large",
#                                     openai_api_key="<你的API密钥>",
#                                     openai_api_base="https://api.aigc369.com/v1")
python
# 多文本嵌入(分割后的文本块列表,返回向量列表)
text_chunks = [
    "LangChain是一个用于构建LLM应用的框架",
    "OpenAI Embeddings可将文本转化为向量",
    "RAG流程包括文档加载、分割、嵌入、检索、生成"
]
# 对文本块列表进行嵌入,返回向量列表(每个向量对应一个文本块)
embeded_result = embeddings.embed_documents(text_chunks)
len(embeded_result)
sh
2
python
embeded_result
sh
[[-0.005549268744966659,
  -0.016023970990018652,
  -0.01250122560200001,
  ...]]
python
# 查看其他结果
print(f"文本块数量:{len(text_chunks)}")
print(f"向量数量:{len(embeded_result)}")  # 与文本块数量一致
print(f"单个向量维度:{len(embeded_result[0])}")  # 如3072(text-embedding-3-large默认)
python
# 如果希望嵌入向量维度更小,可以通过dimensions参数进行指定
embeddings_model = OpenAIEmbeddings(model="text-embedding-3-large", dimensions=1024)
embeded_result = embeddings_model.embed_documents(["Hello world!", "Hey bro"])
len(embeded_result[0])
sh
1024
python
# 单文本嵌入(用户问题,返回单个向量)
user_query = "什么是RAG流程?"
query_embedding = embeddings.embed_query(user_query)
print(f"用户问题向量维度:{len(query_embedding)}")  # 与文本块向量维度一致(确保可计算相似度)
sh
1024
(2)向量的本质

chunk_embeddingsquery_embedding均为浮点数列表,示例(简化): [0.023, -0.012, 0.056, ..., 0.031](共 3072 个浮点数,对应 3072 维向量)。 这些数值无直观含义,但通过数学计算(如余弦相似度)可衡量文本间的语义关联。

4. 嵌入结果的后续用途

文本块的向量(chunk_embeddings)会被存入向量数据库(如 Pinecone、Chroma),用户问题的向量(query_embedding)会用于在向量数据库中 “相似性检索”—— 找到与问题向量距离最近的文本块向量,进而获取对应的原始文本块,为 LLM 生成回答提供上下文。

五、Vector Store 向量数据库,AI模型的海马体

在 RAG(检索增强生成)流程中,“向量数据库” 是衔接 “文本嵌入” 与 “相似性检索” 的核心组件 —— 它专门存储文本块的向量,并通过 “相似性搜索” 快速匹配用户问题与知识库内容,解决传统数据库无法处理非结构化数据语义匹配的痛点。以下从核心原理、工具实战、检索流程三方面展开整理:

1、向量数据库的核心价值:为何不用传统数据库?

传统数据库与向量数据库的核心差异在于 “数据类型适配” 和 “查询机制”,向量数据库的优势集中在非结构化数据的语义匹配

维度传统数据库(如 MySQL、PostgreSQL)向量数据库(如 FAISS、Chroma、Pinecone)
适配数据类型结构化数据(如员工 ID、入职日期,有固定格式和字段定义)非结构化数据的向量(如文本嵌入向量、图像向量,无固定格式)
查询机制精准匹配(如 “员工 ID=002”“工资 > 5000”),依赖关键词或数值比对相似性搜索(如计算向量间余弦相似度),基于语义关联匹配,不依赖精确关键词
典型场景局限无法处理 “语义相似但关键词不同” 的查询(如查 “擅长财务预算”,无法匹配 “预算控制与财务规划经验丰富”)擅长处理语义匹配场景,可快速找到与用户问题 “意思相近” 的文本块

结论:在 RAG 中,需用向量数据库存储文本块向量,实现 “用户问题→语义匹配→相关文本块” 的高效检索,这是传统数据库无法替代的。

2、主流向量数据库与工具选择

常用的向量数据库分为 “开源本地型” 和 “商业云服务型”,本节以FAISS(Facebook 开源,轻量易上手,适合本地测试)为例,讲解具体实现流程:

类型代表数据库特点适用场景
开源本地FAISS、Chroma免费,可本地部署,无需网络,适合小体量知识库个人项目、本地测试、数据隐私敏感场景
商业云服务Pinecone、Weaviate支持大规模数据,高可用,需付费企业级应用、超大规模知识库

3、FAISS 向量数据库实战步骤

3.1. 前期准备:依赖安装与导入

安装 FAISS 依赖

FAISS 提供 CPU 和 GPU 版本,本地测试优先安装 CPU 版本:

pip install faiss-cpu langchain-community

导入核心模块

需导入 LangChain 的FAISS向量存储类,以及前期准备好的 “分割后文本块” 和 “嵌入模型实例”:

python
# 导入FAISS向量数据库
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import TextLoader
# 导入前期准备的组件(文本块+嵌入模型)
from langchain_text_splitters import RecursiveCharacterTextSplitter  # 已分割好的文本块依赖
from langchain_openai import OpenAIEmbeddings  # 已初始化的嵌入模型

3.2. 文本块向量存储:一键生成并存储向量

LangChain 封装了FAISS.from_documents方法,可自动完成 “文本块嵌入→向量存储” 的全流程,无需手动处理向量生成:

python
# 假设前期已完成文本分割,得到split_documents(分割后的Documents列表)
# 假设已初始化嵌入模型embeddings(如OpenAIEmbeddings实例)

# 1. 生成文本块向量并存储到FAISS数据库
vector_db = FAISS.from_documents(
    documents=split_documents,  # 分割后的文本块列表(Documents类型)
    embedding=embeddings        # 嵌入模型实例(用于将文本块转为向量)
)

# (可选)保存FAISS数据库到本地,后续可直接加载(避免重复嵌入)
vector_db.save_local("faiss_local_db")  # 保存到当前目录的faiss_local_db文件夹

# (可选)从本地加载FAISS数据库
# vector_db = FAISS.load_local("faiss_local_db", embeddings)
  • 核心逻辑from_documents方法会遍历split_documents中的每个文本块,通过embeddings模型生成向量,再将 “向量 + 原始文本块” 一起存入 FAISS 数据库。
  • 优势:无需手动调用嵌入模型,LangChain 自动衔接 “嵌入→存储” 流程,简化代码。

3.3. 相似性检索:找到与用户问题最相关的文本块

向量数据库的核心功能是 “相似性检索”,LangChain 通过 “检索器(Retriever)” 封装检索逻辑,步骤如下:

(1)创建检索器

调用向量数据库的as_retriever方法,生成检索器实例,可配置 “返回文本块数量”(默认返回 Top 4):

python
# 创建检索器,设置返回Top 3个最相关的文本块
retriever = vector_db.as_retriever(search_kwargs={"k": 3})
  • search_kwargs={"k": 3}:控制检索结果数量,k 值越大,返回的相关文本块越多(需平衡 “覆盖度” 与 “精准度”,通常 k=3~5 即可)。

(2)执行相似性检索

检索器实现了 LangChain 的Runnable接口,可直接调用invoke方法传入用户问题,返回最相关的文本块列表:

python
# 用户问题(示例:查询与“财务预算”相关的内容)
user_query = "如何制定公司的财务预算?"

# 执行检索,返回Top 3个相关文本块(Documents列表)
relevant_chunks = retriever.invoke(user_query)

# 查看检索结果
print(f"检索到的相关文本块数量:{len(relevant_chunks)}")
for i, chunk in enumerate(relevant_chunks, 1):
    print(f"\n【相关文本块{i}】")
    print(f"内容:{chunk.page_content}")  # 原始文本块内容
    print(f"来源:{chunk.metadata}")      # 文本块元数据(如文档路径、页码,可选)

(3)检索结果说明

  • 结果排序:relevant_chunks按 “相似度从高到低” 排序,第一个文本块与用户问题语义最接近。
  • 语义匹配逻辑:检索器通过计算 “用户问题向量” 与 “数据库中所有文本块向量” 的余弦相似度,筛选出相似度最高的文本块,即使关键词不完全匹配(如用户问 “财务预算”,可匹配 “预算控制与财务规划” 相关文本)。

3.4. 完整代码

python
# -------------------------- 1. 安装依赖(仅首次执行需运行) --------------------------
# 安装FAISS向量数据库的CPU版本(用于本地存储文本向量,轻量易上手)
# !pip install faiss-cpu


# -------------------------- 2. 导入核心模块(对应RAG各环节组件) --------------------------
# 1. 文档加载器:从本地加载TXT纯文本文件
from langchain_community.document_loaders import TextLoader
# 2. 向量数据库:FAISS,用于存储文本块向量并支持相似性检索
from langchain_community.vectorstores import FAISS
# 3. 嵌入模型:OpenAI Embeddings,用于将文本块转化为语义向量
from langchain_openai.embeddings import OpenAIEmbeddings
# 4. 文本分割器:递归字符分割器,将长文本切分为语义完整的短文本块
from langchain_text_splitters import RecursiveCharacterTextSplitter


# -------------------------- 3. 步骤1:加载本地TXT文档(RAG-数据准备) --------------------------
# 初始化TextLoader,传入TXT文件路径(需确保文件在当前代码运行目录下)
# 作用:将TXT文件内容加载为LangChain标准的Document对象(含文本内容和元数据)
loader = TextLoader("./demo2.txt")
# 执行加载操作,返回Document对象列表(1个Document对应整个TXT文件,后续会分割)
docs = loader.load()


# -------------------------- 4. 步骤2:文本分割(RAG-数据处理) --------------------------
# 初始化递归字符分割器,配置分割参数(适配中文文本特性)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,          # 单个文本块的最大长度(单位:字符),避免超LLM上下文窗口
    chunk_overlap=40,        # 相邻文本块的重叠长度(40字符),保持语义连贯性(如避免分割在句子中间)
    separators=["\n\n", "\n", "。", "!", "?", ",", "、", ""]  # 中文适配分割符优先级
    # 分割逻辑:优先按段落(\n\n)→换行(\n)→句子结尾(。!?)→短句分隔(,、)→兜底(任意位置)
)

# 执行分割:将加载的Document列表(整个TXT)切分为多个短文本块Document
# 输出texts为分割后的Document列表,每个元素是一个语义完整的短文本块
texts = text_splitter.split_documents(docs)


# -------------------------- 5. 步骤3:文本嵌入与向量存储(RAG-数据存储) --------------------------
# 初始化OpenAI嵌入模型(需确保环境变量已配置OPENAI_API_KEY,或通过openai_api_key参数传入)
# 作用:将文本块转化为含语义信息的向量(默认text-embedding-3-small模型,1536维向量)
embeddings_model = OpenAIEmbeddings()

# 1. 将分割后的文本块(texts)通过嵌入模型生成向量
# 2. 自动将“向量+原始文本块”存入FAISS向量数据库
# 输出db为FAISS数据库实例,后续可用于相似性检索
db = FAISS.from_documents(texts, embeddings_model)


# -------------------------- 6. 步骤4:创建检索器(RAG-检索准备) --------------------------
# 将FAISS数据库包装为检索器(Retriever),简化相似性检索调用
# 检索器是LangChain的标准Runnable组件,支持invoke方法快速查询
retriever = db.as_retriever()
# 可通过search_kwargs配置检索参数,如search_kwargs={"k":3}(返回Top3相关文本块,默认k=4)


# -------------------------- 7. 步骤5:相似性检索(RAG-检索执行) --------------------------
# 第一次检索:查询“卢浮宫这个名字怎么来的?”
# invoke方法会自动完成:问题→向量转化→FAISS相似性计算→返回TopN相关文本块
retrieved_docs = retriever.invoke("卢浮宫这个名字怎么来的?")
# 打印第一个最相关文本块的内容(page_content为文本块核心内容)
print("【检索结果1:卢浮宫名字由来】")
print(retrieved_docs[0].page_content)
print("-" * 50)  # 分隔线,便于区分不同查询结果

# 第二次检索:查询“卢浮宫在哪年被命名为中央艺术博物馆”
retrieved_docs = retriever.invoke("卢浮宫在哪年被命名为中央艺术博物馆")
# 打印第一个最相关文本块的内容
print("【检索结果2:卢浮宫命名为中央艺术博物馆的年份】")
print(retrieved_docs[0].page_content)

4、当前 RAG 流程进展与后续衔接

截至目前,已完成 RAG 的前两步核心流程:

  1. 数据准备:文档加载→文本分割→文本嵌入→向量存储(存入 FAISS);
  2. 相似检索:用户问题→问题嵌入→向量数据库相似性搜索→返回相关文本块。

下一步需完成 RAG 的最后一步:结合生成—— 将 “用户问题 + 相关文本块” 合并为提示,传给 LLM 生成基于知识库的精准回答。此外,若需实现 “带记忆的连续对话”,还需将检索器与 “对话记忆”“提示模板” 结合,但 LangChain 提供了更简化的方案(如RetrievalChain),无需手动拼接流程。

六、Retrieval Chain 开箱即用的检索增强对话链

在完成 “文档加载→分割→嵌入→向量存储→相似检索” 后,核心需求是 “让 AI 结合检索到的外部文档 + 对话记忆生成回答”。LangChain 提供的Conversational Retrieval Chain(检索增强对话链)已封装好全流程,无需手动拼接 “问题 + 文档 + 记忆”,以下从核心组件、链创建、使用方法、定制化功能四方面整理:

1、Conversational Retrieval Chain 的核心价值

该链是 RAG 与 “对话记忆” 的结合体,解决两大关键问题:

  1. 检索增强:自动将用户问题对应的 “相关文档片段” 作为上下文传给 AI,避免 AI 依赖固有知识(减少幻觉);
  2. 对话记忆:维持多轮对话连贯性,AI 能关联历史对话(如用户追问 “它的具体时间”,AI 知道 “它” 指代上一轮提到的 “卢浮宫命名事件”)。

相比普通ConversationChain(仅带记忆)或RetrievalChain(仅带检索),它同时具备 “检索外部知识” 和 “记忆上下文” 的能力,是实现 “带知识库的连续对话 AI” 的核心工具。

2、链创建前的核心组件准备

需提前准备 3 个关键组件(均为前序步骤已涉及的内容,可直接复用):

组件类型作用实现方式(示例)
聊天模型(LLM)生成回答的核心,需支持对话格式使用ChatOpenAI(如 GPT-3.5/4)
检索器(Retriever)从向量数据库中检索与问题相关的文档片段从 FAISS/Chroma 等向量数据库通过as_retriever()生成
对话记忆(Memory)储存历史对话,维持多轮连贯性使用ConversationBufferMemory等记忆类型,需特殊配置

关键:记忆组件的特殊配置

Conversational Retrieval Chain对记忆的变量名有固定要求,需确保:

  • memory_key="chat_history":链默认通过chat_history键读取 / 更新历史对话,记忆实例的memory_key必须与此一致;
  • return_messages=True:记忆需储存为消息对象列表(而非字符串),确保链能正确解析历史对话;
  • output_key="answer":链默认将 AI 的回答存入answer键,记忆需指定该键以更新对话历史。

示例代码(初始化记忆):

python
from langchain.memory import ConversationBufferMemory

# 初始化符合链要求的记忆实例
memory = ConversationBufferMemory(
    memory_key="chat_history",  # 必须为"chat_history",与链的变量名匹配
    return_messages=True,       # 储存为消息列表
    output_key="answer"         # 链的输出结果中,AI回答对应"answer"键
)

3、创建 Conversational Retrieval Chain

3.1. 导入核心模块

python
# 导入链、聊天模型、记忆(前序步骤已导入检索器和向量数据库)
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

3.2. 初始化其他组件(复用前序代码)

python
# -------------------------- 步骤1:加载文档 --------------------------
# 初始化文本加载器,指定要加载的TXT文件路径
loader = TextLoader("./demo2.txt")
# 执行加载,返回包含文档内容的列表(每个元素是一个Document对象)
docs = loader.load()


# -------------------------- 步骤2:文本分割 --------------------------
# 初始化递归字符分割器,配置中文适配参数
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,          # 单个文本块最大长度(500字符)
    chunk_overlap=40,        # 相邻文本块重叠长度(40字符,保持上下文连贯)
    separators=["\n", "。", "!", "?", ",", "、", ""]  # 中文分割符优先级
)

# 将加载的文档分割为多个短文本块
texts = text_splitter.split_documents(docs)


# -------------------------- 步骤3:创建向量数据库 --------------------------
# 初始化OpenAI嵌入模型(用于将文本转换为向量)
embeddings_model = OpenAIEmbeddings()
# 将分割后的文本块转换为向量并存储到FAISS数据库
db = FAISS.from_documents(texts, embeddings_model)


# -------------------------- 步骤4:创建检索器 --------------------------
# 将向量数据库转换为检索器,用于后续查询相关文本块
retriever = db.as_retriever()


# -------------------------- 步骤5:初始化核心组件 --------------------------
# 初始化聊天模型(使用GPT-3.5-turbo)
model = ChatOpenAI(model="gpt-3.5-turbo")

# 初始化对话记忆(适配ConversationalRetrievalChain的要求)
memory = ConversationBufferMemory(
    return_messages=True,       # 以消息对象列表形式存储记忆
    memory_key='chat_history',  # 记忆在链中的变量名(需与链要求一致)
    output_key='answer'         # 链输出中AI回答的键名(需与链要求一致)
)

3.3. 调用from_llm方法创建链

链的创建通过ConversationalRetrievalChain.from_llm()实现,参数需包含 “模型、检索器、记忆” 三大核心组件:

python
# 创建检索增强对话链
qa_chain = ConversationalRetrievalChain.from_llm(
    llm=llm,                # 聊天模型
    retriever=retriever,    # 文档检索器
    memory=memory,          # 对话记忆
    verbose=False           # 可选:True则打印链的运行日志,便于调试
)

4、使用链实现 “带记忆的 RAG 对话”

4.1. 核心调用方式:invoke方法

链的输入是含 “question” 键的字典question对应用户当前问题),输出是含 “answer”“chat_history” 等键的字典:

python
# 第一轮对话:用户提问(关于外部文档的问题)
user_question1 = "卢浮宫这个名字怎么来的?"
response1 = qa_chain.invoke({"chat_history": memory, "question": user_question1})

# 查看输出结果
print(f"用户问题1:{response1['question']}")
print(f"AI回答1:{response1['answer']}\n")

# 第二轮对话:追问(验证记忆,依赖上一轮上下文)
# user_question2 = "对应的拉丁语是什么呢?"
user_question2 = "它在哪年被命名为中央艺术博物馆?"  # “它”指代卢浮宫
response2 = qa_chain.invoke({"chat_history": memory, "question": user_question2})

print(f"用户问题2:{response2['question']}")
print(f"AI回答2:{response2['answer']}")
print(f"历史对话:{response2['chat_history']}")  # 查看储存的历史对话

4.2. 关键逻辑说明

  • 检索自动触发:调用invoke时,链会先将question传入检索器,获取相关文档片段,再将 “历史对话(chat_history)+ 问题(question)+ 相关文档” 合并为提示传给 LLM;
  • 记忆自动更新:每轮对话后,链会将 “用户问题 + AI 回答” 自动存入memory,无需手动调用save_context
  • 上下文连贯性:第二轮追问中,AI 能识别 “它” 指代 “卢浮宫”,证明记忆生效;同时回答基于检索到的文档片段,证明 RAG 生效。

5、链的定制化功能

5.1. 返回参考文档片段(验证 AI 回答可信度)

默认情况下,链仅返回 AI 的回答,若需验证 “回答是否来自外部文档”(避免幻觉),可在创建链时设置return_source_documents=True

python
# 创建链时开启“返回参考文档”
qa_chain_with_source = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=retriever,
    memory=memory,
    return_source_documents=True  # 开启后,输出会包含"source_documents"键
)

# 调用并查看参考文档
response = qa_chain_with_source.invoke({"chat_history": memory, "question": "卢浮宫名字怎么来的?"})
print(f"AI回答:{response['answer']}")
print("\n【参考文档片段(Top1相关)】")
print(response["source_documents"][0].page_content)  # 打印最相关的文档片段
print(f"文档来源:{response['source_documents'][0].metadata}")  # 打印文档元数据(如路径、页码)
  • 作用:source_documents是检索到的相关文档片段列表(按相似度排序),可直接定位 AI 回答的信息来源,验证回答真实性。

5.2. 潜在问题:文档片段过长导致超窗口

当前默认逻辑是 “将所有检索到的文档片段(如 Top3)全部传给 LLM”,若片段过长或数量过多,可能超过模型上下文窗口限制。解决方案(后续讲解)包括:

  • 限制检索片段数量(如retriever = db.as_retriever(search_kwargs={"k":2}));
  • 对检索到的片段进行二次总结(用create_doc_summary参数);
  • 使用支持大窗口的模型(如 GPT-4 Turbo 128k)。

6、核心流程总结

步骤操作链的作用
1. 组件准备初始化 LLM、Retriever、Memory为链提供 “生成核心”“知识来源”“上下文记忆”
2. 创建链调用from_llm整合组件封装 “检索→拼接提示→生成回答→更新记忆” 全流程
3. 发起对话调用invoke传入用户问题自动触发检索,结合历史对话生成回答,无需手动拼接上下文
4. 验证 / 定制开启return_source_documents查看参考文档,验证回答可信度,避免 AI 幻觉

通过Conversational Retrieval Chain,可快速实现 “带知识库 + 带记忆” 的对话 AI(如企业知识库问答、文档助手),是 LangChain 中 RAG 落地的核心工具之一。

七、Documents Chain 把外部文档塞给模型的不同方式

在 RAG(检索增强生成)中,“文档传递策略” 决定了如何将检索到的相关文本片段传给 LLM 生成回答。除默认的stuff(填充)法外,LangChain 还支持map_reduce(映射规约)、refine(优化)、map_rerank(映射重排序)3 种策略,分别适配 “片段过长 / 过多”“需精准迭代优化”“需快速筛选最优片段” 等场景。以下从原理、优缺点、适用场景、代码实现四方面展开整理:

1、4 种文档传递策略核心原理

所有策略的前提是 “已通过检索器获取 Top N 相关文本片段”,核心差异在于 “片段如何处理并传给 LLM”:

1.1. Stuff(填充法):默认策略,简单直接

原理

所有检索到的文本片段拼接成一个长文本,与用户问题、对话记忆合并为一个提示,一次性传给 LLM 生成回答。

  • 流程:检索片段 → 拼接片段 → 单次 LLM 调用 → 生成回答。
优缺点
优点缺点
1. 仅需 1 次 LLM 调用,速度快、成本低;1. 片段总长度易超 LLM 上下文窗口限制;
2. LLM 能看到所有片段的完整关联,信息无割裂;2. 仅适合片段数量少(如 Top3)、单片段短的场景。
适用场景
  • 知识库片段短(如单片段≤500 字符)、检索结果数量少(如 Top2-3);
  • 对响应速度和成本敏感,且 LLM 上下文窗口足够容纳所有片段(如 GPT-3.5 4k Token)。

1.2. Map Reduce(映射规约法):多片段融合

原理

分 “Map(映射)” 和 “Reduce(规约)” 两阶段处理,解决 “片段总长度超窗口” 问题:

  1. Map 阶段:将每个检索片段单独传给 LLM,生成该片段对应的 “局部回答”(1 个片段→1 个局部回答,N 个片段→N 次 LLM 调用);
  2. Reduce 阶段:将所有 “局部回答” 拼接成 “回答合集”,传给 LLM 生成 “整合所有信息的最终回答”(1 次 LLM 调用)。
  • 流程:检索片段 → 逐个片段生成局部回答(Map) → 合并局部回答 → 生成最终回答(Reduce)。
优缺点
优点缺点
1. 支持超长篇段 / 多片段(单片段不超窗口即可);1. 需 N+1 次 LLM 调用(N 为片段数),成本高、速度慢;
2. 能融合多个片段的信息,适合复杂查询;2. Reduce 阶段可能遗漏局部回答的细节;
3. 可并行处理 Map 阶段,提升效率(LangChain 默认支持)。3. 局部回答间若有冲突,LLM 需手动判断,易出错。
适用场景
  • 检索片段数量多(如 Top5-10)或单片段较长,但每个片段独立包含部分信息;
  • 需整合多来源信息的复杂查询(如 “总结文档中 3 个产品的定价策略”)。

1.3. Refine(优化法):迭代式精准优化

原理

按片段顺序逐次迭代优化回答,让 LLM 基于新片段不断修正已有回答,而非独立处理每个片段:

  1. 第 1 轮:用 “第 1 个片段 + 用户问题” 生成初始回答;
  2. 第 2 轮:用 “初始回答 + 第 2 个片段 + 用户问题” 生成优化后的回答;
  3. 第 N 轮:用 “上一轮回答 + 第 N 个片段 + 用户问题” 生成最终回答(N 个片段→N 次 LLM 调用)。
  • 流程:检索片段 → 基于第 1 片段生成初始回答 → 结合第 2 片段优化 → ... → 结合第 N 片段生成最终回答。
优缺点
优点缺点
1. 回答精度高,LLM 能基于新信息逐次修正,减少遗漏;1. 需 N 次 LLM 调用,成本最高、速度最慢;
2. 能处理超长篇段 / 多片段,且保留上下文关联;2. 片段顺序影响最终结果(若关键片段在后,前期回答易偏差);
3. 适合需要深度理解片段逻辑的场景。3. 无法并行处理,必须按顺序迭代。
适用场景
  • 片段间有逻辑关联(如文档章节顺序),需逐步深入理解的查询(如 “解析论文的实验方法与结论”);
  • 对回答精度要求极高,可接受高成本和慢速度(如专业领域问答、学术解读)。

1.4. Map Rerank(映射重排序法):快速筛选最优

原理

分 “Map(映射)” 和 “Rerank(重排序)” 两阶段,聚焦 “筛选最优片段” 而非 “融合信息”:

  1. Map 阶段:将每个检索片段单独传给 LLM,要求 LLM 生成 “局部回答” 并对 “该片段与问题的相关性” 打分(如 1-10 分,N 个片段→N 次 LLM 调用);
  2. Rerank 阶段:筛选出 “相关性得分最高” 的局部回答,直接作为最终回答(无需额外 LLM 调用)。
  • 流程:检索片段 → 逐个片段生成局部回答 + 打分(Map) → 选择最高分回答作为最终结果(Rerank)。
优缺点
优点缺点
1. 相比 Map Reduce,少 1 次 Reduce 调用,成本略低;1. 不融合多片段信息,仅用最优片段回答,易遗漏其他信息;
2. 速度比 Refine 快,且能解决超窗口问题;2. 依赖 LLM 打分准确性,若打分偏差,结果会出错;
3. 适合只需单个片段即可回答的场景。3. 需在提示中明确打分规则,提示设计较复杂。
适用场景
  • 问题答案仅存在于某一个片段中(如 “文档中提到的卢浮宫命名年份是多少”);
  • 需快速得到答案,且可接受 “不融合多片段信息”(如简单事实查询)。

2、4 种策略对比总结

对比维度Stuff(填充)Map Reduce(映射规约)Refine(优化)Map Rerank(映射重排序)
LLM 调用次数1 次N+1 次(N 为片段数)N 次N 次
响应速度最快中等(可并行 Map)最慢中等(可并行 Map)
成本最低较高最高较高
信息融合能力强(全片段可见)较强(整合局部回答)强(逐次优化)弱(仅用最优片段)
上下文窗口限制易超限制无(单片段不超即可)无(单片段不超即可)无(单片段不超即可)
适用问题类型简单事实查询复杂多信息整合查询深度逻辑理解查询单片段事实查询

3、代码实现:指定 Chain Type

在 LangChain 中,通过ConversationalRetrievalChain创建链时,只需给chain_type参数指定对应策略名称,即可切换文档传递方式。以下是完整代码示例(基于前序 RAG 流程):

3.1. 前期准备(复用组件)

python
# -------------------------- 导入核心模块 --------------------------
# 检索增强对话链:结合检索、记忆和LLM生成回答
from langchain.chains import ConversationalRetrievalChain
# 对话记忆:存储历史对话,支持连续对话
from langchain.memory import ConversationBufferMemory
# 文档加载器:加载本地TXT文本文件
from langchain_community.document_loaders import TextLoader
# 向量数据库:FAISS,用于存储文本向量并支持相似检索
from langchain_community.vectorstores import FAISS
# 聊天模型:OpenAI的对话模型(如GPT-3.5/4)
from langchain_openai import ChatOpenAI
# 嵌入模型:将文本转换为向量的模型
from langchain_openai.embeddings import OpenAIEmbeddings
# 文本分割器:将长文本切分为语义完整的短文本块
from langchain_text_splitters import RecursiveCharacterTextSplitter


# -------------------------- 步骤1:加载文档 --------------------------
# 初始化文本加载器,指定要加载的TXT文件路径
loader = TextLoader("./demo2.txt")
# 执行加载,返回包含文档内容的列表(每个元素是一个Document对象)
docs = loader.load()


# -------------------------- 步骤2:文本分割 --------------------------
# 初始化递归字符分割器,配置中文适配参数
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,          # 单个文本块最大长度(500字符)
    chunk_overlap=40,        # 相邻文本块重叠长度(40字符,保持上下文连贯)
    separators=["\n", "。", "!", "?", ",", "、", ""]  # 中文分割符优先级
)

# 将加载的文档分割为多个短文本块
texts = text_splitter.split_documents(docs)


# -------------------------- 步骤3:创建向量数据库 --------------------------
# 初始化OpenAI嵌入模型(用于将文本转换为向量)
embeddings_model = OpenAIEmbeddings()
# 将分割后的文本块转换为向量并存储到FAISS数据库
db = FAISS.from_documents(texts, embeddings_model)


# -------------------------- 步骤4:创建检索器 --------------------------
# 将向量数据库转换为检索器,用于后续查询相关文本块
retriever = db.as_retriever()


# -------------------------- 步骤5:初始化核心组件 --------------------------
# 初始化聊天模型(使用GPT-3.5-turbo)
model = ChatOpenAI(model="gpt-3.5-turbo")

# 初始化对话记忆(适配ConversationalRetrievalChain的要求)
memory = ConversationBufferMemory(
    return_messages=True,       # 以消息对象列表形式存储记忆
    memory_key='chat_history',  # 记忆在链中的变量名(需与链要求一致)
    output_key='answer'         # 链输出中AI回答的键名(需与链要求一致)
)

3.2. 切换不同 Chain Type

只需修改chain_type参数的值(支持"stuff"map_reduce”“refine”“map_rerank”):

(1)Stuff 策略(默认,可省略 chain_type 参数)
python
# 创建Stuff策略的链
qa_chain_stuff = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=retriever,
    memory=memory,
    chain_type="stuff",  # 默认值,可省略
    return_source_documents=True  # 可选:返回参考片段
)

# 调用链
response = qa_chain_stuff.invoke({"question": "卢浮宫名字怎么来的?"})
print("Stuff策略回答:", response["answer"])
(2)Map Reduce 策略
python
qa_chain_map_reduce = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=retriever,
    memory=memory,
    chain_type="map_reduce",
    return_source_documents=True
)

response = qa_chain_map_reduce.invoke({"question": "总结文档中提到的3个博物馆特点"})
print("Map Reduce策略回答:", response["answer"])
(3)Refine 策略
python
qa_chain_refine = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=retriever,
    memory=memory,
    chain_type="refine",
    return_source_documents=True
)

response = qa_chain_refine.invoke({"question": "解析文档中实验的步骤与结论"})
print("Refine策略回答:", response["answer"])
(4)Map Rerank 策略
python
qa_chain_map_rerank = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=retriever,
    memory=memory,
    chain_type="map_rerank",
    return_source_documents=True,
    # 可选:指定打分规则(需与LLM提示匹配)
    chain_type_kwargs={
        "prompt": "请回答问题,并对该片段与问题的相关性打分(1-10分,格式:回答:xxx;得分:x)"
    }
)

response = qa_chain_map_rerank.invoke({"question": "卢浮宫在哪年被命名为中央艺术博物馆?"})
print("Map Rerank策略回答:", response["answer"])

4、关键注意事项

  1. 提示模板适配map_reduce/refine/map_rerank需 LLM 理解特定指令(如 “生成局部回答”“打分”),LangChain 默认提供基础提示模板,若需定制(如专业领域术语),可通过chain_type_kwargs={"prompt": 自定义提示}修改。
  2. 片段数量控制map_reduce/refine/map_rerank的 LLM 调用次数与片段数(N)正相关,建议通过retrieversearch_kwargs={"k": 3}控制 N(通常 3-5 为宜),平衡精度与成本。
  3. 模型选择:复杂策略(如refine)建议用更擅长逻辑推理的模型(如 GPT-4),简单策略(如stuff)可用 GPT-3.5 降低成本。

通过选择合适的文档传递策略,可在 “精度、速度、成本” 之间找到最优平衡,让 RAG 系统适配不同场景的需求(如快速查询、深度分析、多信息整合)。

Released under the MIT License.