返回文章列表

RAG 检索后处理:不是多召回几个结果,而是把证据整理成能回答问题的样子

聚焦 rerank、去重、压缩和候选整理,说明检索结果如何变成真正适合模型作答的证据集。

RAG 检索后处理:不是多召回几个结果,而是把证据整理成能回答问题的样子

前面几篇文章里,我们已经把资料整理成知识库,也给它准备了更容易被找到的索引。

走到检索阶段之后,很多人会自然地产生一个想法:

既然已经能从向量库里搜出相关 chunk 了,那是不是直接把这些 chunk 塞给大模型就行?

在 demo 里,这样经常能跑通。

但在真实 RAG 系统里,这一步往往不够。

因为“检索到了”不等于“排在前面”,排在前面不等于“证据完整”,证据完整也不等于“适合直接塞进上下文”。

一个用户问题背后,系统可能先召回 20 个、50 个甚至 100 个候选片段。这里面通常会混着:

  • 真正能回答问题的核心证据
  • 只和问题沾边的背景材料
  • 表达相似但答案方向相反的旧文档
  • 同一段内容的重复 chunk
  • 被切碎后缺少上下文的半句话
  • 权限、版本、时间范围不合适的资料
  • 很长但只有一小句有用的段落

如果把这些东西原封不动塞给大模型,模型并不会自动变聪明,反而可能被噪声带偏。

所以 RAG 在“检索”和“生成”之间,还需要一个很关键的环节:

检索后处理。

它要解决的问题不是“怎么找到更多候选”,而是:

在有限上下文预算里,怎样把最能回答问题、最可信、最完整的证据交给模型。

还是看公司制度问答助手里的那个问题:

我去上海出差,高铁二等座和酒店每天最多能报多少?

这个问题看起来很短,但它要求系统同时找到城市、交通工具、住宿标准、金额、适用版本和证据来源。

如果检索后处理做得好,模型拿到的是一组干净证据。

如果做得不好,模型拿到的是一锅候选片段。

两者生成出来的答案,差距会非常大。

一、故事要从“召回结果不一定排得对”开始

检索后处理 手绘图 1:候选结果整理成证据桌面

先看一个最常见的情况。

用户问:

我去上海出差,高铁二等座和酒店每天最多能报多少?

向量检索返回了 8 个候选 chunk:

1. 差旅报销制度总则:员工出差应遵循节约原则……
2. 上海地区住宿标准:普通员工酒店上限 500 元/天……
3. 交通工具标准:高铁二等座可按实际票价报销……
4. 2023 版差旅制度修订通知:酒店标准后续会调整……
5. 北京地区住宿标准:普通员工酒店上限 450 元/天……
6. 差旅审批流程:出差前需提交申请……
7. 上海地区餐补标准:餐补 100 元/天……
8. 住宿发票要求:发票抬头必须为公司全称……

这里真正回答问题的,是第 2 条和第 3 条。

但它们不一定天然排在最前面。

为什么?

因为第一阶段检索通常更像“粗筛”。它主要看语义相似度、关键词匹配、向量距离、元数据过滤等信号。它能判断“这些内容和出差报销有关”,但不一定能精细判断:

这段是不是直接回答了用户问的两个金额?
它是不是比总则更重要?
它是不是当前有效版本?
它是不是只提供背景,而不是答案?

这就像你去图书馆查资料,系统先给你找出一堆可能相关的书页。但真正写答案前,你还要把最关键的几页放到最上面。

这一步,就是重排。

二、Rerank 为什么会出现:第一阶段负责“广撒网”,第二阶段负责“精排序”

检索后处理 手绘图 2:两阶段检索重排

Rerank 通常翻译成重排。

它的意思很直白:

第一阶段检索先拿回一批候选结果,第二阶段再根据用户问题重新给这些候选排序。

为什么不一开始就用最精细的方法检索?

因为精细判断通常更贵。

向量数据库适合在海量数据里快速找候选,比如从 100 万个 chunk 里先找出前 50 个。但如果你让一个强模型逐条阅读 100 万个 chunk,再判断每个 chunk 和问题的关系,成本和延迟都会爆炸。

所以生产系统里经常采用两阶段思路:

第一阶段:召回
从大量知识库里快速找出一批“可能相关”的候选。

第二阶段:重排
在这批候选里精细判断“谁最能回答当前问题”。

