面试知识库

主题 4 · Rerank 和截断优化#

定位:多路召回分数不可比 → Cross-Encoder 统一标尺 + MMR 去冗余 + token 预算压缩,把”检索到了”变成”检索对了且喂得下”。


一、通用知识#

1.1 核心概念与原理#

Bi-Encoder vs Cross-Encoder

面试怎么讲:

“Bi-Encoder 是’先各自编码再比分数’——query 和 document 各过一遍 Transformer,输出两个向量做 cosine。因为 document 的向量可以离线算好,线上只需要算一次 query 向量然后 ANN 检索,所以速度是 O(1) 级别的——这就是向量检索的核心。但代价是 query 和 document 之间没有交叉注意力,语义交互弱。

Cross-Encoder 是’把 query 和 document 拼成一条输入一起过 Transformer’——[CLS] query [SEP] document [SEP],所有 token 之间有完整的注意力交互。精度高很多(因为模型能看到 query 的哪个词和 document 的哪个词在语义上对齐),但每个 (query, document) 对都要跑一次前向传播,推理成本是 O(N)。所以实际部署时只用在精排阶段——先用 Bi-Encoder 从几十万条里召回 top-30,再用 Cross-Encoder 对这 30 条精排。”

架构对比:

Bi-Encoder(独立编码):
  query  → [Transformer] → vec_q  ─┐
                                    ├─ cosine(vec_q, vec_d) → score
  doc    → [Transformer] → vec_d  ─┘
  ✅ doc 向量离线预算,线上 O(1)
  ❌ 无交叉注意力,语义交互弱

Cross-Encoder(交叉注意力):
  [CLS] query [SEP] doc [SEP] → [Transformer] → score
  ✅ 全 token 交叉注意力,精度高
  ❌ 每对 (q,d) 一次前向传播,O(N)
plaintext

MMR(Maximal Marginal Relevance)

公式:

MMR = argmax_{d ∈ R\S} [ λ · Rel(q, d) - (1-λ) · max_{d_j ∈ S} Sim(d, d_j) ]
plaintext
  • R:候选集,S:已选集
  • λ:相关性 vs 多样性的平衡参数(0.5~0.8)
  • Rel(q,d):query-document 相关性(通常用 rerank score 归一化)
  • Sim(d,d_j):document-document 相似度(embedding cosine / n-gram overlap / Jaccard)

面试怎么讲:

“MMR 是贪心算法——每轮从候选集里选一条’既和 query 相关、又和已选结果不重复’的。lambda 控制两头的取舍:lambda=1 退化成纯按相关性排,lambda=0 退化成纯按多样性选。实际调参经验是 0.5~0.8,偏高说明业务更看重相关性(知识 Q&A),偏低说明更看重覆盖面(推荐场景)。我们用 0.7,因为 RAG 场景首要是答对、其次才是信息全面。”

MMR vs DPP(行列式点过程)对比:

维度MMRDPP
算法复杂度O(k*N),贪心O(N^3),需矩阵分解
全局最优否(贪心近似)是(联合概率建模)
工程复杂度低,10 行代码实现高,需构造核矩阵
适用场景top-K 小(<20)需全局多样性保证
RAG 适配主流选择过重,收益不明显

Lost in the Middle(Liu et al., 2023, NeurIPS)

面试怎么讲:

“这篇论文做了一个很重要的实验发现:LLM 对长上下文中间位置的信息利用率显著下降——把关键信息放在 10 条上下文的中间位置,准确率比放在开头或结尾低 20~30 个百分点。这对 RAG 的启示是:top-K 要精不要多。你给 LLM 塞 20 条 chunk,中间那些大概率被忽略;不如只给 5-6 条高质量的,全部落在注意力集中的前部位置。所以 rerank + 截断不是可选的锦上添花,而是必须做的——它决定了 LLM 能不能’看到’你检索到的好内容。”

核心实验结论:

  • 20 条文档输入时,关键信息在第 10 位置,准确率从 ~75%(首位)跌到 ~50%
  • 首尾位置受关注更多(U 型注意力分布)
  • 即使 32K/128K 窗口也不免疫——窗口大不等于利用率高

