给检索改动装一盏红绿灯:评测门禁怎么建、怎么用

给检索改动装一盏红绿灯:评测门禁怎么建、怎么用

改一句 Prompt、调一个召回阈值、换一个模型,改完之后,怎么知道检索质量没变差?

肉眼看不出来。你随便问几个问题,答得都还行,但「还行」不是结论。质量回退最麻烦的地方在于它不报错:没有异常、没有告警,只是答案悄悄变差了一点,等你从用户反馈里发现,已经过了好几版。

所以需要一套能自动跑、能给出明确通过/不通过的东西。这就是评测门禁。


一、先有评测集,才谈得上门禁

门禁要能判「过不过」,得先有个基准:一批问题,加上每个问题期望召回哪几篇文章。这叫黄金问答集。

我这份评测集一共 24 条,刻意分成三类:

类型 条数 期望什么
事实检索 18 问某个技术点,期望召回指定的那篇(或那几篇)
多跳 4 一题牵涉两个知识点,期望同时召回两篇
无答案拒绝 2 语料里没有的问题,期望礼貌拒答,不硬编

为什么要有后两类,而不是清一色的事实题?

多跳题考的是「关联」:如果检索只认字面相似,它能找到最像的那一篇,但找不到第二篇该一起出现的。这类题能把「词面匹配」和「语义检索」区分开。

无答案拒绝题考的是「克制」 :语料里没有答案时,模型最省事的做法是拿最相近的几篇硬凑一个回答。这类题盯的就是这个行为:期望它引用为空、明确说没找到,而不是硬编。

这些题从哪来? 问题不用凭空编,日常用户问过、答得不好的那些,攒起来就是最好的素材,因为它们是真实分布,不是拍脑袋想的。真正花时间的是第二列:期望召回哪几篇,得有人先读过语料才标得出来。这一步没有捷径,标注量就是集子的成本。也正因为贵,起步不需要多,几十条能覆盖住主要场景就够。


二、四项指标,各管一件事

门禁盯四个指标,每一项都对着一个真实的翻车方式:

指标 含义 盯着什么问题
avgRecall@5 期望文章进入前 5 名的比例 该召回的没召回到
avgCitedPrecision 引用的文章里真属期望的占比 引了不相关的文章
avgUnsupportedRate 判为疑似无资料支撑的句子占比 答案里编了资料没说的话
refusalRate 无答案题里引用为空的比例 没资料还硬编

这四项的分母不一样,这正是三类题不能混着看的原因:avgRecall@5 在有效题 上取平均(事实题加多跳题,一题若有多个期望文章,按命中比例算);avgCitedPrecision 和 avgUnsupportedRate 只在有期望文章的题 上算;refusalRate 则只统计无答案题。所以每类题只能用能体现它的那项指标来衡量,没有哪个数字是「总体分」。

前两项来自检索侧和引用侧:一个是「该找的找到了吗」,一个是「找的东西对不对」。

第三项要单独说。它衡量的是未溯源率:答案里的每一句,能不能在引用的资料里找到支撑。这句判定在生成时其实已经算过一遍(忠实度校验),评测直接复用它,不额外跑一次调用。

第四项的判定很直白:答案有内容、但引用为空,就算拒绝成功;引了一堆文章硬凑,就算失败。

这四项最后会汇总成一张检查表,每项一行:指标名、实测值、阈值、比较方向、是否通过。


三、阈值从哪来

阈值走配置,代码里只留一份默认值:

yaml 复制代码
eval.gate:
  min-avg-recall: 60        # avgRecall@5 >= 60
  min-cited-precision: 50   # avgCitedPrecision >= 50
  max-unsupported-rate: 30  # avgUnsupportedRate <= 30
  min-refusal-rate: 50      # refusalRate >= 50,负值则禁用该项
  min-cases: 10             # 召回有效题数下限

四个指标分两类方向:下限类 (召回率、引用精确率、拒绝率,value >= threshold)和上限类 (未溯源率,value <= threshold)。最后一行 min-cases 不是指标,是题数下限,后面单独讲。

边界含等号:实测值正好等于阈值,算通过。这条不是随手定的:含糊的边界会在「刚好卡线」时让人反复怀疑判定逻辑,不如写死。

调阈值这件事我的态度是:不追求「科学」,只追求「能挡住明显回退」。 召回率掉到 45,不用算统计显著性,就是明显退步了。


四、fail-closed:跑不出来,就是不通过

这条是整个机制里我最坚持的。

门禁的判定结果只有两种:通过、不通过。但评测本身可能跑不出来:向量服务挂了、模型不可用了、评测集文件读不到了。这时候按哪种?

最省事的做法是「跑不出来就当通过」,毕竟门禁不该因为基础设施抖动卡住流程。但这么写,门禁就废了:只要向量服务一挂,所有改动都会自动通过。 挡不住回退的门禁,不如没有。

所以判定方向是反的:报告缺失、报告里带 error、召回有效题数不足,一律按 0 分判定,直接不通过。

