RAG 质量

发布门禁、引用可靠性与剩余风险;下方保留 90 题黄金集的完整实验记录

CURRENT RELEASE GATE

当前 RAG 达到发布门槛

可发布

这不是主观评分:主检索、90 题生成与中文厂商噪声专项分别绑定到可追溯 run。 自动引用精度受稀疏标注影响,只作诊断;发布正确性采用段落支持门与人工 P0 审计。

B9-20260810T195839ZGEN-20260810T201738ZSPECIALIST-20260810T183222Z
89.9%
主集 Recall@20 · 门槛 85%
98.8%
事实句引用完整性 · 门槛 95%
93.4%
段落支持达标率 · 门槛 90%
0 / 78
可答题误拒
0 / 12
诱导题错误断言
01

检索覆盖已达标

主集 Recall@20 89.9%;15 题中文厂商专项在加入 真实近邻噪声后仍为 93.3%,没有用第二次查询冒充 A/B。

02

回答出口有硬门

模型引用不能直接下发;服务端绑定原文、移除弱支持句并处理假前提。 90 题中可答题误拒与诱导题错误断言都为 0。

03

上线后仍要持续观察

噪声结论目前只有 15 题专项样本;每次切换生成模型后必须重跑生成回归。 自动 citation precision 不能替代人工高风险数字审计。

下面是算法演进记录,不是当前发布快照。
B1 纯稠密(MRR 0.7630)到 B13(MRR 0.8731),提升 11.0 个点。 这里保留了三轮「不好看」的结果:B2 的并集比基线还差B8 扫完 42 组权重后的结论是不改,以及 B13 修好中文分词后端到端 ±0.0000。 只展示赢的那几轮,这页就不是评测记录而是宣传页。 下面「生成侧与延迟」里还有一条:GEN 写下的假设被 GEN-FIX 证伪, 两轮都留着。

10 轮检索走势

轮次改动Recall@10Recall@20MRRnDCG@10结论
B1
纯稠密基线
bge-m3 向量检索,无融合、无重排。先测基线,再加任何东西。0.8045
0.8876
0.7630
0.7381
基线
B2
稀疏通道
加入 Postgres tsvector 关键词通道,先用轮转交错与稠密合并。0.7637
-4.08pt
0.8733
-1.43pt
0.7480
-1.50pt
0.6936
-4.45pt
有得有失
B3
RRF 融合 + 时间过滤
加权 RRF 取代轮转交错;解析出的时间窗作为两个通道的过滤器。0.7991
+3.54pt
0.9036
+3.03pt
0.6911
-5.69pt
0.6597
-3.39pt
有得有失
B4
交叉编码器重排
bge-reranker-v2-m3 对融合后的前 40 个候选重排序。0.8750
+7.59pt
0.9126
+0.90pt
0.8574
+16.63pt
0.8162
+15.65pt
达标采纳
B7
时效融合
对 freshness_required 为真的问题,在重排之后把新近度与相关性融合,权重 0.30。0.8739
-0.11pt
0.9092
-0.34pt
0.8868
+2.94pt
0.8334
+1.72pt
达标采纳
B8
融合权重网格调优
42 组 dense/sparse/temporal 权重组合全扫一遍。0.8547
-1.92pt
0.8923
-1.69pt
0.8645
-2.23pt
0.8129
-2.05pt
结论:不改
B9
§6 directness / source_fit
在交叉编码器之后补上两个重排维度:问题词项在标题中的覆盖率、信源等级与问题类型的亲和度。0.8611
+0.64pt
0.8923
±0
0.8731
+0.86pt
0.8178
+0.49pt
达标采纳
B10
§6 entity_subject / repost
补上最后两条元数据调整:目标实体为主语 +0.05、重复转载 −0.10。0.8669
+0.58pt
0.8923
±0
0.8741
+0.10pt
0.8203
+0.25pt
达标采纳
B12
自适应重排深度
把重排候选数按问题类型路由:两类走 20,其余保持 40。0.8669
±0
0.8923
±0
0.8731
-0.10pt
0.8191
-0.12pt
达标采纳
B13
中文分词:修好了通道,系统没动
Postgres `simple` 把整段中文切成一个词元。加 CJK 字符 bigram,索引与查询共用同一个 IMMUTABLE 函数(ADR-0018)。0.8626
-0.43pt
0.8838
-0.85pt
0.8731
±0
0.8160
-0.31pt
结论:不改