第一阶段追求的是召回率。

也就是:

答案所在的片段,最好先别漏掉。

第二阶段追求的是排序质量。

也就是:

真正有用的片段,最好排到最前面。

这两个目标不一样。

召回阶段宁可多拿一点,重排阶段再把噪声压下去。

可以把它想成面试筛简历:

初筛:先按关键词、经验年限、岗位方向捞出 100 份简历
复筛:再认真读项目经历,把最匹配岗位的人排到前面

RAG 里的 rerank,就是检索结果的复筛。

三、Rerank 到底在看什么

朴素向量检索主要看“问题”和“chunk”在语义空间里是否接近。

但 rerank 可以看得更细。

它不只是问:

这段和问题像不像?

而是更接近在问:

这段能不能直接回答问题?
它是否包含答案所需的关键实体?
它是否包含明确数值、条件或结论?
它是否比其他候选更完整?
它是否和用户的时间、部门、权限、版本约束一致?

还是用差旅问题举例。

下面两段都和问题相关:

A. 员工出差期间,应按照差旅制度进行交通、住宿、餐饮报销。

B. 上海地区普通员工住宿标准为 500 元/天;高铁二等座可按实际票价报销。

向量检索可能觉得 A 和 B 都很像。

但从回答问题的角度看,B 明显更重要。

因为 B 包含:

  • 上海
  • 住宿标准
  • 500 元/天
  • 高铁二等座
  • 实际票价报销

A 只是背景。

Rerank 的价值就在这里:

它把“语义相关”进一步变成“回答有用”。

常见 rerank 方法可以粗略分成几类。

第一类是 cross-encoder reranker

它会把“用户问题 + 候选文本”一起输入模型,让模型直接判断二者的相关性。

注意这个地方和前面的 embedding 检索不一样。

embedding 检索通常是 bi-encoder 思路:

用户问题 -> 编码成一个向量
候选文本 -> 提前编码成一个向量
两个向量算相似度

这种方式速度很快,因为候选文本的向量可以提前算好,查询时只需要做向量搜索。

但它的缺点也很明显:问题和文本是分开编码的,模型没有机会逐字逐句比较“问题里的条件”和“文本里的证据”是否真正对应。

cross-encoder reranker 则是另一种思路:

输入:用户问题 + 候选文本
输出:这段候选文本对这个问题的相关性分数

它会把 query 和 document 放在同一个模型输入里,让 Transformer 在二者之间做完整注意力计算。这样模型可以更细地看到:

用户问的是“上海 + 高铁二等座 + 酒店上限”
候选文本里是不是同时包含这些条件
候选文本是直接回答,还是只提供背景

所以 cross-encoder 的排序通常比单纯向量相似度更准。

代价是它没法像 embedding 那样提前把所有文档都编码好。每次用户提问后,它都要对“问题 + 候选文本”逐条打分。

直白地说,cross-encoder 适合做第二阶段精排,不适合直接在百万级文档里全量搜索。

常见用法是:

向量检索 / BM25 先召回 top 50
-> cross-encoder 对这 50 条逐条打分
-> 按分数重新排序
-> 取 top 5 或 top 8 进入上下文

常用工具和模型包括:

  • sentence-transformersCrossEncoder,常见模型如 cross-encoder/ms-marco-MiniLM-L-6-v2cross-encoder/ms-marco-MiniLM-L-2-v2
  • BAAI 的 bge-reranker 系列,如 BAAI/bge-reranker-baseBAAI/bge-reranker-largeBAAI/bge-reranker-v2-m3
  • FlagEmbedding,可以直接加载 BGE reranker
  • Cohere Rerank API,适合不想自部署模型时使用
  • Jina Reranker API,常用于多语言、代码或文档检索场景
  • LlamaIndex 的 SentenceTransformerRerankCohereRerankJinaRerank
  • LangChain 的 CrossEncoderRerankerCohereRerankJinaRerankOpenVINOReranker

如果你在本地实验,sentence-transformers + bge-reranker 是很常见的组合。

如果你更在意快速接入和效果稳定,Cohere、Jina 这类 rerank API 会更省事。

如果你已经在用 LlamaIndex 或 LangChain,通常不需要自己手写完整流程,只需要把 reranker 挂到 retriever 后面的 postprocessor 或 compressor 上。