java 复制代码
// 报告无效 → 按 0 分并强制 fail(fail-closed)
boolean recallOk = recall != null && !recall.containsKey("error");
int recallCases = recallOk ? numInt(recall.get("cases"), 0) : 0;
double avgRecall5 = recallOk ? numDouble(recall.get("avgRecall@5"), 0d) : 0d;
// recallEnough 为假时,这一项一律 fail,并在 detail 里写清楚为什么

失败原因还要写进报告明细,不能只给一个「不通过」:是报告缺失、带 error,还是题数不够,得让人一眼看出来该去修哪个。

fail-closed 有一处例外,要单独说:评测集里根本没有 某一类题时(比如压根没写无答案条目,refusalCases = 0),对应那一项跳过、视为通过。

这不是自相矛盾。fail-closed 管的是「该测却没测成」,那是一次故障;这里管的是「这类样本压根没打算测」,那是评测集的覆盖范围。两者不该一视同仁:门禁只能约束存在的样本,硬判它失败,等于逼所有人先往集子里塞几道无答案题,才有资格过门禁。

而且这个跳过是带了标记的 :结果里带 skipped: true,和「真的通过了」在报告里长得不一样。哪天你发现 refusalRate 一直是 skipped,就说明集子里缺这类题,该补了。


五、题数下限:一条题都没跑,不能算通过

评测集加载时会先过一道筛子:问题为空的、或者没写期望文章的题,直接跳过,不计入有效题数。所以「加载成功」不等于「真的跑到了题」。那批期望文章一旦被误清空(改格式、批量替换 ID 时很容易发生),有效题数可能就是 0。

这时候召回率会被算成 0,恰好低于阈值,看起来像一次正常的判失败,其实是巧合:它靠的是「默认分一定低于阈值」这个假设。阈值一调低、或者空集换成了别的默认值,一个什么都没测的评测就会被判通过。

所以加了一道显式的闸:有效题数低于 min-cases,直接判失败。它挡的不是分数低,是根本没跑。


六、离线评测必须和线上走同一套召回

这一条决定了整套评测有没有意义。

评测算的是 recall@k:期望的文章有没有进前 k 名。这个名次从哪来?从召回服务来。

如果评测自己另起一套召回(只用向量、或只用关键词),那算出来的 recall@k 反映的是那套实现,跟线上实际用的那套没关系。线上是向量和关键词两路召回、各自排名后用 RRF 融合(把两路的名次取倒数相加再重排),评测要是只取一路,两个数就失去了可比性。 改完线上看评测分涨了,也说明不了什么。

所以离线评测直接复用线上那个召回入口,连参数都一致:

java 复制代码
// 与线上 RAG 同口径:向量与关键词两路召回,各自排名后 RRF 融合
Recall result = recallService.recall(q, emb, TOP_KS[TOP_KS.length - 1]);
// 按融合排名的顺序取前 k,与 golden 求交 ------ 顺序错了 recall@k 就没意义

这里还有个细节:取前 k 名必须严格按召回服务返回的排名顺序切。如果先把候选塞进一个无序集合再算命中,算出来的其实是 recall@all,「@k」这个限制就丢了。所以代码里按下标遍历:

java 复制代码
for (int i = 0; i < recalled.size() && i < k; i++) {
    if (golden.contains(recalled.get(i))) {
        cnt++;
    }
}

边界

  • 门禁接到哪一步,是使用方式的选择。 做成一个手动调的接口,改完自己跑一次,成本最低;接进 CI 定时跑、不过就拦住合并,约束力更强,但要先接受它每轮几分钟的开销。手动跑最大的风险是「忘了跑」,改动照样能合进去。
  • 24 条是起点,不是终点。 样本量小,个别题波动就会明显影响均值。让评测集长大的常见做法,是把线上收到的问题反馈沉淀成候选问题,人工补上期望文章后并入。
  • 期望文章一旦从语料里消失,那几道题会永久失分。 期望是绑在具体文章上的,文章下架或被替换后,对应的题再也不可能召回成功,得分会莫名下跌。看到某几道题整题归零,先去查语料,别急着当成质量回退。
  • 判断「拒答成功」的规则是简化过的。 现在只看引用是否为空,一个引用了资料但仍说「没找到」的答案会被算作失败。真要细分,得让判定同时看引用和措辞。
相关推荐
LittroInno1 小时前
AI云台相机如何协同成像 识别 跟踪与运动
人工智能·数码相机·计算机视觉·目标跟踪·目标识别·云台相机
chipsense1 小时前
电焊机电源的电流检测:为什么你的焊缝总是不稳定
人工智能·霍尔电流传感器·电流检测·电焊机
鬓戈1 小时前
AI基础之神经网络
人工智能·深度学习·神经网络
空 白II1 小时前
10.14 大语言模型研究简报:Anthropic 推出反蒸馏防御
人工智能·语言模型·归因报告
acd120091 小时前
Go 里 err 被不经意覆盖的几个现场
开发语言·后端·golang
javaDocker1 小时前
从「五个窗口」到「一条流水线」:金融核心交易测试的 AI 协作矩阵设计
人工智能·金融·矩阵·研发效能·ai自测
67X1 小时前
【CISC7021】Build a Toy Llama-2 Language Model
人工智能·语言模型·自然语言处理