RAG 系列(十八):Conversational RAG——多轮对话中的代词陷阱

单轮 RAG 的隐性假设

前面十七篇都在处理同一种问题:给一个独立的问题,返回一个答案。

但真实的对话不是这样的。

用户在问完"RAGAS 是什么"之后,自然会接着问:

yaml 复制代码
Turn 1: RAGAS 是什么?
Turn 2: 它有哪四个核心指标?
Turn 3: 其中哪个最难提升?为什么?

Turn 1 没有问题。Turn 2 的"它"指的是 RAGAS,Turn 3 的"其中"指的是上一轮提到的四个指标。这在人类对话里显而易见------但对检索系统来说,"它有哪四个核心指标"是一个没有主语的查询,向量搜索会拿着这句话去找最相似的文档,找到的结果和 RAGAS 完全无关。

这是单轮 RAG 的隐性假设:每一个问题都是独立完整的。一旦遇到追问,这个假设就失效了。


History-Aware Retriever:在检索前先改写

解决思路很直接:在检索之前,先用一个 LLM 调用把当前问题 + 对话历史合并,改写成一个独立完整的问题,再用改写后的问题做检索。

yaml 复制代码
Turn 1: RAGAS 是什么?           → 直接检索(无历史)
Turn 2: 它有哪四个核心指标?
        ↓ 结合 Turn 1 历史
        "RAGAS 框架的四个核心指标是什么?"
        ↓ 用改写后的问题检索
Turn 3: 其中哪个最难提升?
        ↓ 结合 Turn 1 + Turn 2 历史
        "RAGAS 的四个核心指标中,哪一个最难提升?为什么?"
        ↓ 用改写后的问题检索

LangChain 把这个模式封装成了 create_history_aware_retriever,但为了防止 LLM 输出冗长内容触发嵌入模型 token 限制,本文手动构建了这条链并加了截断保护:

python 复制代码
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableBranch, RunnableLambda

def _extract_standalone_question(text: str) -> str:
    """只取第一行,防止 LLM 输出冗余内容超出嵌入模型 512 token 限制。"""
    lines = [l.strip() for l in text.strip().split("\n") if l.strip()]
    question = lines[0] if lines else text
    return question[:400]  # 硬截断保护

_contextualize_chain = (
    CONTEXTUALIZE_PROMPT
    | llm
    | StrOutputParser()
    | RunnableLambda(_extract_standalone_question)
)

# 无历史时直接检索,有历史时先改写再检索
history_aware_retriever = RunnableBranch(
    (
        lambda x: not x.get("chat_history"),
        (lambda x: x["input"]) | retriever,
    ),
    _contextualize_chain | retriever,
)

核心架构

Contextualize Prompt:改写的关键

python 复制代码
CONTEXTUALIZE_PROMPT = ChatPromptTemplate.from_messages([
    ("system",
     "根据对话历史和最新问题,将最新问题改写为一个独立完整的问题。\n"
     "要求:\n"
     "- 替换所有代词(它、这个、这些、其中等)为具体名词\n"
     "- 补全省略的主语或宾语\n"
     "- 只输出改写后的问题,不加任何解释\n"
     "如果问题本身已经完整独立,原样返回。"),
    MessagesPlaceholder("chat_history"),
    ("human", "{input}"),
])

三个设计要点:

  1. 明确"只输出问题":否则 LLM 会解释改写原因,生成的内容远超嵌入模型限制
  2. 对话历史放在 system 和 human 之间MessagesPlaceholder("chat_history") 会展开为完整的消息列表
  3. 原样返回条件:Turn 1 或语义完整的问题不需要改写,给 LLM 一个退路

完整 ConvRAG 链

python 复制代码
# Step 1: 检索(含问题改写)
history_aware_retriever = ...   # 见上文

# Step 2: 基于历史和检索结果生成答案
ANSWER_PROMPT = ChatPromptTemplate.from_messages([
    ("system",
     "你是一个RAG技术专家。根据以下参考资料回答问题。\n"
     "参考资料:\n{context}"),
    MessagesPlaceholder("chat_history"),   # 历史也参与生成
    ("human", "{input}"),
])
qa_chain  = create_stuff_documents_chain(llm, ANSWER_PROMPT)
rag_chain = create_retrieval_chain(history_aware_retriever, qa_chain)