上下文压缩策略

策略原理压缩比额外 LLM 调用适用场景
LongLLMLinguatoken 级语义压缩,保留高 perplexity token2-5x需小 LM 计算 perplexity超长上下文(>10K token)
Extractive(抽取式)按句 / 段打分,保留 top 句子2-3x可选通用,工程简单
Abstractive(摘要式)LLM 对每条 chunk 生成摘要3-10x每条 chunk 一次文档很长、需高度压缩
RECOMP训练专用压缩器(encoder-decoder)3-5x需微调模型有标注数据时
截断式(DocMind 方案)单条限长 + 总量 token 预算1.5-3x中等长度、延迟敏感

面试怎么讲:

“上下文压缩有从轻到重好几个层次。最轻的是纯截断——单条 chunk 超过 N 字就截,总量超过 token 预算就丢后面的。最重的是 LongLLMLingua 那种 token 级语义压缩,每个 token 算一遍 perplexity 再决定留不留,效果好但额外推理成本高。我们选的是截断式,因为经过前面的 rerank + MMR 之后,进来的 5-6 条 chunk 已经是高质量高多样性的了,简单截断就够了,不值得再加一层 LLM 推理。”

Token 预算管理——为什么不是”越多 chunk 越好”

三角约束:

注意力分布:chunk 越多 → 中间 chunk 被忽略(Lost in the Middle)
成本:context 越长 → input token 越贵(qwen-plus ¥4/M tokens)
延迟:context 越长 → 首 token 延迟越高(TTFT 与 context 长度近似线性)
plaintext

最佳实践:

“上下文窗口利用率有个甜区——不是越大越好。根据经验,RAG 场景给 LLM 喂 2000-4000 token 的上下文是性价比最高的:足够覆盖 4-6 条高质量 chunk,不会触发 Lost in the Middle,首 token 延迟也在可接受范围内。超过这个量,边际收益快速递减,边际成本持续增加。“

1.2 业界主流方案对比(表格形式)#

Reranker 模型对比

模型类型维度/架构延迟(10 docs)多语言成本特点
Cohere Rerank v3API私有~200ms100+ 语言$2/1K queries行业标杆,多语言最强
BGE-Reranker-v2-M3开源/本地568M 参数~80ms(GPU)中英日韩等自部署 GPU 成本开源最强,可私有部署
GTE-Rerank(DocMind)API私有~200ms中英阿里云免费额度DashScope 生态,中文优化
Jina Reranker v2API/开源137M 参数~100ms100+ 语言$1/1K queries轻量,code-aware
RankGPT(LLM-based)LLM 调用GPT-4 级别~2000ms取决于 LLM$20+/1K querieslistwise 排序,精度最高但最贵
FlashRank开源/本地28M 参数~20ms(CPU)英文极低超轻量,纯 CPU

面试怎么讲:

“选 reranker 主要看三个维度:精度、延迟、成本。Cohere 和 RankGPT 精度最高,但 RankGPT 延迟太大(2 秒),不适合实时 RAG。BGE-Reranker-v2-M3 是开源最强,但需要 GPU 部署。我们选 GTE-Rerank 是因为和 DashScope embedding 同一个生态,API 调用简单、免费额度够用、中文效果好。如果要私有部署省延迟,首选 BGE-Reranker-v2-M3。“

1.3 关键论文与技术要点#

论文/工作年份核心贡献面试一句话
Lost in the Middle (Liu et al.)NeurIPS 2023LLM 对长上下文中间位置信息利用率显著下降”top-K 要精不要多,首尾位置更受关注”
CRAG (Yan et al.)arXiv 2024检索结果置信度三档评估 + 自适应策略”HIGH 直通、LOW 兜底、AMBIGUOUS 补搜”
MMR (Carbonell & Goldstein)SIGIR 1998相关性与多样性的贪心平衡”经典算法,RAG 精排后必备去重手段”
RRF (Cormack et al.)SIGIR 2009只看排名不看分数的融合方法”解决多路召回分数不可比的标准方法”
HyDE (Gao et al.)2022用假设文档 embedding 替代 query embedding”解决短 query 与长文档的语义密度鸿沟”
ColBERTv2 (Santhanam et al.)NAACL 2022late interaction 架构,token 级匹配”Bi-Encoder 和 Cross-Encoder 之间的折中”

