拒答评估
在 benign prompt 上的有用性,和在 harmful prompt 上的 refusal,是两个指标,不是一个。两个都要测。
类型: Build 语言: Python 前置要求: 阶段 18 安全相关课程、阶段 19 Track A 第 25-29 课 预计时间: ~90 分钟
问题背景
助手上的一次安全检查会朝两个相反的方向出错。模型拒答了它本该回答的(over-refusal),模型又回答了它本该拒答的(under-refusal)。两者都是 bug。只测 harmful prompt 上 refusal 率的团队,会上线一个连化学作业都不肯帮忙的模型。只测有用性的团队,会上线一个会讲解怎么作恶的模型。正确的指标集合把助手当成一个针对 prompt 安全性的二分类器,由被测的 policy 来决定每条 prompt 落在决策边界的哪一侧。
难点在于,真实的 LLM 在开发期间是个移动靶。改一下 system prompt、改一下 temperature,或者换一个 tool,都会让两个指标一起动。你需要一个基于 fixture 的评估器,底层挂一个确定性的 mock LLM,这样一次代码改动让指标变动,原因是已知的。本课的 mock LLM 是一张小小的分派表:模式、policy、输出。第 87 课的安全防护稍后会把这个 mock 换成一个 HTTP 调用,从而插入一个真实模型。指标框架本身不变。
本课构建评估框架。它产出四个数:under-refusal 率、over-refusal 率、校准度(calibration,模型自报的置信度是否与其准确度相符),以及一份用第 82 课 taxonomy 做的按类别拆分。
核心概念
一次 refusal 评估有四个对象:一个带 safe/unsafe 标签的 prompt 集合,一个挂着脚本化 policy 的 mock LLM,一个作用在模型输出上的 refusal classifier(在 I cannot、I will not、as a responsible 这类短语上做正则),以及一个指标聚合器。
flowchart LR
P[已标注的 prompt 集合] --> M[mock LLM]
M --> R[refusal classifier]
R --> A[聚合器]
A --> UR[under-refusal 率]
A --> OR[over-refusal 率]
A --> CAL[校准度 ECE]
A --> CAT[按类别拆分]mock LLM 的 policy 故意做得很简单,这样测试可审计。默认的 MockPolicyStrict 对命中一列违禁正则模式中任意一条的 prompt 拒答,其余则回答。MockPolicyOverCautious 在一个宽得多的模式集合上拒答,所以它故意 over-refuse。MockPolicyLeaky 只在最明显的情况下拒答,所以它故意 under-refuse。这三个 policy 是脚本化的 bug,框架应该能检测出来。
under-refusal:在标签集合标为 unsafe 的 prompt 上模型回答了(没有 refusal 短语)。over-refusal:在标签集合标为 safe 的 prompt 上模型拒答了(出现了 refusal 短语)。框架还会算 accuracy = (TP + TN) / total,其中 TP 是"unsafe prompt 被正确拒答",TN 是"safe prompt 被正确回答"。
校准度用 Expected Calibration Error(ECE),作用在模型自报的置信度上。mock LLM 可选地在输出里发出一个 confidence:0.X token;框架解析它。ECE 按置信度以十分位分桶,计算每个桶的准确度,并以桶大小为权重对 |conf - accuracy| 求平均。一个说 confidence:0.9 但只有 60% 时候答对的模型,在那个桶上 ECE 大约是 0.3。ECE 与 over/under refusal 无关,因为它度量的是模型是否知道自己什么时候是对的。
按类别拆分把已标注的 prompt 与第 82 课的 taxonomy 产物 join 起来。每条 unsafe prompt 都带一个类别标签(六类之一)。框架报告每个类别的 under-refusal 率,这样团队就能看到,比如说,模型把 instruction-override 处理得很好,但在 multi-turn-ramp 上掉链子。
ci-refusal-quadrant动手构建
code/mock_llm.py 定义三个 policy。每个 policy 是一个把 prompt 映射成响应字符串的可调用对象。响应里把模型的置信度嵌成 [conf=0.X]。code/prompts.py 是一份已标注语料库:25 条 unsafe prompt(按 id 从第 82 课的 taxonomy 里取),外加 30 条 safe prompt(日常 benign 的请求,与第 83 课的 benign 集合不重叠,这样两次评估保持独立)。
code/main.py 运行评估器。refusal classifier 是一个 refusal 短语的正则。聚合器返回一个含 under_refusal、over_refusal、accuracy、ece 和 per_category_under_refusal 的 dict。runner 把三个 mock policy 都扫一遍,并写出一份对比报告。
实际使用
python3 main.py。demo 打印一张对比三个 policy 的表,写出 outputs/refusal_eval_report.json,并确认 MockPolicyOverCautious 的 over-refusal 最高、MockPolicyLeaky 的 under-refusal 最高。strict policy 夹在两者中间;那就是回归基线。
拿去用
outputs/skill-refusal-evaluation.md 记录了各指标的定义,这样下游使用这份报告的人不会读错数字。
练习
- 增加第四个 mock policy,按 prompt 长度来拒答。确认 under-refusal 在编码攻击上上升(这类攻击往往很短)。
- 把 ECE 换成 reliability 曲线,给每个 policy 画一条。指出哪些桶过度自信。
- 增加一份按类别的 safe prompt 列表(benign 的 role-play、关于先前上下文的 benign 指令)。计算每个类别的 over-refusal,看看是不是 role-play 招来的误拒最多。
关键术语
| 术语 | 通常用法 | 精确含义 |
|---|---|---|
| under-refusal | 模型很有用 | 模型回答了一条被标为 unsafe 的 prompt |
| over-refusal | 模型很安全 | 模型拒答了一条被标为 safe 的 prompt |
| calibration | 模型很谦逊 | 自报置信度与观测准确度之间的差距,用 Expected Calibration Error 概括 |
| accuracy | 质量 | safe/unsafe 二分类决策的 (TP + TN) / total |
| per-category breakdown | 一张图 | 与第 82 课 taxonomy 类别 join 后的 under-refusal 率 |
延伸阅读
第 85 课(输出 classifier)和第 87 课(端到端安全防护)都消费本课的指标框架。