03LangChain RAG 与对话式 RAG 知识点详解
LangChain RAG 与对话式 RAG 知识点详解
本文档整理自 LangChain 中文网两篇 RAG 教程:
本文只保留知识点,不写环境安装、API Key、Notebook 等准备内容。
一、RAG 是什么
1. 是什么
RAG 是 Retrieval-Augmented Generation,中文通常叫“检索增强生成”。
它的核心思想是:
先从外部知识库检索相关资料
再把资料和用户问题一起交给模型
最后由模型基于资料生成答案
2. 解决什么问题
大语言模型有几个天然限制:
- 不知道训练截止日期之后的新知识。
- 不知道企业私有文档。
- 对具体资料可能会编造。
- 上下文窗口有限,不能一次塞入所有资料。
RAG 用“外部检索”补足模型知识。
3. 为什么重要
很多实际应用都需要回答特定资料的问题:
- 公司内部文档问答。
- 产品手册问答。
- 法规政策问答。
- 论文知识库问答。
- 客服知识库问答。
这些知识不一定在模型训练数据里,或者要求答案必须来自指定来源。RAG 就是这类应用的基础架构。
二、RAG 的两大阶段
1. 是什么
典型 RAG 应用分成两个阶段:
索引阶段 Indexing
把原始资料处理成可检索的知识库
检索与生成阶段 Retrieval and Generation
用户提问时,检索相关资料并生成答案
2. 解决什么问题
如果用户每问一次,你都重新加载、分割、嵌入所有文档,成本会非常高。索引阶段通常离线完成,运行时只做检索和生成。
3. 为什么要分阶段
因为“准备资料”和“回答问题”的频率不同:
- 准备资料:资料变更时才做。
- 回答问题:每次用户提问都做。
拆开后系统更快、更稳定、更省钱。
三、索引阶段总览
索引阶段通常包含三步:
加载 Load -> 分割 Split -> 存储 Store
1. 加载
把网页、PDF、Markdown、数据库、文本文件等数据加载成 Document。
2. 分割
把大文档切成较小文本块。
3. 存储
把文本块转换成 embedding,并存进向量存储。
四、加载 DocumentLoader
1. 是什么
DocumentLoader 是 LangChain 中用于从数据源读取内容的组件。它把外部数据加载成 Document 列表。
教程中使用 WebBaseLoader 加载网页:
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",)
)
docs = loader.load()
2. 解决什么问题
原始资料来源很多,格式也不同。模型和检索器不应该直接面对网页、PDF、CSV 等复杂格式。Loader 负责把它们统一成 Document。
3. 为什么重要
RAG 的第一步就是“把资料拿进来”。Loader 的质量会影响后续所有流程。
如果加载内容包含太多导航栏、广告、页脚,检索结果就会被噪声污染。
4. bs4.SoupStrainer 的作用
教程中对网页使用了 bs4.SoupStrainer,只解析指定区域:
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header")
)
)
这是为了过滤网页无关内容,只保留正文、标题、头部信息。
五、Document 文档对象
1. 是什么
Document 是 LangChain 表示文本资料的标准对象:
Document(
page_content="正文内容",
metadata={"source": "来源"}
)
2. 解决什么问题
它把文本和来源信息绑定起来。后续检索、引用、调试都依赖它。
3. 为什么 RAG 离不开 Document
RAG 不是单纯让模型回答,而是让模型基于外部资料回答。Document 就是外部资料进入 LangChain 的基本单位。
六、文本分割 TextSplitter
1. 是什么
TextSplitter 用来把长文档切成较小块。
教程中使用:
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
add_start_index=True,
)
all_splits = text_splitter.split_documents(docs)
2. 解决什么问题
长文档有两个问题:
- 很难被精确检索。
- 可能放不进模型上下文窗口。
切块后,系统可以只检索最相关的几个小片段。
3. 为什么不能整篇文档直接入库
如果一个文档很长,用户问一个很小的问题,整篇文档的 embedding 会变得过于宽泛。检索时可能找不到真正相关的段落。
4. chunk_size
chunk_size 控制每个文本块的大致长度。
chunk_size=1000
太小:
- 上下文不完整。
- 一个概念可能被切断。
太大:
- 检索不精准。
- 占用更多 token。
5. chunk_overlap
chunk_overlap 控制相邻文本块之间的重叠。
chunk_overlap=200
它解决“关键句子刚好被切断”的问题。
6. add_start_index
add_start_index=True 会把切块在原文中的起始位置写入 metadata。
这有助于:
- 回溯原文。
- 调试切块。
- 做引用定位。
七、RecursiveCharacterTextSplitter
1. 是什么
递归字符分割器会按照常见分隔符递归切分文本,比如段落、换行、句子等,直到块大小合适。
2. 解决什么问题
相比粗暴按固定字符数切割,它更尽量保留自然语义边界。
3. 为什么推荐用于通用文本
它不依赖特定格式,适合网页、文章、普通文档等大多数非结构化文本。
八、Embedding 嵌入
1. 是什么
Embedding 把文本转换成向量。
文本块 -> embedding 向量
2. 解决什么问题
向量让系统可以按语义相似度搜索文本,而不只是关键词匹配。
3. 为什么在 RAG 中必要
用户问题和文档原文经常表达不同。Embedding 能帮助找到语义相关的资料。
九、VectorStore 向量存储
1. 是什么
向量存储保存文本块、metadata 和 embedding,并支持相似度搜索。
教程中使用 Chroma:
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
vectorstore = Chroma.from_documents(
documents=all_splits,
embedding=OpenAIEmbeddings(),
)
2. 解决什么问题
它让系统可以在大量文档块中快速找到与问题最相关的内容。
3. 为什么是 RAG 的核心基础设施
RAG 的质量很大程度取决于“检索是否找对资料”。向量存储就是检索的基础。
十、Retriever 检索器
1. 是什么
Retriever 接收用户查询,返回相关 Document。
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 6},
)
2. 解决什么问题
它把向量存储封装成一个统一的检索接口,便于接入 RAG 链。
3. 为什么要设置 k
k 表示返回多少个相关文档块。
search_kwargs={"k": 6}
太少可能漏掉关键信息,太多会增加 token 消耗并引入噪声。
十一、检索与生成阶段
1. 是什么
运行时 RAG 的核心流程:
用户问题
|
v
Retriever 检索相关文档
|
v
把文档格式化为 context
|
v
Prompt 组合 question + context
|
v
LLM 生成答案
2. 解决什么问题
模型本身不知道外部资料。RAG 在提示词中注入检索到的上下文,让模型基于这些资料回答。
3. 为什么要把资料放进 prompt
LLM 只能看见本次请求中的内容。检索到的资料必须作为上下文放进 prompt,模型才能使用。
十二、RAG Prompt
1. 是什么
RAG Prompt 是告诉模型如何基于上下文回答问题的提示词。
典型结构:
你是一个问答助手。
使用下面检索到的上下文回答问题。
如果不知道答案,就说不知道。
Question: {question}
Context: {context}
Answer:
2. 解决什么问题
它约束模型不要随意编造,并明确告诉模型上下文和问题在哪里。
3. 为什么要写“如果不知道就说不知道”
RAG 常用于事实问答。与其让模型编造,不如让它在上下文不足时承认不知道。
十三、format_docs
1. 是什么
format_docs 把 Document 列表转换成一个字符串,方便塞进 prompt 的 {context}。
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
2. 解决什么问题
检索器返回的是 List[Document],而 prompt 通常需要字符串。format_docs 负责把结构化文档拼成上下文文本。
3. 为什么可以定制
你可以把来源也拼进去:
def format_docs(docs):
return "\n\n".join(
f"Source: {doc.metadata.get('source')}\n{doc.page_content}"
for doc in docs
)
这样模型回答时更容易带来源。
十四、LCEL 实现 RAG 链
1. 是什么
教程用 LCEL 定义 RAG 链:
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
2. 解决什么问题
用声明式方式把“检索、格式化、提示词、模型、解析”串起来。
3. 为什么字典能出现在链里
LangChain 会把字典转换成并行 Runnable。这里同时构造两个字段:
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
含义:
同一个用户问题
一边传给 retriever 得到 context
一边原样作为 question
4. RunnablePassthrough
RunnablePassthrough() 表示把输入原样传下去。
在这个链里,它负责保留用户原问题。
5. StrOutputParser
模型输出通常是消息对象。StrOutputParser() 把它解析成普通字符串答案。
十五、RAG 链的数据流
假设用户输入:
What is Task Decomposition?
链内部发生:
输入问题
|
+--> retriever 检索相关 Document
| |
| v
| format_docs 拼成 context
|
+--> RunnablePassthrough 保留 question
|
v
{"context": "...", "question": "..."}
|
v
prompt
|
v
llm
|
v
StrOutputParser
|
v
最终答案
十六、内置链 create_stuff_documents_chain
1. 是什么
create_stuff_documents_chain 是 LangChain 提供的便捷函数,用于把检索到的文档全部“塞入”提示词。
from langchain.chains.combine_documents import create_stuff_documents_chain
question_answer_chain = create_stuff_documents_chain(llm, prompt)
2. 解决什么问题
它封装了“把文档放进 prompt,然后调用模型回答”的逻辑。
3. 为什么叫 stuff
stuff 的意思是把检索到的上下文直接放进提示词,不做额外总结、映射或压缩。
4. 适合场景
适合检索出来的文档不太多、总 token 可以放进模型上下文的场景。
十七、内置链 create_retrieval_chain
1. 是什么
create_retrieval_chain 把检索器和问答链组合起来。
from langchain.chains import create_retrieval_chain
rag_chain = create_retrieval_chain(retriever, question_answer_chain)
2. 解决什么问题
它帮你补上检索步骤,并把检索到的上下文传给问答链。
3. 输出包含什么
通常输出会包含:
input:原始问题。context:检索到的文档。answer:模型生成的答案。
这比只返回字符串更适合调试和引用来源。
十八、手写 LCEL RAG vs 内置链
| 方式 | 优点 | 适合场景 |
|---|---|---|
| 手写 LCEL | 透明、可控、适合学习底层流程 | 学习、定制复杂逻辑 |
| 内置链 | 简洁、少写样板代码 | 常规 RAG 问答 |
初学者建议先理解手写 LCEL,再使用内置链。
十九、对话式 RAG 是什么
1. 是什么
对话式 RAG 是带聊天历史的 RAG。它不仅回答当前问题,还能理解多轮对话中的指代。
2. 解决什么问题
用户经常会追问:
用户:什么是任务分解?
AI:任务分解是把复杂任务拆成小步骤。
用户:常见做法有哪些?
第二个问题里的“做法”依赖上一轮上下文。如果直接拿“常见做法有哪些?”去检索,检索器不知道用户问的是“任务分解的常见做法”。
3. 为什么普通 RAG 不够
普通 RAG 只把当前输入交给检索器:
查询 -> 检索器
对话式 RAG 需要:
(当前问题 + 聊天历史) -> 重写成独立问题 -> 检索器
二十、上下文化问题 Contextualize Question
1. 是什么
上下文化问题就是把依赖历史的追问改写成不依赖历史的独立问题。
例如:
聊天历史:用户刚问过任务分解
当前问题:常见做法有哪些?
改写后:任务分解的常见做法有哪些?
2. 解决什么问题
检索器只看查询字符串,不会自动理解聊天历史。把问题改写成独立问题后,检索结果更准确。
3. 为什么不要在这一步回答问题
上下文化子链的职责只是“改写查询”,不是生成答案。
提示词中会强调:
Do NOT answer the question, just reformulate it.
4. 流程
chat_history + input
|
v
LLM 改写问题
|
v
standalone question
|
v
retriever 检索
二十一、MessagesPlaceholder
1. 是什么
MessagesPlaceholder("chat_history") 表示在提示词中插入一组历史消息。
from langchain_core.prompts import MessagesPlaceholder
contextualize_q_prompt = ChatPromptTemplate.from_messages(
[
("system", contextualize_q_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
2. 解决什么问题
聊天历史不是普通字符串,而是一组带角色的消息。MessagesPlaceholder 可以保留消息结构。
3. 为什么重要
模型需要区分哪些是用户说的,哪些是 AI 回复的。消息结构比拼接字符串更清晰。
二十二、create_history_aware_retriever
1. 是什么
create_history_aware_retriever 用来创建历史感知检索器。
from langchain.chains import create_history_aware_retriever
history_aware_retriever = create_history_aware_retriever(
llm,
retriever,
contextualize_q_prompt,
)
2. 解决什么问题
它把“根据聊天历史改写问题,再检索”封装起来。
3. 为什么好用
它会处理两种情况:
- 没有聊天历史:直接用原问题检索。
- 有聊天历史:先改写问题,再检索。
4. 输出是什么
它的输出仍然和普通检索器一样,是相关 Document 列表。
二十三、对话式问答 Prompt
1. 是什么
对话式 RAG 的回答提示词既接收检索上下文,也接收聊天历史和当前输入。
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
2. 解决什么问题
回答阶段也可能需要历史,比如保持语气、承接前文、理解对话状态。
3. 与普通 RAG Prompt 的区别
普通 RAG:
system + input + context
对话式 RAG:
system + chat_history + input + context
二十四、对话式 RAG 链
1. 是什么
对话式 RAG 链把历史感知检索器和问答链组合起来:
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(
history_aware_retriever,
question_answer_chain,
)
2. 解决什么问题
它让系统能正确处理追问:
第一问:What is Task Decomposition?
第二问:What are common ways of doing it?
第二问会结合历史改写后再检索。
3. 输入输出
输入:
{
"input": "What are common ways of doing it?",
"chat_history": [...]
}
输出通常包含:
inputchat_historycontextanswer
二十五、手动管理 chat_history
1. 是什么
教程先用普通列表手动保存历史:
from langchain_core.messages import AIMessage, HumanMessage
chat_history = []
question = "What is Task Decomposition?"
ai_msg_1 = rag_chain.invoke({
"input": question,
"chat_history": chat_history,
})
chat_history.extend(
[
HumanMessage(content=question),
AIMessage(content=ai_msg_1["answer"]),
]
)
2. 解决什么问题
让下一轮调用能拿到上一轮问题和回答。
3. 为什么不适合真实项目
手动维护容易漏加、加错、不同用户混在一起。真实应用需要自动管理和持久化。
二十六、RunnableWithMessageHistory
1. 是什么
RunnableWithMessageHistory 可以自动给链注入聊天历史,并在调用后更新历史。
from langchain_core.runnables.history import RunnableWithMessageHistory
conversational_rag_chain = RunnableWithMessageHistory(
rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
2. 解决什么问题
它把这些重复工作自动化:
- 根据 session_id 找历史。
- 把历史放进链的
chat_history。 - 把当前用户输入保存为 HumanMessage。
- 把模型输出保存为 AIMessage。
3. 三个 key 的含义
| 参数 | 含义 |
|---|---|
input_messages_key="input" | 当前用户输入在哪个字段 |
history_messages_key="chat_history" | 历史消息注入到哪个字段 |
output_messages_key="answer" | 模型答案从哪个字段取出来保存 |
4. 为什么这三个 key 很重要
对话式 RAG 链输入输出是字典,不是简单消息列表。包装器必须知道:
哪里是用户输入
哪里要放历史
哪里是模型回答
二十七、BaseChatMessageHistory 与 ChatMessageHistory
1. 是什么
BaseChatMessageHistory 是聊天历史存储抽象。ChatMessageHistory 是一种简单实现。
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
2. 解决什么问题
用 session_id 隔离不同对话。
3. 为什么生产环境要换存储
教程中的 store = {} 是内存字典,程序重启后会丢失。真实项目通常使用 Redis、数据库等持久化存储。
二十八、session_id
1. 是什么
session_id 是当前对话的唯一标识。
config = {
"configurable": {
"session_id": "abc123"
}
}
2. 解决什么问题
区分不同用户、不同聊天窗口、不同会话。
3. 为什么必须设计好
如果 session_id 混乱,就可能出现:
- 用户 A 看到用户 B 的上下文。
- 一个用户多个窗口互相污染。
- 历史记录找不到。
二十九、Agentic RAG
1. 是什么
Agentic RAG 是把检索器变成工具,让 Agent 自己决定是否检索、怎么检索、检索几次。
2. 解决什么问题
普通对话式 RAG 通常每次都检索。Agentic RAG 更灵活:
- 用户只是问候时,不必检索。
- 用户问题复杂时,可以多次检索。
- 模型可以自己构造检索查询。
3. 为什么更不可预测
Agent 把部分控制权交给模型。它可能选择不检索、检索错误关键词、或多次调用工具。所以灵活性更高,但可控性更弱。
三十、create_retriever_tool
1. 是什么
create_retriever_tool 可以把检索器包装成工具。
from langchain.tools.retriever import create_retriever_tool
tool = create_retriever_tool(
retriever,
"blog_post_retriever",
"Searches and returns excerpts from the Autonomous Agents blog post.",
)
tools = [tool]
2. 解决什么问题
Agent 只能调用工具。要让 Agent 使用知识库,需要把 retriever 转换成 tool。
3. 为什么工具描述很重要
模型根据工具名称和描述决定是否调用它。描述应该说清楚工具能搜索什么内容。
三十一、Agentic RAG vs 固定 RAG 链
| 对比项 | 固定 RAG 链 | Agentic RAG |
|---|---|---|
| 是否每次检索 | 通常每次都检索 | 模型决定 |
| 检索查询 | 开发者流程控制 | 模型生成 |
| 检索次数 | 通常固定 | 可以多次 |
| 可控性 | 更强 | 更弱 |
| 灵活性 | 较弱 | 更强 |
| 适合场景 | 标准知识库问答 | 复杂、多步、需要判断的问答 |
三十二、RAG 常见问题
1. 为什么检索到了资料,回答还是错
可能原因:
- 检索结果不相关。
- 文档切块太大或太小。
- prompt 没明确要求基于上下文。
- 检索结果太多,引入噪声。
- 模型忽略了上下文。
2. 为什么追问时答非所问
普通 RAG 不理解聊天历史。需要对话式 RAG,把追问改写成独立问题。
3. chunk_overlap 是不是越大越好
不是。重叠太大容易造成重复内容,增加存储和 token 成本。
4. k 是不是越大越好
不是。k 越大,上下文越多,但噪声和成本也越高。
5. 对话式 RAG 是否一定要把历史都传给模型
不一定。长对话需要裁剪、摘要或长期记忆策略,否则会超出上下文窗口。
三十三、核心心智模型
1. RAG 不是让模型记住知识
RAG 是在每次提问时临时把相关知识放进上下文。
2. 检索质量决定答案上限
如果检索不到正确资料,模型很难答对。
3. 切块决定可检索粒度
切块太粗,检索不准;切块太细,上下文不足。
4. Prompt 决定模型如何使用资料
好的 RAG prompt 会要求模型基于上下文回答,不知道就说不知道。
5. 对话式 RAG 的关键是问题改写
追问要先变成独立问题,检索器才能找对资料。
三十四、学习顺序建议
- 理解 RAG 为什么需要“检索增强”。
- 学会把资料加载成
Document。 - 学会用
TextSplitter切块。 - 学会用 embedding 和 vectorstore 建索引。
- 学会把 vectorstore 转成 retriever。
- 学会手写 LCEL RAG 链。
- 学会使用
create_stuff_documents_chain和create_retrieval_chain。 - 学会处理追问,理解问题上下文化。
- 学会使用
create_history_aware_retriever。 - 学会用
RunnableWithMessageHistory自动管理聊天历史。 - 最后学习 Agentic RAG。
三十五、一句话总结
标准 RAG 的核心链路是:
Load -> Split -> Embed -> Store -> Retrieve -> Prompt -> Generate
对话式 RAG 比标准 RAG 多了一步:
Chat History + Follow-up Question -> Standalone Question
Agentic RAG 则进一步把检索变成工具:
Agent 决定是否检索、如何检索、检索几次
更多推荐




所有评论(0)