# Step 3: 用 Session 管理多轮历史
store: dict[str, ChatMessageHistory] = {}

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

conv_rag = RunnableWithMessageHistory(
    rag_chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
    output_messages_key="answer",
)

每个 session_id 对应一个独立的对话历史。RunnableWithMessageHistory 会在每次 invoke 前自动把历史注入 chat_history,调用结束后自动把本轮的 Q&A 追加进去。


问题改写效果

三组追问对话,Turn 2 的问题改写结果:

ini 复制代码
[RAGAS追问]
  原始问题: 它有哪四个核心指标?
  改写后:   RAGAS有哪四个核心指标?

[向量数据库追问]
  原始问题: 其中哪个最适合生产环境?
  改写后:   在常见的向量数据库(Chroma、Pinecone、Milvus、Qdrant)中,
           哪个最适合生产环境?

[高级RAG追问]
  原始问题: Graph RAG和Agentic RAG又分别解决什么?
  改写后:   Graph RAG和Agentic RAG各自解决了RAG系统中的哪些问题?

"它"被替换为"RAGAS","其中"被展开为具体的数据库名列表------代词消歧后,这两个问题才有意义去检索。


检索对比:Turn 2 的关键差异

用"它有哪四个核心指标"直接检索 vs 用改写后的"RAGAS 框架的四个核心指标是什么"检索:

erlang 复制代码
Baseline 检索到(原始问题"它有哪四个核心指标?"):
  doc1: RAG的核心流程:检索→增强→生成。RAG由Meta AI在2020年提出...
  doc2: 文档分块策略影响RAG检索质量:固定大小分块适合通用场景...

ConvRAG 检索到(改写问题"RAGAS框架的四个核心指标是什么?"):
  doc1: RAGAS是专为RAG系统设计的评估框架,由Es等人在2023年提出。
        RAGAS的四个核心指标:1. context_recall ... 2. context_precision ...
  doc2: Embedding模型将文本转换为向量,决定了语义检索的质量上限...

Baseline 拿着"它有哪四个"去检索,"它"没有指向,搜到的是 RAG 基础介绍和分块策略------完全无关。ConvRAG 改写后明确了主语,第一篇文档就是 RAGAS 的四个指标。

这个差距是质性的,不是量的。


RAGAS 指标:一个有趣的反转

diff 复制代码
======================================================================
  RAGAS 指标对比(Baseline vs Conversational RAG)
======================================================================

  指标                    Baseline      ConvRAG       变化
  ──────────────────────────────────────────────────────────────
  context_recall            0.667          0.400      ↓-0.267  ◀
  context_precision         0.880          0.870      →-0.010
  faithfulness              1.000          1.000      →+0.000
  answer_relevancy          0.432          0.430      →-0.002
======================================================================
  注:评估对象为每轮对话的最后一轮(Turn 3)

ConvRAG 的 context_recall 反而低了 0.267。这看起来很奇怪------为什么"更好的检索"反而拿到更少的相关内容?

原因在于 RAGAS 评估的是什么。

本次实验评估的是 Turn 3,也就是每组对话的最后一轮:

  • "其中哪个最难提升?为什么?"
  • "如果我的团队刚开始做RAG,应该选哪个?"
  • "这四种技术的演进关系是什么?"

这三个 Turn 3 问题语义本身就是完整的------即使没有对话历史,直接用它们去检索也能找到相关文档。Baseline 在这三个问题上直接检索,反而精准命中了 ground truth 需要的内容。

ConvRAG 做了什么?它把 Turn 3 的问题结合前两轮历史做了改写,比如"这四种技术的演进关系是什么"可能被改写为"Self-RAG、CRAG、Graph RAG 和 Agentic RAG 这四种技术的演进关系是什么"------语义上更完整,但检索结果的覆盖范围可能因为措辞变化而有所偏移,导致 context_recall 反而更低。

