RAG
什么是 RAG
RAG(Retrieval-Augmented Generation,检索增强生成)是一种将外部知识检索与大语言模型生成相结合的技术。核心思想是:在 LLM 生成回答之前,先从外部知识库中检索相关信息,然后将检索结果作为上下文提供给模型,从而生成更准确、更有依据的回答。
简单来说,RAG 就是让模型"先查资料,再回答问题"。
为什么需要 RAG
大语言模型存在一些固有的局限性,RAG 可以有效缓解这些问题:
| 问题 | 说明 | RAG 如何解决 |
|---|---|---|
| 知识截止 | 模型训练数据有时间截止点,无法获知最新信息 | 通过检索实时获取最新知识 |
| 幻觉问题 | 模型可能"编造"不存在的事实 | 基于检索到的真实文档生成回答 |
| 领域知识不足 | 通用模型对专业领域知识覆盖有限 | 接入专业领域的知识库 |
| 上下文长度限制 | 无法将所有相关资料一次性输入模型 | 检索最相关的片段,精准投喂 |
| 无法引用来源 | 回答缺乏可追溯性 | 基于检索文档回答,可溯源 |
RAG 核心组件
RAG 系统由以下核心组件构成,每个组件都有对应的 LangChain 实现:
文档加载器(Document Loaders)
文档加载器负责从各种数据源读取数据,并将其转换为统一的 Document 对象。每个 Document 包含两个核心字段:
page_content:文档的文本内容metadata:元数据(来源、页码等)
from langchain_core.documents import Document
# Document 的基本结构
doc = Document(
page_content="这是文档的正文内容",
metadata={"source": "example.txt", "page": 1}
)常用文档加载器
LangChain 提供了丰富的文档加载器,覆盖文件、网页、数据库等多种数据源:
文件类加载器:
| 加载器 | 用途 | 安装包 |
|---|---|---|
TextLoader | 纯文本文件(.txt) | langchain-community |
CSVLoader | CSV 文件 | langchain-community |
PyPDFLoader | PDF 文件 | pypdf |
UnstructuredLoader | 多种格式(PDF/Word/HTML 等) | unstructured |
JSONLoader | JSON 文件 | langchain-community |
DirectoryLoader | 批量加载目录下所有文件 | langchain-community |
MarkdownLoader | Markdown 文件 | unstructured |
网页类加载器:
| 加载器 | 用途 | 安装包 |
|---|---|---|
WebBaseLoader | 网页内容 | beautifulsoup4 |
SitemapLoader | 网站 Sitemap | beautifulsoup4 |
FireCrawlLoader | 动态渲染网页 | firecrawl-py |
其他常用加载器:
| 加载器 | 用途 | 安装包 |
|---|---|---|
NotionDirectoryLoader | Notion 导出 | langchain-community |
GitLoader | Git 仓库文件 | langchain-community |
WikipediaLoader | 维基百科 | langchain-community |
ArxivLoader | arXiv 论文 | langchain-community |
文档加载器使用示例
from langchain_community.document_loaders import TextLoader
loader = TextLoader(
file_path="example.txt",
encoding="utf-8", # 文件编码
autodetect_encoding=False, # 是否自动检测编码
)
docs = loader.load()
print(docs[0].page_content)参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
file_path | str | 必填 | 文件路径 |
encoding | str | "utf-8" | 文件编码,中文文件常用 "utf-8" 或 "gbk" |
autodetect_encoding | bool | False | 是否自动检测文件编码(需要 chardet 包) |
from langchain_community.document_loaders import CSVLoader
loader = CSVLoader(
file_path="./example.csv",
encoding="utf-8", # 文件编码
source_column=None, # 指定哪一列作为文档来源(写入 metadata)
csv_args=None, # 传递给 csv.reader 的参数
)
docs = loader.load()
print(docs[0].page_content)参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
file_path | str | 必填 | CSV 文件路径 |
encoding | str | "utf-8" | 文件编码 |
source_column | str | None | 指定某列作为 metadata["source"],默认使用文件路径 |
csv_args | dict | None | 传递给 csv.reader 的参数,如 {"delimiter": ","} |
metadata_func | Callable | None | 自定义函数,从每行数据中提取元数据 |
from langchain_community.document_loaders import JSONLoader
loader = JSONLoader(
file_path="data.json",
jq_schema=".content", # 使用 jq 语法提取目标字段
text_content=False, # True 时将内容视为纯文本
json_lines=False, # True 时按 JSON Lines 格式解析(每行一个 JSON)
)
docs = loader.load()
print(docs[0].page_content)参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
file_path | str | 必填 | JSON 文件路径 |
jq_schema | str | 必填 | jq 查询表达式,用于提取 JSON 中的目标字段 |
content_key | str | None | 指定哪个字段作为 page_content,默认使用 jq_schema 的结果 |
text_content | bool | True | 是否将提取内容视为纯文本(False 时保留结构化数据) |
json_lines | bool | False | 是否按 JSON Lines 格式解析(每行一个独立的 JSON 对象) |
metadata_func | Callable | None | 自定义函数,从原始 JSON 对象中提取元数据 |
# 方式一:使用 Unstructured(支持 .doc 和 .docx)
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
loader = UnstructuredWordDocumentLoader(
file_path="document.docx",
mode="single", # "single" 整篇为一个 Document | "elements" 按元素拆分
strategy="fast", # "fast" 快速解析 | "hi_res" 高精度(需要额外模型)
)
docs = loader.load()
print(docs[0].page_content)
# 方式二:使用 docx2txt(仅支持 .docx,更轻量)
from langchain_community.document_loaders import Docx2txtLoader
loader = Docx2txtLoader("document.docx")
docs = loader.load()
print(docs[0].page_content)UnstructuredWordDocumentLoader 参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
file_path | str | 必填 | Word 文件路径 |
mode | str | "single" | "single" 整篇合并为一个 Document;"elements" 按段落/标题等元素拆分 |
strategy | str | "fast" | 解析策略:"fast" 快速解析,"hi_res" 高精度(需安装额外模型) |
需要安装:
pip install unstructured python-docx(UnstructuredWordDocumentLoader)或pip install docx2txt(Docx2txtLoader)
from langchain_community.document_loaders import UnstructuredMarkdownLoader
loader = UnstructuredMarkdownLoader(
file_path="README.md",
mode="single", # "single" 整篇为一个 Document | "elements" 按标题/段落拆分
)
docs = loader.load()
print(docs[0].page_content)参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
file_path | str | 必填 | Markdown 文件路径 |
mode | str | "single" | "single" 整篇合并为一个 Document;"elements" 按标题、段落、代码块等元素拆分 |
需要安装:
pip install unstructured
from langchain_community.document_loaders import BSHTMLLoader
loader = BSHTMLLoader(
file_path="page.html",
open_encoding="utf-8", # 文件编码
bs_kwargs={"features": "html.parser"}, # BeautifulSoup 解析器参数
)
docs = loader.load()
print(docs[0].page_content)
# 如果需要更精细的控制,可以使用 WebBaseLoader
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
file_path="page.html",
bs_kwargs={"parse_only": None}, # None 表示解析全部内容
)
docs = loader.load()BSHTMLLoader 参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
file_path | str | 必填 | HTML 文件路径(本地文件) |
open_encoding | str | None | 文件编码,如 "utf-8"、"gbk" |
bs_kwargs | dict | None | 传递给 BeautifulSoup 的参数,如 {"features": "html.parser"} |
需要安装:
pip install beautifulsoup4
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader(
file_path="document.pdf",
password=None, # PDF 密码(加密 PDF 时使用)
extract_images=False, # 是否提取图片中的文本(需要 pytesseract)
)
docs = loader.load() # 每页一个 Document
print(f"共 {len(docs)} 页")
print(docs[0].page_content) # 第一页内容参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
file_path | str | 必填 | PDF 文件路径(也支持 URL) |
password | str | None | PDF 密码,用于加密文件 |
extract_images | bool | False | 是否提取 PDF 中图片的文字(需要 pytesseract) |
需要安装:
pip install pypdf
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
web_paths=["https://example.com/article"], # 支持单个 URL 或 URL 列表
bs_kwargs=None, # BeautifulSoup 参数,用于过滤提取内容
requests_kwargs=None, # requests 参数,如 headers、proxies
)
docs = loader.load()
print(docs[0].page_content)参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
web_paths | list[str] | 必填 | 网页 URL 列表,支持同时加载多个页面 |
bs_kwargs | dict | None | 传递给 BeautifulSoup 的参数,可用 SoupStrainer 只提取部分内容 |
requests_kwargs | dict | None | 传递给 requests.get 的参数,如 {"headers": {...}, "proxies": {...}} |
raise_for_status | bool | False | 请求失败时是否抛出异常 |
需要安装:
pip install beautifulsoup4
批量加载目录文件:
from langchain_community.document_loaders import DirectoryLoader, TextLoader
loader = DirectoryLoader(
path="./docs", # 目录路径
glob="**/*.md", # glob 模式匹配文件
loader_cls=TextLoader, # 指定文件加载器类
loader_kwargs={"encoding": "utf-8"}, # 传递给加载器的参数
show_progress=True, # 显示加载进度条
use_multithreading=True, # 多线程加载
max_concurrency=4, # 最大并发数
silent_errors=True, # 跳过加载失败的文件(不抛异常)
)
docs = loader.load()
print(f"共加载 {len(docs)} 个文件")DirectoryLoader 参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
path | str | 必填 | 目录路径 |
glob | str | "**/[!.]*" | glob 模式匹配文件,如 "**/*.pdf"、"*.txt" |
loader_cls | Loader | TextLoader | 指定文件加载器类,如 TextLoader、PyPDFLoader |
loader_kwargs | dict | {} | 传递给加载器的额外参数 |
show_progress | bool | False | 是否显示进度条(需要 tqdm) |
use_multithreading | bool | False | 是否使用多线程并行加载 |
max_concurrency | int | 10 | 最大并发加载数 |
silent_errors | bool | False | 遇到加载错误时是否跳过(True 时不抛异常) |
exclude | list[str] | [] | 排除的文件模式列表,如 ["*.tmp"] |
手动实现简易加载器:
如果不想安装额外依赖,也可以手动实现:
import pypdf
from langchain_core.documents import Document
def load_pdf_pages(file_path: str) -> list[Document]:
reader = pypdf.PdfReader(file_path)
return [
Document(
page_content=page.extract_text() or "",
metadata={"source": file_path, "page": i},
)
for i, page in enumerate(reader.pages)
]
docs = load_pdf_pages("document.pdf")文本切分器(Text Splitters)
文档加载后,往往需要切分成更小的片段(chunks)。原因:
- 模型上下文窗口有限:过长的文本无法放入上下文
- 检索精度:较小的片段更容易匹配到相关内容
- 避免信息稀释:大段文本中关键信息可能被"淹没"
切分策略对比
| 切分器 | 切分方式 | 适用场景 |
|---|---|---|
RecursiveCharacterTextSplitter | 按分隔符递归切分(段落→句子→字符) | 通用文本(推荐默认选择) |
CharacterTextSplitter | 按单个分隔符切分 | 简单文本 |
TokenTextSplitter | 按 Token 数量切分 | 需要精确控制 Token 时 |
MarkdownTextSplitter | 按 Markdown 标题切分 | Markdown 文档 |
HTMLSectionSplitter | 按 HTML 标签切分 | HTML 文档 |
RecursiveJsonSplitter | 按 JSON 结构切分 | JSON 数据 |
RecursiveCharacterTextSplitter(推荐)
这是 LangChain 最推荐的通用文本切分器。它会按照分隔符的层级递归切分,尽量保持较大的文本单元完整:
- 先尝试按
\n\n(段落)切分 - 如果段落仍然过大,按
\n(行)切分 - 如果仍然过大,按
(空格/句子)切分 - 最后按字符切分
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每个片段的最大字符数
chunk_overlap=200, # 相邻片段的重叠字符数(保持上下文连贯)
length_function=len, # 长度计算函数(默认按字符数)
add_start_index=True, # 在元数据中记录片段在原文中的起始位置
)
# 切分 Document 对象
splits = text_splitter.split_documents(docs)
print(f"切分为 {len(splits)} 个片段")基于 Token 的切分
如果你希望按 Token 数量精确控制(例如匹配模型的 Token 限制),可以使用基于 Token 的切分器:
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 使用 tiktoken 编码器(适配 OpenAI 模型)
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=250,
chunk_overlap=50,
)
splits = text_splitter.split_documents(docs)Markdown 专用切分
from langchain_text_splitters import MarkdownTextSplitter
splitter = MarkdownTextSplitter(
chunk_size=1000,
chunk_overlap=100,
)
splits = splitter.split_documents(markdown_docs)切分参数调优建议
| 参数 | 建议值 | 说明 |
|---|---|---|
chunk_size | 500 - 1500 | 太小丢失上下文,太大降低检索精度 |
chunk_overlap | chunk_size 的 10%-20% | 确保跨片段信息不丢失 |
add_start_index | True | 方便定位片段在原文中的位置 |
Embedding 模型(文本嵌入)
Embedding 模型将文本转换为向量(一组数字),使得语义相近的文本在向量空间中距离更近。这是实现语义检索的基础。
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# 将文本转换为向量
vector = embeddings.embed_query("什么是 RAG?")
print(f"向量维度: {len(vector)}") # 例如 3072常用 Embedding 模型
| 模型 | 提供商 | 维度 | 特点 |
|---|---|---|---|
text-embedding-3-large | OpenAI | 3072 | 高精度,成本较高 |
text-embedding-3-small | OpenAI | 1536 | 性价比高 |
text-embedding-ada-002 | OpenAI | 1536 | 旧版,不推荐新项目 |
BGE-M3 | BAAI | 1024 | 开源,支持多语言 |
nomic-embed-text | Nomic | 768 | 开源,可本地部署 |
向量数据库(Vector Stores)
向量数据库用于存储 Embedding 向量,并提供高效的相似度检索能力。
常用向量数据库
| 数据库 | 类型 | 特点 |
|---|---|---|
FAISS | 内存 | Facebook 开源,高性能,适合本地开发 |
ChromaDB | 内存/持久化 | 轻量级,开箱即用 |
InMemoryVectorStore | 内存 | LangChain 内置,无需额外安装 |
Pinecone | 云服务 | 托管服务,无需运维 |
Weaviate | 自托管/云 | 支持混合检索 |
Milvus | 自托管 | 高性能,适合大规模数据 |
PGVector | PostgreSQL 扩展 | 利用现有 PostgreSQL |
Qdrant | 自托管/云 | Rust 实现,高性能 |
使用示例
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
# 创建向量库并存储文档
embeddings = OpenAIEmbeddings()
vectorstore = InMemoryVectorStore.from_documents(
documents=splits,
embedding=embeddings,
)
# 也可以分步操作
vectorstore = InMemoryVectorStore(embeddings)
vectorstore.add_documents(splits)检索器(Retrievers)
检索器是 RAG 系统中负责根据用户问题查找相关文档的组件。VectorStore 可以直接转换为检索器:
# 将向量库转换为检索器
retriever = vectorstore.as_retriever(
search_type="similarity", # 检索类型
search_kwargs={"k": 4}, # 返回前 4 个最相关的结果
)
# 使用检索器
docs = retriever.invoke("什么是 RAG?")
for doc in docs:
print(doc.page_content[:100], "...")检索类型
| 检索类型 | 说明 |
|---|---|
similarity | 基于向量相似度检索(默认) |
mmr | 最大边际相关性(平衡相关性和多样性) |
similarity_score_threshold | 带相似度阈值的检索 |
# MMR 检索:避免返回过于相似的结果,提高多样性
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 4, "fetch_k": 20, "lambda_mult": 0.5},
)完整 RAG 实战示例
传统 RAG 流程
下面是一个完整的 RAG 系统实现,包含文档加载、切分、存储、检索和生成:
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
import bs4
# 1. 加载文档
loader = WebBaseLoader(
web_paths=["https://example.com/article"],
bs_kwargs={"parse_only": bs4.SoupStrainer(class_=("post-content",))},
)
docs = loader.load()
# 2. 切分文档
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
add_start_index=True,
)
splits = text_splitter.split_documents(docs)
# 3. 创建向量库并存储
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = InMemoryVectorStore.from_documents(
documents=splits,
embedding=embeddings,
)
# 4. 创建检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
# 5. 构建 RAG 链
llm = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_template("""
基于以下上下文回答问题。如果上下文中没有相关信息,请说明你不确定。
上下文:
{context}
问题:{question}
""")
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 6. 提问
answer = rag_chain.invoke("这篇文章的主要观点是什么?")
print(answer)使用 LCEL 组装 RAG 链
LangChain 的 LCEL(LangChain Expression Language)提供了声明式的链式调用语法,用 | 操作符将各组件串联起来,非常直观:
# 上面的 RAG 链可以用更简洁的方式表达
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 流式输出
for chunk in rag_chain.stream("解释一下 RAG 的工作原理"):
print(chunk, end="", flush=True)进阶 RAG 技术
文档预处理
在切分前,可以对文档进行预处理来提升质量:
import bs4
from langchain_community.document_loaders import WebBaseLoader
# 使用 BeautifulSoup 过滤器只提取关键内容
loader = WebBaseLoader(
web_paths=["https://example.com/article"],
bs_kwargs={
"parse_only": bs4.SoupStrainer(
class_=("post-title", "post-content", "post-meta")
)
},
)
docs = loader.load()多查询检索(Multi-Query)
同一个问题可以有不同的表述方式。多查询检索通过 LLM 生成多个查询变体,扩大检索覆盖面:
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
multi_query_prompt = ChatPromptTemplate.from_template(
"你是一个 AI 助手。请为以下问题生成 3 个不同角度的检索查询,每行一个:\n"
"问题:{question}"
)
# 生成多个查询
chain = multi_query_prompt | llm | StrOutputParser()
queries = chain.invoke({"question": "RAG 的优势是什么?"}).strip().split("\n")
# 用所有查询进行检索,合并去重
all_docs = []
seen = set()
for query in queries:
for doc in retriever.invoke(query):
if doc.page_content not in seen:
seen.add(doc.page_content)
all_docs.append(doc)上下文压缩(Contextual Compression)
检索到的文档片段可能包含无关内容。上下文压缩通过 LLM 提取或压缩文档中的关键信息:
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=retriever,
)
# 返回的文档只包含与问题相关的内容
compressed_docs = compression_retriever.invoke("RAG 的核心组件有哪些?")Agentic RAG
传统 RAG 是"检索-生成"的线性流程。Agentic RAG 将 Agent 的推理能力引入 RAG,让模型自主决定何时检索、检索什么、以及检索结果是否足够。
传统 RAG vs Agentic RAG
实现示例
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
# 假设已有 vectorstore
retriever = vectorstore.as_retriever()
@tool
def search_knowledge_base(query: str) -> str:
"""搜索知识库,获取与查询相关的文档内容。当需要查找内部知识时使用此工具。"""
docs = retriever.invoke(query)
return "\n\n".join(doc.page_content for doc in docs)
# 创建带检索工具的 Agent
agent = create_agent(
model="gpt-4o-mini",
tools=[search_knowledge_base],
system_prompt=(
"你是一个专业的问答助手。你可以使用 search_knowledge_base 工具"
"来搜索知识库获取信息。请基于检索到的信息回答问题。"
),
)
# Agent 会自主决定是否需要检索
response = agent.invoke({
"messages": [("human", "RAG 和微调有什么区别?")]
})Agentic RAG 的优势
| 特性 | 传统 RAG | Agentic RAG |
|---|---|---|
| 检索时机 | 固定在生成前 | Agent 自主决定 |
| 查询策略 | 直接使用用户问题 | 可改写、分解查询 |
| 结果判断 | 无 | 判断是否需要再次检索 |
| 多工具调用 | 不支持 | 支持多种检索工具 |
| 复杂问题 | 效果一般 | 可分步推理 |
RAG vs 微调(Fine-tuning)
RAG 和微调是两种让模型掌握特定知识的主要方式,各有优劣:
| 维度 | RAG | 微调 |
|---|---|---|
| 知识更新 | 实时更新,修改知识库即可 | 需要重新训练模型 |
| 成本 | 无需训练,运行时成本较高 | 训练成本高,推理成本低 |
| 可解释性 | 可追溯到具体来源文档 | 难以追溯知识来源 |
| 适用场景 | 知识频繁更新、需要引用来源 | 特定风格/格式的生成 |
| 幻觉控制 | 较好,基于检索文档生成 | 仍有幻觉风险 |
| 上下文长度 | 受模型上下文窗口限制 | 知识内化在模型参数中 |
简单决策:如果知识频繁更新或需要引用来源,选择 RAG;如果需要模型学习特定的输出风格或格式,选择微调。两者也可以结合使用。