B4 与 B7 之后的绝对值不完全可比:B4 当时排除了 2 道标注不可用的题, 零分块修复后 90 题全部参与。B7 之后是同语料同题集,可以直接比。

逐轮记录

B1 · 纯稠密基线

基线
B1-20260803T155249Z
改了什么
bge-m3 向量检索,无融合、无重排。先测基线,再加任何东西。
判据
无门槛——这一轮的目的是产出后续所有轮次的比较对象。
结果
总分掩盖了三件事:recent_updates 是最弱类别(MRR 0.468,最差两题首个命中排第 16、17 位),因为稠密向量里没有时间;MXFP4 单题 Recall@20 只有 0.33,被 fact_check 整体 0.9556 盖住;可答题最低分 0.5778 与不可答题最高分 0.7205 重叠 0.1427,说明拒答不可能靠相似度阈值决定。
最新动态
MRR 0.4676
R@10 0.6189
时间线
MRR 0.7944
R@10 0.8200
对比
MRR 0.7363
R@10 0.8000
事实核查
MRR 0.8833
R@10 0.9556
原理解释
MRR 0.9467
R@10 0.8556
不可答
MRR 0.6970
R@10 0.6667

B2 · 稀疏通道

有得有失
B2-20260803T155644Z
改了什么
加入 Postgres tsvector 关键词通道,先用轮转交错与稠密合并。
判据
修好 B1 点名的精确词失败(MXFP4 题 Recall@20 0.33)。
结果
预测被证实:MXFP4 题 R@20 0.33 → 1.00、排名 4 → 1。没预测到的是并集比纯稠密还差——轮转交错无条件给弱通道一半名额,好结果被挤出去了。RRF 因此从「验证权重」升级为必需项。另量到中文缺口 52.1 个百分点:稀疏通道对含 ASCII 专名的问句 R@20 = 0.5798,纯中文只有 0.0588。

同轮对照 纯稀疏通道单独测: Recall@10 0.3613 · MRR 0.3138 · nDCG@10 0.2759

最新动态
MRR 0.4241
R@10 0.4678
时间线
MRR 0.7956
R@10 0.8367
对比
MRR 0.7019
R@10 0.7333
事实核查
MRR 0.8889
R@10 0.9444
原理解释
MRR 0.9400
R@10 0.8556
不可答
MRR 0.6970
R@10 0.6667

B3 · RRF 融合 + 时间过滤

有得有失
B3-20260803T174553Z
改了什么
加权 RRF 取代轮转交错;解析出的时间窗作为两个通道的过滤器。
判据
把 B1 点名的最大缺口 recent_updates 关上。
结果
recent_updates MRR 0.4676 → 0.7333(+26.6pt),R@20 做到 0.9036 击败 B1 与 B2。起作用的是把时间窗当过滤器,而不是加一条独立的时间通道(实测只贡献 +0.004 Recall)。但其余五类 MRR 下降——答案进了候选集却排得靠后,这正是 reranker 的适用场景,B4 因此有了可证伪的门槛。
最新动态
MRR 0.7333
R@10 0.6800
时间线
MRR 0.6249
R@10 0.7922
对比
MRR 0.6430
R@10 0.8389
事实核查
MRR 0.7472
R@10 0.9222
原理解释
MRR 0.6952
R@10 0.7222
不可答
MRR 0.7500
R@10 1.0000

B4 · 交叉编码器重排