1.4 常见面试问答#

Q1: Bi-Encoder 和 Cross-Encoder 什么时候用哪个?

A: Bi-Encoder 用在召回阶段——document 向量可以离线算好存入向量库,线上只需算一次 query 向量然后 ANN 检索,速度是 O(1)。Cross-Encoder 用在精排阶段——把 (query, document) 拼成一条输入做交叉注意力,精度高但每对都要跑一次前向传播,只能对 top-N(通常 20-50 条)精排。两者是互补关系:Bi-Encoder 负责”从百万级候选里快速筛出相关的”,Cross-Encoder 负责”从几十条相关的里精确排出最好的”。

Q2: Rerank 延迟怎么优化?

A: 四个方向。第一,控制候选数量——只对 RRF 融合后的 top-30 精排,不是全量候选。第二,超时降级——设置 connect 2s、read 5s 超时,API 不可用时降级到关键词匹配打分,质量下降但不阻塞。第三,模型选型——用 API 级的轻量 reranker(GTE-Rerank ~200ms)而非 LLM-based(RankGPT ~2000ms)。第四,私有部署——如果 API 延迟不满意,可以部署 BGE-Reranker-v2-M3 到本地 GPU,延迟降到 ~80ms。

Q3: MMR 的 lambda 怎么调?

A: lambda 越大越偏相关性,越小越偏多样性。RAG 知识问答场景用 0.7——答对是第一优先级,多样性是次要的;推荐/搜索场景可能用 0.5 甚至更低。调参经验:先设 0.7,如果发现返回的 chunk 高度重复(来自同一文档相邻段落),往下调到 0.6;如果发现返回的 chunk 虽然多样但偏离问题,往上调到 0.8。生产环境建议做成动态配置,热更新不重启。

Q4: Lost in the Middle 怎么缓解?

A: 三个层面。第一,减少 chunk 数量——经过 rerank + MMR + 压缩后只保留 5-6 条高质量 chunk,避免中间位置被忽略。第二,排序优先——最相关的 chunk 放在最前面(Cross-Encoder 精排保证了这一点),因为首位注意力最高。第三,prompt 结构——把关键约束放在 system prompt 的首尾而非中间,利用 U 型注意力分布。最根本的缓解是”精而不多”——宁可少给几条高质量 chunk,不要贪多稀释注意力。

Q5: 为什么不用 LLM 做 rerank(RankGPT 方案)?

A: RankGPT 用 GPT-4 做 listwise 排序确实精度最高,但有三个问题。第一,延迟太高——一次排序 2 秒以上,RAG 场景端到端延迟要控制在 3 秒内,光 rerank 就占了大半。第二,成本太高——每次排序消耗几千 token,每查询成本 $0.02+,是 Cross-Encoder API 的 10 倍以上。第三,不稳定——LLM 输出的排序结果受 temperature 和 prompt 影响,可复现性差。Cross-Encoder 是专门训练的排序模型,精度够用且确定性高,是更工程化的选择。

Q6: 不同来源(向量/BM25/Web)的分数怎么统一?

A: 这正是需要 rerank 的核心原因之一。向量检索返回 cosine 相似度 01、BM25 返回 TF-IDF 分数(任意正数)、Web 搜索(Tavily)返回自带的相关性分数 01——三者量纲完全不同,直接比较没意义。先用 RRF 融合——只看排名不看分数,消除量纲差异;再用 Cross-Encoder 对融合后的候选统一打分——所有候选过同一个模型,输出 0~1 的统一相关性分数。这两步叠加后,不同来源的 chunk 才在同一标尺上可比。

Q7: Rerank 之后还需要做 MMR 吗?Cross-Encoder 不是已经排好了吗?

