一、RAG 怎么让AI知道私人数据?检索增强生成
当大语言模型(LLM)面临 “知识覆盖不足”(如小众领域、未训练数据)或 “实时性缺失” 问题时,检索增强生成(Retrieval-Augmented Generation,简称 RAG) 成为关键解决方案。它通过 “连接外部知识库” 让模型基于实时、专属数据生成准确回答,以下从核心问题、实现流程、优劣势对比三方面展开整理:
1、RAG 的核心应用场景:解决 LLM 的固有局限
LLM 的知识依赖训练数据,存在天然短板,RAG 正是为弥补这些短板而生:
- 小众 / 专业领域知识不足:若训练数据中某领域文本覆盖少(如特定行业技术文档、学术研究),LLM 无法生成精准回答。
- 私有数据无法访问:企业内部数据(如员工手册、客户档案)、个人私密文档(如日记、医疗记录)不会纳入公开 LLM 的训练数据,LLM 无法直接回答相关问题。
- 知识时效性缺失:LLM 的训练数据有 “截止日期”(如 GPT-4 截止到 2023 年 10 月),无法回答训练后出现的新信息(如 2024 年新政策、新事件)。
RAG 的核心价值:让 LLM “实时访问外部知识库”,无需重新训练模型,即可基于专属 / 最新数据生成回答,典型应用包括:
- 企业知识库问答(如员工查询内部制度);
- 个人文档问答(如基于 PDF 简历回答职业经历);
- 行业专业工具(如基于医疗文献回答病症问题)。
2、RAG 的实现流程:三步完成 “检索 - 增强 - 生成”
RAG 的完整流程分为 “数据准备”“相似检索”“结合生成” 三大核心步骤,环环相扣确保模型获取有效外部信息:
步骤 1:数据准备(离线阶段)—— 构建可检索的向量数据库
此阶段为后续检索打基础,核心是将 “非结构化外部文档” 转化为 “结构化向量数据”:
- 文档加载与分割:
- 加载外部文档(如 PDF、TXT、Word),由于 LLM 上下文窗口有限(无法处理超长文本),需将文档切分成短文本块(如每块 200-500 字符),避免信息超出窗口或分割过细导致语义断裂。
- 文本向量化(嵌入):
- 通过 “嵌入模型(Embedding Model)” 将每个文本块转化为固定长度的向量(如 1536 维数字串)。
- 关键特性:向量需保留文本的 “语义 / 语法关联”—— 相似文本的向量在 “向量空间” 中的距离更近(如 “猫抓老鼠” 和 “猫咪捕捉老鼠” 向量距离近),无关文本距离远(如 “猫抓老鼠” 和 “行星运行” 向量距离远),为后续相似检索提供数学依据。
- 向量存储:
- 将所有文本块的向量存入 “向量数据库”(如 Pinecone、Chroma、FAISS),向量数据库专门优化 “相似性查询” 效率,可快速找到与目标向量最接近的结果。
步骤 2:相似检索(在线阶段)—— 匹配用户问题与知识库
当用户提出问题时,需从向量数据库中找到最相关的文本块:
- 问题向量化:将用户的问题(如 “这份文档中提到的产品定价策略是什么?”)通过同一嵌入模型转化为向量。
- 相似性查询:向量数据库计算 “问题向量” 与 “所有文本块向量” 的距离,筛选出距离最近的 Top N 个文本块(如 Top 3)—— 这些文本块是知识库中与用户问题最相关的内容。
步骤 3:结合生成(在线阶段)—— LLM 基于检索结果生成回答
将 “用户问题 + 相关文本块” 合并为提示,传给 LLM 生成精准回答:
构建提示:按 “问题 + 上下文” 格式组合内容,示例:
“基于以下上下文回答问题: 【上下文】文档中提到,2024 年产品定价策略为:基础版 99 元 / 月,专业版 299 元 / 月,企业版按用户数计费(10 人以内 1999 元 / 月,每增加 10 人加 1000 元)。 【问题】这份文档中提到的产品定价策略是什么?”
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 的核心价值总结
- 无需训练,快速适配新领域:无需对 LLM 进行微调(Fine-tuning),仅需更新知识库,即可让模型处理新领域问题,降低技术门槛与成本。
- 知识可控且可追溯:回答基于明确的外部文档,可追溯信息来源(如 “回答来自文档第 3 章”),避免 LLM “幻觉”(生成虚假信息)。
- 灵活支持私有 / 实时数据:企业可将内部数据构建为私有知识库,个人可接入私密文档,且能通过更新知识库实现 “知识实时迭代”(如新增 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
可直接读取其内容。
使用步骤
# 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 的核心库):
pip install pypdf
使用步骤
# 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 的库):
pip install wikipedia
使用步骤
# 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、文档加载的通用注意事项
编码问题:加载 TXT 文件时,若遇编码错误(如中文乱码),可在
TextLoader
中指定编码格式,示例:pythonloader = TextLoader("demo.txt", encoding="utf-8") # 常用编码:utf-8、gbk
大文件处理:加载超大文件(如 100MB 以上的 TXT/PDF)时,建议先分割文件或使用 “流式加载”(部分加载器支持
load_incrementally=True
),避免内存溢出;元数据利用:加载后可通过元数据筛选内容(如仅保留 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
库,需先安装:shpip install langchain-text-splitters
导入模块:
pythonfrom langchain_text_splitters import RecursiveCharacterTextSplitter
2. 关键参数解析
创建分割器实例时,需配置 4 个核心参数,直接影响分割效果:
参数名 | 作用 | 示例值 |
---|---|---|
chunk_size | 单个文本块的最大长度(单位:字符,部分场景可设为 Token 数) | 500 |
chunk_overlap | 相邻文本块的重叠长度(用于保持上下文连贯性,避免分割处信息丢失) | 50 |
separators | 分割符列表(按优先级排序,优先用靠前的分割符切分,失败则尝试下一个) | 中文适配列表 |
length_function | 计算文本长度的函数(默认len ,即字符数;可自定义为 Token 计数器) | len |
(1)chunk_size
与chunk_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
为英文场景设计(含空格、英文标点),中文文档需自定义分割符列表,按 “从大到小” 的语义单元排序(优先按段落、再按句子、最后按短句),示例:
# 中文适配的分割符列表(优先级从高到低)
chinese_separators = [
"\n\n", # 段落分隔(优先按段落切分)
"\n", # 换行分隔(段落内按换行切分)
"。", # 中文句号(句子结束)
"!", # 感叹号
"?", # 问号
",", # 逗号(短句分隔,尽量避免,仅在必要时使用)
"、", # 顿号
"" # 空字符串(最后兜底,任意位置切分,避免无法分割)
]
- 逻辑:先尝试用 “段落分隔(\n\n)” 切分,若单个段落仍超
chunk_size
,再用 “换行(\n)”,以此类推,最后用空字符串兜底,确保所有文本都能被分割。
3. 文本分割实战
以 “加载后的中文文档” 为例,完整分割流程如下
#!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
[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'})]
print(texts[0].page_content)
罗浮宫(法语:Musée du Louvre,英语 /ˈluːv(rə)/ ),正式名称为罗浮博物馆,位于法国巴黎市中心的塞纳河边,原是建于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 相关模块:
pip install openai langchain-openai
(2)配置 API 密钥
- 方式 1:将 API 密钥存入环境变量(推荐,避免硬编码):
- Windows:
set OPENAI_API_KEY="你的API密钥"
- macOS/Linux:
export OPENAI_API_KEY="你的API密钥"
- Windows:
- 方式 2:在代码中直接传入密钥(仅用于测试,不推荐生产环境)。
3.2、初始化 OpenAI Embeddings 实例
从langchain-openai
导入OpenAIEmbeddings
,并配置模型参数:
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
(多文本,常用于文本块嵌入)方法实现:
#!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")
# 多文本嵌入(分割后的文本块列表,返回向量列表)
text_chunks = [
"LangChain是一个用于构建LLM应用的框架",
"OpenAI Embeddings可将文本转化为向量",
"RAG流程包括文档加载、分割、嵌入、检索、生成"
]
# 对文本块列表进行嵌入,返回向量列表(每个向量对应一个文本块)
embeded_result = embeddings.embed_documents(text_chunks)
len(embeded_result)
2
embeded_result
[[-0.005549268744966659,
-0.016023970990018652,
-0.01250122560200001,
...]]
# 查看其他结果
print(f"文本块数量:{len(text_chunks)}")
print(f"向量数量:{len(embeded_result)}") # 与文本块数量一致
print(f"单个向量维度:{len(embeded_result[0])}") # 如3072(text-embedding-3-large默认)
# 如果希望嵌入向量维度更小,可以通过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])
1024
# 单文本嵌入(用户问题,返回单个向量)
user_query = "什么是RAG流程?"
query_embedding = embeddings.embed_query(user_query)
print(f"用户问题向量维度:{len(query_embedding)}") # 与文本块向量维度一致(确保可计算相似度)
1024
(2)向量的本质
chunk_embeddings
和query_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
向量存储类,以及前期准备好的 “分割后文本块” 和 “嵌入模型实例”:
# 导入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
方法,可自动完成 “文本块嵌入→向量存储” 的全流程,无需手动处理向量生成:
# 假设前期已完成文本分割,得到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):
# 创建检索器,设置返回Top 3个最相关的文本块
retriever = vector_db.as_retriever(search_kwargs={"k": 3})
search_kwargs={"k": 3}
:控制检索结果数量,k 值越大,返回的相关文本块越多(需平衡 “覆盖度” 与 “精准度”,通常 k=3~5 即可)。
(2)执行相似性检索
检索器实现了 LangChain 的Runnable
接口,可直接调用invoke
方法传入用户问题,返回最相关的文本块列表:
# 用户问题(示例:查询与“财务预算”相关的内容)
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. 完整代码
# -------------------------- 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 的前两步核心流程:
- 数据准备:文档加载→文本分割→文本嵌入→向量存储(存入 FAISS);
- 相似检索:用户问题→问题嵌入→向量数据库相似性搜索→返回相关文本块。
下一步需完成 RAG 的最后一步:结合生成—— 将 “用户问题 + 相关文本块” 合并为提示,传给 LLM 生成基于知识库的精准回答。此外,若需实现 “带记忆的连续对话”,还需将检索器与 “对话记忆”“提示模板” 结合,但 LangChain 提供了更简化的方案(如RetrievalChain
),无需手动拼接流程。
六、Retrieval Chain 开箱即用的检索增强对话链
在完成 “文档加载→分割→嵌入→向量存储→相似检索” 后,核心需求是 “让 AI 结合检索到的外部文档 + 对话记忆生成回答”。LangChain 提供的Conversational Retrieval Chain
(检索增强对话链)已封装好全流程,无需手动拼接 “问题 + 文档 + 记忆”,以下从核心组件、链创建、使用方法、定制化功能四方面整理:
1、Conversational Retrieval Chain 的核心价值
该链是 RAG 与 “对话记忆” 的结合体,解决两大关键问题:
- 检索增强:自动将用户问题对应的 “相关文档片段” 作为上下文传给 AI,避免 AI 依赖固有知识(减少幻觉);
- 对话记忆:维持多轮对话连贯性,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
键,记忆需指定该键以更新对话历史。
示例代码(初始化记忆):
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. 导入核心模块
# 导入链、聊天模型、记忆(前序步骤已导入检索器和向量数据库)
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. 初始化其他组件(复用前序代码)
# -------------------------- 步骤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()
实现,参数需包含 “模型、检索器、记忆” 三大核心组件:
# 创建检索增强对话链
qa_chain = ConversationalRetrievalChain.from_llm(
llm=llm, # 聊天模型
retriever=retriever, # 文档检索器
memory=memory, # 对话记忆
verbose=False # 可选:True则打印链的运行日志,便于调试
)
4、使用链实现 “带记忆的 RAG 对话”
4.1. 核心调用方式:invoke
方法
链的输入是含 “question” 键的字典(question
对应用户当前问题),输出是含 “answer”“chat_history” 等键的字典:
# 第一轮对话:用户提问(关于外部文档的问题)
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
:
# 创建链时开启“返回参考文档”
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(规约)” 两阶段处理,解决 “片段总长度超窗口” 问题:
- Map 阶段:将每个检索片段单独传给 LLM,生成该片段对应的 “局部回答”(1 个片段→1 个局部回答,N 个片段→N 次 LLM 调用);
- 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 个片段 + 用户问题” 生成初始回答;
- 第 2 轮:用 “初始回答 + 第 2 个片段 + 用户问题” 生成优化后的回答;
- 第 N 轮:用 “上一轮回答 + 第 N 个片段 + 用户问题” 生成最终回答(N 个片段→N 次 LLM 调用)。
- 流程:检索片段 → 基于第 1 片段生成初始回答 → 结合第 2 片段优化 → ... → 结合第 N 片段生成最终回答。
优缺点
优点 | 缺点 |
---|---|
1. 回答精度高,LLM 能基于新信息逐次修正,减少遗漏; | 1. 需 N 次 LLM 调用,成本最高、速度最慢; |
2. 能处理超长篇段 / 多片段,且保留上下文关联; | 2. 片段顺序影响最终结果(若关键片段在后,前期回答易偏差); |
3. 适合需要深度理解片段逻辑的场景。 | 3. 无法并行处理,必须按顺序迭代。 |
适用场景
- 片段间有逻辑关联(如文档章节顺序),需逐步深入理解的查询(如 “解析论文的实验方法与结论”);
- 对回答精度要求极高,可接受高成本和慢速度(如专业领域问答、学术解读)。
1.4. Map Rerank(映射重排序法):快速筛选最优
原理
分 “Map(映射)” 和 “Rerank(重排序)” 两阶段,聚焦 “筛选最优片段” 而非 “融合信息”:
- Map 阶段:将每个检索片段单独传给 LLM,要求 LLM 生成 “局部回答” 并对 “该片段与问题的相关性” 打分(如 1-10 分,N 个片段→N 次 LLM 调用);
- 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. 前期准备(复用组件)
# -------------------------- 导入核心模块 --------------------------
# 检索增强对话链:结合检索、记忆和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 参数)
# 创建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 策略
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 策略
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 策略
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、关键注意事项
- 提示模板适配:
map_reduce
/refine
/map_rerank
需 LLM 理解特定指令(如 “生成局部回答”“打分”),LangChain 默认提供基础提示模板,若需定制(如专业领域术语),可通过chain_type_kwargs={"prompt": 自定义提示}
修改。 - 片段数量控制:
map_reduce
/refine
/map_rerank
的 LLM 调用次数与片段数(N)正相关,建议通过retriever
的search_kwargs={"k": 3}
控制 N(通常 3-5 为宜),平衡精度与成本。 - 模型选择:复杂策略(如
refine
)建议用更擅长逻辑推理的模型(如 GPT-4),简单策略(如stuff
)可用 GPT-3.5 降低成本。
通过选择合适的文档传递策略,可在 “精度、速度、成本” 之间找到最优平衡,让 RAG 系统适配不同场景的需求(如快速查询、深度分析、多信息整合)。