达标采纳
B4-20260803T174404Z
改了什么
bge-reranker-v2-m3 对融合后的前 40 个候选重排序。
判据
先立后测:MRR 需 > 0.7630 且 R@20 ≥ 0.9036,两条都达标才准进 MVP。
结果
MRR 0.8574、R@20 0.9126、nDCG +23.7%,全部达标。B3「Recall 升、MRR 降」的形态被完全纠正。候选数 40 比 100 在四项指标上都略高,而且快 3.2 倍(1376ms vs 4378ms)——融合排在 40 名之后的候选几乎不含答案,喂进去只是给交叉编码器加噪声。唯一退化的是 recent_updates(MRR 0.7333 → 0.6484):交叉编码器不知道时间。

同轮对照 100 候选: Recall@10 0.8665 · MRR 0.8562 · nDCG@10 0.8108

最新动态
MRR 0.6484
R@10 0.7911
时间线
MRR 0.9022
R@10 0.8867
对比
MRR 0.8244
R@10 0.8944
事实核查
MRR 0.9667
R@10 0.9556
原理解释
MRR 0.9667
R@10 0.8222
不可答
MRR 0.7500
R@10 1.0000

B7 · 时效融合

达标采纳
B7-20260804T075951Z
改了什么
对 freshness_required 为真的问题,在重排之后把新近度与相关性融合,权重 0.30。
判据
补回 B4 唯一的退化,且不得影响其他类别。
结果
recent_updates MRR +10.4pt,其余五类逐位相同——不是影响很小,是没有影响。这是作用域正确的改动应有的样子。权重刻意只占 0.30:相关性仍主导,「只是新」不能压过真正回答问题的。
最新动态
MRR 0.7522
R@10 0.7911
时间线
MRR 0.9133
R@10 0.9033
对比
MRR 0.8690
R@10 0.8810
事实核查
MRR 0.9643
R@10 0.9524
原理解释
MRR 0.9667
R@10 0.8222
不可答
MRR 0.7500
R@10 1.0000

B8 · 融合权重网格调优

结论:不改
B7-20260804T102253Z
改了什么
42 组 dense/sparse/temporal 权重组合全扫一遍。
判据
规格要求权重必须用评测集调优,而不是拍脑袋。
结果
负结果,保持原权重不变。 网格确实找到更好的融合序(融合前 MRR +5.9 点),但接上重排后两组权重的差距塌缩到 0.0004,四个类别逐位完全相同。→ 融合前指标是个会骗人的优化目标,它测的是一个马上会被交叉编码器覆盖的中间态。网格的真正价值是证明现行值在平台区(全网格 R@40 只波动 0.0043)而非悬崖边(sparse ≥ 0.8 才开始伤 R@20)。

同轮对照 网格最优权重: Recall@10 0.8530 · MRR 0.8641 · nDCG@10 0.8101

最新动态
MRR 0.7522
R@10 0.7911
时间线
MRR 0.9022
R@10 0.8867
对比
MRR 0.8244
R@10 0.8556
事实核查
MRR 0.9000
R@10 0.8889
原理解释
MRR 0.9667
R@10 0.8222
不可答
MRR 0.7500
R@10 1.0000

B9 · §6 directness / source_fit

达标采纳
B9-20260804T104220Z
改了什么
在交叉编码器之后补上两个重排维度:问题词项在标题中的覆盖率、信源等级与问题类型的亲和度。
判据
增益应落在 timeline 与 explainer,其余类别不得退化。
结果
timeline MRR +4.45pt、explainer R@10 +3.34pt,其余类别在噪声内(最差 −0.07pt),abstention 逐位相同。R@20 不变是对的——这两维只在重排结果内部重排序,不引入新文档。写这个模块时踩到中文分词坑:`MIN_TERM_CHARS = 2` 把中文全删了,directness 对每道纯中文问题稳定返回 0.0 且不报错。
最新动态
MRR 0.7522
R@10 0.7911
时间线
MRR 0.9467
R@10 0.8867
对比
MRR 0.8244
R@10 0.8556
事实核查
MRR 0.9000
R@10 0.8889
原理解释
MRR 0.9667
R@10 0.8556
不可答
MRR 0.7500
R@10 1.0000

