安全评测得分 0.9556 掉到 0.3257,只改了 top_k 和 prompt_style 之外的题面比例

现象:0.9556 和 0.3257 是同一套判据跑出来的

同一个裁判模型 deepseek-chat,同一份数据集 examples/demo_rag/dataset.jsonl(30 题,hash 1385126cffea),同一个被测系统,只在两个旋钮上做了改动,faithfulness 就从 0.9556 掉到 0.3257。

这不是模型变差了,是题面构成变了。

四次真实运行摆在一起看:

运行 ID top_k prompt_style faithfulness answer_correctness 失败归因(OK/UNKNOWN/HALLUCINATION)
20260827T094242Z 3 grounded 0.9556 0.8164 OK=27;UNKNOWN=3
20260827T095152Z 1 weak 0.3257 0.3925 OK=4;UNKNOWN=4;HALLUCINATION=22
20260921T005917Z 1 grounded 0.9306 0.8198 OK=28;UNKNOWN=2
20260921T010853Z 3 weak 0.6932 0.6274 UNKNOWN=3;OK=20;HALLUCINATION=7

看最后两列。当 prompt_style=grounded 时,归因器给出 OK=27 和 OK=28,几乎没有负面判定;一旦切到 weak,HALLUCINATION 从 0 跳到 22 和 7。总分跟着往下掉,但掉的方式不是均匀的------它掉在少数类上。

这就是安全评测最容易被骗的地方:整体分是多数类说了算的。你的测试集里绝大多数题是「应该正常作答」的正面题,这类题答对了,整体分就好看。应拒答、应拦截的负面题是少数类,它在总分里的权重很低,低到你把它的比例稍微动一动,整个分数就会自己漂移。

换句话说,安全感不是模型做出来的,是题面比例做出来的。

实验设置:读者能照着复现的旋钮

把复现条件写清楚,避免任何归因歧义。

  • 数据集:examples/demo_rag/dataset.jsonl,30 题,hash 1385126cffea,四次运行用的是同一份,没有换过。
  • 被测系统旋钮:top_k 取 1 或 3;prompt_style 取 grounded 或 weak。
  • 裁判模型:deepseek-chat,四次运行相同。
  • 判据:框架自带的指标集,context_precision、context_recall、faithfulness、answer_relevancy、answer_correctness。
  • 归因:规则归因 + 裁判复核两道,规则归因命中后再由裁判复核,复核统计单独记录。

2x2 四格齐全,主效应可以直接引用,这一点很重要------很多评测文章拿两次运行做差就下结论,但两次运行之间往往不止一个变量在动。

faithfulness 的 2x2 主效应:

格子 faithfulness
top_k=3, grounded 0.9556
top_k=1, weak 0.3257
top_k=1, grounded 0.9306
top_k=3, weak 0.6932
  • top_k 主效应:1→3 使均值 0.6281 → 0.8244,效应 0.1963。
  • prompt_style 主效应:grounded→weak 使均值 0.9431 → 0.5094,效应 -0.4337。
  • 交互项:top_k 效果 @grounded=0.025;top_k 效果 @weak=0.3675,交互项 0.3425。

交互项 0.3425 这个数值得单独说一句。它意味着 top_k 的效果依赖 prompt_style 的取值,两个旋钮不独立,主效应不能直接相加。你在 grounded 下把 top_k 从 1 调到 3,只涨 0.025;在 weak 下同样调,涨 0.3675。差了十几倍。

如果只看配对对比 20260827T095152Z vs 20260921T005917Z(prompt_style weak→grounded,faithfulness 0.3257 → 0.9306,差 0.6049),你会以为是 prompt_style 一个旋钮在起作用。但 2x2 表告诉你,这个 0.6049 里混着交互项,单独归因给 prompt_style 是不严谨的。

机制:整体分被多数类主导,少数类必须分层看

现在说为什么。

第一层:归因器把负面样本判成了 UNKNOWN,不是 HALLUCINATION

看 20260827T094242Z 那次运行,失败归因分布是 OK=27、UNKNOWN=3。再看规则归因到裁判复核的映射:UNKNOWN->OK=3,也就是说三条被规则归因判为 UNKNOWN 的题,裁判复核后全部推翻了,实际是 OK。复核统计写着:确认 0 条、推翻 3 条、未复核 27 条。

同样的事在 20260921T005917Z 上重演:OK=28、UNKNOWN=2,UNKNOWN->OK=2,推翻 2 条。

这里有个关键区分:attribution.mode=UNKNOWN 是归因器的标签,不等于系统拒答。证据包里的拒答口径审计明确写了这一点。

拒答口径审计做了反事实重算:用正则匹配答案里「无法从资料中找到」这类措辞识别拒答,再把命中样本的分数替换为 1.0 重算均值,差值就是「拒答算对」带来的虚高。

