2442 字
12 分钟
对话式RAG与检索策略

对话式RAG与检索策略#

本篇承接 02_检索链与RAG实战。前两篇已经解决了”如何把文档做成可检索知识库”和”如何搭出一条基础 RAG 问答链”。但真实项目中的 RAG 很快会继续演进:多轮对话需要利用历史改写查询,结构化条件需要从自然语言中抽出,单一检索器往往又不够稳。于是,检索层会进一步扩展为三类策略:history-aware retrieval、self-query retrieval、ensemble retrieval。


1 适用场景与问题类型#

1.1 上下文省略型问题#

基础 RAG 默认假设:用户本轮问题本身就足够完整,可以直接拿去检索

但多轮对话里,这个前提经常不成立:

  • “LangChain 是什么时候开源的?”
  • “它的作者后来做了什么?”

第二问里的”它”如果直接送去检索,召回效果通常会很差,因为向量库并不知道”它”指的是 LangChain。

1.2 结构化过滤与混合召回#

除了上下文省略,还常见两类问题:

  1. 用户在自然语言里夹带结构化过滤条件
    例如:“找 2020 年之后、评分高于 8 分的科幻电影”

  2. 用户既包含语义意图,也包含精确关键词
    例如:“PEP 703 和禁用 GIL 的实现思路是什么?”

这三类问题分别对应不同策略:

问题类型更适合的策略
代词、省略、上下文引用create_history_aware_retriever
自然语言中的过滤条件SelfQueryRetriever
关键词 + 语义混合需求EnsembleRetriever / hybrid search
核心理解

基础 RAG 解决的是”能不能检索”;这篇讲的内容解决的是”在复杂输入下,如何检得更准”。


2 History-aware retrieval#

2.1 create_history_aware_retriever#

create_history_aware_retriever 可以理解为一个带查询改写能力的 retriever 包装层

它的核心逻辑很直接:

  • 没有 chat_history:当前 input 直接送给底层 retriever
  • chat_history:先用 Prompt + LLM 把当前问题改写成独立可理解的问题
  • 再把改写后的问题送给 retriever

这和 04_Prompt编排与会话历史 中的 RunnableWithMessageHistory 很像,但关注点不同:

  • RunnableWithMessageHistory 解决整条链如何自动带历史
  • create_history_aware_retriever 解决检索前是否要先利用历史改写查询

2.2 查询改写与历史注入#

检索器的输入通常是一个字符串查询,不是消息列表。
因此,“把所有聊天历史直接拼成一长串查询文本”通常并不是好方案:

  • 噪声太多
  • 最后一问的意图被稀释
  • Token 变多,但召回不一定提升

更稳的做法是分两步:

  1. 根据 chat_history 把最后一问改写成完整问题
  2. 用这个完整问题检索

而在最终回答阶段,再把 chat_history 与检索上下文一起送给 LLM。

2.3 完整示例#