B10 · §6 entity_subject / repost

达标采纳
B9-20260804T114016Z
改了什么
补上最后两条元数据调整:目标实体为主语 +0.05、重复转载 −0.10。
判据
此前判定为「拿不到数据」,本轮先回库核对该判定。
结果
先前的「拿不到数据」是错判,从没去库里核对过。 实际查:item_entity.role 有 subject 1848 / mention 2593 / object 1496,`duplicate_of_id` 也一直在写——数据从 M2 起就在库里。修完后 timeline R@10 +1.66pt、recent_updates +1.33pt、abstention nDCG +2.31pt,没有任何一个类别、任何一项指标下降,这在十轮评测里是第一次。至此 §6 的五条调整全部实现。
最新动态
MRR 0.7522
R@10 0.8044
时间线
MRR 0.9467
R@10 0.9033
对比
MRR 0.8244
R@10 0.8556
事实核查
MRR 0.9000
R@10 0.8889
原理解释
MRR 0.9667
R@10 0.8556
不可答
MRR 0.7778
R@10 1.0000

B12 · 自适应重排深度

达标采纳
B9-20260807T070516Z
改了什么
把重排候选数按问题类型路由:两类走 20,其余保持 40。
判据
只有在某一类的指标逐位不变时才允许减半,任何下降都保持 40。
结果
先死掉两个直觉规则,才找到能站住的那个。 规则一「简单事实题跳过重排」——B3→B4 实测 fact_check 从重排拿到 +0.2195 MRR,是收益第二大的类别,直觉完全反了;唯一不受重排影响的是 abstention,而 B1 已证明不可答题无法事先识别(与可答题的相似度区间重叠 0.14)。规则二「候选少时跳过重排」逻辑上成立——候选数不超过证据预算时,重排改变不了模型读到哪些段落——但 128 次真实查询里融合候选数从未低于 60,规则永远不触发。真正成立的是深度而不是有无:depth 20 整体掉 2.9 个点,但 comparison 与 recent_updates 逐位完全相同,而 explainer −6.3pt、fact_check −5.9pt、timeline −3.6pt。于是只对那两类减半,形状与 B7 一致:在测过有效的地方生效,其余原样不动。样本量写在明处:每类 15 题。

同轮对照 重排深度 20: Recall@10 0.8365 · MRR 0.8438 · nDCG@10 0.7907

最新动态
MRR 0.7522
R@10 0.8044
时间线
MRR 0.9467
R@10 0.9033
对比
MRR 0.8244
R@10 0.8556
事实核查
MRR 0.9000
R@10 0.8889
原理解释
MRR 0.9667
R@10 0.8556
不可答
MRR 0.7500
R@10 1.0000

B13 · 中文分词:修好了通道,系统没动

结论:不改
B9-20260807T114536Z
改了什么
Postgres `simple` 把整段中文切成一个词元。加 CJK 字符 bigram,索引与查询共用同一个 IMMUTABLE 函数(ADR-0018)。
判据
B2 量到纯中文问句稀疏 R@20 仅 0.0588(含 ASCII 专名 0.5798),本轮应把这个差距关上。
结果
通道确实修好了,端到端却没动。 MRR ±0.0000,R@10 −0.0043,六类里五类逐位相同。原因 B2 自己就写过、当时没被当回事:「稠密通道对中文只低 4.9 个百分点——缺口在通道设计边界内,不在系统层」。稀疏通道的中文失败一直被稠密通道兜住了。但站内搜索是另一回事,它只有 FTS 没有稠密兜底,实测:「开源」4 → 130 条、「量化」5 → 48、「融资」2 → 18、「智谱」0 → 2,而 ASCII 查询「GLM」15 → 15 不变。所以改动保留——收益在另一个界面上,且是真的;RAG 侧记为负结果,代价是索引 15MB → 18MB。