第二类是 LLM reranker

也就是让大模型读候选结果,然后按相关性、完整性、可信度重新排序。

它的输入通常长这样:

用户问题:
我去上海出差,高铁二等座和酒店每天最多能报多少?

候选文档:
[1] 差旅制度总则……
[2] 上海地区住宿标准……
[3] 高铁二等座交通标准……
[4] 旧版差旅修订通知……

请选出最能回答问题的 3 条证据,并按重要性排序。

LLM reranker 的优势是灵活。

它不只能判断语义相关,还可以读懂更复杂的排序规则:

  • 正式制度优先于草案
  • 新版本优先于旧版本
  • 直接证据优先于背景说明
  • 同时覆盖多个条件的证据优先
  • 如果两个候选冲突,要保留更权威来源
  • 如果用户问的是“能不能”,要优先找规则边界和否定条件

这类判断有时很难只靠一个 cross-encoder 分数表达。

比如下面两段:

A. 上海酒店标准为 500 元/天。
B. 2025 年正式制度规定:上海普通员工酒店标准为 500 元/天,高管标准为 700 元/天。

cross-encoder 可能觉得 A 很相关,因为它很短、很直接。

但 LLM reranker 可能更偏向 B,因为 B 带了版本、身份条件和制度来源,作为最终证据更完整。

LLM reranker 常见有三种做法。

第一种是 pointwise

让模型逐条给候选打分:

这条候选对回答问题有多大帮助?请给 0-10 分。

它简单,但每条独立判断,容易出现分数尺度不一致。

第二种是 pairwise

让模型比较两条候选谁更好:

候选 A 和候选 B,哪一个更适合作为回答依据?

它更接近排序,但比较次数会变多。

第三种是 listwise

让模型一次读一组候选,直接输出排序后的编号:

请从 10 条候选中选出最相关的 5 条,并按顺序返回编号。

这种方式最接近人类整理资料,也常用于 RAG 后处理。

常用工具包括:

  • LlamaIndex 的 LLMRerank
  • LangChain 的 LLMListwiseRerank
  • 直接用 OpenAI、Anthropic、Gemini 等聊天模型写结构化输出 prompt
  • 在自研系统里让 LLM 输出候选编号、理由、保留/丢弃决策和证据标签

LLM reranker 的问题也很现实:

  • 成本比小型 reranker 高
  • 延迟更明显
  • 候选太多时会被上下文窗口限制
  • 如果 prompt 不稳定,排序结果可能漂移
  • 如果不强制结构化输出,解析会很麻烦

所以它更适合处理复杂问题、少量高价值候选,或者作为 cross-encoder 之后的更精细一层。

一个常见组合是:

向量 / 关键词召回 top 80
-> cross-encoder 先压到 top 15
-> LLM reranker 再选 top 5,并解释保留理由
-> 进入上下文压缩和生成

这样既不会让 LLM 读太多候选,又能利用它对复杂规则的理解能力。

第三类是规则或混合打分。

比如把向量相似度、关键词命中、元数据新旧、权限匹配、文档等级、点击反馈、业务权重一起加权:

final_score =
  vector_score * 0.4
+ keyword_score * 0.2
+ metadata_score * 0.2
+ authority_score * 0.2

真实系统里经常不是只用一种方法,而是混合使用。

比如:

向量检索召回 top 50
-> BM25 / 关键词检索补充 top 20
-> 合并候选
-> 规则过滤掉无权限和过期文档
-> reranker 重排 top 20
-> 取 top 5 进入上下文

这条链路比“向量库 top 5 直接塞给模型”要稳得多。

四、重排之后,还要过滤:不是所有相关内容都应该进入上下文

Rerank 解决的是排序问题。

但检索后处理还不止排序。

有些内容即使相关,也不应该进入最终上下文。

比如:

旧版差旅制度:上海酒店上限为 450 元/天。
新版差旅制度:上海酒店上限为 500 元/天。

两段都很相关,但如果新版已经生效,旧版就应该被过滤,或者至少被标注为历史版本。

再比如:

人力资源部内部草案:上海酒店标准拟调整为 550 元/天。
正式发布制度:上海酒店标准为 500 元/天。

草案也相关,但不能作为正式答案依据。

所以检索后处理通常要做一层过滤。

