编程与 AI12 分钟阅读更新于 2026-07-31

RAG 中的重排与压缩检索:别把召回结果直接塞给模型

围绕相似性检索失败案例、MMR、metadata 过滤、SelfQueryRetriever 与 ContextualCompressionRetriever,讲清 RAG 召回结果为什么要重排、过滤和压缩。

相关工具

召回到了,不等于能直接回答

很多人第一次做 RAG,会把重点放在加载文档、切分 chunk、写入向量库、相似度搜索。这个流程跑通以后,系统确实能从知识库里找出一些相关片段。但资料稍微多一点,就会遇到一个很现实的问题:召回结果看起来相关,真正放进 prompt 后却不好用。

相关概念 在检索章节里展示过几个典型失败。用户问“第二讲里对 Figure 说了什么”,相似性搜索返回的结果里却混进了第一讲。另一个例子里,Top-K 结果前两条内容几乎一样,都在解释同一个主题。还有一种情况是,检索到的 chunk 很长,真正和问题相关的只有其中几句,剩下的内容会占用上下文。

所以 RAG 的检索不能停在 similarity_search。它至少还要做三件事:用 MMR 减少重复,用 metadata 把范围限定清楚,用压缩检索把长片段裁短。召回是把候选材料找出来,重排和压缩是在回答前把材料整理干净。

RAG 检索增强三段式流程图
RAG 检索增强三段式

从初筛召回到 MMR、metadata 过滤、压缩检索,再把干净的证据交给模型生成答案。

相似度搜索最容易输在重复和范围

相似度搜索的逻辑很直接:把用户问题转成 embedding,再去向量库里找距离最近的文本块。它适合大多数普通问题,文中也提到基本语义相似度可以解决相当多的检索需求。但它不是万能的,因为“语义接近”不等于“适合当前问题”。

第一类问题是重复。文档切分时如果 overlap 较大,或者原始资料本来就有重复段落,相似度搜索很可能把几条内容差不多的 chunk 一起召回。模型拿到这些材料后,看到的不是更多证据,而是同一句话被换了几个位置。Top-K 名额有限,重复内容会挤掉真正有补充价值的片段。

第二类问题是范围不准。用户明明问第二讲,相似度搜索却可能命中第一讲,因为第一讲里也有 Figure 这个词。用户问某个产品版本,系统可能把旧版本文档一起带出来。对人来说,问题里的“第二讲”“2025 版”“某个来源”是硬条件;对普通向量检索来说,它们有时只是一段语义。

MMR 解决的是“别都说同一件事”

MMR 的全称是 Maximum Marginal Relevance,文中把它解释为同时考虑查询与文档的相关度,以及文档之间的相似度。简单说,它不是只挑最像问题的几条,而是在相关的前提下,尽量让结果之间有差异。

蘑菇示例很能说明问题。普通相似性搜索会返回两条意思很接近的文本,都在讲毒鹅膏菌和大型子实体。MMR 会保留一条最相关的,再选择另一条不那么重复、但仍有信息增量的内容。对于 RAG 问答,这种差异很重要,因为模型需要的是足够覆盖问题的证据,不是重复确认同一件事。

在实际系统里,MMR 适合放在初筛之后。可以先从向量库拿到较多候选,比如 fetch_k 取大一点,再让 MMR 挑出最终进入上下文的 k 条。这样不会过早丢掉可能有用的材料,也能减少重复 chunk 对上下文的占用。

metadata 过滤解决的是“到底查哪一份资料”

MMR 能减少重复,但它不能自动理解所有业务边界。第二讲案例,根本问题不是结果太重复,而是来源错了。用户问第二讲,答案材料应该来自第二讲。这个时候要靠 metadata 过滤。

metadata 是附在 chunk 上的上下文信息。示例里常见的字段是 source 和 page,source 表示来自哪份文档,page 表示原始页码。企业知识库里还可以加 product、version、department、publishedAt、permission、locale 这些字段。只要字段可靠,检索时就能先限定范围,再做相似度匹配。

很多 RAG 错答不是模型写错,而是检索阶段把不该进来的资料放进来了。制度问答混入旧制度,产品问答混入旧版本,内部知识库混入无权限文档,最后生成答案自然会乱。metadata 过滤的价值,就是把“哪里可以找答案”先说清楚。

SelfQueryRetriever 把用户问题拆成查询词和过滤条件

