🔍 高级 RAG:检索策略 ★
LC5 章你写过最朴素的 RAG({context, question} | prompt | model)。但生产 RAG 远不止"检索+拼接"——召回率不够、相关性差、上下文太长都是真实痛点。本章精读 LangChain 的检索策略源码(多查询/混合/父子/重排/MMR),构建一个生产级 RAG 管道。
本章目标
- 理解朴素 RAG 的三大痛点及对应解法
- 精读四种文档合并策略(stuff/map_reduce/refine/map_rerank)
- 掌握多查询检索(MultiQuery)、混合检索(Ensemble + RRF)、父子文档、Cross-encoder 重排
- 理解 MMR 多样性检索与 Lost-in-the-middle 重排
- 构建一个完整的高级 RAG 管道
朴素 RAG 的三大痛点
回顾 LC5 的朴素 RAG:query → embed → 向量检索 top-k → 拼进 prompt → LLM 生成。生产中三大问题:
文档合并四种策略
检索到多份文档后,怎么"喂"给 LLM?LangChain 提供四种策略(chains/combine_documents/),是教科书级的取舍对比:
| 策略 | 源码 | 原理 | 优缺点 |
|---|---|---|---|
| stuff | stuff.py:25 | 全部文档塞进一个 prompt | ✓ 简单 ✓ 一次调用;✗ 文档多时超 context |
| map_reduce | map_reduce.py:29 | 先对每份文档单独 map 处理,再 reduce 汇总 | ✓ 可处理超量文档 ✓ 并行;✗ 多次调用贵 |
| refine | refine.py:33 | 逐份文档迭代精炼答案 | ✓ 上下文连贯;✗ 串行慢、不能并行 |
| map_rerank | map_rerank.py:30 | map 后让 LLM 给每份打分,取最高分 | ✓ 自动选最优;✗ 依赖打分准确性 |
默认用 stuff(简单够用)。文档多到超 context 才考虑 map_reduce(可并行)或 refine(要连贯)。map_rerank 适合"多份文档里找最相关的那份"。现代趋势:用 create_stuff_documents_chain(LCEL 版)替代旧的 Chain 类。
多查询检索:提升召回
不同用户问法可能匹配不到同一份文档。MultiQueryRetriever 用 LLM 把一个 query 改写成多个变体,分别检索后合并去重:
class MultiQueryRetriever(BaseRetriever):
"""用 LLM 生成多个 query 变体,扩大召回。
流程:原始 query → LLM 生成 N 个变体 → 每个变体各自检索 → 合并去重
- LineListOutputParser (:23) 解析 LLM 输出的多行 query
- _unique_documents (:45) 去重(同一文档不被多次返回)
"""
混合检索 + RRF 融合
向量检索擅长语义匹配,关键词检索(BM25)擅长精确匹配。EnsembleRetriever 把两者结合,用 RRF(Reciprocal Rank Fusion)融合排名:
class EnsembleRetriever(BaseRetriever):
"""混合检索:多个 retriever 结果加权融合。
典型用法:向量检索(语义)+ BM25(关键词)混合。
weighted_reciprocal_rank (:304) 是融合算法核心。
"""
Reciprocal Rank Fusion(倒数排名融合):对每个文档,在各个检索结果里的排名取倒数求和。score = Σ 1/(rank + c)(c 常数,通常 60)。这样不需要分数归一化(向量分数和 BM25 分数量纲不同),只看排名。简单有效,是混合检索的事实标准。
父子文档:解决上下文长度
检索时用小 chunk(精准匹配),但返回大父文档(完整上下文)。ParentDocumentRetriever(parent_document_retriever.py:11):
Cross-encoder 重排:提升相关性
向量检索(双塔模型)快但粗。检索后用 Cross-encoder(把 query 和 doc 一起喂给模型算精确相关性)重排 top-k:
class CrossEncoderReranker(BaseDocumentCompressor):
"""用 cross-encoder 对检索结果重新打分排序。
与向量检索(双塔,query/doc 分别编码)不同,
cross-encoder 把 query+doc 拼一起输入,算精确相关性分数。
更准但更慢,所以只对 top-N 重排(两阶段检索)。
"""
MMR:多样性检索
top-k 全是相似文档会信息冗余。MMR(Maximal Marginal Relevance)在相关性和多样性间平衡:
class VectorStore(ABC):
def max_marginal_relevance_search(self, query, k=4, fetch_k=20, lambda_mult=0.5):
"""MMR 检索:既相关又多样。
先 fetch_k 个候选(相关),再迭代选 k 个:
每次选「与 query 相关 + 与已选文档不相似」综合最高的。
lambda_mult: 0=最多样, 1=最相关(退化为普通检索)。
"""
Lost in the middle 重排
研究表明 LLM 对prompt 中间位置的信息容易忽略("lost in the middle"现象)。所以检索结果应把最相关的放首尾:
# LongContextReorder:把最相关的文档放到首尾位置
# [最相关, 次相关, ..., 倒数第二, 倒数第一相关]
# 避免"重要信息埋在中间被忽略"
实战:完整高级 RAG 管道
pip install langchain langchain-openai chromadb rank_bm25。下面组合混合检索 + 重排 + Lost-in-middle。
# pip install langchain langchain-openai chromadb rank_bm25
# export OPENAI_API_KEY="sk-..."
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain.retrievers.document_compressors import DocumentCompressorPipeline
from langchain_community.document_transformers import (
EmbeddingsRedundantFilter, LongContextReorder,
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# ① 准备文档(实际从文件加载)
docs = [
Document(page_content="LangChain 是一个 LLM 应用开发框架...", metadata={"source": "intro"}),
Document(page_content="LCEL 用管道 | 组合组件...", metadata={"source": "lcel"}),
# ... 更多文档
]
# ② 切分(父子文档思路:小 chunk 检索)
splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50)
chunks = splitter.split_documents(docs)
# ③ 构建两个检索器:向量(语义)+ BM25(关键词)
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(chunks, embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
# ④ 混合检索(Ensemble + RRF 自动融合)
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.5, 0.5], # 向量和 BM25 各半
)
# ⑤ 检索后处理管道:去冗余 → 重排(关键内容放首尾)
compressor_pipeline = DocumentCompressorPipeline(
transformers=[
EmbeddingsRedundantFilter(embeddings=embeddings), # 去冗余
LongContextReorder(), # 重要内容放首尾
]
)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor_pipeline,
base_retriever=ensemble_retriever,
)
# ⑥ 组装 RAG 链(LCEL,LC5 学过)
model = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_messages([
("system", "根据以下上下文回答。不知道就说不知道。\n上下文:{context}"),
("human", "{question}"),
])
def format_docs(docs):
return "\n\n".join(d.page_content for d in docs)
rag_chain = (
{"context": compression_retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)
# ⑦ 测试
print(rag_chain.invoke("LCEL 是什么?"))
# 流程:混合检索(向量+BM25) → RRF融合 → 去冗余 → 首尾重排 → 生成
对比 LC5 的朴素 RAG,这个管道多了:混合检索(召回↑)、RRF 融合(排名↑)、去冗余(噪声↓)、首尾重排(利用率↑)。每一步都解决一个具体痛点。生产 RAG 就是这种"流水线优化",没有银弹,靠组合。
索引同步:避免重复写入
文档更新后重新入库,怎么避免重复写入?Indexing API(langchain_core/indexing/api.py:296):
from langchain_core.indexing import index
# 基于 RecordManager 的时间戳+内容哈希,自动增量同步
result = index(
docs, # 新文档
vectorstore, # 目标向量库
record_manager, # 记录管理器(追踪已入库)
cleanup="incremental", # 增量:只加新的、改的,删过期的
source_id_key="source", # 用哪个 metadata 字段做去重键
)
print(result)
# → IndexingResult(num_added=2, num_updated=1, num_deleted=0, num_skipped=10)
# 自动跳过没变的、更新变了的、删除消失的
与生产实践对照
| RAG 策略 | OpenCode 对应 | |
|---|---|---|
| 多查询/混合检索 | → | grep + glob 多工具组合检索文件 |
| 父子文档(上下文完整) | → | read 工具按行读 + instruction.resolve 沿目录收集相关指令 |
| 重排/筛选相关 | → | LLM 自己判断哪些文件相关(agentic) |
| Lost-in-middle 重排 | → | compaction 压缩 + reminders 注入关键信息 |
| 索引同步 | → | 无(每次实时检索,不入库) |
有趣的是,OpenCode 作为编码 Agent 不用向量检索 RAG——它用 grep/glob/read 工具实时搜代码,由 LLM 决定搜什么、读什么。这是 Agentic RAG(AD4 详讲)——把检索本身变成 agent 的工具调用。传统 RAG(向量库)适合静态文档库;Agentic RAG 适合动态、结构化、可操作的数据源(如代码库)。
小结
- 朴素 RAG 三痛点:召回低→多查询/混合;相关性差→重排;上下文长→父子/MMR/重排。
- 四种合并策略:stuff(默认)/map_reduce(并行)/refine(连贯)/map_rerank(选优)。
- 多查询扩召回;Ensemble+RRF 混合检索;父子文档平衡精准与完整;Cross-encoder 精排;MMR 多样性;Lost-in-middle 重排。
- 生产 RAG = 流水线组合优化,Indexing API 管增量同步。
下一章 AD4 · Agentic RAG:把 RAG 升级为会自我反思的 Agent——CRAG/Self-RAG/Adaptive RAG,检索后判断够不够好,不够就改写 query 重试。精读 LangGraph 官方 RAG notebook。