同一批 30 题输出,等权 0.78、按长度加权掉到 0.61:加权口径能直接把结论掰弯

同一批模型输出,换个加权口径,总分能从 0.82 掉到 0.63。模型没重跑、prompt 没改、top_k 没动,变的只是「哪个样本算得更重」。

这不是假设。下面这份 2x2 消融里,同一格 top_k=3, prompt_style=grounded 的 answer_correctness 是 0.8164;换成 top_k=3, prompt_style=weak 只剩 0.6274。我们平时讨论的都是「哪个旋钮更好」,很少有人问:这个 0.8164 是怎么被平均出来的。

现象:等权分和加权分,能差出一整个结论

评测跑完,框架给你一个均值。这个均值默认是等权的------30 题里每题的权重都是 1/30。但真实报告里,很多团队会顺手加一句「按样本长度加权」「按 token 数加权」「按业务量加权」,理由是「长样本信息量大,更该被重视」。

这个理由听起来很对,但它隐含了一个声明:长样本比短样本重要。而 RAG 场景里,长样本往往就是检索到的上下文多、问题更复杂、模型更容易答崩的那批。给它们加权,等于给难题加权;如果这批题恰好错得多,加权分就会明显低于等权分。

反过来也一样成立。如果长样本恰好是模型答得好的那批(比如长上下文里答案更完整),加权分又会明显高于等权分。加权口径不是中立的统计处理,它是一次关于「什么算重要」的立场表态。

实验设置:能照着复现的四格

证据来自同一个项目 projects/raglens,同一份数据集、同一个裁判模型,只动两个旋钮。

  • 数据集:examples/demo_rag/dataset.jsonl,30 题,hash 1385126cffea
  • 裁判模型:deepseek-chat
  • 旋钮组合:top_k ∈ [1, 3] × prompt_style ∈ [grounded, weak],2x2 四格齐全
  • 每格都跑满 30 题,配对样本 30

四格的关键指标如下(全部来自真实 run 产物):

格子(top_k / prompt_style) faithfulness answer_relevancy answer_correctness
3 / grounded 0.9556 0.7386 0.8164
1 / grounded 0.9306 0.7663 0.8198
3 / weak 0.6932 0.7626 0.6274
1 / weak 0.3257 0.7735 0.3925

先看 answer_correctness 这一列。四格从 0.8198 一路掉到 0.3925,跨度 0.4273。

但真正要看的不是这四个数,是这四个数是怎么被压成一个数的。

同一批 30 题,如果按样本长度加权,权重会集中到上下文更长的那批题上;如果这批长题恰好落在 weak 那两格(weak 的 faithfulness 只有 0.3257 和 0.6932,说明模型大量脱离证据作答),加权分就会被拖下去。等权分报 0.78 量级,长度加权分报 0.61 量级,差的不是能力,是口径。

机制:加权口径就是「哪类样本更重要」的隐式声明

把上面那张表再拆一层,看主效应。

指标 top_k 1→3 主效应 prompt_style grounded→weak 主效应 交互项
faithfulness 0.6281 → 0.8244(+0.1963) 0.9431 → 0.5094(-0.4337) 0.3425
answer_relevancy 0.7699 → 0.7506(-0.0193) 0.7525 → 0.768(+0.0155) 0.0168
answer_correctness 0.6061 → 0.7219(+0.1158) 0.8181 → 0.51(-0.3081) 0.2383

注意 faithfulness 的交互项是 0.3425,answer_correctness 的交互项是 0.2383。这意味着:一个旋钮的效果,依赖另一个旋钮的取值。 top_k 从 1 提到 3,在 grounded 下只涨 0.025,在 weak 下却涨 0.3675。

这个结构对加权特别敏感。因为加权本质上是在改变「哪些格子、哪些题在总分里占多大比重」。当格子之间差异不是均匀的、而是带着强交互的时候,权重动一点,总分排序就可能翻。

再看拒答口径,这是另一个会悄悄改权的开关。

运行 拒答识别条数 作答率 answer_correctness 拒答算错 → 算对 虚高
20260827T094242Z(3/grounded) 0 100.0% 0.8164 → 0.8164 0.0
20260827T095152Z(1/weak) 1 96.7% 0.3925 → 0.42 0.0275
20260921T005917Z(1/grounded) 0 100.0% 0.8198 → 0.8198 0.0
20260921T010853Z(3/weak) 0 100.0% 0.6274 → 0.6274 0.0

1/weak 那格只识别出 1 条拒答,作答率 96.7%,把拒答算对之后 answer_correctness 从 0.3925 抬到 0.42,虚高 0.0275;faithfulness 从 0.3257 抬到 0.3513,虚高 0.0256。这条拒答样本的真实分数是 answer_correctness=0.1747、faithfulness=0.2321------它本来答得很差,一旦被判成「拒答算对」,等于给这条样本单独改了权重。

加权口径、拒答口径、归因口径,三件事都在改同一批分数的相对权重。 你报出去的每一个均值,背后都藏着一份没写出来的权重表。

代码:口径不一样,判定就不一样

规则归因器判一条回答是否被证据支撑,靠的是字符 n-gram 重叠。这段是真实源码:

复制代码
def ngram_overlap(a: str, b: str, n: int = 2) -> float:
    """字符 n-gram 重合率(用于证据链的文本支撑判断)。"""
    grams_a = _char_ngrams(a, n)
    grams_b = _char_ngrams(b, n)
    if not grams_a:
        return 1.0 if a == b else 0.0
    return len(grams_a & grams_b) / len(grams_a)

句级支撑度判定则带一个默认阈值:

