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 系统。

相关推荐
leonshi13 小时前
使用embedchain快速建立rag知识库,本地大模型
ai·rag·ollama
大流星20 小时前
LangChainJs之基础模型(一)
javascript·langchain
AIOps打工人20 小时前
我以为 LangChain 就是调用大模型,直到我写出第一条 Chain
langchain
大模型真好玩2 天前
LangChain DeepAgents 速通指南(十)—— DeepAgents Code 智能体服务核心源码解读
人工智能·langchain·agent
花千树_0103 天前
多工具调用只是开始:用 Regnexe 构建真正会反思的 Java Agent
langchain·agent
大模型真好玩7 天前
LangChain DeepAgents 速通指南(九)—— 生产级智能体框架 DeepAgents Code 源码导读
人工智能·langchain·agent
早点睡啊9 天前
精读 LangChain 官方文档(二)Model 篇:把模型调用升级成工程化推理接口
人工智能·langchain
星始流年10 天前
从 Tool 到 Skill——基于 LangChain 的服务端Skill实现
前端·langchain·agent
codedx11 天前
LangChain 和 LangGraph 构建的 Agent 项目模版
后端·langchain·agent
颜酱11 天前
LangGraph 入门指南
langchain