结论:RAGAS 指标没有捕捉到 Conversational RAG 的核心价值。

核心价值体现在 Turn 2------代词消歧让检索从"找到无关内容"变为"直接命中"。但 RAGAS 评估的是 Turn 3,而 Turn 3 的问题刚好不需要历史就能正确检索。实验设计偏向于 Baseline 的场景,从而掩盖了 ConvRAG 的真实价值。


什么时候用 Conversational RAG?

场景 Baseline RAG Conversational RAG
每个问题独立完整 ✅ 直接检索,成本低 ⚠️ 改写增加延迟和 cost
追问含代词("它"、"其中") ❌ 检索失效 ✅ 改写后正确检索
追问省略主语 ❌ 检索失效 ✅ 补全后正确检索
多轮深入探讨同一主题 ⚠️ 每轮独立,无上下文积累 ✅ 历史积累,答案更连贯

额外成本:每轮多 1 次 LLM 调用(问题改写)。如果问题类型单一且每次都是独立完整的问题(比如搜索引擎式查询),Baseline RAG 的成本优势明显。如果是对话场景,追问是常态,ConvRAG 的改写才真正值回这次调用。

记忆管理的权衡:本文使用完整历史(所有轮次都保留),优点是信息完整,缺点是随对话轮次增加 token 消耗增大。生产系统常见的替代方案:

  • 滑动窗口:只保留最近 N 轮历史
  • 摘要记忆:用 LLM 将历史压缩为摘要,再加上最近 1 轮详情

完整代码

代码已开源:

github.com/chendongqi/...

核心文件:

  • conversational_rag.py --- 完整实现,含两条对比流水线和 RAGAS 评估

运行方式:

bash 复制代码
git clone https://github.com/chendongqi/llm-in-action
cd 18-conversational-rag
cp .env.example .env
pip install -r requirements.txt
python conversational_rag.py

小结

本文实现了 Conversational RAG,核心发现:

  1. 代词消歧是刚需:追问中的"它""其中""这些"在没有历史时会让检索完全失效,Turn 2 的检索对比清楚展示了这一点
  2. 问题改写效果显著:GLM-4-flash 能准确把"它有哪四个核心指标"改写为"RAGAS 框架的四个核心指标是什么",消歧质量令人满意
  3. RAGAS 指标出现了反转:ConvRAG 的 context_recall 低于 Baseline(0.400 vs 0.667)------原因是 Turn 3 的测试问题语义本身足够完整,不需要历史辅助也能正确检索;而 RAGAS 评估的恰好是这一轮
  4. 指标无法覆盖场景价值:ConvRAG 的价值在"代词追问失效"这个场景,RAGAS 没有测到这个场景,所以数字不反映真实优势

这是整个系列里 RAGAS 和实际价值偏差最明显的一次。指标是工具,不是答案------每次都要想清楚"这个指标在当前实验设计里,测到了什么,没测到什么"。


参考资料

相关推荐
渣渣苏1 小时前
硬核拆解 HNSW:亿级向量如何实现毫秒级召回?(下篇:实战调参与工程优化)
人工智能·算法·agent·向量数据库·hnsw·智能体
俊哥V2 小时前
每日 AI 研究简报 · 2026-05-16
人工智能·ai
冬奇Lab2 小时前
一天一个开源项目(第103篇):Open-Generative-AI - 开源 AI 视频与图像创作中心
人工智能·开源·aigc
耕烟煮云2 小时前
从Prompt到Context Engineering再到Harness,AI工程的演进
人工智能·prompt
user29876982706542 小时前
一、扩展 Claude Code:开篇
人工智能
user29876982706542 小时前
二、Skills 基础:编写第一个自定义技能
人工智能
JavaAgent架构师2 小时前
前端AI工程化(三):异步编程与并发控制
前端·人工智能
VALENIAN瓦伦尼安教学设备2 小时前
填补国内空白!瓦伦尼安发布首台船机机械故障诊断振动实验台
大数据·人工智能·嵌入式硬件