我给 RAG 检索加了个相似度阈值过滤、大于零点八才算相关想滤掉噪声,结果有的提问明明库里有答案却召回一片空白、有的提问又混进一堆牛头不对马嘴的片段,我反复调那个阈值怎么都调不出一个对所有问题都合适的值,最后才想通相似度的绝对分数根本不能跨查询用同一把尺子去卡
这是一次让我把 RAG 里"相似度阈值"这件事,从"设个分数线滤掉不相关的",重新理解成"相似度的绝对分数跨查询不可比、用一个固定阈值卡所有查询必然失效"的事故。我给 RAG 检索加了个相似度阈值过滤,大于 0.8 才算相关,想滤掉噪声。结果有的提问明明库里有答案却召回一片空白、有的提问又混进一堆牛头不对马嘴的片段。我反复调那个阈值,怎么都调不出一个对所有问题都合适的值。最后才想通:相似度的绝对分数,根本不能跨查询用同一把尺子去卡。这篇就把这次"固定相似度阈值卡不准"的事故,从头到尾复盘一遍。
故障现场:同一个阈值,有的查询全砍光、有的放太松
我的 RAG 检索本来是"把查询转成向量,找最相似的几个块"。为了不让"勉强沾边"的块混进去喂给大模型,我加了一道过滤:只保留余弦相似度 ≥ 0.8 的块,低于 0.8 的当作不相关丢掉。我觉得这样很干净。
可上线后效果两极分化。有些提问,明明库里就有非常相关的文档,却被告知"没有找到相关内容"——我一查,那些相关块的相似度是 0.7 多,没够到 0.8,被全砍光了。另一些提问,却召回了一堆明显不相关的片段——那些块的相似度居然有 0.85,过了线。我于是开始调阈值:调低到 0.7,前一类问题好了,可后一类问题混进的噪声更多了;调高到 0.85,后一类干净了,可前一类更多问题召回为空。无论我把这条线画在哪,总有一批查询被它坑。我百思不得其解,直到我把不同查询的相似度分数都打出来一看,才彻底明白根因——不同查询的相似度分数,分布和量级完全不同:有的查询,它和最相关文档的相似度也就 0.7 出头(可能这个查询比较短、比较泛、或用词和文档差异大);有的查询,它和一堆不那么相关的文档相似度都能到 0.85(可能这个查询的措辞恰好和很多文档撞了高频词)。也就是说,"0.8 分"这个绝对数值,在不同查询之间根本没有统一的、可比的含义:对查询 A 来说 0.75 已经是"最相关"了,对查询 B 来说 0.85 可能还是"不相关"。我却用一把固定的、绝对的尺子(0.8),去卡所有这些分数分布迥异的查询——结果必然是:对那些整体分数偏低的查询,这把尺子太高、把真正相关的也砍了;对那些整体分数偏高的查询,这把尺子太低、把不相关的也放进来了。这条线画在哪都不对,因为根本不存在一个对所有查询都合适的固定阈值。
# 我的写法: 固定绝对相似度阈值过滤
def retrieve(query, threshold=0.8):
hits = vector_db.search(embed(query), top_k=20)
return [h for h in hits if h.score >= threshold] # ★ 一刀切 0.8
# 不同查询的相似度分布完全不同:
# 查询A(短/泛/用词独特): 最相关块也才 0.72 → 全被 0.8 砍光 → 召回为空
# [0.72, 0.68, 0.65, ...] ✗ 明明有答案却说没找到
# 查询B(措辞和很多文档撞高频词): 不相关块都有 0.85 → 全过线
# [0.86, 0.85, 0.84, ...] ✗ 一堆不相关的混进来
# 调低阈值 → A 好了 B 更脏; 调高阈值 → B 干净了 A 更空
# → 不存在一个对所有查询都合适的固定阈值!
# 根因: 相似度绝对分数跨查询不可比 —— 分布和量级随查询/embedding 而变
# "0.8" 对 A 是高不可攀, 对 B 是稀松平常; 同一把尺子卡所有查询必然失效
问题被钉死在这个认知错位上:我以为"相似度分数是个绝对的、统一的'相关程度'刻度,0.8 就代表'挺相关'",但相似度的绝对值,跨查询根本不可比:它的分布和量级,会随查询本身(长短、泛特、用词)和 embedding 模型而剧烈变化。同一个 0.8 分,对一个查询是"高不可攀的最高分",对另一个查询可能只是"稀松平常的中等分"。我用一个固定的绝对阈值,去裁决所有这些分数分布迥异的查询,等于用同一把刻度的尺子,去量一堆量纲都不一样的东西——必然有的被量得太长、有的太短。这条阈值线无论画在哪个绝对值上,都只是"恰好适合分数分布在那个区间的查询",对分布偏高或偏低的查询统统失效。我把"一个查询内部、不同结果之间的相对高低"(这是有意义的),误当成了"跨所有查询都通用的绝对相关度"(这是没有的)。我以为我有一把能量遍所有东西的标准尺子,其实每个查询的分数都是用它自己那把没刻度统一过的尺子量出来的,我拿一个固定刻度去卡,自然处处对不上。
第一件事:想明白相似度绝对值不可跨查询比较
把这次事故彻底想清楚,关键是理解向量相似度分数(余弦相似度等)的绝对值,只在"同一个查询的不同候选结果之间比较相对高低"时有意义(它能告诉你"对这个查询,A 比 B 更相关"),而不具备跨查询的、统一的"相关程度"含义。因为相似度的分布和量级,会随查询的特性(长度、宽泛度、用词独特性)、文档库的内容、以及 embedding 模型而显著变化——同样是"最相关的那个结果",不同查询给出的相似度可能从 0.6 到 0.95 不等。所以用一个固定的绝对阈值去过滤所有查询,必然在分数分布偏离这个阈值的那些查询上失效(偏低的全砍、偏高的全放)。
这就引出了正确的做法:不要依赖固定的绝对相似度阈值来判断"相不相关",而要用对查询特性更鲁棒的方式——其一,用 top-K(取最相似的 K 个,不管绝对分数多少),把"选多少"和"绝对分数"解耦;其二,用 相对阈值(如只保留"达到本查询最高分某个比例"的结果,或分数出现明显断崖的地方截断),让标准随每个查询自适应;其三,真要判断"到底相不相关",交给专门的 rerank/cross-encoder 模型 或让 LLM 判断,而不是用一个拍脑袋的相似度数字;其四,如果非要用绝对阈值,也必须用带标注的评估集去校准这个值(且接受它只是个粗糙的兜底),而不是凭感觉定 0.8。关键认知是:一个"分数/度量"的绝对值,在不同情境下往往含义不同、量纲不一,不能跨情境用同一个固定阈值去裁决;它通常只在"同一情境内比较相对高低"时可靠。要做跨情境的判断,要么用相对的(排名、比例),要么针对每个情境校准,要么换一个真正可比的判据。
# 正解1: 用 top-K, 把"选多少"和"绝对分数"解耦(最常用、最鲁棒)
def retrieve(query, k=5):
return vector_db.search(embed(query), top_k=k) # 取最相似的 k 个, 不卡绝对分
# 正解2: 相对阈值 —— 标准随每个查询自适应(相对本查询最高分)
def retrieve_relative(query, k=20, ratio=0.85):
hits = vector_db.search(embed(query), top_k=k)
if not hits:
return []
top = hits[0].score
# 只保留达到"本查询最高分 ratio 倍"的, 阈值随查询浮动
return [h for h in hits if h.score >= top * ratio]
# 正解3: 真要判相关性 → 用 rerank(cross-encoder)精排, 它给的相关性更可靠
candidates = vector_db.search(embed(query), top_k=20)
reranked = reranker.rank(query, candidates) # 专门的相关性模型打分
final = [h for h in reranked if h.rel_score >= R][:5] # rerank 分更具可比性
# 正解4: 非要用绝对阈值, 必须用标注评估集校准、且当粗兜底而非主判据
# 收集 (query, 相关块, 不相关块), 扫不同阈值看准召回, 选个折中, 别拍脑袋 0.8
想通这一层,我才明白自己错在哪:我把相似度分数当成了一把"全局统一刻度的尺子",以为 0.8 在任何查询上都代表同样的"相关程度",却忽略了这个绝对值的分布和量级随查询而剧变、跨查询根本不可比。我反复调那个固定阈值,本质是在做一件不可能的事——找一个对所有"量纲都不同"的查询都合适的绝对刻度,这种值根本不存在。根治之道,是放弃固定绝对阈值,改用 top-K、相对阈值、或 rerank 这些对查询特性鲁棒的方式;真要用绝对阈值也得拿评估集校准、只当粗兜底。不是去找一个万能的绝对分数线,而是认清相似度绝对值跨查询不可比,改用相对的、或按情境校准的判据。
第二件事:正解——用 top-K/相对阈值/rerank,别用固定绝对阈值
找到根因,正解就清晰了:别用固定的绝对相似度阈值判相关,改用对查询特性鲁棒的方式——首选 top-K(取最相似的 K 个,把"选多少"和"绝对分"解耦);要过滤就用 相对阈值(相对本查询最高分的比例、或分数断崖处截断),让标准随查询自适应;真要判"到底相不相关"就上 rerank/cross-encoder 模型或让 LLM 判断;非要用绝对阈值,就拿带标注的评估集校准、且只当粗兜底。
# 错误: 固定绝对阈值, 跨查询失效
return [h for h in hits if h.score >= 0.8] # ✗ 有的全砍有的放太松
# 正解1: top-K, 最常用、最鲁棒(选数量, 不卡绝对分)
hits = vector_db.search(embed(query), top_k=5)
# 正解2: 相对阈值 + 断崖检测(标准随本查询自适应)
def filter_relative(hits, ratio=0.85, gap=0.15):
if not hits:
return []
top = hits[0].score
out = [hits[0]]
for prev, cur in zip(hits, hits[1:]):
if cur.score < top * ratio: # 低于本查询最高分的 85% 就停
break
if prev.score - cur.score > gap: # 出现明显断崖也停
break
out.append(cur)
return out
# 正解3: rerank 精排判相关(cross-encoder 的相关性分更可比)
candidates = vector_db.search(embed(query), top_k=20)
reranked = reranker.rank(query, candidates)
final = reranked[:5] # 取重排后 top5; 必要时再配 rerank 自己的阈值
# 正解4: 让 LLM 在生成时判断"检索到的内容是否真能回答"(而非靠相似度数字)
# prompt: "若下列资料不足以回答, 就说不知道", 把相关性判断交给更强的判据
这套做法的精髓,是不再指望一个固定的绝对分数线去裁决"相关与否",而是改用"同一查询内的相对高低"(top-K、相对阈值、断崖)或"更可比的判据"(rerank、LLM 判断)——这些都不依赖"0.8 对所有查询都意味着相关"这个不成立的假设。top-K 把"要几个"和"分多高"解耦,是最简单鲁棒的;相对阈值让过滤标准跟着每个查询的分数分布浮动;rerank 用专门的相关性模型给出更具可比性的判断。不是去找那个不存在的万能绝对阈值,而是用相对的、自适应的、或更可靠的判据来决定取舍。
【做 RAG 检索过滤, 我现在认死的几条】
1. 相似度绝对值只在"同一查询内比相对高低"时有意义, 跨查询不可比
2. 同样是"最相关结果", 不同查询的相似度可能从 0.6 到 0.95
3. 固定绝对阈值(如 0.8)必在分布偏离它的查询上失效(全砍/放太松)
4. 首选 top-K: 把"选多少"和"绝对分"解耦, 最鲁棒
5. 要过滤用相对阈值: 相对本查询最高分的比例 / 分数断崖处截断
6. 真判相关性用 rerank(cross-encoder)/LLM, 别靠拍脑袋的相似度数字
7. 非用绝对阈值不可, 就拿标注评估集校准、且只当粗兜底
第三件事:其他"把只在局部有意义的分数,当成全局可比的绝对标准"的同类坑
顺着"把一个只在局部/同情境内有意义的分数,当成跨情境通用、可用固定阈值卡的绝对标准"这条线,我把同类的坑都排查了一遍:
第一个,模型置信度/概率当成绝对可信度卡阈值。分类模型输出的"概率 0.9"在不同类别、不同模型上含义不同,用固定阈值(>0.9 才信)跨场景卡,会在校准不同的地方失灵;要按业务校准或看相对排序。
第二个,不同评分体系的分数直接比大小。两个不同算法/数据源给的"评分"量纲不同,直接比或加权相加无意义;要先归一化到可比的尺度。
第三个,用固定绝对值阈值判异常。"响应时间 > 500ms 算异常"对不同接口不通用;有的接口本来就慢,要用相对基线(同比/分位数)而非固定绝对值。
第四个,跨班级/科目直接比原始分数(类比)。不同试卷难度不同,原始分不可直接比,要用排名/标准分;同理跨查询比相似度也得换算。
第四件事:固定绝对阈值 vs top-K/相对/rerank——一张对照表
我把几种检索过滤方式摆在一起对比,核心看"跨查询稳不稳、对分数分布敏不敏感":
| 方式 | 怎么决定取舍 | 跨查询稳健性 | 适用 |
|---|---|---|---|
| 固定绝对阈值 | score ≥ 0.8 才留 | 差, 分布偏离就失效 | 几乎别单独用 |
| top-K | 取最相似的 K 个 | 好, 不依赖绝对分 | 最常用, 首选 |
| 相对阈值 | 达本查询最高分某比例 | 较好, 随查询自适应 | 要按相关性过滤时 |
| 断崖检测 | 分数明显下跌处截断 | 较好 | 结果数量不定时 |
| rerank/cross-encoder | 专门模型判相关性 | 好, 分更可比 | 要高质量相关判断 |
看清这张表,选择就有谱了:首选 top-K(不依赖绝对分、最鲁棒);要按相关性过滤用相对阈值或断崖检测(随查询自适应);要高质量相关判断用 rerank;固定绝对阈值几乎不该单独用、非用就校准+当兜底。我这次踩坑,正是单用固定绝对阈值 0.8 去卡所有分数分布迥异的查询。绝对阈值看着简单,却建立在"相似度跨查询可比"这个不成立的假设上。
第五件事:我曾经对相似度阈值想当然的几个误区
这次事故也把我对相似度阈值的一堆"想当然"照了个底朝天:
| 我以为 | 实际上 |
|---|---|
| 相似度 0.8 在任何查询上都代表"挺相关" | 绝对值跨查询不可比, 0.8 对不同查询含义不同 |
| 调出一个好阈值就能一劳永逸 | 不存在对所有查询都合适的固定绝对阈值 |
| 召回为空是库里没相关内容 | 可能是相关块分数没到固定阈值被全砍了 |
| 混进噪声是检索模型不行 | 可能是阈值对这个查询太松, 换相对判据即可 |
| 相似度分数是统一刻度的相关度 | 它只在同一查询内比相对高低可靠 |
这些误区的根子是同一个:我把一个"只在同一查询内部、用来比较哪个结果更相关"的相对分数,误当成了一个"跨所有查询都统一刻度、可以用固定值卡的绝对相关度"。相似度分数确实能告诉你"对这个查询,A 比 B 更像",但它不能告诉你"0.8 分就等于相关"——因为这个 0.8 在不同查询里量纲根本不同。我拿一个绝对刻度去量一堆量纲各异的分数,自然量谁都不准。把局部可比的相对分数,当成全局可比的绝对标准,是这类"阈值怎么调都不对"困境的共同根源。
第六件事:做检索过滤、排查"阈值怎么调都不对"时,我现在的自检习惯
现在每当我给检索/分类加阈值过滤、或排查"同一个阈值有的查询召回全空、有的混进一堆噪声",我都会先按这张图问自己:
这张图的精髓,是"阈值两极先看各查询分数分布是否一致;不一致就别用绝对阈值, 改 top-K/相对阈值/rerank"。设计就用 top-K 把选数量和绝对分解耦、要过滤用相对阈值或断崖、要判相关用 rerank、排查就打印不同查询的分数分布看是不是量级各异、绝对阈值注定卡不准。这套习惯,让我从"调一个万能阈值"变成了"先看分数能不能跨查询比"——核心始终是:向量相似度分数(余弦相似度等)的绝对值只在同一个查询的不同候选结果之间比较相对高低时有意义(它能告诉你对这个查询 A 比 B 更相关),而不具备跨查询的统一的相关程度含义——因为相似度的分布和量级会随查询的特性(长度、宽泛度、用词独特性)、文档库内容、以及 embedding 模型而显著变化,同样是最相关的那个结果不同查询给出的相似度可能从 0.6 到 0.95 不等;所以用一个固定的绝对阈值去过滤所有查询必然在分数分布偏离这个阈值的那些查询上失效(偏低的全砍、偏高的全放),不存在一个对所有查询都合适的固定绝对阈值;正确做法是不依赖固定绝对相似度阈值来判相不相关而用对查询特性更鲁棒的方式:其一用 top-K 取最相似的 K 个把选多少和绝对分数解耦,其二用相对阈值(只保留达到本查询最高分某个比例的结果或分数出现明显断崖处截断)让标准随每个查询自适应,其三真要判到底相不相关就交给专门的 rerank/cross-encoder 模型或让 LLM 判断而不是用一个拍脑袋的相似度数字,其四非要用绝对阈值也必须用带标注的评估集去校准且接受它只是个粗糙兜底;一个分数/度量的绝对值在不同情境下往往含义不同量纲不一不能跨情境用同一个固定阈值去裁决、它通常只在同一情境内比较相对高低时可靠,要做跨情境判断要么用相对的(排名比例)要么针对每个情境校准要么换一个真正可比的判据。
我立下的几条规矩
这场"固定相似度阈值卡不准"的事故,换来了我做检索过滤时,刻进骨子里的几条铁律:
- 相似度绝对值只在"同一查询内比相对高低"时可靠,跨查询不可比。
- 同样是"最相关结果",不同查询的相似度可能从 0.6 到 0.95。
- 固定绝对阈值必在分布偏离它的查询上失效(全砍/放太松)。
- 首选 top-K:把"选多少"和"绝对分"解耦,最鲁棒。
- 要过滤用相对阈值:相对本查询最高分的比例 / 分数断崖处截断。
- 真判相关性用 rerank(cross-encoder)/LLM,别靠拍脑袋的相似度数字。
- 非用绝对阈值不可,就拿标注评估集校准、且只当粗兜底。
附:我现在做检索过滤的"top-K + 相对阈值 + rerank"骨架
这是我现在做 RAG 检索过滤固定套的骨架——把这次踩坑的教训(别用固定绝对阈值、用 top-K/相对阈值/rerank、要按评估集校准)固化成一套结构,让"固定相似度阈值卡不准"那种坑再不会埋进系统:
def retrieve(query, k=5, recall=20, rel_ratio=0.85):
# 1) 召回一批候选(取较多, 给后面过滤/重排留空间)
hits = vector_db.search(embed(query), top_k=recall)
if not hits:
return []
# 2) 相对阈值过滤(随本查询自适应), 而非固定绝对阈值
top = hits[0].score
filtered = []
for prev, cur in zip([hits[0]] + hits, hits):
if cur.score < top * rel_ratio: # 低于本查询最高分 85% 就停
break
filtered.append(cur)
# 3) rerank 精排(相关性判断交给更可比的 cross-encoder), 取 top-k
reranked = reranker.rank(query, filtered) if reranker else filtered
return reranked[:k]
# 评估集校准: 用 (query, 应命中块) 标注集, 扫不同策略/参数看准召回, 据此定
EVAL = [
{"q": "怎么退货", "gold": "doc_refund"},
{"q": "X-2000Pro 保修", "gold": "doc_x2000"}, # 注意覆盖分数分布不同的查询
]
def eval_recall(fn):
hit = sum(c["gold"] in [h.id for h in fn(c["q"])] for c in EVAL)
return hit / len(EVAL)
# 对比: 固定阈值 0.8 vs top-K vs 相对阈值 vs +rerank, 看哪个召回最稳
# 反例(别这么写): 固定绝对阈值, 跨查询分布不同必失效
# return [h for h in hits if h.score >= 0.8]
这套骨架把我这次的教训钉死在了结构里:先召回一批候选、再用相对阈值(随本查询最高分自适应)而非固定绝对阈值过滤、然后用 rerank 把相关性判断交给更可比的模型、最后取 top-k;并用覆盖不同分数分布查询的评估集去对比各策略的稳定召回率、按数据定参而非拍脑袋。这样,无论查询的相似度分布偏高还是偏低,过滤标准都跟着它自适应、相关性由 rerank 兜底,而不再是当初那个"一条固定的 0.8 线、调到哪都坑一批查询"的局面。把"分清分数是局部相对还是全局可比、别拿固定绝对阈值跨情境一刀切"这个道理,沉淀成检索过滤的固定骨架,这是我对这次"阈值怎么调都不对"最实在的交代——毕竟,要量一堆量纲各异的东西,与其找一把不存在的万能尺子,不如让每个东西用它自己的刻度说话。
写在最后
回头看,这场由"固定相似度阈值"引发的"检索过滤两极分化"事故,真正教给我的,远不止"改用 top-K"这一个技巧。它让我对"当我们拿到一个'分数'(相似度、置信度、评分、指标),我们极容易把它当成一把'全局统一刻度的尺子',以为'同样的数值就代表同样的程度',于是用一个固定的阈值去跨越各种不同的情境一刀切;可很多分数其实是'局部的'——它只在'同一个情境内部、比较谁高谁低'时有意义,它的绝对值在不同情境之间量纲根本不同、压根不可比;用一把固定刻度的尺子,去量一堆量纲各异的东西,必然量谁都不准",有了一次刻骨的体会。我栽跟头,是因为我把一个"只在同一查询内部用来排序的相对分数",当成了一个"跨所有查询都统一刻度的绝对相关度"——我看到相似度有个 0~1 的漂亮数值,就理所当然地以为"0.8 在哪都代表挺相关";我没意识到,这个分数的量级和分布会随每个查询剧烈变化:对一个查询 0.7 已经是它能拿到的最高分,对另一个查询 0.85 还遍地都是;于是我拿一条固定的线去卡所有这些"各自用不同刻度量出来"的分数,这条线无论画在哪,都只对分数恰好落在那个区间的查询合适,对其余的不是太严就是太松。这让我领悟到一个关于"绝对值与相对值、局部可比与全局可比"的深刻认知:很多分数、度量、指标,是"序数"性质的而非"基数"性质的——它能可靠地告诉你"在同一情境内, 谁比谁更高"(相对排序),却不能可靠地告诉你"这个绝对数值意味着多大的程度"(绝对刻度),更不能跨情境直接比较或用同一个固定阈值裁决;因为这些分数的量纲、分布、基准,会随情境(查询、类别、接口、个体)而变,同一个数值在不同情境里代表的"程度"天差地别;所以用它们做判断时,要分清你依赖的是它"可靠的那部分(同情境内的相对高低)",还是它"不可靠的那部分(跨情境的绝对含义)":跨情境的取舍,要么改用相对的(排名、比例、分位),要么针对每个情境单独校准基准,要么换一个真正经过归一化、跨情境可比的判据,而绝不能天真地拿一个固定的绝对阈值去一刀切。这给了我一种看待"一切'用分数/指标做跨情境判断'之事"时的清醒:每当我想用一个分数、置信度、指标去做过滤或判断、尤其是跨多个情境用同一个阈值时,要追问"这个分数的绝对值,在不同情境下含义一致吗?它是全局可比的刻度,还是只在同情境内可比的相对排序?我用固定阈值一刀切, 会不会在分布不同的情境上失效"——对只在局部可比的分数,用相对的方式或按情境校准,而不是套一个全局固定阈值;"分清分数是局部相对还是全局可比、别拿固定绝对阈值跨情境一刀切",是做对 RAG 检索过滤、也是用对一切分数/指标的关键。认清相似度绝对值跨查询不可比、固定阈值必失效、要用 top-K/相对/rerank——这,是我用一次"相似度阈值怎么调都不对"的事故,换来的、关于 AI、也关于如何区分相对分数与绝对刻度的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次想给检索结果加一道"相似度 > X 才算相关"的过滤时,先把几个不同查询的分数分布打出来看一眼、再决定用 top-K 还是相对阈值,那我对着那个"调到哪都有一批查询被坑"的固定阈值较劲的大半天,就值了。
—— 别看了 · 2026