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": [...]
}

输出通常包含:

  • input
  • chat_history
  • context
  • answer

二十五、手动管理 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 的关键是问题改写

追问要先变成独立问题,检索器才能找对资料。

三十四、学习顺序建议

  1. 理解 RAG 为什么需要“检索增强”。
  2. 学会把资料加载成 Document
  3. 学会用 TextSplitter 切块。
  4. 学会用 embedding 和 vectorstore 建索引。
  5. 学会把 vectorstore 转成 retriever。
  6. 学会手写 LCEL RAG 链。
  7. 学会使用 create_stuff_documents_chaincreate_retrieval_chain
  8. 学会处理追问,理解问题上下文化。
  9. 学会使用 create_history_aware_retriever
  10. 学会用 RunnableWithMessageHistory 自动管理聊天历史。
  11. 最后学习 Agentic RAG。

三十五、一句话总结

标准 RAG 的核心链路是:

Load -> Split -> Embed -> Store -> Retrieve -> Prompt -> Generate

对话式 RAG 比标准 RAG 多了一步:

Chat History + Follow-up Question -> Standalone Question

Agentic RAG 则进一步把检索变成工具:

Agent 决定是否检索、如何检索、检索几次
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