RAG检索增强
影响回答质量的因素:
- 文档是否被正确加载
- 文本是否被合理切块
- Embedding 是否稳定
- 检索是否召回了真正相关的片段
- Prompt 是否把上下文用对了
| 方案 | 本质 | 优势 | 局限 |
| 直接问模型 | 不接外部知识,直接生成 | 上手最快 | 容易不知道私有 / 最新知识,幻觉较多 |
| RAG | 先检索资料,再生成 | 更新快、改动小、可追溯 | 依赖文档质量、分块策略、检索效果 |
| 微调 | 调整模型参数 | 能改变回答风格、任务习惯 | 成本高、更新慢,不适合高频改知识 |
| RAG + 微调 | 外部知识 + 参数适配 | 兼顾知识与表达 | 成本和系统复杂度更高 |
缺点:
- 响应时延更高:每次问答前都要多做一次检索,有时还会经过过滤、重排等步骤。
- Token 消耗更高:检索结果会进入 Prompt,召回内容越多,送给模型的上下文越长。
- 效果依赖链路质量:文档质量、切块策略、Embedding 质量、检索效果、Prompt 约束方式,都会影响最终回答。
管道式RAG
先检索,再生成。适合:企业知识库问答、文档问答、规范 / 手册 / FAQ 检索增强
Agent式RAG
模型自己决定要不要检索、什么时候检索、检索几次
LangChain组件
| 组件 | 作用 | 常见类 / 说明 |
| Document | LangChain 中统一的文档对象 | 由 page_content + metadata 组成 |
| 文档加载器 | 从 TXT、PDF、Word、Markdown、JSON、CSV 等读入文档 | TextLoader、PyPDFLoader、UnstructuredWordDocumentLoader 等 |
| 文本分割器 | 把长文档切成较小片段 | RecursiveCharacterTextSplitter 最常见 |
| 嵌入模型 | 把文本片段转成向量 | 本项目常见 DashScopeEmbeddings |
| 向量数据库 | 存储向量并支持相似检索 | Redis / RedisStack、Chroma、FAISS 等 |
| 检索器 | 用户提问时从向量库召回相关片段 | 常见由 vector_store.as_retriever() 得到 |
| 提示词模板 | 把检索结果和用户问题组织成 Prompt | PromptTemplate、ChatPromptTemplate |
| 聊天模型 | 基于上下文生成最终答案 | ChatOpenAI、init_chat_model() 等 |
两种常见入库方式
| 方法 | 适合场景 | 手里通常有什么数据 | 常见理解 |
from_documents |
已经完成“加载 + 分割”之后的一步入库 | Document 列表 |
更像“把文档片段整批建库” |
add_texts |
已经有向量库实例,需要持续追加内容 | 字符串列表 + 可选 metadata | 更像“往现有索引里追加文本” |
- 文档流驱动:
Loader -> Splitter -> List[Document] -> from_documents(...)(典型 RAG 建索引) - 纯文本流驱动:
texts + metadata -> add_texts(...)(接口落库、增量追加、脱离 Loader 的实验)
相似性检索
from langchain_redis import RedisConfig, RedisVectorStore
from langchain_community.embeddings import DashScopeEmbeddings
import os
from dotenv import load_dotenv
load_dotenv()
# 1. 嵌入模型
embeddingsModel = DashScopeEmbeddings(
model="text-embedding-v3",
dashscope_api_key=os.getenv("aliQwen-api")
)
# 2. 连接已有索引
vector_store = redisVectorStore(
embeddingsModel,
config=RedisConfig(index_name="newsgroups",redis_url="redis://localhost:26379")
)
# 3. 查询文本 → 向量化 → 在库中做相似度检索;这里取前 3 条结果
query = "我喜欢什么手机"
results = vector_store.similarity_search_with_score(query, k = 3)
print("=== 查询结果 ===")
for i,(doc,score) in enumerate(results, 1):
# 这里把“距离”近似换算成“相似度”只是为了展示更直观;工程里请以具体返回定义为准
similarity = 1 - score
print(f"结果 {i}:")
print(f"内容: {doc.page_content}")
print(f"元数据: {doc.metadata}")
print(f"相似度: {similarity:.4f}")
文档加载器
load():一次性加载全部文档lazy_load():按需流式加载,适合大文件或大批量数据
为什么加载后要统一成 Document
Document 是 LangChain 在 RAG 里的基础数据结构,它通常有两个核心字段:
page_content:正文内容metadata:来源、页码、文件名、分类等附加信息
如何选择加载器
- TXT 用
TextLoader - PDF 用
PyPDFLoader - Word 用
UnstructuredWordDocumentLoader或Docx2txtLoader - Markdown 用
UnstructuredMarkdownLoader - JSON 用
JSONLoader - CSV 用
CSVLoader
加载各种文件
Text
# TEXT
from langchain_community.document_loaders import TextLoader
file_path = "assets/sample.txt"
encoding = "utf-8"
# load() 为 BaseLoader 统一接口,返回 List[Document]'
docs = TextLoader(file_path,encoding).load()
print(docs)
#[Document(metadata={'source': 'assets/sample.txt'}, page_content='LangChain 是一个用于构建基于大语言模型(LLM)应用的开发框架,旨在帮助开发者更高效地集成、管理和增强大语言模型的能力,....')]
# pip install langchain_community pypdf
from langchain_community.document_loaders import PyPDFLoader
docs = PyPDFLoader(
file_path="assets/sample.pdf",
extraction_mode="plain", # plain 纯文本;layout 按版面
).load()
print(docs)
word
# pip install langchain_community unstructured[docx] python-docx
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
docs = UnstructuredWordDocumentLoader(
file_path="assets/alibaba-more.docx",
mode="single", # single 整篇一个 Document;elements 按元素切分
).load()
print(docs)
markDown
# pip install langchain_community unstructured[md]
from langchain_community.document_loaders import UnstructuredMarkdownLoader
docs = UnstructuredMarkdownLoader(
file_path="assets/sample.md",
mode="elements", # single 整篇;elements 按元素切分
).load()
print(docs)
JSON
# pip install jq langchain_community
from langchain_community.document_loaders import JSONLoader
docs = JSONLoader(
file_path="assets/sample.json",
jq_schema=".", # 提取所有字段
text_content=False, # 是否按字符串处理内容
).load()
print(docs)
cvs
# pip install langchain_community
from langchain_community.document_loaders.csv_loader import CSVLoader
# 方式一:不指定列 → 整行(所有列)拼成一条字符串作为 page_content,metadata 通常只有 source 等
docs_all = CSVLoader(file_path="assets/sample.csv").load()
print("=== 方式一:整行作为 page_content ===")
print(
"page_content 示例:",
(
docs_all[0].page_content[:80] + "..."
if len(docs_all[0].page_content) > 80
else docs_all[0].page_content
),
)
print("metadata 示例:", docs_all[0].metadata, "\n")
# 方式二:指定 content_columns 与 metadata_columns → 正文只取 content 列,title/author 进 metadata,便于检索时按作者/标题过滤
docs_split = CSVLoader(
file_path="assets/sample.csv",
metadata_columns=["title", "author"],
content_columns=["content"],
).load()
print("=== 方式二:content 列作为正文,title/author 进 metadata ===")
print("page_content 示例:", docs_split[0].page_content)
print("metadata 示例:", docs_split[0].metadata)
文本分割器
文档加载之后,通常还不能直接拿去建 RAG。原因很简单:原始文档经常太长
会带来两个现实问题:
- 检索效果差:整篇文档太大,向量表达会过于粗糙,难以精确定位到真正相关的小段内容。
- 生成成本高:就算检索回来整篇文档,也很可能塞不进模型上下文,或者把大量无关内容一并送给模型。
所以,RAG 中几乎都会有“切块(chunking)”这一步。
LangChain 官方也明确建议:面对通用文本时,RecursiveCharacterTextSplitter 往往是最推荐的入门分割器。它会尽量优先保留较大的语义单位,例如段落、句子;如果某一段还太长,再继续往更小层级切。
为什么一定要切块
可以把切块理解成“把大文档拆成多个更容易被检索的小段”。这样做有几个直接好处:
- 更容易召回真正相关的片段
- 更容易控制每次送给模型的上下文长度
- 更容易降低无关内容干扰
- 更适合做来源标注和精确引用
在真实项目里,切块策略会直接影响 RAG 的最终效果。很多时候不是模型不行,而是:
- 块切得太大,相关信息被淹没
- 块切得太小,语义被切碎
- 没有重叠,导致一句话被拦腰截断
所以,分割策略本身就是 RAG 质量的重要一环。
常见分割器与场景
| 分割器 | 作用 |
| RecursiveCharacterTextSplitter | 通用首选,优先保持较大语义单位,必要时递归切得更细 |
| CharacterTextSplitter | 按指定分隔符切,简单直接 |
| MarkdownHeaderTextSplitter | 按 Markdown 标题层级切分 |
| HTMLHeaderTextSplitter | 按 HTML 标题结构切分 |
| TokenTextSplitter | 按 token 数控制块大小,更贴近模型上下文限制 |
| 语义切分(Semantic Chunking) | 按语义变化切分,尽量让相关内容保留在同一块中,但成本更高、实现更复杂 |
| 代码类分割器 | 按函数、类、逻辑块切分代码文本 |
RecursiveCharacterTextSplitter参数
| 参数 | 含义 | 实践理解 |
| chunk_size | 单块最大长度 | 块太大不利于检索,块太小又容易语义碎片化 |
| chunk_overlap | 相邻块重叠长度 | 防止句子、语义被截断,常见取块大小的 10%~20% |
| length_function | 长度计算方式 | 默认常用 len,即按字符数;也可按 token 数 |
| separators | 优先切分分隔符 | 决定先按段落、换行、句号还是更细粒度去切 |