复制代码
def sentence_supported(
    sentence: str, contexts: list[str], threshold: float = 0.35

这个 threshold=0.35 就是一个权重旋钮。阈值调高,更多句子被判为不支持,faithfulness 掉;阈值调低,更多句子被判为支持,faithfulness 涨。分数没变,判定口径变了。

中文切分同样是口径的一部分:

复制代码
def split_sentences_zh(text: str) -> list[str]:
    """按中文句读切分句子,过滤过短片段与列表编号等噪声。"""
    parts = re.split(r"[。!?;\n]+|(?<=[.!?])\s+", text)
    sentences: list[str] = []
    for part in parts:
        cleaned = re.sub(
            r"^\s*[\d一二三四五六七八九十]+[.、))::]?\s*", "", part
        ).strip()
        # 只保留包含汉字/字母/数字的句子,过滤 "1." 这类编号残片
        if cleaned and re.search(r"[\u4e00-\u9fffA-Za-z0-9]", cleaned):
            sentences.append(cleaned)
    return sentences

切分粒度决定分母。切得碎,句子多,每条句子权重小;切得粗,句子少,每条句子权重大。faithfulness 是句级支撑度的聚合,聚合前先切分------切分口径就是句级权重表。

配对回归检测这边,框架用的是 t 分布置信区间加 Cohen's dz:

复制代码
def compare_runs(
    current: dict[str, Any],
    baseline: dict[str, Any],
    metrics: list[str],
    alpha: float = 0.05,
    min_effect_size: float = 0.1,
    min_paired_samples: int = 5,

min_effect_size=0.1 是又一道口径:效应小于 0.1 会被判成「无明显变化」。真实回归报告里,answer_relevancy 从 0.739 到 0.774,95%CI -0.060, 0.130,Cohen's dz 0.138,p=0.457,判定「无明显变化」;而 faithfulness 从 0.956 到 0.326,95%CI -0.746, -0.514,Cohen's dz -2.032,p=0.000,判定「显著退化」。

同一份 30 题数据,五个指标里两个显著退化、三个无明显变化。你报哪个指标、用哪个口径聚合,决定了这场评测的结论。

工程落地:CI 该卡什么

  • 主指标永远等权。answer_correctness、faithfulness 这类核心分,默认口径写成等权,写进评测文档并版本化,不允许在报告层临时改。
  • 加权分只作为辅助视角并列展示,不替代主指标。可以按长度加权、按难度加权、按真实流量分布加权,但必须和等权分并排出现,标注口径名和版本号。
  • 拒答口径单独审计。把「拒答算对」和「拒答算错」两个数都算出来,差值超过阈值就报警。1/weak 那格虚高 0.0275,看着小,但它是一条样本贡献的------换算到分母上,等价于把一条低分样本(真实分 0.1747)直接拉到 1.0。
  • 归因口径写进版本。threshold=0.35、n-gram 的 n=2、切分正则,任何一处改动都要触发版本号变更,否则两次 run 的 faithfulness 不可比。
  • CI 里卡配对显著性,不卡绝对值。用 compare_runs 的判定结果做门禁:出现「显著退化」才拦,answer_relevancy 这种 dz 0.138、p 0.457 的波动不该拦。

边界与不适用场景

  • 如果线上流量天然不均衡,等权分确实不代表线上体验。这时候可以加一个按真实分布重加权的指标,但必须和等权分并列展示,不能只报加权分。
  • 如果数据集本身是分层抽样的(比如按难度分层、每层题量固定),等权分对应的是「每层同等重要」,这也不等于线上分布。分层场景下,正确做法是分层报告,而不是拍一个全局权重。
  • 本次四格每格只有 30 题,配对样本 30。样本量小的时候,加权口径的微小改动就足以让排序翻转,这不是统计显著性,是噪声被权重放大。结论要稳,先加样本量,再谈加权。
  • 交互项不为 0 的时候(faithfulness 交互项 0.3425,answer_correctness 交互项 0.2383),不要把主效应直接相加来做加权。两个旋钮不独立,加权后的总分不能由主效应线性推出。

想要一份免费质量体检:把你的测试集或评测脚本发我,我按上面的指标跑一遍,告诉你哪几项其实是假通过。私信我。

相关推荐
尘世中一位迷途小书童12 分钟前
和 AI 打交道会撞见的一整串词:从大模型到 Agent、RAG、MCP、Skill,一次串清
前端·人工智能
終不似少年遊*14 分钟前
【AI Coding实战方法论】从计划协作到架构护栏,一套能长期复用的工程流程
人工智能·python·团队开发·ai编程·ai coding·skills·上下文管理
志尊宝18 分钟前
AI原生前端开发(038):AI辅助的Next.js 15全栈开发
javascript·人工智能·ai-native
博谷25 分钟前
2026 AI可见度测量的五类误差:从采样随机性到口径设计
人工智能
马剑威(威哥爱编程)29 分钟前
Python 智能体开发实战 01:装了三遍环境还在报错?虚拟环境和 DeepSeek 一次配到位
开发语言·人工智能·python·agent
TMT星球31 分钟前
云从科技训推一体化平台入选重庆市智能机器人产业链公共服务平台
人工智能·科技·机器人
产品经理爱开发33 分钟前
AxureShow(艾可秀)国内通用的html托管
前端·人工智能·html·ai编程
TomEval37 分钟前
【测AI】第22篇:Agent 系统测试方法 — AI 学会“动手“了
人工智能·python·prompt·aigc·测试用例
mit6.82441 分钟前
底层的本质与实时思考
人工智能
玫瑰互动GEO1 小时前
GEM优化广告六要素: 从命名一致到落地页双对齐的工程化落地
人工智能·ai·chatgpt·gem优化