A: 需要。Cross-Encoder 保证的是每条 chunk 和 query 的相关性排序,但不考虑 chunk 之间的冗余——如果 top-5 全是同一文档相邻段落的不同切片,内容高度重叠,送给 LLM 就是浪费 token。MMR 在 rerank 之后运行,从精排的 top-K 里贪心选出既相关又不重复的子集。两者是串联关系:Cross-Encoder 保证”选出来的都相关”,MMR 保证”选出来的不重复”。

Q8: 上下文压缩用截断够了吗?为什么不用 LongLLMLingua 这种语义压缩?

A: 取决于管线前面做了多少筛选。如果直接拿 50 条原始召回做压缩,截断确实粗糙,语义压缩更好。但我们的管线是 RRF 融合 → Cross-Encoder 精排 → MMR 去重之后,进来的只有 5-6 条高质量高多样性的 chunk——这时候轻量的 query-aware 截断(命中段居中 + 总量 3000 token)就够了,不需要 LongLLMLingua 那种 token 级 LM 推理(算 perplexity),延迟增加但收益很有限。工程上的原则是:把精力花在前面的筛选上,后面的压缩就可以很简单。


二、DocMind 实践#

30 秒口述版#

“多路召回的分数不可比——向量 cosine、BM25 TF-IDF、Web Tavily 分数量纲完全不同,直接比毫无意义。我的做法是:先用 RRF 只看排名不看分数做粗融合,再用 Cross-Encoder(DashScope gte-rerank)对融合后的候选统一打分——注意,即使候选数小于等于 topK 也要调 rerank,因为原始 score 来自不同检索源,不统一标尺会误导下游 CRAG 评分。rerank 之后做 MMR 去重——多样性项默认用向量 cosine(小候选集批量 embed 一次,语义级去重,Jaccard 作零依赖降级兜底),lambda=0.7 偏重相关性。最后是 query-aware 压缩:近重复去除 + 命中段居中截断(不再无脑保头,避免砍掉父块中后段的命中正文)+ skip 装箱按 token 预算控总量。API 失败时降级到关键词频度排序,超时 connect 2s read 5s。全链路 MRR 从 0.421 提到 0.731。“

详细展开#

背景/痛点(Situation)#

DocMind 的多路召回架构(向量检索 Milvus + BM25 Lucene + Web 搜索 Tavily)带来一个核心问题:三路分数量纲不可比

  • 向量检索返回 cosine 相似度,范围 01,但实际分布通常集中在 0.60.9
  • BM25 返回 TF-IDF 分数,范围 0 到任意正数,和 query/document 长度强相关
  • Web 搜索(Tavily)返回自带的相关性分数,范围 0~1 但标尺和 cosine 不同

直接取 union 按原始分数排序会出现系统性偏差——Web 来源的分数经常接近 1.0(Tavily 给自己的结果打高分),如果不做统一打分,Web 结果会被虚高分推到最前面,挤掉 KB 里真正相关的内容。

在 Agentic 循环路径下问题更严重:多轮工具调用累积的 chunks 来自不同迭代、不同检索源,分数分布更加混乱。如果直接用原始 score 当 rerankScore,CRAG 的 fast_high 路径(topScore >= 阈值直通)就会被虚高的 Web 分数误触发。

此外,首轮评测发现一个关键信号:V2(RRF 融合)到 V3(+Cross-Encoder)的 MRR 提升(+0.137)远大于 Recall 提升(+0.077)——说明重排的核心价值不是”找到新文档”,而是”把已经找到的相关文档推到更靠前的位置”。结合 Lost in the Middle 现象,排在前面的 chunk 对 LLM 生成质量的影响远大于排在后面的,所以精排序的收益直接传导到最终答案质量上。

做了什么(Action)#

