AI模型安全扫描器评测:F1之外,还需覆盖率和故障恢复 📅 发布时间:2026/8/31 1:42:32 👁 浏览次数: 当一个 AI 模型安全扫描器在测试集上跑出 0.98 的 F1 分数时很多团队会认为它可以放心上线。然而一旦接到真实模型情况往往完全不同新出现的提示注入变体没有被识别扫描器在某个输入格式下直接抛异常甚至进程崩溃后没有任何重试机制评测阶段的高分无法转化为生产环境的可靠性。这里的 F1 是分类任务中精确率与召回率的调和平均不是键盘上的 F1 功能键。它衡量的是扫描器“判断得准不准”却没有回答另外两个问题扫描器到底能覆盖多大范围的安全风险它遇到自身故障时能不能恢复正常继续工作围绕 AI 模型安全扫描器的工程化落地下面讨论的评测主线是在 F1 之外补上覆盖率coverage和故障恢复failure recovery两个评估维度并搭建一个最小可运行的评测框架。通过这套框架你可以把模糊的“安全感”变成可量化、可对比、可回归的数据也能在扫描器误报、漏报、卡死、崩溃时定位问题出在检测算法还是外围调用逻辑。1. 为什么 F1 不能单独衡量 AI 模型安全扫描器的价值1.1 F1 到底在衡量什么F1 Score 是精确率Precision和召回率Recall的调和平均公式为F1 2 * Precision * Recall / (Precision Recall)精确率回答的是“扫描器判定为风险的内容里有多少确实有风险”召回率回答的是“真实有风险的内容里扫描器找回了多少”。对于正负样本不平衡的风险检测场景F1 比准确率更有参考价值因为它不会让“全部判为正常”这种偷懒策略拿到高分。但 F1 有一个隐含前提被评估的样本必须已经被明确标记而且扫描器必须返回一个可分类的判定结果。真实生产环境并不总是满足这个前提。扫描器可能超时、崩溃、返回空值、返回未知类别这些情况如果直接当作“正常”或“未命中”处理F1 就会被严重扭曲。1.2 相对准确率覆盖率更能反映“扫得到”覆盖率Coverage在 AI 模型安全扫描器场景里指的是“扫描器能够有效处理给定评估样本的比例”。它不关心判断是否完全准确只关心扫描器有没有“接得住”。F1 只计算有明确标签且能返回判定结果的样本。如果评测集合里根本没有“提示注入”类别扫描器对“提示注入”的检测能力再差F1 也不会暴露。覆盖率会暴露它会把“当前评测集覆盖了多少风险类别”“哪些类别样本没有被有效处理”单独列出来。举个实际例子。一个扫描器对有害内容检测很准精确率和召回率都在 0.95 以上。但它的评测集只包含“暴力内容”和“色情内容”两类没有包含“提示注入”和“数据泄露探测”。真实模型上线后第一批恶意输入恰好是提示注入扫描器完全没有识别。此时 F1 仍然很高但覆盖率的类别维度会直接暴露出“缺少两个风险类别”。问题F1 是否反映覆盖率是否反映故障恢复是否反映正负样本不平衡部分反映不直接反映不反映某个风险类别完全没有样本不反映直接反映不反映扫描器超时或崩溃不反映不反映直接反映扫描器返回无效格式不反映应反映不直接反映故障后是否能继续服务不反映不反映直接反映1.3 故障恢复能力决定扫描器能不能长期“扛得住”AI 模型安全扫描器本质上是一个服务或一个库它和任何线上组件一样会面对超时、异常、内存溢出、进程崩溃。评测阶段如果只给扫描器“正常样本”不给“故障样本”就看不到它在生产环境里的真实表现。故障恢复Failure Recovery评估的是扫描器在发生单次调用失败、进程级故障、服务过载之后能否在预期时间内自动恢复并且不丢失正在处理的任务。F1 不会反映这个问题甚至会把故障样本当作单纯漏报或误报导致排错方向完全错误。经验做法是把“可恢复的失败”和“检测能力不足”分开统计。超时、崩溃、无效输出属于调用链问题不属于算法问题。评估报告应该独立呈现这些数据。2. 先定义清楚覆盖率、故障恢复和评测基线2.1 覆盖率不是单一维度覆盖率不是一个简单百分比在 AI 模型安全扫描器评估中至少应该拆成四个维度。风险类别覆盖率评估的是扫描器需要支持的全部风险类别中评测集覆盖了多少扫描器又处理了多少。输入形态覆盖率关注的是文本长度、语言、编码、结构化输入、特殊字符等输入形态。模型实例覆盖率关注扫描器是否适配不同的模型供应商、模型版本、量化方式和部署环境。业务语义覆盖率关注的是不同业务场景下的风险特征例如客服系统、代码生成助手、内容社区同一类风险的表现形式可能完全不同。覆盖维度要回答的问题常见评估方法风险类别覆盖率是否所有风险类别都有样本且扫描器能处理按类别构造样本统计有效处理比例输入形态覆盖率不同输入格式是否不会导致扫描器失效长度、语言、JSON、特殊字符分组模型实例覆盖率不同模型版本和部署环境下是否一致同一批样本跑多个模型实例业务语义覆盖率当前业务场景的风险特征是否被覆盖从线上日志采样补充评测集2.2 故障恢复的三个等级不是所有故障都需要同一个恢复策略。评估之前先把故障恢复分成等级。第一级是单次调用失败后重试成功。常见于网络抖动、瞬时超时。扫描器本身没有崩溃只是这一次调用没有在限定时间内返回。重试是这一级最常见的恢复手段。第二级是进程级故障后自动重启并恢复服务。常见于子进程崩溃、内存占用过高被系统杀掉。评估时要在进程被杀后观察扫描器能否自动拉起并且健康检查能否通过。第三级是过载场景下的排队和降级。当扫描请求量超过处理能力时扫描器应该拒绝新请求保护正在处理的请求而不是全部超时。这一级最难评估通常需要压测工具配合。注意故障恢复评估的关键不是“有没有异常日志”而是“故障发生后系统能否回到可用状态”。健康检查用例应该覆盖扫描器的核心能力不能只检查进程还在不在。2.3 评测基线要先固定覆盖率对比和恢复指标对比都有一个前提基线一致。数据集版本、模型样本版本、扫描器版本、依赖版本、硬件环境、超时配置、重试次数任何一项变化都会让对比失去意义。例如第一次评测时超时时间设置为 10 秒第二次设置为 2 秒扫描器在 3 秒返回结果。两次测评的覆盖率可能差 20%但这个差异并不代表扫描器变差了只代表配置变了。因此项目里要维护一份环境指纹至少包含以下内容。环境项固定方式影响评测数据集版本使用带版本号的 JSONL 文件影响负样本分布和覆盖率上限扫描器版本记录 Git commit 或依赖锁文件影响检测能力和故障行为超时时间统一放在配置文件中影响超时类失败数量重试次数统一配置影响恢复指标随机种子如果涉及采样固定 seed影响评测可复现性硬件与部署拓扑记录 CPU/GPU、内存、容器配置影响性能和内存类故障3. 用最小评测框架把三个维度量化出来3.1 项目结构和评测流程评估框架不追求复杂关键是流程完整。建议的评测流程是准备评测用例 - 加载扫描器适配器 - 逐条执行并记录结果 - 单独注入故障 - 计算覆盖率、恢复指标、分类指标 - 生成报告。目录结构可以按下面的方式组织实际项目可以根据团队习惯调整。eval_ai_scanner/ ├── cases/ │ ├── normal.jsonl │ ├── risk.jsonl │ └── failure_probe.jsonl ├── scanner_adapter.py ├── coverage.py ├── recovery.py ├── report.py └── run_eval.pyscanner_adapter.py负责对接真实扫描器是框架和扫描器之间的边界。run_eval.py是总入口串联执行、统计和报告。coverage.py和recovery.py分别计算覆盖率和恢复指标。3.2 定义评测用例和标签格式评测用例使用 JSONL 格式每行一个 JSON 对象。字段至少包含id、category、input_text、expected。故障探针用例额外增加inject_failure字段用来模拟超时和崩溃避免把真实攻击样本写进代码。{id: case-001, category: benign, input_text: 请介绍一下机器学习, expected: pass} {id: case-002, category: prompt_injection, input_text: 评测集统一维护的测试输入, expected: block} {id: case-003, category: harmful_content, input_text: 评测集统一维护的测试输入, expected: block} {id: fault-001, category: benign, input_text: 健康检查输入, expected: pass, inject_failure: timeout} {id: fault-002, category: benign, input_text: 健康检查输入, expected: pass, inject_failure: crash}这里不展示具体风险输入内容因为评测集应由安全团队单独维护并经过脱敏和授权。框架只关心标签结构和评估逻辑。3.3 覆盖率统计模块覆盖率模块的输入是执行结果列表输出是分类覆盖率和样本覆盖率。有效处理的定义是扫描器在配置时限内返回了符合预期结构的结果且没有抛出未预期异常。def compute_coverage(results, required_categories): total_categories len(required_categories) covered_categories set() handled_samples 0 for r in results: if r[status] ok: handled_samples 1 covered_categories.add(r[case][category]) sample_coverage handled_samples / len(results) if results else 0.0 category_coverage len(covered_categories) / total_categories if total_categories else 0.0 return { sample_coverage: sample_coverage, category_coverage: category_coverage, handled_samples: handled_samples, total_samples: len(results), covered_categories: sorted(covered_categories), }这里有一个容易忽略的点required_categories不是从评测集倒推出来的而应该是产品需求里明确要支持的风险类别集合。否则评测集缺少某个类别时覆盖率不会下降问题会被掩盖。3.4 故障注入与恢复验证模块故障注入不是去攻击系统而是通过受控方式模拟扫描器外围故障。示例中用一个FakeScannerAdapter模拟扫描器行为真实项目中替换成对应 SDK 的调用即可。import time class ScannerTimeout(Exception): pass class ScannerCrash(Exception): pass class Runner: def __init__(self, adapter, timeout3.0, max_retries2): self.adapter adapter self.timeout timeout self.max_retries max_retries def invoke(self, case): if case.get(inject_failure) timeout: raise ScannerTimeout(scanner did not respond before timeout) if case.get(inject_failure) crash: raise ScannerCrash(scanner process crashed) return self.adapter.scan(case[input_text], case[category]) def invoke_with_retry(self, case): last_exception None for attempt in range(self.max_retries 1): try: return self.invoke(case) except ScannerTimeout as exc: last_exception exc except ScannerCrash as exc: last_exception exc raise last_exception恢复验证的基本思路是每执行一个故障探针之后立即执行一组健康检查用例。如果健康检查全部通过认为恢复成功同时记录从故障发生到恢复成功的时间用于计算平均恢复时间。def evaluate_recovery(runner, failure_probes, health_checks): events [] for probe in failure_probes: start time.monotonic() try: runner.invoke_with_retry(probe) events.append({ case_id: probe[id], failure_type: none, recovered: True, recovery_seconds: 0.0, }) except Exception as exc: failure_type type(exc).__name__ recovered True recovery_seconds 0.0 try: health_start time.monotonic() for health_case in health_checks: runner.invoke_with_retry(health_case) recovery_seconds time.monotonic() - health_start except Exception: recovered False events.append({ case_id: probe[id], failure_type: failure_type, recovered: recovered, recovery_seconds: recovery_seconds, }) return events真实项目中健康检查用例应该来自线上流量采样并且要有断言逻辑不能只检查“有没有抛异常”还要检查返回的判定类别是否符合预期。3.5 汇总指标并生成报告报告的职责是把分类指标、覆盖率、恢复指标汇总到一张表里方便团队做决策。下面的函数示例计算了 F1、精确率、召回率。def compute_classification_metrics(results): tp fp tn fn 0 for r in results: if r[status] ! ok: continue expected r[case][expected] verdict r[verdict] if expected block and verdict block: tp 1 elif expected pass and verdict block: fp 1 elif expected pass and verdict pass: tn 1 elif expected block and verdict pass: fn 1 precision tp / (tp fp) if (tp fp) else 0.0 recall tp / (tp fn) if (tp fn) else 0.0 f1 2 * precision * recall / (precision recall) if (precision recall) else 0.0 return { precision: precision, recall: recall, f1: f1, tp: tp, fp: fp, tn: tn, fn: fn, }这里的关键设计是超时和崩溃样本不进入分类指标计算而是由恢复模块单独统计。这样避免了“把故障误判为漏报”的经典错误。4. 关键指标怎么算从原始结果到可决策的数据4.1 分类结果表和扫描失败事件执行完评测之后每条样本会落在一个状态里。常见状态包括正常判定、超时、崩溃、无效输出。建议把状态划分清楚再分别计算指标。状态含义对 F1 的影响对覆盖率的影响对恢复指标的影响ok返回了合法判定结果参与计算计入有效处理不参与timeout超过配置时限未返回不计入不计入有效处理作为故障事件crash进程崩溃或异常退出不计入不计入有效处理作为故障事件invalid_output返回内容不符合 schema不计入建议不计入建议作为故障事件无效输出容易被忽略。扫描器返回了文本但文本不是block或pass也没有置信度字段。这种情况如果直接解析失败并抛异常会污染整个评测链路。比较好的做法是单独拦截并给出明确报错信息。4.2 覆盖率计算方式在最小框架中覆盖率可以拆成三个公式。样本覆盖率表示所有评测样本中扫描器有效处理的比例。公式为sample_coverage 有效处理样本数 / 总样本数类别覆盖率表示产品要求支持的风险类别中扫描器实际处理过的类别比例。公式为category_coverage 有效处理过的类别数 / 产品要求支持的风险类别数输入形态覆盖率需要先把样本按输入形态分组比如“短文本、长文本、结构化 JSON、特殊字符”然后计算有效处理的分组比例。公式与类别覆盖率类似input_group_coverage 有效处理过的输入形态分组数 / 全部输入形态分组数在实际项目中如果某类样本完全缺失应该先补样本再谈覆盖率。不要为了覆盖率好看刻意去掉困难类别。4.3 恢复指标MTTR、恢复成功率、重试成功率恢复指标至少应该包含三个。平均恢复时间MTTR表示从故障发生到系统恢复可用所消耗的平均时长。计算方式为MTTR 所有故障恢复耗时之和 / 成功恢复的故障次数恢复成功率表示所有注入的故障中最终成功恢复并健康检查通过的比例。计算方式为recovery_success_rate 成功恢复次数 / 故障注入总次数重试成功率表示在配置的最大重试次数内调用最终成功的比例。计算方式为retry_success_rate 重试后成功的调用次数 / 初始失败但可重试的调用次数这三个指标回答的问题不同。MTTR 关注恢复快不快恢复成功率关注恢复得稳不稳重试成功率关注瞬时故障能不能被自动消化。4.4 综合评分不要拍脑袋不建议把 F1、覆盖率和恢复指标压缩成一个综合分。综合分会掩盖细节比如覆盖率只有 0.4 但 F1 达到 0.99综合分可能仍然很高团队不容易发现问题。更建议的做法是设置发布门禁。例如如果 F1 低于 0.9禁止发布先优化检测算法。如果类别覆盖率低于 0.8禁止发布先补齐评测集和扫描器能力。如果恢复成功率低于 0.9禁止发布先完善重试、熔断和重启机制。如果 MTTR 超过业务容忍上限禁止发布先优化故障转移方案。门禁阈值根据业务风险承受能力制定没有统一标准但一定要写清楚。5. 跑通评测实验并解读结果5.1 准备一个最小实验为了验证框架准备一个最小实验。数据集包含 50 个正常样本、20 个风险样本、5 个故障探针。风险样本覆盖三类风险但产品需求中要求支持五类风险。这样的设计是为了让 F1 很高但覆盖率暴露问题。数据集样本数说明normal.jsonl30正常输入期望 passrisk.jsonl20风险输入覆盖 danger_a、danger_b、danger_cfailure_probe.jsonl5其中 3 个 timeout2 个 crash理论上如果扫描器对现有风险样本判断准确F1 会很高。但由于缺少 danger_d 和 danger_e 两类样本类别覆盖率最高只有 0.6。5.2 运行代码和预期输出在项目根目录执行cd eval_ai_scanner python run_eval.py \ --cases-dir cases \ --failure-probe cases/failure_probe.jsonl \ --timeout 3.0 \ --max-retries 2框架会输出类似下面的报告。这里的作用是展示格式不涉及真实扫描器数据因此请忽略具体数值的含义。Classification Metrics precision: 0.95 recall: 0.97 f1: 0.96 Coverage Metrics sample_coverage: 0.92 category_coverage: 0.60 covered_categories: danger_a, danger_b, danger_c Recovery Metrics failure_count: 5 recovery_success_rate: 0.80 mttr_seconds: 1.42 retry_success_rate: 0.90从结果看F1 达到 0.96看起来不错。但category_coverage只有 0.60说明产品要求的五个风险类别里有两个完全没有被有效处理。这个信号比 F1 更值得关注。5.3 结果解读为什么 F1 高但覆盖率低F1 高通常意味着扫描器对“样本里出现过的正负类别”判断得不错。覆盖率低则说明“样本里没出现过的类别”可能完全没有检测能力或者评测集中根本就没有这些类别的样本。两种情况需要区别处置。如果评测集缺少 danger_d 和 danger_e那么问题出在评测集不完整需要补充样本后重新评测。如果评测集存在这两个类别的样本但扫描器全部超时或返回无效输出那么问题出在扫描器适配层或类别映射逻辑。最糟糕的处理方式是为了让覆盖率数字好看把没有样本的类别从需求类别列表里删除。这会让覆盖率失去意义。5.4 从结果反推配置问题覆盖率下降并不一定是扫描器算法差也可能是配置问题。常见的反推路径如下。超时配置过短会导致大量 timeout样本覆盖率和恢复指标都会受影响。建议先检查扫描器在正常负载下的 p99 耗时再把评测超时时间设置为该值的 2 到 3 倍。类别映射缺失会导致扫描器返回unknown被记为无效输出从而拉低覆盖率。检查扫描器返回的类别列表和评测标签是否对齐。重试次数设置不合理会导致瞬时故障无法恢复。如果日志显示大量第一次调用失败、第二次成功说明重试机制是有效的如果重试后仍然失败再考虑熔断和降级。6. 常见问题排查覆盖率虚高、恢复失效、评测不稳定6.1 覆盖率虚高覆盖率虚高是评测中最常见的问题。现象是上报的覆盖率很高但真实流量中扫描器仍然频繁“接不住”。产生虚高主要有两个原因。第一个原因是评测集过于简单所有样本都来自同一模板扫描器只需要记住模板就能通过。第二个原因是对“有效处理”的定义太宽松把返回空值、返回未知类别、兜底成功都当成有效处理。检查方式有两个方向。一个是随机抽样评测集确认风险类别的多样性另一个是查看原始结果确认是否存在大量unknown或默认pass被错误标记为 ok。处理建议评测集必须包含难度梯度无效输出必须单独标记覆盖率定义必须写明“返回合法判定结果才算有效处理”。问题现象常见原因检查方式处理建议覆盖率虚高评测集模板化有效处理定义过宽随机抽样检查多样性统计 unknown 输出增加难度梯度严格定义有效处理恢复测试无效故障注入没有触发异常健康检查用例太简单查看故障日志确认异常类型使用受控故障注入增强健康检查断言评测不可复现依赖版本、随机种子、超时配置不一致记录环境指纹固定配置使用 CI 固定环境固定随机种子6.2 故障恢复测试无效故障恢复测试无效通常表现为报告里恢复成功率是 100%但线上扫描器一崩溃就不可用。排查时先看故障是否真的被注入了。如果inject_failure字段没有在适配器里被处理执行时根本不会抛异常恢复指标自然全是成功。检查方式是在故障探针执行前后打印日志确认异常确实发生。再看健康检查用例是否有判断力。如果健康检查只调用了一个pass样本扫描器即使内部状态已经损坏也可能返回兜底结果。健康检查应该覆盖核心分类能力并且对返回结构做严格校验。6.3 评测结果不可复现评测结果不可复现通常是因为环境不一致。今天跑出来 F1 是 0.96明天变成 0.88但代码没有变化。优先检查随机种子。如果评测集从全量数据中采样不固定随机种子每次跑出的数据集都不一样。如果扫描器内部有阈值抖动或采样逻辑也必须有可重复性设置。建议在评测入口记录环境指纹包括 Python 版本、依赖包版本、数据集文件哈希、扫描器 commit 号。这样可以快速定位“结果变了”是因为代码变了还是环境变了。7. 生产环境评估的最佳实践与检查清单7.1 学习环境、测试环境、生产环境的评估差异学习环境里用小规模数据集跑通评测框架重点是理解指标含义。测试环境里用接近真实的样本库评估 F1 和覆盖率并通过故障注入验证恢复机制。生产环境里不能直接拿全部线上流量做全量扫描评估应该采用灰度测试、影子模式、逐步扩大流量比例的方式。生产环境额外要做三件事。一是把评测脚本接入 CI扫描器每次发布前自动跑回归。二是在线监控扫描器的超时率、崩溃率、无效输出率这些指标应该和覆盖率、恢复指标关联观察。三是保留历史评测报告方便做趋势分析和回滚决策。7.2 可复用清单AI 模型安全扫描器评测清单上线之前按下面的清单逐项确认比单纯看 F1 数字更可靠。评测集是否覆盖了产品要求支持的所有风险类别评测集中是否包含困难样本、异常输入、超时探针每个样本是否记录了预期的判定结果和类别标签扫描器返回unknown、空值、异常时是否单独归类超时时间、重试次数、并发度是否统一配置并记录是否单独统计了超时、崩溃、无效输出事件故障注入后是否使用健康检查用例验证恢复是否记录了数据集版本、扫描器 commit、依赖版本、随机种子是否存在发布门禁且门禁条件覆盖 F1、覆盖率、恢复指标这个清单可以在每次评测前十分钟内过完能够拦截大多数“F1 好看但上线不稳定”的问题。7.3 后续扩展持续评估、回归守护、模型漂移把覆盖率纳入评测只是第一步。更进一步的实践是建立持续评估机制让安全扫描器的质量不是一次性检查而是长期可观测。一个推荐的做法是每周从线上流量中抽样人工标注后加入评测集并重新计算覆盖率。新风险类别出现时先补样本再评估扫描器是否能够识别。模型版本升级时也要重新跑一次评测因为同样的输入在不同模型上的行为可能不同扫描器的表现会跟着变化。回归守护可以放在 CI 里每次扫描器依赖变化或代码变化时自动运行最小评测集防止“改一个依赖结果全变”的情况出现。所以下次看到某个 AI 模型安全扫描器报出很高的 F1 时先不要急着上线。补充问一句数据覆盖面是什么故障后能不能自己恢复如果这两个问题答不上来再高的分数也只能说明它在“已知且能处理的样本”上表现不错。把评价体系从 F1 扩展到 Coverage 和 Failure Recovery才是把安全扫描器真正工程化的第一步。