# pip install langchain langchain-openai langchain-community faiss-cpu
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_texts(
[
"LangChain 由 Harrison Chase 于 2022 年 10 月开源。",
"Harrison Chase 后来创立了 LangChain 相关公司,并推动 LangSmith、LangGraph 等产品发展。",
"LangGraph 是 LangChain 生态中的有状态 Agent 编排框架。",
],
embeddings,
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
contextualize_q_prompt = ChatPromptTemplate.from_messages([
("system",
"给定聊天历史和用户最新问题,如果问题引用了历史上下文,"
"请将它改写成一个脱离聊天历史也能独立理解的问题。"
"只做改写,不要回答问题。"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
qa_prompt = ChatPromptTemplate.from_messages([
("system",
"你是一个知识库问答助手。请基于以下参考资料回答用户问题。"
"如果资料中没有答案,请明确说明不知道。\n\n"
"{context}"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
])
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
chat_history = [
HumanMessage(content="LangChain 是什么时候开源的?"),
AIMessage(content="LangChain 由 Harrison Chase 于 2022 年 10 月开源。"),
]
result = rag_chain.invoke({
"input": "它的作者后来做了什么?",
"chat_history": chat_history,
})
print(result["answer"])

2.4 ConversationalRetrievalChain 迁移#

旧教程里经常会见到 ConversationalRetrievalChain,但当前官方已经明确给出迁移方向:

  • ConversationalRetrievalChain 已废弃
  • 推荐改用 create_history_aware_retriever + create_retrieval_chain

这样拆开的好处是:

  1. 查询改写和回答生成职责更清晰
  2. retriever、Prompt、combine documents 策略都更容易替换
  3. 更符合 LCEL / Runnable 的组合方式

2.5 检索层与生成层的 chat history#

对话式 RAG 里,历史通常会出现在两个位置:

位置作用
检索前把省略问题改写成独立问题
回答时让最终回答保持对话连续性

只做回答层历史注入、不做检索层 query rewrite,是一个非常常见的误区。


3 SelfQueryRetriever#

3.1 作用#

SelfQueryRetriever 解决的不是多轮代词问题,而是:如何从自然语言里抽出结构化过滤条件

例如:

“找两部 2010 年之后、评分高于 8 分的科幻电影”

这里同时包含:

  • 主题语义:科幻电影
  • 结构化过滤:年份 > 2010,评分 > 8,数量 = 2

普通向量检索通常只擅长第一层。SelfQueryRetriever 的思路则是:先用 LLM 生成查询词和 metadata filter,再去调用底层向量库。

3.2 前提条件#

要用好 SelfQueryRetriever,一般需要满足:

  1. 文档 metadata 设计清晰
  2. metadata 字段有明确类型
  3. 底层向量库支持 metadata filtering
  4. 安装 lark
关键前提

SelfQueryRetriever 无法凭空创造 metadata。如果你的文档一开始没有存 year、genre、rating 这类字段,后面就没有过滤基础。

3.3 示例#

# pip install langchain langchain-openai langchain-chroma lark
from langchain_core.documents import Document
from langchain.chains.query_constructor.schema import AttributeInfo
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
docs = [
Document(
page_content="一名宇航员穿越虫洞寻找新家园。",
metadata={"genre": "science fiction", "year": 2014, "rating": 8.6},
),
Document(
page_content="玩具们在主人不在时会活过来。",
metadata={"genre": "animated", "year": 1995, "rating": 8.3},
),
]
vectorstore = Chroma.from_documents(
docs,
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
collection_name="self_query_demo",
)
metadata_field_info = [
AttributeInfo(name="genre", description="电影类型", type="string"),
AttributeInfo(name="year", description="电影上映年份", type="integer"),
AttributeInfo(name="rating", description="电影评分,范围 1-10", type="float"),
]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
retriever = SelfQueryRetriever.from_llm(
llm,
vectorstore,
"电影简介",
metadata_field_info,
enable_limit=True,
verbose=True,
)
results = retriever.invoke("找两部 2010 年之后评分高于 8 分的科幻电影")
for doc in results:
print(doc.page_content, doc.metadata)

3.4 适用场景#

  • 商品、电影、论文、工单等 metadata 丰富的数据集
  • 用户经常把筛选条件写在自然语言里的场景
  • 不希望继续维护脆弱的手写解析规则

3.5 不适用场景#

  • metadata 很脏、字段含义不稳定
  • 底层向量库不支持过滤
  • 用户问题几乎没有结构化条件,纯语义检索就够

4 EnsembleRetriever 与混合检索#

4.1 关键词检索与向量检索#

向量检索擅长语义相似,但对某些精确关键词并不总是最优:

  • PEP 703
  • 错误码
  • 类名 / 函数名
  • 产品 SKU

这些场景里,关键词检索(如 BM25)往往同样重要。因此实际系统常会采用:

  • BM25 负责关键词召回
  • Dense Retriever 负责语义召回
  • EnsembleRetriever 负责融合排序

4.2 rank fusion#

EnsembleRetriever 的本质不是简单拼接结果,而是做排名融合(rank fusion)
官方 API 中可配置:

  • retrievers
  • weights
  • c
  • id_key

其中:

  • weights 用于控制不同检索器的重要性
  • id_key 用于文档去重识别

4.3 示例#

# pip install langchain langchain-openai langchain-community faiss-cpu rank-bm25
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
texts = [
"PEP 703 提出了让 CPython 支持可选的无 GIL 模式。",
"Python 3.12 改进了错误提示信息。",
"LangChain 使用 Runnable 作为统一抽象。",
"GIL 是 Python 多线程性能讨论中的核心概念。",
]
bm25_retriever = BM25Retriever.from_texts(texts)
bm25_retriever.k = 3
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_texts(texts, embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6],
)
docs = ensemble_retriever.invoke("PEP 703 和禁用 GIL 的实现思路是什么?")
for doc in docs:
print(doc.page_content)

4.4 适用场景#

场景为什么适合混合检索
技术文档搜索既有概念语义,也有 API 名称、版本号、错误码
法律 / 规范文档条款号重要,语义近邻也重要
企业知识库术语别名和自然语言描述并存
商品检索SKU / 品牌词 + 语义需求同时存在

4.5 与 SelfQueryRetriever 的关系#

两者解决的问题并不相同:

  • SelfQueryRetriever:从自然语言中抽出过滤条件
  • EnsembleRetriever:把多个召回来源融合起来

因此在复杂系统里,它们可以组合:

  1. 先用 SelfQueryRetriever 做 metadata-aware dense retrieval
  2. 再和 BM25 / 全文搜索一起做 EnsembleRetriever
  3. 最后接 Reranker

5 高级RAG流水线#

更成熟的 RAG 系统一般会分成几层:

  1. 文档层:Load / Split / Embed / Store
  2. 召回层:Vector / BM25 / SelfQuery / ParentDocument
  3. 融合层:Ensemble / MultiQuery / MMR
  4. 精排层:Reranker / Compression
  5. 生成层:Prompt + LLM
  6. 会话层:History-aware rewrite + history-aware answer

6 常见错误与排查#

  • 只把 chat_history 用在回答阶段,不用在检索阶段
  • 使用 SelfQueryRetriever,但没有明确 metadata schema
  • 混合检索没有做去重,导致上下文重复
  • 高级检索能力堆得过多,延迟失控

7 总结#

7.1 三类策略各自解决什么问题#

策略解决的问题核心收益
create_history_aware_retriever多轮对话中的代词、省略、上下文引用让检索问题更完整
SelfQueryRetriever自然语言中的结构化筛选条件让检索更可控
EnsembleRetriever单一路径召回不稳让召回更全面

7.2 一句话总结#

一句话总结

对话式 RAG 解决”用户没把问题说全”,SelfQueryRetriever 解决”用户把筛选条件说在自然语言里”,EnsembleRetriever 解决”单一检索器不够稳”。三者合起来,才更接近真实生产环境里的检索层设计。


相关笔记

对话式RAG与检索策略
https://fuwari.vercel.app/posts/ai/llm/langchain/notes/05_rag/03_对话式rag与检索策略/
作者
OopsYanxi
发布于
2026-04-27
许可协议
CC BY-NC-SA 4.0