1. Cross-Encoder 统一标尺(CrossEncoderReranker

调用 DashScope gte-rerank API,对候选集做 (query, chunk) 对的精细化语义排序。关键设计决策:

  • 即使 candidates <= topK 也要调 rerank:代码注释里写明了原因——chunk.getScore() 来自不同检索源(vector cosine / web Tavily / BM25 RRF),量纲不一致,直接当 rerankScore 会让 Web 来源永远拿到接近 1.0 的虚高分,误导 CRAG 的 fast_high 判定。这个决策是被真实 trace 验证的,不是理论推导
  • 超时防护@PostConstruct 初始化带超时的 RestTemplate(connect 2s, read 5s)。裸 new RestTemplate() 无超时,DashScope 限流/网络抖动时会无限挂起,占着 worker 线程不释放(多跳下会逐跳放大)
  • 降级策略:API 异常时自动降级到 fallbackRerank——基于查询词在 chunk 中的关键词匹配频度打分,公式 rerankScore = originalScore * 0.6 + matchScore * 0.4,保留原始分数的相对排序并叠加关键词信号

2. MMR 多样性重排(MMRDiversifier

在 Cross-Encoder 精排之后运行,从 top-K 里按 MMR 贪心选择:

  • 相似度计算默认用向量 cosine(I-perf-4):MMR 候选集小(精排后 ≤ targetK),一次性批量 embed 全部候选(单次往返)再 O(K²) cosine——能识别”换同义词讲同一件事”的语义重复、也不把字面重叠高但语义不同的中文误判冗余。任何失败(embedding 异常 / 维度不一致 / 显式配 mmr.similarity=jaccard)自动整体降级字符 bigram Jaccard,零额外依赖、永不阻断;cosine 后端对零向量/空串占位有 NaN 防护
  • lambda=0.7:通过 AiConfigHolder 动态配置(mmr.lambda),读取后钳制到 [0,1](误配 >1 让多样性项 (1−λ) 变负反而奖励冗余)。0.7 意味着 70% 权重看相关性、30% 看多样性,RAG 问答场景偏重”答对”
  • relevance 归一化:用 rerankScore / maxRelevance 归一化到 0~1,避免 rerank 分数和相似度量纲不对等
  • bigram cache:对所有候选的 bigram 集合预计算并缓存(buildBigramCache),避免 O(k^2) 轮次里重复计算

3. query-aware 压缩(内联到 CrossEncoderReranker.compress,#35)

ContextCompressor 独立组件只有三步逻辑(去重、保头截断、限总量),精排后只剩 6 条 chunk,独立组件价值低,下沉到 reranker 内部省一层抽象;并升级为 query-aware 四步

  • 精确去重:使用 RetrievedChunk.dedupeKey()——优先用稳定的 id,缺失时退化到内容前 100 字符 hash。取代散落在各处的 content.substring(0, 50/60) 前缀 key——模板化开头(“第十二条…”、”## 参数说明…”)的不同 chunk 会被前缀 key 误判为同一条而静默丢证据
  • 近重复去除compress.near_dup_enabled 默认开):包含关系 + 字符 bigram Jaccard ≥ compress.near_dup_jaccard(默认 0.85)→ 抓父/子块重叠、web boilerplate 这种”id 不同但内容雷同”的冗余
  • query-aware 命中段居中截断(取代旧的无脑保头):单条超 800 字时以命中段为中心取窗口——做了父块展开后命中正文常落中后段,保头会砍掉命中段造成”检索到却答不出”。命中段定位优先用展开前子块原文(getChildContent()),其次 query 词命中位,都没有才退回保头;窗口做句界吸附避免从句中切
  • 总量控制(skip 装箱):按 token 预算(语种感知 estimateTokens:CJK 0.6/字、Latin 4/字符、代码 2.5/字符,计空白+向上取整防低估)贪心累加,超预算的块 skip 而非 break——让后续更短的高分块继续装箱,至少保留 1 条

4. 防御性拷贝机制(RetrievedChunk.copy/copyAll

RRF / rerank / MMR 等算子会就地 mutate chunk 的 score / rerankScore / source 等字段。当同一 chunk 实例被多个并行子问题或 agentic 循环的不同跳共享时,就地写入会跨上下文串台(data race)。在 Worker 产出进入编排层的边界做 copy(),保证每个路径持有独立的 chunk 图。

5. Agentic 循环的统一收尾(AgenticSearchOrchestrator.finalize

多轮工具调用累积的 chunks 来源不同、分数不可比,必须做一次统一收口:

累积所有工具调用产出 → copyAll 防御拷贝 → dedupeKey 去重
→ Cross-Encoder rerank(对并集一次,统一标尺)
→ MMR diversify → compress → CRAG grade
plaintext

这个收尾管道在一次性检索路径(SupervisorAgent.oneShotRetrieval)和 agentic 路径(AgenticSearchOrchestrator.finalize)中复用同一组件,保证两条路径的输出质量一致。

量化结果(Result)#

  • Cross-Encoder 引入后 MRR 提升 +0.137(0.594 → 0.731),说明精排把相关文档从平均第 2.4 位推到第 1.3 位
  • 全链路 MRR 提升 +0.310(0.421 → 0.731),覆盖了 RRF 融合 + Cross-Encoder 的累计收益
  • 忠实度提升 +0.067——来源不是”检索到了新文档”,而是”最相关的 chunk 被排到最前面”,LLM 更充分地利用了它(Lost in the Middle 效应的具体体现)
  • 降级机制生效:评测期间触发过 DashScope 限流(429),自动降级到关键词排序,评测未中断

代码锚点#

类/方法路径职责
CrossEncoderRerankerservice/rag/CrossEncoderReranker.javarerank + compress + fallbackRerank + callDashScopeRerank,统一精排+压缩
CrossEncoderReranker.rerank()同上调 DashScope gte-rerank API,即使 candidates<=topK 也调
CrossEncoderReranker.compress()同上query-aware 压缩:精确去重 + 近重复去除 + 命中段居中截断 + skip 装箱,替代原 ContextCompressor
CrossEncoderReranker.fallbackRerank()同上API 异常降级:关键词频度 + 原始分数加权排序
CrossEncoderReranker.initRestTemplate()同上带 connect/read 超时的 RestTemplate 初始化
MMRDiversifier.diversify()service/rag/MMRDiversifier.javaMMR 贪心选择,多样性项默认向量 cosine(I-perf-4),失败降级 bigram Jaccard;λ clamp[0,1]
MMRDiversifier.jaccardBigram()同上字符 bigram 集合的 Jaccard 系数计算(cosine 失败时的零依赖降级后端)
RetrievedChunk.copy()service/rag/RetrievedChunk.java深拷贝,防止跨上下文 mutate 串台
RetrievedChunk.dedupeKey()同上统一去重 key:id 优先,缺失退化到 content hash
AgenticSearchOrchestrator.finalize()agent/supervisor/AgenticSearchOrchestrator.javaagentic 循环的统一收尾:去重→rerank→MMR→compress→CRAG
SupervisorAgent.fuseRerankCompress()agent/supervisor/SupervisorAgent.java一次性检索路径的 RRF→rerank→MMR→compress 管道
RRFFusion.fuse()service/rag/RRFFusion.javaRRF 融合:只看排名不看分数,dedupeKey 去重
RetrievalGrader.grade()service/rag/RetrievalGrader.javaCRAG 三档评估,基于 rerankScore 的快路径 + 灰区仲裁

量化数据#

指标数值基线来源
MRR(+Cross-Encoder)0.7310.594(V2 RRF only)52 条离线评测
MRR(全链路)0.7310.421(V1 纯向量)52 条离线评测
MRR 增量(V2→V3 重排)+0.13752 条离线评测
Recall@5 增量(V2→V3)+0.07752 条离线评测
忠实度增量(V2→V3)+0.06752 条离线评测
connect timeout2s无超时reranker.connect_timeout_ms
read timeout5s无超时reranker.read_timeout_ms
MMR lambda0.7mmr.lambda 动态配置
chunk max chars800 字DEFAULT_CHUNK_MAX_CHARS
token 预算3000 tokenrag.context_max_tokens
rerank top-K8retrieval.rerank.top_k
rerank 延迟~200-400msDashScope gte-rerank API

三、追问应对#

面试官想听到的信号#

  • 清楚 Bi-Encoder/Cross-Encoder 的计算成本差异,知道为什么精排只能对 top-N 做
  • 理解”不同检索源分数不可比”这个核心问题,不是简单的”加个 rerank 就好了”
  • 知道 Lost in the Middle 对 RAG 的具体影响——top-K 要精不要多
  • MMR 不是拍脑袋加的——精排后 chunk 可能来自同一文档相邻段落,高度重复浪费 token
  • 降级不是理论说说——有具体的超时配置、降级公式、评测期间真实触发过
  • 压缩策略的选型有取舍思考——不是不知道 LongLLMLingua,是权衡后认为在当前管线深度下截断够用
  • dedupeKey 的演进有工程思考——前缀 key 的误判 bug 是真实踩过的坑

追问预判与应答#

Q1: 你怎么确定 rerank 真的有用,而不是 RRF 融合就够了?

A: 评测数据说明一切。V2(RRF only)MRR 是 0.594,V3(+Cross-Encoder)MRR 提到 0.731,+0.137。更有意思的是 Recall 只提了 +0.077——说明 rerank 不是”找到了新文档”,而是”把已有的相关文档从第 2、3 位推到第 1 位”。结合 Lost in the Middle 效应,排在首位的 chunk 对答案质量的影响显著高于排在第 5 位的,所以 MRR 的提升直接传导到了忠实度 +0.067。

Q2: 即使 candidates 小于 topK 为什么也要调 rerank?这不是浪费一次 API 调用吗?

A: 不是浪费,这是一个被 trace 验证的必要决策。chunk 的原始 score 来自不同检索源——向量 cosine 通常 0.6~0.9,而 Tavily Web 经常给接近 1.0 的分数。如果 candidates 少于 topK 就跳过 rerank、直接用原始 score 当 rerankScore,Web 结果会被虚高分推到最前面,下游 CRAG 的 fast_high 路径(topScore >= 0.65 直通)就会被误触发——模型拿到的是一堆联网搜来的泛泛而谈的内容,KB 里真正相关的被挤掉了。调一次 gte-rerank API 大概 200ms、几毛钱,换来分数标尺统一,非常划算。

Q3: fallback 降级到关键词排序质量差多少?

A: 精度确实有下降,但比完全不排序好。降级公式是 rerankScore = originalScore * 0.6 + matchScore * 0.4——保留原始检索分数的相对排序(60% 权重),叠加查询词在 chunk 中的匹配频度(40% 权重)。评测期间确实触发过(DashScope 限流 429),降级后的 MRR 大约在 V2 和 V3 之间——不如 Cross-Encoder 但比纯 RRF 好。关键是不阻塞用户请求——限流是暂时的,降级几秒后下一个请求可能就恢复了。

Q4: MMR 多样性项为什么从 bigram Jaccard 改成了向量 cosine(I-perf-4)?

A: 早期用 bigram Jaccard 是图它纯本地、零依赖。但 Jaccard 只看字面:① “换了同义词讲同一件事”识别不出(漏判冗余);② “字面重叠高但语义不同的中文片段”又被误判冗余。MMR 候选集很小(精排后 ≤ targetK,通常几十条以内),一次性批量 embed 全部候选只是一次 API 往返、成本可忽略,换来语义级去重,所以默认改成了向量 cosine。关键是保留 Jaccard 作零依赖降级兜底——embedding 异常 / 维度不一致 / 显式配 mmr.similarity=jaccard 时整体降级,永不阻断主流程;cosine 后端对零向量/空串占位还做了 NaN 防护。这是一个”小候选集下,语义准确度 > 省一次 embedding”的取舍翻转。

Q5: 800 字截断会不会丢关键信息?

A: 有这个风险,但被上游的切块策略缓解了。入库时 TextChunker 的切块目标是 400 字,加上 50 字重叠,单条 chunk 通常在 300-500 字之间。800 字的截断线覆盖了绝大多数 chunk——只有极少数(Parent Document Retrieval 的父块 ~1500 字、或 Web 搜索返回的长文本)会被截断。被截断的 chunk 通过 chunk.copy() 深拷贝后修改 content,保留所有元数据,不影响前端的来源展示和引用定位。

Q6: dedupeKey 为什么从 content 前缀改成了 id 优先?

A: 踩过一个真实的 bug。用 content 前 50 字符做 dedupeKey 时,模板化开头的 chunk——比如”第十二条 本规定适用于…”和”第十二条 违反本规定的处理办法…”——前 50 字很可能相同(“第十二条”+ 部分正文),被误判为同一条而静默丢掉其中一条。改成 id 优先后,有 Milvus/MySQL id 的 chunk 用 id 做 key,保证唯一;只有缺失 id 的场景(比如 Web 搜索结果没有 DB id)才退化到 content 前 100 字符的 hash。前缀长度也从 50 放宽到 100,进一步降低碰撞率。

Q7: 你说 compress 下沉到 reranker 内部,这不违反单一职责吗?

A: 表面看是的,但要看具体场景的 trade-off。独立的 ContextCompressor 只有三步逻辑(去重、截断、限总量),精排后只剩 6 条 chunk,去重很少触发,截断也很少触发——整个组件在绝大多数请求中是 pass-through。为了这个 pass-through 多一层抽象、多一个类、多一次方法调用,工程上得不偿失。下沉到 reranker 内部后,compress 方法仍然是 public 的,文档直读路径(不需要 rerank)也能单独调 compress,灵活性没丢。这是一个”组件粒度 vs 抽象开销”的取舍——当组件太轻量时,维护独立类的成本大于复用收益。

Q8: 如果 DashScope API 长时间不可用怎么办?有没有考虑过自部署 reranker?

A: 考虑过。首选是 BGE-Reranker-v2-M3(568M 参数),开源最强、中文效果好,GPU 部署后延迟 ~80ms(比 API 的 200ms 还快),而且没有限流问题。没上的原因是当前阶段 DashScope 免费额度足够(每分钟 30 次),项目规模还没到需要私有部署的程度。但已经在架构上预留了——CrossEncoderRerankerrerankApiUrlrerankModel 都是可配置的,换成自部署的 endpoint 只需要改配置不需要改代码。评测期间撞过 429 限流,是一个真实的预警信号:如果多用户并发,API 额度会成为瓶颈。

Q9: CRAG 评分依赖 rerankScore,rerank 降级后 CRAG 准确度会受影响吗?

A: 会。降级后的 rerankScore 是关键词匹配分数(0~1 范围但分布不同于 Cross-Encoder),CRAG 的阈值(high=0.65, low=0.25)是按 Cross-Encoder 分数分布调的,降级后可能出现偏差。不过 CRAG 的灰区仲裁(heuristic 模式)不只看 topScore,还看 top1-top2 gap 和 avg score 的分布形态,这些相对指标受降级影响小一些。最终的安全网是 needsFallback 只在 compressed 为空时触发——即使 CRAG 判定偏差,只要有证据就不会走 0-chunk 兜底,用户至少能看到有来源的回答。

反击引导#

“rerank 和压缩解决的是’检索到了’到’检索对了且喂得下’的问题——保证 LLM 看到的 chunk 是高质量、不重复、在 token 预算内的。但在 rerank 之前,多路召回的融合策略也很关键——我们用的 RRF 融合是无参数方法,后来做了 Query-Aware 加权扩展,精确查询给 BM25 高权重、语义查询给向量高权重。如果感兴趣可以聊聊检索召回层的设计(→ 03-检索召回优化)。

另外 rerank 之后的 CRAG 三档评分也是一个有意思的设计——HIGH 直通、LOW 带证据走 low-confidence 模板、只有完全无证据才走 0-chunk 兜底。这块涉及可信生成的设计(→ 05-可信生成与评测)。

还有一个’加了又删’的架构演进故事——我原来有独立的 ContextCompressor,后来发现在精排后的场景下它基本是 pass-through,就下沉到 reranker 里了。类似的收敛决策在 Query Decomposition 和 Self-Reflection 上也做过。这些’判断力’故事可以聊聊架构收敛(→ 07-架构收敛)。”