Rerank 与上下文工程:重排、压缩与引用溯源的收益边界
一句话结论:Rerank 解决的是"排序",不是"召回"。它的收益取决于初召回质量与候选集大小;上下文工程的核心不是"塞更多",而是"放对位置、去掉噪声、可溯源"。
适用版本 :LangChain4j
1.23.0/ JDK21
0. 先说结论
先区分三个问题:
- 没召回:正确文档不在候选集里 → Rerank 救不了,要修召回;
- 召回了但排得靠后:正确文档在 Top-20,但不在 Top-5 → Rerank 有用;
- 召回了、也排对了,但模型没用:上下文太长/位置不好/噪声多 → 上下文工程。
很多团队把这三个问题都归因于"模型不行",然后换模型、加 Rerank,最后效果仍然不稳定。
本文按"召回 → 重排 → 压缩 → 注入 → 引用"拆开讲。
1. Rerank 解决的是排序,不是召回
RAG 的召回阶段通常追求"别漏",所以会取 Top-20/Top-50;但最终喂给模型的只有 5--10 条。Rerank 的作用是从候选集中挑出最相关的几条,并排好序。
| 问题 | Rerank 是否有用 | 应该先做什么 |
|---|---|---|
| 正确文档不在候选集 | ❌ | 修分块/混合检索/Query 改写 |
| 正确文档在候选集但排名低 | ✅ | 加 Rerank |
| 候选集噪声太多 | ✅ | Rerank + 压缩 |
| 上下文太长导致忽略 | ⚠️ | 压缩/重排位置 |
| 答案需要多文档整合 | ⚠️ | 提高召回 + 结构化上下文 |
先确认"正确文档在不在候选集"。 不在,Rerank 只是给错误候选排序。
1.1 一个"加 Rerank 没效果"的案例
背景:团队给 RAG 加了 Rerank,nDCG 几乎没变。
排查:
- 正确文档在初召回中排第 35;
- Rerank 只拿到 Top-20 候选;
- 正确文档根本没进候选集。
修复:
- 改分块(表格整表成块);
- 加 BM25 混合召回;
- 正确文档进入 Top-8;
- 再加 Rerank,排到 Top-3。
结论:Rerank 只能"在候选集里挑",不能"把没召回的文档变出来"。先修召回,再谈重排。
2. Bi-Encoder 与 Cross-Encoder
| 类型 | 工作方式 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| Bi-Encoder | query 与 doc 分别编码,算向量相似度 | 快、可预计算 | 交互少、精度有限 | 初召回 |
| Cross-Encoder | query + doc 一起输入模型打分 | 交互充分、通常更准 | 慢、不能预计算 | 重排 |
| LLM Rerank | 让 LLM 对候选打分/排序 | 灵活、可解释 | 贵、慢、不稳定 | 小候选集 |
Rerank 的成本与候选数成正比:20 条候选要跑 20 次 cross-encoder,50 条就是 50 次。候选 K 不是越大越好。
2.1 先判断:你到底需不需要 Rerank
text
1. 统计正确文档在初召回 Top-K 中的比例
2. 如果比例低 → 先修召回(分块/混合/Query 改写)
3. 如果比例高但 nDCG 低 → Rerank 有空间
4. 估算 Rerank 延迟与成本
5. 小流量灰度,成对比较
| 观察 | 结论 |
|---|---|
| Recall@20 高、nDCG@5 低 | Rerank 值得试 |
| Recall@20 低 | 先修召回,Rerank 无用 |
| Recall 高、nDCG 也高 | 不需要 Rerank |
| 答案错但证据在 Top-3 | 上下文/位置/压缩问题 |
先做这个判断,能省掉一半"上了 Rerank 没效果"的无效工作。
2.2 Cross-Encoder 的输入怎么构造
| 项 | 建议 | 原因 |
|---|---|---|
| 输入 | query + [SEP] + chunk |
保留交互 |
| 长度 | 截断到模型上限 | 超长会截掉关键证据 |
| 截断位置 | 保留开头+结尾 | 中间易丢 |
| 批大小 | 按延迟/显存调 | 批太小慢,批太大爆 |
| 元数据 | 不放进打分文本 | 避免噪声 |
截断策略很重要:如果块很长,直接截断可能把答案截掉。优先保留包含 query 词的句子窗口。
3. 一条完整的重排与上下文管线
#mermaid-svg-PPwYESvt6iyUAHY8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PPwYESvt6iyUAHY8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PPwYESvt6iyUAHY8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PPwYESvt6iyUAHY8 .error-icon{fill:#552222;}#mermaid-svg-PPwYESvt6iyUAHY8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PPwYESvt6iyUAHY8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PPwYESvt6iyUAHY8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PPwYESvt6iyUAHY8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PPwYESvt6iyUAHY8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PPwYESvt6iyUAHY8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PPwYESvt6iyUAHY8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PPwYESvt6iyUAHY8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PPwYESvt6iyUAHY8 .marker.cross{stroke:#333333;}#mermaid-svg-PPwYESvt6iyUAHY8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PPwYESvt6iyUAHY8 p{margin:0;}#mermaid-svg-PPwYESvt6iyUAHY8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PPwYESvt6iyUAHY8 .cluster-label text{fill:#333;}#mermaid-svg-PPwYESvt6iyUAHY8 .cluster-label span{color:#333;}#mermaid-svg-PPwYESvt6iyUAHY8 .cluster-label span p{background-color:transparent;}#mermaid-svg-PPwYESvt6iyUAHY8 .label text,#mermaid-svg-PPwYESvt6iyUAHY8 span{fill:#333;color:#333;}#mermaid-svg-PPwYESvt6iyUAHY8 .node rect,#mermaid-svg-PPwYESvt6iyUAHY8 .node circle,#mermaid-svg-PPwYESvt6iyUAHY8 .node ellipse,#mermaid-svg-PPwYESvt6iyUAHY8 .node polygon,#mermaid-svg-PPwYESvt6iyUAHY8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PPwYESvt6iyUAHY8 .rough-node .label text,#mermaid-svg-PPwYESvt6iyUAHY8 .node .label text,#mermaid-svg-PPwYESvt6iyUAHY8 .image-shape .label,#mermaid-svg-PPwYESvt6iyUAHY8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PPwYESvt6iyUAHY8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PPwYESvt6iyUAHY8 .rough-node .label,#mermaid-svg-PPwYESvt6iyUAHY8 .node .label,#mermaid-svg-PPwYESvt6iyUAHY8 .image-shape .label,#mermaid-svg-PPwYESvt6iyUAHY8 .icon-shape .label{text-align:center;}#mermaid-svg-PPwYESvt6iyUAHY8 .node.clickable{cursor:pointer;}#mermaid-svg-PPwYESvt6iyUAHY8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PPwYESvt6iyUAHY8 .arrowheadPath{fill:#333333;}#mermaid-svg-PPwYESvt6iyUAHY8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PPwYESvt6iyUAHY8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PPwYESvt6iyUAHY8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PPwYESvt6iyUAHY8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PPwYESvt6iyUAHY8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PPwYESvt6iyUAHY8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PPwYESvt6iyUAHY8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PPwYESvt6iyUAHY8 .cluster text{fill:#333;}#mermaid-svg-PPwYESvt6iyUAHY8 .cluster span{color:#333;}#mermaid-svg-PPwYESvt6iyUAHY8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PPwYESvt6iyUAHY8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PPwYESvt6iyUAHY8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PPwYESvt6iyUAHY8 .icon-shape,#mermaid-svg-PPwYESvt6iyUAHY8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PPwYESvt6iyUAHY8 .icon-shape p,#mermaid-svg-PPwYESvt6iyUAHY8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PPwYESvt6iyUAHY8 .icon-shape rect,#mermaid-svg-PPwYESvt6iyUAHY8 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PPwYESvt6iyUAHY8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PPwYESvt6iyUAHY8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PPwYESvt6iyUAHY8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-PPwYESvt6iyUAHY8 .client>*{fill:#0E2A33!important;stroke:#22D3EE!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .client span{fill:#0E2A33!important;stroke:#22D3EE!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .client tspan{fill:#E8EEF9!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .recall>*{fill:#0F2A1D!important;stroke:#22C55E!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .recall span{fill:#0F2A1D!important;stroke:#22C55E!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .recall tspan{fill:#E8EEF9!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .candidate>*{fill:#111A2E!important;stroke:#4F8CFF!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .candidate span{fill:#111A2E!important;stroke:#4F8CFF!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .candidate tspan{fill:#E8EEF9!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .rerank>*{fill:#2A1F3D!important;stroke:#A78BFA!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .rerank span{fill:#2A1F3D!important;stroke:#A78BFA!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .rerank tspan{fill:#E8EEF9!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .compress>*{fill:#2A1F0B!important;stroke:#F5A524!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .compress span{fill:#2A1F0B!important;stroke:#F5A524!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .compress tspan{fill:#E8EEF9!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .context>*{fill:#1E293B!important;stroke:#94A3B8!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .context span{fill:#1E293B!important;stroke:#94A3B8!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .context tspan{fill:#E8EEF9!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .answer>*{fill:#0F2A1D!important;stroke:#22C55E!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .answer span{fill:#0F2A1D!important;stroke:#22C55E!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-PPwYESvt6iyUAHY8 .answer tspan{fill:#E8EEF9!important;} Query
初召回
向量 + BM25
候选集
Top-20/50
Rerank
Cross-Encoder
压缩/去重
保留证据
上下文注入
位置/顺序
回答 + 引用
图 1:召回、重排、压缩、注入、引用。每一步都有独立的收益与成本。
3.1 Rerank 的三种实现方式
| 方式 | 代表 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 托管 Rerank API | Cohere/Jina 等 | 接入快、效果好 | 外部依赖、按次计费 | 快速上线 |
| 本地 Cross-Encoder | ONNX/BGE 等 | 数据不出域、可控 | 需要模型/机器 | 合规/成本敏感 |
| LLM 打分 | 通用大模型 | 灵活、可解释 | 贵、慢、波动 | 小候选集 |
LangChain4j 侧通常通过 ScoringModel 与 ReRankingContentAggregator 接入(类名与签名以 1.23.0 为准)。如果使用托管 API,注意:
- 超时与重试;
- 批量打分,减少网络往返;
- 结果缓存(同一 query + 候选可复用);
- 失败时回退到原排序。
3.2 Rerank 与 RRF 的关系
两者不冲突,顺序是:
text
多路召回 → RRF 融合 → 候选集 → Rerank → 最终 Top-K
- RRF 解决"多路结果怎么合并";
- Rerank 解决"合并后怎么精排"。
不要对每一路分别 Rerank 再合并------成本高,且候选重复。先去重/融合,再统一重排。
4. 候选 K 的收益边界
| K | 召回上限 | Rerank 成本 | 风险 |
|---|---|---|---|
| 5 | 低 | 低 | 正确文档可能进不来 |
| 10 | 中 | 中 | 常见起点 |
| 20 | 较高 | 较高 | 延迟上升 |
| 50 | 高 | 高 | 成本/延迟显著 |
怎么定 K:
- 先看正确文档在初召回中的排名分布;
- 如果 90% 的正确文档在 Top-20,K=20 足够;
- 如果大量正确文档在 30--50,先修召回,不要盲目加 K;
- 用 nDCG@K / Recall@K 评估,不要只看"感觉"。
没有"最佳 K"。它由你的数据、召回质量、延迟预算共同决定。
4.1 候选集构建:先去重,再截断
多路召回后,候选集通常有重复:
- 同一块被向量与 BM25 同时召回;
- 同一文档的相邻块内容重叠;
- 父子块同时命中。
处理顺序:
- 按
chunkId去重; - 按文档限制每篇最多 2--3 块;
- 文本相似度去重(可选);
- 按分数截断到 K;
- 保留多样性(MMR 等)。
| 规则 | 作用 | 风险 |
|---|---|---|
| chunkId 去重 | 去重复 | 无 |
| 每文档上限 | 防单文档霸屏 | 可能漏多段证据 |
| 近重复去重 | 去模板/页眉 | 可能误删 |
| MMR | 增多样性 | 增加计算 |
候选集越干净,Rerank 越省、越准。
4.2 候选 K 的实验方法
固定其他变量,只改 K:
| K | nDCG@5 | 答案正确率 | P95 | 成本 |
|---|---|---|---|---|
| 10 | 待测 | 待测 | 待测 | 低 |
| 20 | 待测 | 待测 | 待测 | 中 |
| 50 | 待测 | 待测 | 待测 | 高 |
选择"拐点":再增加 K,指标提升很小,但延迟/成本明显上升。不要因为"候选多更安心"就取 100。
5. 上下文压缩:少即是多
| 方法 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 去重 | 按 chunkId/文本相似度 | 降 Token、降噪声 | 可能误删 |
| 抽取式压缩 | 只保留相关句子 | 保真、可引用 | 需要句子级打分 |
| 生成式压缩 | 让模型总结候选 | 短、连贯 | 可能改写/丢事实 |
| 元数据过滤 | 按版本/权限/时间 | 精准 | 依赖元数据质量 |
| 结构化注入 | 表格/要点/字段 | 模型易读 | 需要预处理 |
压缩的目标不是"越短越好",而是保留回答所需的最小证据集。
生成式压缩要特别小心:它可能把"7 天"改成"一周",把"不支持"改成"支持"。涉及数字、否定、条件的场景,优先抽取式。
5.1 抽取式压缩的实现
java
// 伪代码:按句子打分,保留证据句及上下文
List<String> sentences = splitSentences(chunk.text());
List<ScoredSentence> scored = sentences.stream()
.map(s -> new ScoredSentence(s, score(query, s)))
.sorted(byScoreDesc())
.limit(maxSentences)
.toList();
String compressed = joinWithContext(scored, 1); // 每句带前后各 1 句
打分可以用:
- query 词重叠;
- 句子 embedding 与 query 相似度;
- Rerank 模型对句子的分数。
压缩时必须保留:
- 数字、日期、金额;
- 否定词(不、禁止、除外);
- 条件(如果、仅限、除非);
- 引用所需的来源标识。
如果压缩后句子读不通,宁可不压缩。可读性也是上下文质量的一部分。
6. Lost in the Middle:位置会影响使用
长上下文里,模型对开头和结尾的信息更敏感,中间内容容易被忽略。这不是"模型不认真",而是注意力分布的现实。
三个应对:
- 按相关性排序:最相关的放前面;
- 关键证据放首尾:不要埋在中间;
- 减少无关上下文:压缩比"加更多"更有效。
| 位置 | 模型使用概率 | 建议 |
|---|---|---|
| 开头 | 高 | 放核心结论/最强证据 |
| 中间 | 低 | 放补充材料 |
| 结尾 | 高 | 放需要强调的约束/引用 |
6.1 上下文注入顺序的设计
推荐的结构:
text
[系统约束]
[证据 1 | source=... | page=...]
[证据 2 | source=... | page=...]
...
[用户问题]
[输出格式:结论 + 引用 chunkId]
三个细节:
- 最强证据放最前,次强放最后,补充材料放中间;
- 每条证据带来源标识,方便模型引用;
- 把"只依据证据回答"的约束放在系统消息里,不要只放在用户消息。
如果证据之间冲突,在证据前加"以下资料可能存在版本差异,请以最新版本为准",并保留版本元数据。
6.2 多文档冲突怎么处理
| 冲突类型 | 例子 | 策略 |
|---|---|---|
| 版本冲突 | 新旧制度同时存在 | 按 effectiveDate 取最新,引用版本 |
| 权威冲突 | 官方文档 vs 社区帖子 | 权威排序,优先官方 |
| 范围冲突 | 通用政策 vs 特殊条款 | 特殊优先,并引用两条 |
| 事实冲突 | 两个文档数字不同 | 不强行回答,提示冲突并给来源 |
处理原则:
- 先按元数据(版本/权威/范围)排序;
- 冲突无法消解时,明确告诉用户"资料存在冲突",不要编一个折中答案;
- 引用必须覆盖冲突双方,让用户可判断。
7. 引用溯源:让答案可验证
引用不是"附几个链接",而是"每个结论能定位到证据"。
java
public record Citation(
String chunkId,
String source,
int page,
String quote) {}
三个层次:
| 层次 | 做法 | 价值 |
|---|---|---|
| 文档级 | 回答后列出文档 | 弱,无法定位 |
| 块级 | 每个结论标注 chunkId | 中,可定位 |
| 句子级 | 引用具体句子 + 页码 | 强,可验证 |
实现要点:
- 块元数据保留
docId、page、section、version; - 让模型输出结构化引用(chunkId + quote),服务端校验 quote 是否真的在块里;
- 引用不存在的 chunkId 直接判失败/重试;
- 前端支持点击跳转到原文位置。
引用做得好,用户会更信任;引用造假,比不引用更糟。
7.1 引用格式与校验
让模型输出结构化引用:
json
{
"answer": "签收后 7 天内可无理由退货。",
"citations": [
{
"chunkId": "policy-2026-11#3",
"quote": "签收后 7 天内可无理由退货"
}
]
}
服务端必须校验:
chunkId是否存在于本次上下文;quote是否是块文本的子串;- 页码/来源是否与元数据一致;
- 没有引用的结论是否允许(如"需要人工确认")。
java
boolean valid(Citation c, Map<String, TextSegment> context) {
TextSegment seg = context.get(c.chunkId());
return seg != null && seg.text().contains(c.quote());
}
校验失败时,优先重试一次;仍失败则返回"无法确认来源",不要编造引用。
8. 什么时候不该上 Rerank
- 初召回 Top-5 已经足够准;
- 延迟预算极紧,Rerank 成为瓶颈;
- 没有可用的 Rerank 模型/服务;
- 语料很小,排序差异不大;
- 真正的问题是分块/召回,Rerank 只是掩盖。
Rerank 是"精排补丁",不是"RAG 必备件"。
8.1 上下文预算怎么分
| 部分 | 建议占比 | 说明 |
|---|---|---|
| System/约束 | 10% | 人设、边界、输出格式 |
| 证据 | 60% | 检索/重排后的块 |
| 用户问题 | 10% | 当前问题/历史摘要 |
| 输出预留 | 20% | 模型回答空间 |
如果证据超预算:
- 先去重/压缩;
- 再降 Top-K;
- 最后才截断单块(保留首尾与引用句)。
不要为了塞更多证据而牺牲输出空间;回答被截断比少一条证据更糟。
9. 评估:看排序,也看最终答案
| 指标 | 衡量 | 说明 |
|---|---|---|
| nDCG@K | 排序质量 | 关注相关性等级 |
| Recall@K | 候选集是否包含正确文档 | Rerank 不改变召回 |
| Answer Correctness | 最终答案 | 人工/LLM Judge |
| Citation Accuracy | 引用是否对得上 | 抽样校验 |
| P95 延迟 | 端到端 | Rerank 增量 |
| Token 成本 | 上下文大小 | 压缩收益 |
评估要成对看:Rerank 提升了 nDCG,但 Token 成本翻倍,是否值得?答案取决于业务。
本文不提供未实测的"提升百分比"。用你的黄金集跑前后对比。
9.1 评估实验设计
成对比较,不要跨数据集:
| 项 | 基线 | 实验 |
|---|---|---|
| 候选集 | 同一批 Top-20 | 同一批 Top-20 |
| Rerank | 无 | 有 |
| 模型 | 同一模型/参数 | 同一模型/参数 |
| 数据集 | 同一黄金集 | 同一黄金集 |
| 指标 | nDCG/Recall/答案/延迟 | 同上 |
至少记录:
- 正确文档在候选集中的比例;
- Rerank 前后的 nDCG@5;
- 答案正确率(人工或 Judge);
- P95 增量;
- 失败案例类型。
如果提升只出现在"候选集本来就有正确文档"的子集,说明 Rerank 在做它该做的事;如果整体没提升,先看候选集质量。
9.2 评估结果的解读
- 小样本上的小幅提升可能只是噪声;
- 分场景看,避免平均值掩盖退化;
- 用配对比较(同一 query 前后对比),不要跨数据集;
- 报告置信区间/样本量,不报单一数字;
- 业务指标(答案正确率)优先于排序指标。
Rerank 的评估不是"分数涨了就上线",而是"在目标场景、可接受延迟下,答案真的更好"。
10. 常见失败模式
| 现象 | 根因 | 修复 |
|---|---|---|
| Rerank 后更差 | 候选集本身缺正确文档 | 先修召回 |
| 延迟翻倍 | K 太大/串行 | 降 K、并行、批处理 |
| 引用对不上 | 块元数据缺失 | 补 docId/page |
| 答案忽略证据 | 证据在中间/被压缩掉 | 重排位置、保留证据 |
| 压缩后事实错 | 生成式压缩改写 | 改用抽取式 |
| 成本上升 | 上下文没压缩 | 去重 + 抽取 |
10.1 Rerank 的降级策略
| 故障 | 行为 | 兜底 |
|---|---|---|
| 超时 | 放弃重排 | 返回原排序 |
| 限流 | 降级 | 只对 Top-5 重排 |
| 服务不可用 | 熔断 | 关闭 Rerank 开关 |
| 分数异常 | 丢弃 | 用原排序 |
| 成本超预算 | 降 K | 或按 query 路由 |
三个工程要求:
- Rerank 必须有超时,且小于端到端预算;
- 结果可缓存(query + 候选哈希);
- 监控"重排后 Top-1 变化率",异常时自动降级。
Rerank 是增强项,不能成为单点故障。
11. FAQ
Rerank 能提升召回率吗?
不能。它只重排已有候选;正确文档不在候选集时,Rerank 无能为力。
候选 K 取多少?
看正确文档在初召回中的排名分布。常见起点是 20,结合 nDCG@K 与 P95 调。
Cross-Encoder 一定比 Bi-Encoder 准吗?
通常更准,但取决于模型与领域;成本也更高。用黄金集验证,不要假设。
生成式压缩安全吗?
涉及数字、否定、条件时不安全。优先抽取式;生成式只做辅助,并保留原文引用。
引用一定要句子级吗?
至少块级。强合规/高信任场景建议句子级,并做 quote 校验。
11.1 Rerank 需要和谁一起评估
至少三层一起看:
- 召回层:正确文档是否在候选集;
- 重排层:nDCG/Recall 是否改善;
- 生成层:最终答案与引用是否正确。
只看重排分数,可能忽略"模型根本没用证据"的问题。端到端指标才是上线依据。如果三层无法同时满足,优先保召回与答案正确率,再优化排序与延迟;更不要为了漂亮的重排指标牺牲召回,请务必记住这一点。
12. 上线检查清单
- 正确文档在候选集中的比例已统计;
- K 有评估依据,不是拍脑袋;
- Rerank 有超时与降级;
- 压缩保留数字/否定/条件;
- 关键证据不在上下文中部;
- 引用可定位到块/句子;
- 引用做了存在性校验;
- 有 nDCG/Recall/延迟的回归集。
12.1 一个最小重排接口
java
public interface Reranker {
List<Content> rerank(String query, List<Content> candidates, int topK);
}
实现可以是托管 API、本地模型或 LLM。无论哪种,都要满足:
- 输入候选数有上限;
- 超时可配置;
- 失败时返回原排序;
- 结果可缓存;
- 有单元测试(固定输入、固定输出)。
接口小,替换成本才低。
13. 系列导航
- 上一篇:A09《混合检索实战:BM25 + 向量 + RRF 的工程实现与消融实验》
- 下一篇:A11《RAG 评估体系:黄金集、检索指标与 LLM-as-Judge 落地》
- 全系列:《LangChain4j 生产级实战》20 篇路线图(专栏内查看)