常见过滤条件包括:

  • 权限:用户有没有资格看到这份文档
  • 时间:文档是否仍然有效
  • 版本:是否是最新正式版本
  • 文档类型:草案、通知、制度、FAQ 的可信度不同
  • 业务范围:是否属于用户所在部门、地区或产品线
  • 语言:是否和用户查询语言匹配
  • 内容质量:是否太短、乱码、解析失败、缺少来源

过滤的核心不是“减少数量”,而是减少错误证据。

因为 RAG 最怕的一类问题不是“没搜到”,而是“搜到了看起来很像真的错误材料”。

模型看到错误材料以后,往往会非常自信地回答错误答案。

这比不知道还危险。

五、去重:重复证据会挤占上下文,也会放大偏见

检索后处理 手绘图 3:融合、去重与上下文压缩

检索结果里还有一种常见噪声:重复。

重复不一定是完全一样的文本。

它可能长这样:

chunk 1:上海地区住宿标准为 500 元/天……
chunk 2:普通员工上海住宿标准为 500 元/天……
chunk 3:差旅制度第 4.2 条:上海酒店上限 500 元/天……
chunk 4:FAQ:上海酒店可以报销 500 元吗?可以……

这些内容都在说同一件事。

适当重复有好处,可以增强证据可信度。但如果上下文里塞了 5 段几乎一样的内容,就会带来几个问题:

  • 挤掉其他必要证据,比如高铁二等座规则
  • 增加 token 成本
  • 让模型误以为某个角度特别重要
  • 降低答案覆盖面

所以后处理阶段通常要做去重或多样性控制。

目标不是只保留一条,而是保留“信息增量”。

比如用户问的是:

我去上海出差,高铁二等座和酒店每天最多能报多少?

最终上下文最好覆盖两个证据面:

证据 1:上海住宿标准
证据 2:高铁二等座报销标准

而不是塞进 5 条上海住宿标准,却漏掉交通规则。

这就是为什么有些系统会在 rerank 后再做 MMR 之类的多样性选择。

它不只看“最高分”,也看“和已选内容是否重复”。

简单说就是:

既要相关,也要互补。

六、融合:多路检索结果需要合并,而不是互相打架

前面讲检索前处理时,我们提到过成熟 RAG 往往不是只走一条检索通道。

一个问题可能同时触发:

  • 向量检索
  • 关键词检索
  • 元数据过滤
  • SQL 查询
  • 图谱查询
  • FAQ 检索
  • 多模态检索

这些通道各有优点。

向量检索擅长语义相似。

关键词检索擅长精确术语、编号、错误码、专有名词。

SQL 擅长结构化统计。

图谱擅长实体关系和多跳路径。

问题是:多路结果回来以后,怎么合并?

比如同一个差旅问题,系统可能拿到:

向量检索:差旅制度说明段落
关键词检索:包含“上海”“高铁二等座”的制度条款
SQL 查询:差旅标准表里上海对应行
FAQ 检索:常见问题“上海酒店报销上限是多少”

这些结果不能简单拼接。

否则可能出现:

  • 同一证据重复出现
  • 不同通道分数不可比
  • SQL 结果很关键但文本短,排序被压低
  • FAQ 表达友好但不是正式依据
  • 图谱结果是路径,不适合直接当正文证据

所以后处理阶段还要做结果融合。

常见做法包括:

  • 按通道归一化分数
  • 使用 reciprocal rank fusion 之类的排序融合方法
  • 给权威数据源更高权重
  • 把结构化结果转成可读证据卡片
  • 保留每条证据的来源类型,方便生成时引用

融合阶段的目标是:

不是让所有通道抢第一,
而是把不同通道提供的证据拼成一组完整上下文。

对于差旅问题,最终上下文可能应该长这样:

证据 A:差旅标准表,上海,住宿上限 500 元/天
证据 B:交通标准条款,高铁二等座按实际票价报销
证据 C:制度版本信息,2025-01-01 生效

这比单纯 top 3 文本块更适合生成答案。

七、上下文压缩:不是把内容变短,而是把无关信息挤出去

即使经过重排、过滤、去重和融合,候选证据仍然可能太长。

比如一个 chunk 里有 1000 字,但真正和用户问题相关的只有两句。

直接塞进去会浪费上下文。

这时就需要上下文压缩。

上下文压缩不是简单摘要。

简单摘要可能会把关键金额、限定条件、原文措辞压丢。

