RAG 实战 (下):打造多轮对话知识助手

文章目录

  • [1. 基础 RAG 链 (Single Turn)](#1. 基础 RAG 链 (Single Turn))
    • [标准 LCEL 管道](#标准 LCEL 管道)
  • [2. 进阶:多轮对话 RAG (Conversational RAG)](#2. 进阶:多轮对话 RAG (Conversational RAG))
    • [步骤 1: 创建历史感知检索器](#步骤 1: 创建历史感知检索器)
    • [步骤 2: 创建文档回答链](#步骤 2: 创建文档回答链)
    • [步骤 3: 组装最终链 (Retrieval Chain)](#步骤 3: 组装最终链 (Retrieval Chain))
  • [3. 实战演示:带记忆的对话](#3. 实战演示:带记忆的对话)
  • [4. 生产级特性:流式输出 (Streaming)](#4. 生产级特性:流式输出 (Streaming))
  • [5. 面试必问:如何评估 RAG 的效果?(RAGAS)](#5. 面试必问:如何评估 RAG 的效果?(RAGAS))

核心痛点:简单的 RAG 只能回答单次提问。如何让 AI 记住上下文(解决代词指代),并告诉我们要查找的答案出自哪篇文档?

学习目标

  1. 用 LCEL 构建标准的 Retriever-Generator 管道。
  2. 理解并实现"历史感知检索器"(History Aware Retriever)。
  3. 使用 create_retrieval_chain 封装完整的问答系统。
  4. 实现带有"引用来源"的回答。

1. 基础 RAG 链 (Single Turn)

首先,我们把上一篇构建的向量库(Retriever)和 LLM 串联起来,实现最简单的"检索-生成"流程。

标准 LCEL 管道

python 复制代码
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

# 0. 准备环境 (假设已完成上一篇的向量库构建)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# 加载持久化的 Chroma
db = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
retriever = db.as_retriever(search_kwargs={"k": 3})
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

# 1. 定义 Prompt
template = """你是一个乐于助人的助手。请根据以下上下文回答问题。
如果不知道答案,直接说不知道,不要编造。

上下文:
{context}

用户问题:{question}
"""
prompt = ChatPromptTemplate.from_template(template)

# 2. 定义格式化函数 (将 Doc 对象列表转换为字符串)
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# 3. 构建链 (LCEL 核心)
# RunnablePassthrough.assign 允许我们向字典中添加新字段
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# 4. 运行
response = rag_chain.invoke("LangChain 的核心组件有哪些?")
print(response)

2. 进阶:多轮对话 RAG (Conversational RAG)

在多轮对话中,用户常说"它怎么安装?"、"能给个代码示例吗?"。直接拿这些问题去检索向量库通常找不到结果,因为丢失了"它"指代的主语(LangChain)。

我们需要一个两阶段的系统:

  1. 查询改写 (Query Rewriting):结合历史记录,把"它怎么安装"改写为"LangChain 怎么安装"。
  2. 文档问答 (QA):用改写后的问题去检索和回答。

步骤 1: 创建历史感知检索器

python 复制代码
from langchain.chains import create_history_aware_retriever
from langchain_core.prompts import MessagesPlaceholder

# 这是一个专门用来"改写问题"的 Prompt,不负责回答
system_prompt_rewrite = """给定一段聊天记录和最新的用户问题(该问题可能引用了聊天上下文),
请构造一个可以独立理解的搜索查询。
不要回答问题,只是重写它使其语义完整。如果不需要重写,直接返回原样。"""

prompt_rewrite = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt_rewrite),
        MessagesPlaceholder("chat_history"), # 历史记录占位符
        ("human", "{input}"),
    ]
)

# history_aware_retriever 的功能:输入 {input, chat_history} -> 输出 List[Document]
# 它会自动调用 LLM 改写问题,然后调用 retriever 检索
history_aware_retriever = create_history_aware_retriever(
    llm, retriever, prompt_rewrite
)

步骤 2: 创建文档回答链

这一步负责根据 Document 生成最终答案。

python 复制代码
from langchain.chains.combine_documents import create_stuff_documents_chain

system_prompt_qa = """你是一个问答助手。请使用以下检索到的上下文来回答问题。
如果不知道答案,就说不知道。使用了上下文后,请在回答末尾简要注明来源。

{context}"""

prompt_qa = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt_qa),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)

# question_answer_chain 的功能:输入 {context, input, chat_history} -> 输出答案字符串
# create_stuff_documents_chain 会自动将 docs 填充到 prompt 的 {context} 中
question_answer_chain = create_stuff_documents_chain(llm, prompt_qa)

步骤 3: 组装最终链 (Retrieval Chain)

create_retrieval_chain 会自动处理中间逻辑:

  1. 拿用户问题 + 历史 -> 调用 history_aware_retriever -> 得到 Docs。
  2. 拿 Docs + 用户问题 + 历史 -> 调用 question_answer_chain -> 得到答案。
python 复制代码
from langchain.chains import create_retrieval_chain

# 最终链:输入 {input, chat_history} -> 输出 {answer, context}
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)

3. 实战演示:带记忆的对话

为了自动管理 chat_history,我们使用 RunnableWithMessageHistory

python 复制代码
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 简单的内存存储,实际生产中可以使用 Redis
store = {}

def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

conversational_rag_chain = RunnableWithMessageHistory(
    rag_chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history", # 对应 Prompt 中的 MessagesPlaceholder 名字
    output_messages_key="answer",
)

# --- 模拟对话 ---

# 第一轮
response1 = conversational_rag_chain.invoke(
    {"input": "LangChain 是什么?"},
    config={"configurable": {"session_id": "user123"}}
)
print("AI:", response1["answer"])

# 第二轮:测试指代消解
response2 = conversational_rag_chain.invoke(
    {"input": "它支持 Python 吗?"}, # "它" 指代 LangChain
    config={"configurable": {"session_id": "user123"}}
)
print("AI:", response2["answer"])

# 查看引用来源
print("\n--- Source Documents ---")
for i, doc in enumerate(response2["context"]):
    print(f"Doc {i+1}: {doc.metadata.get('source', 'Unknown')}")

4. 生产级特性:流式输出 (Streaming)

RAG 系统通常比普通对话要慢(因为多了检索步骤)。为了防止用户看着空白屏幕发呆,流式输出是必须的。

LCEL 天生支持流式,我们只需要把 .invoke() 换成 .stream()。默认情况下,RunnableWithMessageHistory 也支持流式。

python 复制代码
# 使用 .stream() 替代 .invoke()
# 注意:配置和参数保持不变
chunks = conversational_rag_chain.stream(
    {"input": "请详细介绍一下 LangSmith 的功能。"},
    config={"configurable": {"session_id": "user123"}}
)

print("AI: ", end="", flush=True)
for chunk in chunks:
    # chunk 也是一个字典,包含 'answer' 字段的片段
    if "answer" in chunk:
        print(chunk["answer"], end="", flush=True)

print("\n") # 换行

5. 面试必问:如何评估 RAG 的效果?(RAGAS)

这是 RAG 面试中的终极拷问:"你怎么知道你的 RAG 系统好不好?"、"怎么量化改进效果?"。靠人工看太慢,我们需要自动化评估。

RAGAS (RAG Assessment) 是目前最主流的评估框架,它通过 LLM 来给 LLM 打分。

核心指标 (The RAG Triad)

  1. 忠实度 (Faithfulness)
    • 检查点:Generator
    • 含义:生成的答案是否完全基于检索到的上下文?有没有产生幻觉?
  2. 上下文相关性 (Context Relevancy)
    • 检查点:Retriever
    • 含义:检索回来的文档里,包含答案的比例是多少?是否混入了太多噪音?
  3. 答案相关性 (Answer Relevance)
    • 检查点:End-to-End
    • 含义:生成的答案是否真正回答了用户的问题?

代码示例 (伪代码概念)

python 复制代码
# 这是一个概念演示,实际运行需要安装 ragas 库
# pip install ragas

from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
from datasets import Dataset

# 准备测试集
data = {
    "question": ["LangChain 是什么?"],
    "answer": ["LangChain 是一个开发框架..."], # 你的 RAG 生成的答案
    "contexts": [["LangChain is a framework for..."]], # 你的 Retriever 找回的文档
    "ground_truth": ["LangChain 是一个用于构建 LLM 应用的框架"] # 人工标注的标准答案
}

dataset = Dataset.from_dict(data)

# 自动跑分
results = evaluate(
    dataset=dataset,
    metrics=[
        context_precision, # 检索准不准
        faithfulness,      # 有没有瞎编
        answer_relevancy,  # 有没有答非所问
    ],
)

print(results)
# {'context_precision': 0.9, 'faithfulness': 0.95, 'answer_relevancy': 0.88}

至此,一个功能完备的、具备记忆和私有知识库,且经过科学评估的 RAG 系统就搭建完成了。之后我们将探讨如何使用 LangGraph 构建更复杂的 Agent 系统。

相关推荐
彭于晏Yan2 小时前
LangChain4j实战三:图像模型
java·spring boot·后端·langchain
ZaneAI3 小时前
🚀 Claude Agent SDK 使用指南:会话管理(Session )
langchain·agent·claude
JaydenAI6 小时前
[LangChain之链]Runnable,不仅要可执行,还要可存储、可传输、可重建、可配置和可替换
python·langchain
UIUV6 小时前
AI Agent 开发实战:从原理到最小化实现
后端·langchain·node.js
ZWZhangYu7 小时前
【LangChain专栏】LangChain Memory 核心解析
windows·microsoft·langchain
coding者在努力7 小时前
LangChain之解析器核心组件.2026年新版讲解,超详细
windows·python·机器学习·langchain·pip
彭于晏Yan9 小时前
LangChain4j实战二:集成到Springboot
java·spring boot·后端·langchain
每天都要加加油王得坤9 小时前
langchain学习笔记
笔记·学习·langchain