匿名 AI 模型审计中最难的不是评估输出质量,而是在不接触权重与内部结构的前提下,通过黑盒接口确认模型身份。现实中的模型接口常常只给一个公网地址和鉴权信息,响应里可能没有model字段,模型卡、权重、推理代码都不会开放。此时审计方要面对一个看似矛盾的问题:供应商声称接口背后运行的是某个模型,但审计方只能通过输入文本、观察输出文本来验证这句声明。
这类任务通常叫匿名 AI 模型的黑盒身份验证,英文可以称为 Black-Box Identity Verification。它和模型能力评测不同,核心目标不是“模型强不强”,而是“接口背后的行为和参照模型是否一致、差异是否超出采样误差”。如果只是做一次 benchmark,高能力模型可能得到相近分数,低能力模型也可能因为训练数据重叠而表现接近,能力评测并不能充当身份证据。身份验证需要有稳定的探针集、可重复的采样策略和明确的统计判定规则。
下面这套方法把整个审计过程拆成四个阶段:固定声明与参照基线、采集行为指纹、做统计身份匹配、输出审计结论并进入持续复核。四阶段协议在授权审计、内部模型治理、供应商准入和模型版本回归场景中可以直接作为执行框架使用。所有探测查询都应在数据使用授权范围内进行,不能使用未脱敏个人信息或受版权限制的私密数据作探针,否则审计本身会引入合规风险。
1. 匿名 AI 模型为什么需要黑盒身份验证
1.1 匿名模型接口的常见审计场景
先解释一下“匿名 AI 模型”在这里指什么。它不是说模型作者匿名,而是指模型以 API 或网关形式暴露给调用方时,没有暴露模型卡、权重、训练数据或内部版本信息。调用方看到的只是一个可对话或可补全的接口,模型身份处在黑盒状态。
实际工作中至少有三类场景要求做身份核验:
- 供应商准入。合同约定使用某个版本的模型,但实际接口由供应商自研网关转发,调用方无法直接确认后端是否真的切换到了约定版本。
- 内部多个模型的统一接入。平台方把多个开源模型做成统一 API,路由策略可能按租户、按请求量、按时间窗口切换,审计时要确认租户请求实际命中了哪个模型。
- 模型迭代回归。上游模型从 7B 升级到 13B,从 FP16 量化为 INT8,或者从精调版本切换为基座版本,行为变化是否超出允许范围。
这些场景有一个共同点:审计方拿到的是模型的黑盒观测面,而不是模型内部文件。只看网络请求并不能证明“某个模型在运行”,因为网关层可以随意修改返回字段。哪怕响应里写了"model": "candidate-v1",这个字段也只代表路由标记,不构成身份证据。
1.2 为什么不能只信任响应中的模型名字段
很多模型服务在返回结果时会附带模型名称。正常情况下它有助于定位问题,但不能把它当作审计依据。原因有两层。
第一层是字段可能被网关覆盖。企业采购的模型服务通常不是直连官方模型,而是经过 API 网关、路由代理和流量治理层。网关在转发时可能重新写入或清洗model字段,也可能为了兼容旧版本统一返回一个默认名称。
第二层是字段与真实计算单元可能不同步。运营人员切流时可能只改了路由权重,没有同步更新响应字段;也可能同一个模型部署了多个推理引擎,量化位宽、batch 策略、缓存逻辑不同,导致同样的文本输入产生不同输出。此时model字段虽然是真实的,却不能说明部署形态一致。
所以黑盒身份验证必须观察模型本身的输出行为。审计协议设计的出发点,就是假定元数据不可信,用输入输出证据替代自报家门。
1.3 身份验证要回答的不是分数问题
黑盒身份验证要回答的问题是:给定黑盒目标模型 T,以及一个已声明身份 C,T 的行为是否与 C 的行为统计上一致。
这不同于“谁的能力更高”。两个模型可能都是优秀模型,但在同样前缀下的下一个 token 分布差异很大;反过来,同一个模型在不同采样参数下也可能给出不同答案。因此,验证协议需要同时控制两个变量:
- 模型本身的差异。
- 采样和部署带来的噪声。
四阶段协议的后两个阶段主要处理这一个问题。前面先做阶段一和阶段二,是为了确认比对对象、缓解实验噪声。
2. 四阶段协议的整体结构与每段交付物
2.1 为什么用协议而不用单一工具
模型身份验证很难靠一个脚本解决问题。原因是接口格式、概率输出能力、采样参数暴露程度、候选模型版本都会影响结果。如果只做一次“答案是否一致”的比较,很容易因为提示词太简单或参差不齐而被误导。
用协议来组织审计过程,是为了让人在任何一步停下来都能判断结果是否可信。每个阶段都有明确输入、明确操作和明确交付物。四阶段分别如下:
| 阶段 | 核心任务 | 典型交付物 |
|---|---|---|
| 阶段一 | 固定审计声明、授权范围和参照模型集 | 审计配置、候选模型基线说明 |
| 阶段二 | 构造探针集并采集黑盒行为指纹 | 探针集、采样配置、指纹 JSON 证据 |
| 阶段三 | 对行为指纹做统计比对,输出匹配结论 | 距离分数、置信区间、结论标签 |
| 阶段四 | 编写审计报告并设计持续复核机制 | 审计报告、复测计划、变更触发器 |
这四阶段不是一次性顺序执行完就结束。生产环境里,模型供应商可能悄悄升级版本,路由策略也可能变化。所以阶段四通常会回到阶段一,形成循环审计。
2.2 每一阶段最该守住的原则
阶段一最该守住的原则是“声明必须大于推断”。只有当审计方知道候选模型是谁,才能做参照比对。如果完全没有候选身份,黑盒探针只能形成行为画像,不能可靠地下结论说“这个接口就是某个具体模型”。
阶段二最该守住的准则是“控制采样噪声”。请求参数不一致会直接污染指纹,比如候选 A 使用temperature=0,候选 B 使用temperature=0.8,两者输出差异会被误判成模型身份差异。
阶段三最该守住的准则是“阈值需要校准”。任何距离阈值都必须结合已知同源模型和已知异源模型做对照,不能拿一个固定数字套所有任务。
阶段四最需要守住的准则是“保留证据”。所有请求原文、响应原文、采样参数、时间戳、哈希值都要归档,否则审计结论无法复核,出了争议也难以定位。
3. 阶段一落地:声明、授权与参照基线
3.1 先固定目标接口的已声明身份
审计启动时,第一步不是写代码调用接口,而是把“这家供应商声称接口后面是什么模型”记录下来。记录内容包括:
- 审计目标端点地址。
- 供应商声明的模型名称和版本,例如
internal-llm-v2.3。 - 发起审计的原因,例如“新合同验收”“上线前评估”“随机抽检”。
- 可使用的请求预算,例如“每日最多 3000 次调用”。
- 数据使用范围,例如“只能使用公司自备合成数据”。
这些信息会进入审计配置。在真实项目里,审计配置最好用 YAML 管理,避免在脚本中散落多个硬编码 URL。
audit: name: "anonymous-model-v2.3-verification" mode: "authorized" start_time: "2025-01-01T00:00:00+08:00" target: name: "anonymous-target" base_url: "${TARGET_BASE_URL}" claimed_identity: "internal-llm-v2.3" candidates: - name: "internal-llm-v2.3-ref" base_url: "${REF_V23_BASE_URL}" - name: "internal-llm-v2.1-ref" base_url: "${REF_V21_BASE_URL}" probe: max_queries: 1200 max_tokens: 32 temperature: 0.0 top_p: 1.0 repeat_times: 5 timeout_seconds: 60这段配置说明了一个关键做法:审计目标只有一个,但参照候选可以放多个。如果只放一个候选模型,所有距离分数都只针对“是不是某个模型”这一个结论,缺少区分度。放两个以上候选,可以观察目标与不同候选之间的相对距离,结论会稳健很多。
3.2 准备参照模型接口
身份验证必须建立在参照模型之上。参照模型应该满足三个条件:
- 与声明身份同源,优先使用同一供应商在稳定的部署环境中提供的接口。
- 版本信息明确,不能是某个未知派生版本或早期灰度版本。
- 有能力限制采样参数,至少能让审计方获得稳定的输出观测。
如果供应商只承诺模型名,不提供独立参照端点,可以让对方提供一小段可复核日志,但日志不能替代主动采样。最好的参照介质是能够接收相同提示词的 Oracle 接口,由审计方自己发请求。
没有参照模型时,四阶段协议不应该进入结论输出阶段。此时只能输出“能力画像”,不能输出“身份验证结论”。这一点在审计报告中要写得非常明确。
3.3 阶段一的检查点
阶段一做完整后,应确认以下问题都有明确答案:
- 目标端点和候选参照端点是否在同一个网络可达条件下?
- 探针数据是否已经完成脱敏和授权审批?
- 是否准备了至少两个候选模型,用于校准阈值?
- 采样参数是否能在所有候选上保持一致?
如果这些答案存在“不清楚”,不要急着进入阶段二。否则后面采集上来的指纹就算看起来漂亮,审计结论也没有可信基础。
4. 阶段二落地:设计探针集并采集行为指纹
4.1 探针集不是普通测试集
身份验证用到的探针集不需要追求“高难度”,它追求的是“区分度”。一段提示词如果任何模型都能给出同样答案,它只能证明系统在工作,不能用于判断身份。一段提示词如果同源模型之间稳定一致,而异源模型之间差异明显,它就是理想探针。
探针集应包含三部分:
- 低自由度确定性题。例如“只输出 A、B、C、D 中的一个选项”,模型在
temperature=0时输出应该稳定。 - 需要延续前缀的开放题。例如给定一段代码片段或一个故事开头,让模型续写有限长度,用于观察风格、词汇和格式特点。
- 边界与格式题。例如要求 JSON 输出、要求多行代码、要求给出空行、要求重复特殊符号,用来观察模型对格式指令的执行差异。
probe_items = [ { "id": "choice-001", "type": "choice", "prompt": "请只输出选项字母:A. 北京 B. 上海 C. 广州 D. 深圳。\n答案:" }, { "id": "continuation-002", "type": "continuation", "prompt": "请续写下面的函数,只写 Python 代码:\ndef deduplicate(items):\n" }, { "id": "format-003", "type": "json_format", "prompt": "请输出 JSON,字段包含 name 和 count,不要输出其他内容。" } ]这些只是示例,实际项目要根据候选模型类型调整。代码模型应增加代码探针,公文模型应增加公文格式探针,多模态模型如果允许文本输入则只能验证文本链路,不能验证视觉链路。
4.2 一个最小黑盒探针客户端
阶段二需要写一段可重复调用的客户端代码。下面的示例不绑定特定供应商,而是假设接口兼容常见的POST /completions协议。不同网关可能改路径,也可能要求把请求体包在固定结构中,使用时必须按真实协议调整。
import os import time import json import requests class BlackBoxModelClient: def __init__(self, name: str, base_url: str): self.name = name self.base_url = base_url.rstrip("/") self.headers = { "Authorization": f"Bearer {os.environ['AUDIT_API_TOKEN']}", "Content-Type": "application/json", } def complete( self, prompt: str, max_tokens: int = 16, temperature: float = 0.0, top_p: float = 1.0, ): payload = { "prompt": prompt, "max_tokens": max_tokens, "temperature": temperature, "top_p": top_p, } response = requests.post( self.base_url + "/completions", headers=self.headers, json=payload, timeout=(5, 60), ) response.raise_for_status() return response.json()这里要注意,Authorization头来自环境变量,不是把密钥写在源码里。如果黑盒接口不支持temperature、top_p参数,客户端里可以不传这些字段,但阶段三比对的原始记录里必须注明“目标与候选接口采样参数不统一”。
4.3 采集行为指纹并保存证据
探针执行时不要只保存最终文本,最好保存原始 JSON。原始 JSON 里可能包含 finish reason、token 数、概率字段、缓存命中信息,这些都能帮助排查异常。
def collect_fingerprints( client, probe_items, repeat_times: int = 3, save_dir: str = "evidence", ): os.makedirs(save_dir, exist_ok=True) records = [] for item in probe_items: for round_no in range(repeat_times): raw = client.complete(item["prompt"]) record = { "client": client.name, "probe_id": item["id"], "round": round_no, "prompt": item["prompt"], "raw_response": raw, "timestamp": time.time(), } with open( f"{save_dir}/{client.name}-{item['id']}-{round_no}.json", "w", encoding="utf-8", ) as fp: json.dump(record, fp, ensure_ascii=False, indent=2) records.append(record) time.sleep(0.2) return records保存证据时,还可以额外计算每个原始响应的 SHA-256 哈希。这样后续一旦发生“证据被编辑过”的争议,可以通过哈希快速校验。
import hashlib def evidence_hash(raw_obj) -> str: blob = json.dumps(raw_obj, sort_keys=True, ensure_ascii=False) return hashlib.sha256(blob.encode("utf-8")).hexdigest()4.4 从原始响应中抽取出可用于比对的指纹
原始响应不适合直接做统计。阶段二最后要做一步归一化,把输出转换成稳定结构。常见指纹结构如下:
{ "probe_id": "choice-001", "client": "target", "sample_count": 5, "sampling": { "temperature": 0.0, "max_tokens": 16 }, "choice_frequency": { "A": 0.8, "B": 0.0, "C": 0.2, "D": 0.0 }, "summary": "target tends to output A on this probe" }如果接口返回 logprobs,可以在一轮请求里直接拿到 top-k token 概率。如果接口不返回概率,就通过多次重复采样统计频率。两种方式都需要把输出做规范化,例如去掉两端空白、统一大小写、把数字1和中文一映射到同一类别。规范化逻辑应该是全程唯一确定的,不能在不同候选上分别实现。
5. 阶段三落地:统计比对与身份判断
5.1 用离散选择题的概率分布做基础比对
最简单的黑盒身份比对是直接看多次重复采样的答案频率。设目标模型在某个探针上的频次分布为 P,候选模型分布为 Q。如果两个模型同源,且采样条件相同,P 和 Q 不应相差太大;如果两个模型异源,差异通常会在多个探针上稳定出现。
下面是一段计算 Jensen-Shannon 散度的参考实现。它适合处理归一化的概率字典。
import math import statistics def _kl_divergence(p, q): keys = set(p) | set(q) divergence = 0.0 for key in keys: pv = max(p.get(key, 0.0), 1e-12) qv = max(q.get(key, 0.0), 1e-12) divergence += pv * math.log(pv / qv) return divergence def js_divergence(p, q): keys = set(p) | set(q) m = {} for key in keys: pv = max(p.get(key, 0.0), 1e-12) qv = max(q.get(key, 0.0), 1e-12) m[key] = 0.5 * (pv + qv) return 0.5 * _kl_divergence(p, m) + 0.5 * _kl_divergence(q, m)两个完全相同分布的距离为 0。分布差异越大,距离越大。代码里的平滑处理是为了防止“某项概率为 0”导致对数计算直接崩溃。
5.2 对多个探针取平均距离,并加入对照候选
单个探针的距离没有意义,某道题可能因为训练数据重叠而让多数模型表现一致。审计时应该对全部探针计算距离,然后取平均,并与多个候选模型做对照。
def compare_clients( target_fingerprints, candidate_fingerprints, probe_ids, ): distances = [] for probe_id in probe_ids: target_dist = target_fingerprints[probe_id] candidate_dist = candidate_fingerprints[probe_id] if "choice_frequency" in target_dist: p = target_dist["choice_frequency"] q = candidate_dist["choice_frequency"] else: p = target_dist["token_probability"] q = candidate_dist["token_probability"] distances.append(js_divergence(p, q)) return statistics.mean(distances)运行后可以得到结果表:
| 比较对象 | 平均 JSD | 判定倾向 |
|---|---|---|
| 目标 vs 声明版本 v2.3 | 0.028 | 很接近 |
| 目标 vs 旧版本 v2.1 | 0.214 | 距离明显 |
| 目标 vs 完全不同的模型 C3 | 0.386 | 距离很大 |
这样的相对比较比单纯看一个绝对阈值更可信。如果目标与声明版本的分数显著低于与其他候选的分数,结论会偏向“支持声明身份”。
5.3 判定标签:匹配、不匹配、不确认
黑盒身份判断最好不要用“是”或“不是”这种二值语言,更稳妥的是三标签体系。
| 标签 | 含义 |
|---|---|
| 支持一致 | 目标与声明版本在可控探针集上差异很小,未发现超出采样噪声的证据 |
| 不支持一致 | 目标与声明版本存在系统性差异,且差异在多个探针上重复出现 |
| 无法确认 | 请求量不足、候选缺失、探针区分度低或参照版本不稳定 |
三标签体系可以预防一个重要问题:把“没有发现差异”错误理解为“完全证明同一”。黑盒观测不可能覆盖所有输入,只能覆盖采样空间的一小部分。某个结果支持一致,不等于模型权重完全相同。
5.4 阈值如何才能可靠
阈值不能随便拍。推荐用“负对照”校准:
- 找两个已知不同的模型,采集同样探针集,得到一组异源距离。
- 再找同一个模型的两次稳定部署,采集同样探针集,得到一组同源距离。
- 观察两组距离分布是否有清晰分界。
如果异源距离和同源距离分布大量重叠,说明探针集区分度不够,应该回到阶段二增加探针或调整任务类型。如果两组距离分得开,再根据分界点设置阈值。可以把阈值直接写到审计配置里,并在报告里注明校准来源。
6. 阶段四落地:报告管理、误判控制与持续审计
6.1 审计报告应包含的事项
阶段四写报告不是简单输出一个概率分数。报告要能回答三个问题:
- 审计依据是什么。
- 为什么得出这个判断。
- 如果判断错误,风险有多大。
因此报告至少包含以下部分:
| 章节 | 示例内容 |
|---|---|
| 审计对象 | 匿名目标接口 URL、发起时间、请求量 |
| 声明身份 | 供应商声称的模型名称及版本 |
| 参照模型 | v2.3 参照、v2.1 参照 |
| 探针配置 | 探针数量、类别分布、重复次数 |
| 采样参数 | temperature、max_tokens、top_p |
| 比对结果 | 距离分数表、置信区间信息 |
| 结论 | 支持一致 / 不支持一致 / 无法确认 |
| 证据归档 | 证据目录、哈希值、复现方法 |
报告里还要写清楚限制条件。比如“本次审计只覆盖文本接口,不覆盖视觉链路”“采样预算限制在 1200 次以内”“目标与参照接口的采样温度字段存在差异,距离可能被放大”。
6.2 审计算法也会产生误判
黑盒身份验证存在两类误判。第一类是假阳性,把不同模型误判为同一个模型。出现概率高的场景是探针任务过于简单,大家都输出“是”或“正确”。第二类是假阴性,把同一个模型的两次部署误判为不同模型。出现概率高的场景是模型量化、温度参数差异、路由模型版本有灰度。
控制误判要组合使用手段:
- 提高探针集区分度。
- 增加负对照候选。
- 多次重复采样,观察距离的稳定性。
- 对“不支持一致”的结论,先复查阶段二原始响应。
如果报告显示目标与声明模型不一致,不要急着给供应商定结论。应该先看是否有版本灰度、路由比例、采样参数差异和部署批次变化。必要时请供应商提供变更窗口,在变更前后重新采集一轮。
6.3 进入持续审计循环
轮询式审计适合在以下时间点触发:
- 供应商服务上线或升级时。
- 供应商通知模型版本变更时。
- 目标接口长期低负载后切换到新节点时。
- 每隔固定周期,例如每月抽取 200 个探针做回归。
持续审计可以复用同一套探针集。每次结果与基线距离对比,一旦超过告警阈值,就自动创建审计任务。这个机制的作用不是定位故障,而是在模型身份发生变化时提前暴露风险,避免劣化版本或错误路由在无人察觉的情况下长期运行。
7. 黑盒身份审计常见坑与排查路径
7.1 常见坑一:只用简单常识题做探针
很多第一批探针集都喜欢加入“1+1 等于几”“中国的首都是哪里”这类问题。这些问题正确率很高,但区分度极低。几乎所有模型都能答对,目标与参照模型的距离会普遍接近 0,导致审计方误以为模型身份一致。
解决办法是增加高熵探针。高熵指的是在模型候选答案空间中,多个候选答案都有一部分概率,但不同模型对答案的偏爱明显不同。开放续写和格式化输出通常比常识问答更有区分度。
7.2 常见坑二:忽略输出长度对相似度的影响
假设目标模型最大输出 128 token,候选模型默认最大输出 32 token。两个相同模型相同参数,但故事续写长度不同,会导致平均距离显著增大。这不是身份不一致,而是参数配置不一致。
在阶段二和阶段三,所有候选接口的max_tokens、stop、temperature都应保持一致。如果接口不开放这些参数,必须在证据文件里标明。
7.3 常见坑三:直接比较不同词表下的 top logprobs
logprobs 是很有价值的信息,但它依赖模型词表。同一个 token 在不同词表里可能是不同 ID,比较 ID 没有意义。即使都返回中文文本,tokenizer 的变化也会让 top-k 序列不同。
建议先做两步归一化:
- 把 logprobs 输出映射成真实文本片段或字符。
- 或者只比较归一化后的选项概率,不直接比较 token ID。
如果无法把 logprob 映射到可比较的文本层,就不要把它当作主要证据。
7.4 排查链路:从异常结论倒推原因
当审计结论出现“不支持一致”但业务方认为不可能时,按下面顺序排查:
- 检查请求是否真的到达目标端点,有没有被本地缓存或代理命中。
- 检查目标与参照模型的响应是否在同一个时间段内采集。
- 检查两种接口的采样参数是否一致。
- 检查输出清理逻辑是否一致,例如是否保留空格、换行和标点。
- 检查探针集是否偏向特定主题,是否只在特定领域表现差异。
- 检查参照模型是否已经升级,而不只是检查目标模型。
- 检查输出长度字段,确认没有把被截断的文本误当成完整答案。
任何一步异常,都应先修正后再重新计算距离。不要把带噪声的结果直接写入审计报告。