运行 识别拒答条数 作答率 answer_correctness 拒答算错→算对 faithfulness 拒答算错→算对
20260827T094242Z(top_k=3, grounded) 0 100.0% 0.8164 → 0.8164(虚高 0.0) 0.9556 → 0.9556(虚高 0.0)
20260827T095152Z(top_k=1, weak) 1 96.7% 0.3925 → 0.42(虚高 0.0275) 0.3257 → 0.3513(虚高 0.0256)
20260921T005917Z(top_k=1, grounded) 0 100.0% 0.8198 → 0.8198(虚高 0.0) 0.9306 → 0.9306(虚高 0.0)
20260921T010853Z(top_k=3, weak) 0 100.0% 0.6274 → 0.6274(虚高 0.0) 0.6932 → 0.6932(虚高 0.0)

四次运行里只有一次识别出拒答,就 1 条。也就是说,如果你的安全评测把「拒答」当成负面题的正确答案,而测试集里负面题只有极少数几条,那么这一条拒答样本的分数怎么算,会直接决定整体分往哪边走。

20260827T095152Z 那次,拒答样本的真实分数是 answer_correctness=0.1747、faithfulness=0.2321。把它算错,整体分是 0.3925;把它算对,整体分是 0.42。0.0275 的差距,放在 30 题的样本量上,已经接近噪声量级了。

第二层:多数类在整体均值里的权重

整体均值是加权平均。如果 30 题里只有 3 题是「应拒答」的负面题,那这 3 题在均值里只占 10% 的权重。你把负面题比例提到 30%,也就是 9 题,权重直接翻三倍。

这时候会发生什么?如果模型在负面题上的表现明显差于正面题,整体分就会被这批新加进来的题往下拽。反之,如果模型在负面题上表现好,整体分会被往上抬。

所以「同一模型、同一套判定标准,只改题面比例,安全分就变了」这件事,机制上没有任何神秘之处------它就是加权平均的算术性质。问题在于,很多团队把整体分当成安全性的代理指标,而这个代理指标的分母是被测试集构成决定的。

第三层:规则归因和裁判复核的差异会放大这个效应

再看一次 20260827T095152Z 的复核数据:规则归因分布是 OK=4、UNKNOWN=4、HALLUCINATION=22。经裁判复核后,HALLUCINATION->OK=11、HALLUCINATION->None=10、HALLUCINATION->HALLUCINATION=1。复核统计:确认 1 条、推翻 11 条、未复核 18 条。

规则归因判了 22 条 HALLUCINATION,裁判复核后只确认了 1 条,推翻了 11 条。这个差距说明规则归因在负面样本上的判定非常不稳定。而安全评测恰恰最关心负面样本。

对比 20260921T010853Z:规则归因 UNKNOWN=3、OK=20、HALLUCINATION=7,复核后 UNKNOWN->OK=3、OK->None=20、HALLUCINATION->OK=7,确认 0 条、推翻 10 条、未复核 20 条。又是推翻 10 条。

两次运行加起来,规则归因在负面标签上的判定被裁判推翻了 21 条(11+10)。这个量级意味着:如果你直接拿规则归因的结果算安全指标,你算出来的分数有一大半不是模型的行为,是归因器的行为。

代码:判据是怎么算出来的

上面所有关于「支撑度」「拒答识别」「配对显著性」的结论,都来自下面这几段真实源码。整段照抄,不做改写。

句级支撑度判定

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

这是规则归因的核心入口。threshold 默认 0.35,低于这个值的句子会被判为无支撑,进而可能落到 HALLUCINATION 或 UNKNOWN。安全评测里负面题本来就容易触发这类判定,阈值稍微一动,标签分布就会整体位移。

字符 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)

注意分母是 len(grams_a),不是并集。这是单向覆盖率,不是对称相似度。拒答类答案通常很短,grams_a 很小,分母小,一点噪声就能让比值剧烈波动。这就是为什么拒答样本的 faithfulness 只有 0.2321 这种量级------它是短文本打分尺度的产物。

中文句子切分

复制代码
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

切分粒度决定了有多少个句子参与支撑度计算。拒答答案往往只有一句话,切出来就是一个句子,这一个句子的支撑度直接等于整条样本的分数。样本量越小,方差越大。

配对回归检测

复制代码
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,

这个函数是 CI 门禁的基础。alpha=0.05、min_effect_size=0.1、min_paired_samples=5,三个参数一起决定一条退化判定会不会被卡住。

工程落地:CI 该卡什么

看框架自带的回归报告,diff_20260827T095152Z_vs_20260827T094242Z.md:

指标 基线 当前 95%CI Cohen's dz p 判定
context_precision 1.000 1.000 0.000, 0.000 0.000 1.000 无明显变化
context_recall 1.000 1.000 0.000, 0.000 0.000 1.000 无明显变化
faithfulness 0.956 0.326 -0.746, -0.514 -2.032 0.000 显著退化
answer_relevancy 0.739 0.774 -0.060, 0.130 0.138 0.457 无明显变化
answer_correctness 0.816 0.393 -0.526, -0.321 -1.544 0.000 显著退化

这份报告配对 30 题。faithfulness 的 Cohen's dz 是 -2.032,answer_correctness 是 -1.544,两个都是显著退化。但 context_precision 和 context_recall 都是 1.000 → 1.000,p=1.000。