同轮对照 修复前(无 bigram): Recall@10 0.8669 · MRR 0.8731 · nDCG@10 0.8191

最新动态
MRR 0.7522
R@10 0.7822
时间线
MRR 0.9467
R@10 0.9033
对比
MRR 0.8244
R@10 0.8556
事实核查
MRR 0.9000
R@10 0.8889
原理解释
MRR 0.9667
R@10 0.8556
不可答
MRR 0.7500
R@10 1.0000

与 RAGAS 术语的对应

RAGAS在问什么本项目的对应指标当前值
Faithfulness答案有没有说出证据里没有的东西support_mean / support_supported(交叉编码器对「论断 × 被引段落」打分)
另有一条硬约束不在指标里:`check_invariants` 会把「有 [n] 却解析不到引用」或「零引用却不是拒答」的回答直接判为不可发布。指标衡量程度,不变量划定底线。
0.8907 / 0.9344
Answer Relevancy答案是不是在回答这个问题must_contain_hit + over_refusal_rate
误拒率必须与拒答率一起看:全都拒答的系统在拒答指标上满分且毫无用处。把「该答的没答」算作最严重的一种不切题。
1.0000 / 0.0000
Context Precision检索到的上下文里相关的占比,且相关的是否排在前面citation_precision + MRR + nDCG@10
拆成两个数:引用精度看模型选了什么,MRR/nDCG 看检索排了什么。B8 那一轮的教训是这两者会分离——融合前的排序指标提升 5.9 点,接上重排后差距塌缩到 0.0004。
0.6686 / 0.8608 / 0.8066
Context Recall该被检索到的内容有没有被检索到Recall@10 / Recall@20(对黄金集标注)
标注是人工的,127 个 item 全部经库校验;零分块的条目会被守卫拦下并计数。
0.8541 / 0.8994
(无对应)同一事件被多少家独立信源覆盖story_coverage
RAGAS 没有这一项,因为它假设文档之间彼此独立。资讯语料不是:四家媒体报道同一次披露是一条证据不是四条,M3 的事件聚类就是为此存在的。
0.6921
(无对应)答案里带引用的句子占比citation_coverage
本项目的核心主张是「每条事实可回原文」,所以句级覆盖率是一个独立的验收项。
0.9881
Noise Sensitivity(专项小样本)混入无关上下文时答案会不会被带偏15 题同候选快照:entity / noise Recall@20
已用 15 道中文厂商专项题与 8 个经原文核验的真实近邻噪声做同候选快照 A/B。这是专项小样本,不冒充完整 90 题噪声回归。
0.9333 / 0.9333

三项没有 RAGAS 对应:story_coverage(RAGAS 假设文档彼此独立, 而四家媒体报道同一次披露是一条证据不是四条)、citation_coverage(句级引用覆盖率是本项目的核心主张)、 以及误拒率。Noise Sensitivity 目前采用 15 题中文厂商专项集与 8 个真实近邻噪声 做同候选快照 A/B;它已经覆盖最容易串线的场景,但仍只是专项小样本, 不能冒充完整 90 题噪声回归。

生成侧与延迟

GEN · 生成侧评测(含拒答判定)