RAG 里的上下文压缩更像是在做:

围绕当前问题,从候选材料里抽出真正有用的证据。

常见压缩方式有三种。

第一种是抽取式压缩。

从原文中摘出和问题直接相关的句子、段落、表格行。

比如原文很长:

差旅住宿标准按城市等级划分。北京、上海、深圳为一类城市。
普通员工一类城市住宿上限为 500 元/天。
部门负责人上限为 700 元/天。
特殊情况需提前审批。

用户问普通员工去上海,压缩后可以保留:

上海属于一类城市;普通员工一类城市住宿上限为 500 元/天。

第二种是过滤式压缩。

让模型或规则判断每个候选片段是否和当前问题有关,无关的直接丢掉。

比如餐补、审批流程、发票抬头对当前问题不是核心,就可以不进入上下文。

第三种是生成式压缩。

让模型把多个候选证据整理成更短的中间上下文。

这种方式很灵活,但也有风险:模型可能在压缩阶段就改写、遗漏或幻觉。

所以生成式压缩最好保留来源引用,并避免把精确数值、法律条款、制度边界过度改写。

可以把上下文压缩理解成 RAG 的最后一道整理工序:

候选材料很多
-> 只保留回答当前问题所需的句子
-> 保留来源、版本、页码、行号
-> 让模型看到的是证据,不是资料堆

压缩做得好,答案会更短、更准、更有依据。

压缩做得差,答案可能看起来清爽,但关键条件被压没了。

八、把检索后处理放回完整流水线

到这里,可以把检索后处理串成一条更完整的链路:

用户问题
-> 查询改写 / 路由 / 过滤条件生成
-> 多路召回候选
-> 合并候选
-> 权限、版本、时间过滤
-> rerank 重排
-> 去重与多样性控制
-> 上下文压缩
-> 证据打包
-> 拼接 Prompt
-> 模型生成答案

画成流程图大概是这样:

07.检索后处理 图 1

注意这里有一个很重要的变化:

早期 demo 可能是:

检索 top 5
-> 塞给模型

生产系统更接近:

先多召回
-> 再筛选
-> 再排序
-> 再压缩
-> 再组织成证据

这不是为了把系统做复杂。

而是因为 RAG 的答案质量,很大程度上取决于模型看到的上下文质量。

模型不是魔法,它只能基于你递给它的材料回答。

检索后处理就是在决定:

模型最后到底看到什么。

九、选修:cross-encoder 和 LLM reranker 的实现原理

检索后处理 手绘图 4:不同 reranker 的取舍

如果只是使用 rerank,你知道“先召回,再重排”就够了。

但如果你想自己选模型、调延迟、做工程优化,就需要稍微理解它们背后的实现差异。

1. 为什么 embedding 检索快,但排序不够细

embedding 检索的核心是提前算向量。

离线阶段:

chunk 1 -> embedding 向量
chunk 2 -> embedding 向量
chunk 3 -> embedding 向量
...
写入向量数据库

查询阶段:

用户问题 -> query embedding
query embedding 和库里的 chunk embedding 做近邻搜索

这种方式快,是因为文档向量已经提前存好了。

但它的比较方式比较粗:

两个向量近不近?

它不擅长处理一些细节判断:

  • 用户问的是上海,候选是不是北京?
  • 用户问的是普通员工,候选是不是高管?
  • 用户问的是新版制度,候选是不是旧版通知?
  • 用户问的是两个条件,候选是不是只回答了一个?

这些细节不是不能体现在向量里,而是会被压缩进一个固定长度向量中,信息会变粗。

2. cross-encoder 为什么更准,但更慢

cross-encoder 不提前给文档单独存一个最终相关性分数。

它每次都把 query 和 document 拼在一起:

[CLS] 用户问题 [SEP] 候选文本 [SEP]

然后送进 Transformer。

因为 query 和 document 在同一个输入里,模型内部的注意力层可以让问题里的 token 和文本里的 token 互相“看见”。

比如:

问题 token:上海
候选 token:上海 / 北京 / 一类城市

问题 token:高铁二等座
候选 token:高铁二等座 / 飞机经济舱 / 交通工具

问题 token:酒店每天最多
候选 token:住宿上限 / 500 元/天 / 餐补

模型最后输出一个相关性分数。

可以粗略理解为:

score = model(query, document)

然后系统按 score 排序。

这种方法更准,是因为它做的是“带着问题读文本”。

但它更慢,也是因为它必须对每个候选都读一遍:

top 50 候选 -> 跑 50 次 query-document 打分
top 200 候选 -> 跑 200 次 query-document 打分

所以 cross-encoder 的典型位置是第二阶段:

先用向量库从 100 万条里召回 50 条
再用 cross-encoder 精排这 50 条

它不是替代向量数据库,而是接在向量数据库后面。

3. LLM reranker 为什么灵活

LLM reranker 的底层不是简单输出一个相关性分数,而是让通用大模型做一次“资料筛选任务”。

它通常会在 prompt 里给模型:

  • 用户问题
  • 候选证据列表
  • 排序标准
  • 输出格式

然后让模型返回:

{
  "selected": [
    {"id": 2, "reason": "包含上海住宿上限"},
    {"id": 3, "reason": "包含高铁二等座报销规则"}
  ]
}

它的优势来自大模型的通用理解能力。

比如你可以在 prompt 里写:

正式制度优先于草案。
新版优先于旧版。
如果候选只提供背景,不要选。
如果候选缺少金额、城市或适用人群,要降权。

这些业务规则,cross-encoder 不一定天然懂,除非你专门训练或微调。

LLM reranker 可以直接通过 prompt 注入这些规则。

但它的问题也来自这里:

规则越复杂,prompt 越长
候选越多,token 成本越高
输出越自由,解析越麻烦

所以工程上通常会要求 LLM reranker 做结构化输出,只返回候选编号、排序和简短理由,不让它顺手生成最终答案。

4. 一个简单的自研实现长什么样

如果不用框架,最小 rerank 流程大概是这样:

1. retriever.search(query, top_k=50)
2. 对每个候选构造 query-document pair
3. reranker.predict(pairs)
4. 把分数写回候选
5. 按分数排序
6. 做过滤、去重和 top_n 截断
7. 把最终证据交给 prompt

用伪代码表示:

query = "我去上海出差,高铁二等座和酒店每天最多能报多少?"

candidates = retriever.search(query, top_k=50)

pairs = [
    (query, candidate.text)
    for candidate in candidates
]

scores = reranker.predict(pairs)

for candidate, score in zip(candidates, scores):
    candidate.rerank_score = score

ranked = sorted(
    candidates,
    key=lambda item: item.rerank_score,
    reverse=True,
)

final_context = select_top_with_dedup(ranked, top_n=5)

真实系统会再加上:

  • batch 推理,避免一条一条跑太慢
  • GPU 或推理服务部署
  • 超时和降级策略
  • rerank 结果缓存
  • 候选长度截断
  • 权限和版本过滤
  • 分数归一化
  • 线上日志和评估集回放

直白地说,rerank 的模型原理不复杂:

给 query-document pair 打分。

真正复杂的是把它放进一条稳定、可观测、成本可控的 RAG 链路里。

十、工程上怎么选择:先加 rerank,再逐步补齐其他后处理

如果你正在从 demo 走向可用系统,不需要一开始就把所有后处理都做满。

比较务实的顺序是:

第一步,先记录检索候选。

不要只看最终答案,要把每次问题的 top 20 或 top 50 候选、分数、来源、排序位置记录下来。

因为你要先知道问题出在哪里:

答案根本没被召回?
答案被召回了但排得很低?
答案排得高但被其他噪声干扰?
答案在上下文里但模型没用?

第二步,加入 rerank。

先让第一阶段召回更多候选,比如 top 30 或 top 50,再用 reranker 选出 top 5 或 top 8。

这是性价比很高的一步,尤其适合这种情况:

答案其实在候选里,但经常排不到前面。

第三步,加入元数据过滤。

优先处理权限、版本、时间、文档类型这些硬约束。

这些约束如果不处理,模型很容易引用错误文档。

第四步,做去重和多样性控制。

当你发现上下文里总是重复同一类材料,或者答案只覆盖问题的一半,就该做这一步。

第五步,再做上下文压缩。

当候选证据已经比较准,但上下文仍然太长、太吵、成本太高时,压缩才最有价值。

不要一上来就依赖压缩救场。

如果前面的召回和重排很差,压缩只是在错误材料里提炼错误重点。

十一、怎么评估检索后处理有没有用