纯文本分割
# RecursiveCharacterTextSplitter 按字符递归切,尽量保持语义完整,是通用文本场景里最常见的入门分割器。
# chunk_size:单块最大长度(按 length_function 计算,默认 len 即字符数);chunk_overlap:相邻块重叠字符数,常用 size 的 10%~20%。
# split_text(content):把字符串切成字符串列表;create_documents(texts):把字符串列表转成 Document 列表(或直接用 split_documents 处理 Document)。
# 重叠部分会重复出现在相邻块中,总字符数会大于原文,这不是 bug,而是为了减少「半句话被截断」的问题
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 1. 待分割的原文
content = (
"大模型RAG(检索增强生成)是一种结合生成模型与外部知识检索的技术,通过从大规模文档或数据库中检索相关信息,"
"辅助生成模型以提升回答的准确性和相关性。其核心流程包括用户输入查询、系统检索相关知识、"
"生成模型基于检索结果生成内容,并输出最终答案。RAG的优势在于能够弥补生成模型的知识盲区,"
"提供更准确、实时和可解释的输出,广泛应用于问答系统、内容生成、客服、教育和企业领域。"
"然而,其也面临依赖高质量知识库、可能的响应延迟、较高的维护成本以及数据隐私等挑战。"
)
# 2. 分割器:块大小 100 字符,重叠 30 字符,长度按 len(字符数)计算
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=30,
length_function=len
)
# 3. 先切成字符串列表 .split_text()
splitter_texts = text_splitter.split_text(content)
# 4. 再转成 Document 列表(便于后续与向量库、检索器对接) ..create_documents()
splitter_documents = text_spliter.create_documents(splitter_texts)
print(f"原始文本大小:{len(content)}")
print(f"分割文档数量:{len(splitter_documents)}")
for splitter_document in splitter_documents:
print(
f"文档片段大小:{len(splitter_document.page_content)},文档内容:{splitter_document.page_content}"
)
分割document对象(先加载在分割)
# pip install langchain-unstructured(部分环境加载本地文件还需 python-magic-bin)
from langchain_text_spltters import RecursiveCharacterTextSplitter
from langchain_unstructured import UnstructuredLoader
# 1. 加载文档得到 Document 列表
loader = UnstructuredLoader("rag.txt")
documents = loader.load()
# 2. 同一套分割参数:块 100 字符,重叠 30
text_splitter = RecursiveCharacterTextSplitter(
chunk_size="100",
chunk_overlap="30",
length_function=len
)
# 3. 直接对 Document 列表分割,返回更小的 Document 列表(不能改用 split_text:入参是 Document 列表,且需保留 metadata)
splitter_documents = text_splitter.split_documents(documents)
print(f"分割文档数量:{len(splitter_documents)}")
for splitter_document in splitter_documents:
print(f"文档片段:{splitter_document.page_content}")
print(
f"文档片段大小:{len(splitter_document.page_content)}, 文档元数据:{splitter_document.metadata}"
)
split_text():字符串 -> 字符串列表create_documents():字符串列表 ->Document列表split_documents():Document列表 -> 更小的Document列表
其中在真实 RAG 项目里,最常见的往往是最后一种,也就是:
loader.load() -> split_documents() -> embeddings -> vector store
案例
from langchain_core.prompt import PromptTemplate
from langchain.chat_model import init_chat_model
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import Docx2txtLoader
from langchain_core.runnables import RunnablePassthrough
from langchain_community.vectorstores import Redis
from langchain_classic.text_splitter import CharacterTextSplitter
from langchain_community.embeddings import DashScopeEmbeddings
load_dotenv()
# 大模型
llm = init_chat_model(
model="qwen-plus",
model_provider="openai",
api_key=os.getenv("aliQwen-api"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
# 提示词模板:{context} 由检索器填充,{question} 由用户输入填充;最终会生成一段字符串 Prompt 再交给聊天模型
prompt_template = """
请使用以下提供的文本内容来回答问题。仅使用提供的文本信息,
如果文本中没有相关信息,请回答"抱歉,提供的文本中没有这个信息"。
文本内容:
{context}
问题:{question}
回答:
"
"""
prompt = PromptTemplate(
template= prompt_template,
input_variables= ["context", "question"]
)
# 嵌入模型:用于文档与查询的向量化
embeddings = DashScopeEmbeddings(
model="text-embedding-v3",
dashscope_api_key= os.getenv("aliQwen-api")
)
# 加载docx
loader = Docx2txtLoader("alibabba-java.docx")
documents = loader.load()
# 2. 分割(此处用 CharacterTextSplitter 便于快速跑通;真实项目里更常见的通用首选是 RecursiveCharacterTextSplitter)
text_splitter = CharacterTextSplitter(
chunk_size=1000,
chunk_overlap=0,
length_function=len
)
texts = text_splitter.split_document(documents)
print(f"文档个数:{len(texts)}")
# 3. 向量化并写入 Redis,建立索引(必须用分割后的 texts,否则整篇文档作为一块)
vector_store = Redis.from_documents(
documents = texts,
embedding = embeddings,
redis_url = "redis://localhost:26379",
index_name = "my_index3"
)
# 4. 检索器:按相似度取前 k 条作为 context
retriever = vector_store.as_retriever(search_kwargs={"k": 2})
# 5. LCEL 链:输入 question → context 由 retriever 查得,question 直通 → 拼 prompt → 调 llm
rag_chain = {"context": retriever,"question": RunnablePassthrough()} | prompt | llm
# 6. 提问并打印答案
question = "langchain学习路线是什么"
result = rag_chain.invoke(question)
print("\n=== 有外挂知识库(RAG:从 alibaba-java.docx 检索)===")
print("回答:", result.content)
# 7. 对比演示:同一问题但「无外挂知识库」(context 为空,不查向量库,模拟未挂载文档)
no_rag_chain = (
{
"context": lambda _: "(未提供相关文档,模拟无外挂知识库)",
"question": RunnablePassthrough(),
},
| prompt | llm
)
result_no_rag = no_rag_chain_invoke(question)
print("回答:", result_no_rag.content)
实际流程:
- 用
Docx2txtLoader加载alibaba-java.docx - 用
CharacterTextSplitter把文档切成块 - 用
DashScopeEmbeddings把片段向量化 - 用 Redis 向量库 存储这些片段
- 用
as_retriever()生成检索器 - 用
PromptTemplate组织context + question - 用 聊天模型 基于检索结果生成最终答案
值得注意:
-
综合案例用的是
CharacterTextSplitter
这能帮助你快速跑通流程;但在通用文本场景里,实际项目中更常见的首选仍然是RecursiveCharacterTextSplitter。 -
Prompt 里明确写了“如果文本中没有相关信息,请直接说明”
这是 RAG 里很重要的一个工程习惯。因为检索不是百分百准确,Prompt 应该引导模型“基于上下文回答”,而不是脱离上下文自行发挥。 -
脚本做了“有 RAG / 无 RAG”的对比演示
这一点非常适合教学,也非常适合项目早期验证价值。只有做对比,你才更容易判断:问题究竟出在模型本身,还是出在检索链路。 -
Redis 只是这个案例里的向量存储后端
RAG 的本质不是绑定 Redis,而是“检索增强生成”这条流程。以后你换成 Chroma、FAISS、Milvus、PgVector,整体思路并不会变。

浙公网安备 33010602011771号