手动写 filter 很稳,但用户不会总把条件说得规整。文中介绍的 SelfQueryRetriever,就是让 LLM 先读用户问题,再拆出两部分:一部分是 search term,用来做向量搜索;另一部分是 filter,用来过滤 metadata。

比如用户问“他们在第二讲中对 Figure 做了些什么”,系统可以拆出查询词 Figure,同时拆出过滤条件 source 等于第二讲。这样向量数据库检索时,不会在第一讲、第三讲里乱找。示例里还定义了 metadata_field_info,告诉 LLM 有哪些字段可以作为过滤条件,例如 source 和 page。

这类方法尤其适合文档来源多、用户问题又经常带条件的场景。用户可能说“只看 2025 版合同”“不要参考维基百科”“查第三章第 2 节”“按上海地区政策回答”。这些条件如果只交给 embedding,效果不稳定;先抽成 filter,再检索,边界会清楚很多。

SelfQueryRetriever 拆解问题流程图
SelfQueryRetriever 如何拆问题

LLM 先把问题拆成查询词和过滤条件,再交给向量数据库检索对应来源的文档。

压缩检索解决的是“片段太长”

向量库返回的通常是 chunk,不是答案。chunk 为了保证上下文完整,往往会比用户问题需要的内容长。文中提到,如果查询“蘑菇的营养价值”,检索可能返回整篇有关蘑菇的长文档,但真正相关的只是其中几句。直接把整段塞进 prompt,会浪费 token,也会让模型被旁枝信息干扰。

ContextualCompressionRetriever 的做法是先用基础检索器拿候选文档,再用压缩器根据问题抽取相关部分。示例里使用 LLMChainExtractor 作为压缩器,只保留和问题相关的句子。压缩之后,文档更短,内容更贴问题。

压缩检索不等于摘要。摘要会重新组织语言,可能改变细节;压缩更像裁剪,把不相关的部分去掉,把相关句留下来。对于需要可追溯的 RAG,压缩后仍然要保留 source、page 等 metadata,让答案能回到原文。

压缩检索前后对比图
压缩检索前后对比

Compression LLM 从长 chunk 中保留和问题有关的句子,减少上下文浪费。

把 MMR 和压缩放在一起用

后文给出了一种组合方式:先定义压缩器,再把基础检索器设成 MMR。这样做的意思是,底层先尽量返回不重复的候选文档,再让压缩器抽取每个候选里真正相关的句子。

这个顺序适合资料重复度高的知识库。比如多个文档都介绍 Matplotlib,普通相似度搜索会重复返回同一段定义;MMR 会让候选结果更分散;压缩器再把每个候选裁短。最后进入 prompt 的材料既不太重复,也不会太长。

但组合不是越多越好。每加一层处理,就会增加延迟、成本和调试难度。资料少、问题简单时,普通相似度搜索可能已经够用;文档多、重复高、来源复杂时,再上 MMR、metadata、自查询和压缩。RAG 优化要从真实失败案例出发。

调参时别只看最终回答

调 RAG 很容易只盯最终答案。答案看起来顺,就以为检索没问题;答案错了,又把锅甩给模型。更稳的办法是把检索过程拆开看:初筛召回了哪些 chunk,MMR 去掉了哪些重复,metadata 过滤是否生效,压缩器留下了哪些句子。

可以为每条测试问题保存一份记录:问题、过滤条件、候选文档、重排结果、压缩后片段、最终答案、人工判断。这样一看就知道错误发生在哪一层。范围错了,先查 metadata;重复太多,调 MMR;片段太长,调压缩;答案没用上证据,再看 prompt。

RAG 的质量不是靠某个检索器名字撑起来的。真正有用的是一条清楚的证据链:用户问了什么,系统去哪里找,为什么选这些片段,哪些内容进了模型,最终答案有没有引用这些证据。把这条链路看明白,重排和压缩才有意义。

常见问题

MMR 和普通相似度搜索有什么区别?

普通相似度搜索只看问题和文档的相似度,MMR 还会考虑已选文档之间的相似度,尽量减少重复结果。

metadata 过滤应该放在什么时候?

通常应尽早使用。先用 source、page、version、权限等字段限定范围,再做语义检索,能减少错误来源进入上下文。

压缩检索会不会丢信息?

可能会,所以要保留原始 source 和 page,并用测试集检查压缩后是否仍包含回答所需的关键句。

RAG、知识库与长期记忆

继续阅读

返回专题