检索后处理不能只靠感觉评估。

最容易误判的情况是:你试了一个问题,答案看起来变好了,于是以为 rerank 有效。

但检索质量不是一个 demo 问题,而是一批真实问题上的分布问题。

你至少需要一批问题集,比如 50 个、100 个或更多,覆盖不同类型:

  • 精确事实问题
  • 多条件问题
  • 版本敏感问题
  • 权限敏感问题
  • 多跳关系问题
  • 表格查询问题
  • 容易混淆的相似问题

然后分别观察几个指标。

第一,看召回阶段。

正确答案所在 chunk 是否进入候选集?

如果没进入,rerank 救不了。问题在数据导入、分块、embedding、索引或查询构建。

第二,看排序阶段。

正确答案进入候选集以后,重排前排第几?重排后排第几?

如果答案进入了候选但排得低,rerank 才是对症下药。

第三,看上下文阶段。

最终塞给模型的上下文里,是否包含完整证据?
是否包含冲突证据?
是否包含过期或无权限证据?

第四,看生成阶段。

上下文已经有正确证据时,模型是否真的用了?
答案是否引用了正确来源?
是否把限定条件说清楚?

这几个层次要分开看。

否则你会把所有问题都归因到模型,或者把所有问题都归因到检索。

一个很实用的排查表是:

现象更可能的问题
正确答案没进候选集导入、分块、嵌入、索引、查询构建
正确答案进了候选但排很低rerank、融合、排序策略
上下文里重复内容很多去重、多样性控制
上下文里有旧制度或错误版本元数据过滤、版本治理
上下文太长但有效信息少上下文压缩
上下文正确但回答错Prompt、模型能力、引用约束、生成评估

这个表能帮你避免乱调。

RAG 优化最怕的是:明明召回阶段漏了答案,却拼命改 prompt。

十二、几个常见误区

第一个误区是:top_k 越大越好。

多召回确实能提高答案进入候选集的概率,但 top_k 变大以后,噪声也会变多。如果没有 rerank、过滤和压缩,更多候选可能反而让模型更乱。

更合理的思路是:

召回阶段适当放大 top_k
后处理阶段严格筛选
最终上下文保持精简

第二个误区是:rerank 可以解决所有检索问题。

Rerank 只能重排已经召回的候选。

如果答案根本没进候选集,它没有材料可排。

所以 rerank 不能替代好的数据导入、分块、嵌入和查询构建。

第三个误区是:压缩就是摘要。

摘要追求短,RAG 压缩追求和当前问题相关。

对于制度、合规、财务、代码这类场景,压缩时尤其要保留原文里的数字、条件、否定词、版本和引用位置。

第四个误区是:只看最终答案,不看中间证据。

RAG 系统必须能观察中间链路。

你要能看到:

召回了什么
过滤掉什么
为什么重排
最终给模型看了什么
模型引用了什么

否则优化就会变成凭感觉调参。

十三、回到一句话:检索后处理是在给模型整理证据桌面

现在再回头看最开始的问题:

我去上海出差,高铁二等座和酒店每天最多能报多少?

一个粗糙 RAG 系统可能这样做:

向量检索 top 5
-> 直接塞给模型

它可能刚好答对,也可能被总则、旧制度、审批流程、餐补标准带偏。

一个更稳的系统会这样做:

先召回更多候选
-> 过滤掉无权限、旧版本、草案和低质量内容
-> 用 rerank 把真正能回答问题的证据排到前面
-> 去掉重复片段,保留住宿和交通两个证据面
-> 压缩出和问题相关的句子
-> 带着来源、版本、页码交给模型

这时模型拿到的不是一堆资料,而是一张整理好的证据桌面:

证据 1:上海普通员工住宿上限为 500 元/天,来源:2025 版差旅制度
证据 2:高铁二等座可按实际票价报销,来源:交通工具报销标准
证据 3:制度自 2025-01-01 起生效

模型基于这组证据回答,才更可能准确、简洁、可追溯。

所以,检索后处理的核心可以记成一句话:

召回负责把可能有用的材料找回来,rerank 和后处理负责把这些材料整理成模型真正该看的证据。

如果说数据导入和分块决定“知识能不能被找到”,检索前处理决定“问题能不能问对”,那么检索后处理决定的就是:

找到以后,模型看到的是证据,还是噪声。