这说明什么?说明检索侧指标在这两次运行里完全没有区分度,全满分。全满分指标在 CI 里是没有信息量的,卡它等于没卡。真正有信息量的是 faithfulness 和 answer_correctness。

所以 CI 门禁该这么配:

  • 整体分只作为观察项,不作为门禁项。整体分受测试集构成影响,比例一变它就变,卡它会误伤。
  • 安全类指标按类别分层报。至少拆成「应正常作答」和「应拒答/应拦截」两类,每类单独给召回和误杀。
  • 类别比例写进数据集元信息,和 hash 一起版本化。examples/demo_rag/dataset.jsonl 的 hash 是 1385126cffea,四次运行都一致,这是好事;但比例本身没有在报告里体现,这是缺口。
  • 判据阈值(比如 sentence_supported 的 0.35)要进版本控制。阈值改了,历史分数不可比。
  • 规则归因的结果不能直接进 CI,必须先过裁判复核。20260827T095152Z 那次推翻 11 条,20260921T010853Z 推翻 10 条,不复核直接用,门禁会疯狂误报。
  • 拒答口径要显式定义。这次审计用的是正则匹配「无法从资料中找到」类措辞,这个口径本身也要版本化,否则换一版正则,历史数据就断了。

还有一个成本维度值得写进 CI 预算。裁判开销:20260827T094242Z 是 315 次调用、输入 234321 token、输出 26017 token、约 0.0919 美元;20260827T095152Z 是 255 次调用、输入 236008 token、输出 128406 token、约 0.205 美元。注意输出 token 差了近 5 倍(26017 vs 128406),因为 weak 模式下裁判要写更多解释。如果你打算把负面题比例从 10% 提到 30%,裁判的调用量和输出 token 都会跟着涨,预算要提前算。

延迟也要看:20260827T094242Z 均值 589.0ms、中位 593.4ms、P95 781.22ms;20260827T095152Z 均值 4022.7ms、中位 3180.4ms、P95 9216.3ms。整轮耗时分别是 527.13s 和 594.9s。如果负面题变多导致裁判复核轮次增加,整轮耗时还会往上走。

边界与不适用场景

这套「比例决定分数」的结论,有明确的适用边界。

第一,比例不是越高越好。 把负面题比例往 30% 甚至更高推,会让误杀率被放大。因为负面题多了,模型拒答的机会多了,但正面题被误拒的机会也多了。如果你的线上真实分布里负面请求只占 5%,测试集用 30%,你测出来的误杀率会显著高于线上实际体验,团队会为了压这个虚高的误杀率去做过度优化。

第二,比例要贴近线上真实分布。 这不是说不能做压力测试,而是说压力测试的结果不能当作常规门禁基线。建议拆两套:一套按线上分布采样的回归集,用来卡门禁;一套负面题加权的压力集,用来做探索性评估,不卡门禁。

第三,样本量不足时结论不成立。 这次的运行都是 30 题。30 题里 10% 是 3 题,30% 是 9 题。3 题和 9 题的差距在统计上非常脆弱,一条样本翻转就能让比例失真。compare_runs 里 min_paired_samples=5 这个默认值,在 30 题的规模下其实很容易被触到边界。如果你的安全评测集只有几十题,别急着下「比例决定分数」的定量结论,先扩样本。

第四,交互项存在时不能单变量归因。 faithfulness 的交互项是 0.3425,top_k 的效果在 grounded 和 weak 下差十几倍。这意味着你不能说「把 top_k 调大就安全了」,也不能说「换成 grounded 就安全了」。两个旋钮要一起配。

第五,这套方法不适用于纯生成质量评测。 如果你评的是创意写作、摘要流畅度这类没有明确正负样本之分的任务,类别分层的思路用不上。分层的前提是你能把样本明确划进互斥的类别。

最后回到那个数字。0.9556 和 0.3257,同一个裁判、同一份数据集 hash、同一套判据。差的不是模型能力,是你怎么组题、怎么分层、怎么归因。安全评测里最容易假通过的地方,从来不是模型答对了,是测试集没让它有机会答错。


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

相关推荐
slacker-kian1 小时前
[实践]-用LLM对文本结构化:把多段文本描述变成规范的 JSON 知识库
人工智能·llm·prompt·结构化·pydantic·one-shot 提示
品牌常新2 小时前
景区巡逻机器人怎么选?派宝S5全地形守护解析
运维·人工智能·机器人
冬奇Lab2 小时前
LLM 驱动的自动化测试系列(13):Flaky Test 不是重跑几次就能解决的分类问题
人工智能·测试
血小溅2 小时前
当交付不再带来增长,医疗产品如何借 AI 持续进化?
人工智能
幻灭行度2 小时前
基于昇腾 910B NPU 部署 SenseVoiceSmall 语音识别服务
人工智能·语音识别
冬奇Lab2 小时前
一天一个开源项目(第237篇):DeepSeek Bot —— 把 DeepSeek Harness 变成一个有记忆、能群聊、会互相派活的 Bot 团队
人工智能·开源·deepseek
larance3 小时前
DecisionTree 二分类
人工智能·分类·数据挖掘
诸神缄默不语3 小时前
小春学习日刊 2026年第3期:2026.10.15
人工智能