GEN-20260807T120415Z
测了什么
90 题端到端真实生成。本轮新增拒答判定:对 12 道诱导题,用受控的二值判官问「这段回答有没有把那个假前提当事实陈述出来」。
结果
先修指标,再读结果。 原来的 `refusal_rate_on_unanswerable` 从 66.67% 掉到 8.33%,看着像核心能力崩了。查下去发现它测的是形式不是正确性:拒答被定义为「文本为空或零引用」,而 §3.13 刻意把死胡同式拒答改成了有据可查的否认——「证据里没有提到罚款,也没有任何监管机构开出罚单的记载」并附上检索到的内容。这类回答有引用,于是被判成「没拒答」。另一个信号 `must_not_claim` 是子串匹配,它自己的注释就写着「否认时提到该词也算命中」,而且只覆盖 15 题里的 4 题。两个信号都在测表面形态。新增判官后:12 道诱导题里断言假前提的为 0,支持度 0.8330 → 0.8313 基本不动,引用覆盖率 +12.5pt。结论:拒答能力没有退化,退化的是衡量它的方式。本轮同时暴露一条真问题:误拒率 1.28% → 7.69%(1 题变 6 题)、Story 覆盖 −8.2pt。当时记下的假设是「语料涨到 1473 条后证据位竞争加剧」,并写明那是假设不是结论——下一轮证明它是错的。
questions
90
answerable
78
unanswerable
12
citation_coverage
0.868
citation_precision
0.5844
story_coverage
0.6976
support_mean
0.8313
support_supported
0.9078
must_contain_hit
0.9091
avg_citations
3.47
latency_ms_mean
6893
refusal_rate_on_unanswerable
0.0833
forbidden_term_mentioned
4
judged_unanswerable
12
presupposition_asserted_rate
0
over_refusal_rate
0.0769

GEN-FIX · 生成侧复测:误拒归零

GEN-20260807T153537Z
测了什么
同样 90 题,只改了一处:解析模型输出时的失败分支。把上一轮那六道误拒的原始模型响应抓出来看了之后改的。
结果
语料竞争那个假设是错的,六道题一道都不是检索问题。 抓原始响应发现两条各自独立的失效路径,都在出口处:①模型直接用 markdown 回答、没套 JSON——答案完整、`[E1][E2]` 一个不缺,只是 `json.loads` 失败,于是正文被清空、判成拒答;②模型把证据编号只写进 `claims[].evidence_ids`、正文里不标——而绑定只扫正文,四条论断六个编号绑出零条引用。两条都是「模型偶发偏离输出契约,而每次偏离赔上整个答案」,也解释了为什么两轮之间会从 1 题跳到 6 题:这是随机脱靶,不是某类问题的固定缺陷。修法是给失败分支加带守卫的回退(不像 JSON 且至少有一个编号,才按正文解析;正文一个编号都没有,才回退到 claims),核验一寸不放——两条路径照走同一套绑定与不变量检查,编造的编号照样剥掉。结果:误拒率 7.69% → 0.00%(78 道可答题零误拒,比 08-04 基线还低),Story 覆盖收回 5.6pt,引用覆盖率 90.7%,must_contain 命中 100%,而断言假前提仍是 0——放宽没有换来幻觉。唯一下行的是支持度 −2.2pt:被救回的六题分数确实偏低(0.7046 对 0.7707),但它们的 Story 覆盖是 0.8889、远高于全体的 0.7419,救回来的是覆盖面更宽的实质答案;其余 72 题代码路径逐字未变却也动了 −2.0pt,那是模型自身的轮间抖动。多发表六个答案换来均值略降,这是正确的代价。
questions
90
answerable
78
unanswerable
12
citation_coverage
0.9069
citation_precision
0.5782
story_coverage
0.7532
support_mean
0.809
support_supported
0.8602
must_contain_hit
1
avg_citations
3.9
latency_ms_mean
7501
refusal_rate_on_unanswerable
0
forbidden_term_mentioned
4
judged_unanswerable
12
presupposition_asserted_rate
0
over_refusal_rate
0

LAT · 端到端延迟

LAT-20260804T092526Z
测了什么
24 题实测 p50 / p95 与各阶段耗时占比。
结果
这组数字推翻了此前的一个判断。 原以为「父块/折叠是打磨项,检索是大头」,数据说反了:本地计算(dense + sparse + fuse + select + parent)合计不到 1%,99% 的时间花在三次外部 API 往返上。直接后果是压延迟只能动网络侧,而任何「多算几路」的实验成本可以忽略。
p50_ms
10521
p95_ms
13875
max_ms
14488
mean_ms
10642