供应商又发新博客了,CTO亲自写的,说测试覆盖率96.8%——注意措辞,“这是我们的基线,不是上限”。另一家则在GitHub上开源了一个评估脚本,号称跑一下就能看到真实覆盖率,三天攒了240颗星。两家都在抢同一份年度合同,桌上全是宣传稿,没有一个数字你能拍着胸脯说“这我信”。
这篇咱们搭的就是这个:一套独立审计AI测试供应商“宣称覆盖率”的交叉验证工作流。大概四十分钟跑完,到手一份能复用、能丢进任意仓库的脚本和核对清单——不是看完点点头那种文章,是你真能拿它对着一个96.8%跑出自己数字的那种。
开搞之前先确认这几件事都齐了:
- 本地 Python 3.10+ 和 git 已经装好
- 目标供应商的评估仓库能 clone 下来,依赖装得上
- 手里有一份脱敏过的真实历史测试数据,没有的话从业务日志里攒一份也行
都齐了咱们就开搞。缺哪样先去补哪样。
对了,插播一句:我一开始没打算写第一步——直接拿真实数据跑多痛快。写到后面才发现,没有第一步造出来的模拟数据,最后一步的交叉对照根本没法做。所以补在前面,你先别嫌这步多余。
Step 1:按对方公开的方法学重建模拟数据
覆盖率这种数字,只在你承认对方那套“口径”的时候才有意义。所以第一步不是质疑,是复现:把对方公开文档里的数据生成规则搬出来,写成脚本,让它完全按描述的口径造一批模拟数据。
# synth_data.py
# 按对方公开方法学重建数据分布:域比例、采样逻辑、正负样本比
import random
from pathlib import Path
random.seed(42) # seed 不锁,后面复现全部完蛋
def build(ratios, n=5000):
rows = []
for domain, weight in ratios.items():
for _ in range(int(n * weight)):
rows.append({
"domain": domain,
"label": random.choice(["P", "N"]),
"payload": f"{domain}-{random.randint(0, 10**6)}",
})
Path("synthetic_style_a.jsonl").write_text(
"\n".join(str(r) for r in rows)
)
if __name__ == "__main__":
build({"web": 0.6, "api": 0.4})
⚠️ 这一步很多人会卡在随机种子:seed 不锁,每次跑出来的模拟数据都不一样,最后交叉对照的结果没法复现,等于前面全白干。锁上。怎么验证这步成了:连跑两次,两次文件 hash 一致,这一步就通了。
Step 2:跑对方的公开评估管线,拿第一个基线
有了模拟数据,去跑评估管线。以开源了脚本的那家为例——它写的是“跑一下就能看到真实覆盖率”,那就真的跑一下。clone 下来,装依赖,把模拟数据喂进去:
git clone "$VENDOR_EVAL_REPO" vendor-eval
cd vendor-eval
pip install -r requirements.txt
python eval.py --input ../synthetic_style_a.jsonl --output baseline_synth.json
脚本名以仓库实际为准,有出入就换成真的那个。这一步的意义是验证一个事:供应商的宣称数字,至少要在它自己的口径下能被复现出来。如果模拟数据按它的口径喂进去,连它自己的宣称都跑不出来,后面根本不用往下审。那个开源脚本早就有评论者拿去测一家宣称覆盖率超过90%的平台,跑出来只有54%——不是抬杠,脚本公开,谁都能跑。
按公开方法学重建模拟数据再跑另一边,实测覆盖率71.3%,和宣称的96.8%差了25.5个百分点。同样的方法学、同样的规则,数字对不上,这已经不是工具差异能解释的了。
Step 3:真实历史数据进场
模拟数据是摸脾气的,真实数据才是审判。把脱敏过的历史数据喂进同一条管线——注意,一条参数都不许改,和跑模拟数据时完全一致。改了参数,对方就有话说了:你用的不是我们的标准流程。
python eval.py --input ../history_data.jsonl --output eval_on_history.json
跑完输出里查 coverage 字段:56.2%。而这家自己报告过的数字是82.1%和84.6%。同一份历史数据,它自己的管线现在跑出来只有56.2%,差了近30个百分点。先把这个数字存下来,别急着下结论。差的这30个点不一定是谎报,也可能只是它改过什么没告诉你。
Step 4:查依赖漂移,版本历史里藏着答案
两个数字差距这么大,最合理的解释是:评估管线本身变了,而且是在你没注意的时候变的。去仓库里查引用模型版本的历史:
cd vendor-eval
git log --oneline -- models.yaml
git diff HEAD~5 HEAD -- models.yaml
diff 一出来,事情就清楚了:在客户提交数据大约三周后,有人把 models.yaml 里的通用模型换成了一个垂直领域微调版。新模型在垂直领域上召回率确实漂亮,但通用测试数据的召回率从0.82掉到了0.61。对外报告的数字,还是用旧模型跑出来的那套。模型换了,历史报告没重跑,新旧数字叠在一起给你看,你看到的永远是更顺眼的那个。
⚠️ 这一步很多人会卡在只看当前文件内容——打开 models.yaml 看一眼,当前版本没问题,就以为没改过。要看 diff,不是看当前内容。单独看一份文件看不出“被改过”,只有 diff 能告诉你发生了什么。
这里我选了最简单的 git diff 方案,另一种做法是先把每个版本的依赖哈希全锁下来,逐个跑一遍覆盖率回归。后者更彻底,但运行时间直接翻倍。我还没想清楚值不值得为这篇教程加这么重的东西——先往下走,这个话题以后单开一篇细讲。
Step 5:交叉对照,审数据和审管线分开
整套审计从设计上就走两条独立轨道:数据一条,管线一条。单审数据可能被管线带偏,单审管线会放过数据造假。只有交叉验证,两边互相印证。
把两种数据、两条管线交叉喂一遍:
# 交叉对照矩阵(示意,实际数值来自上面两步的运行结果)
matrix = {
"A 风格模拟数据 → 开源管线": 0.94,
"真实历史数据 → A 复核流程": 0.81,
"A 宣称 / 实测": (0.968, 0.713),
"开源管线宣称 / 实测": (0.821, 0.562),
}
for case, val in matrix.items():
print(case, val)
四个数字摆一起,结论已经不用别人替你写:A 风格的模拟数据在任何一条管线上都能跑出接近94%,说明数据风格远比工具能力更容易刷高分数。真实数据一到手,分数就掉到81%,而它自己的实测只有71.3%。另一家更麻烦,数据是准的,管线已经漂了,历史数字全部作废。两家宣称的覆盖率,一个死在方法学自洽上,一个死在管线漂移上,结论都一样:不能直接采信。
顺便说一句,覆盖率这个指标本身值不值得被这么严肃地对待,够写两万字,以后单开。这里只需要记住一点:你审计的是口径,不是工具。
完整版:抄作业脚本
上面每一步都过了一遍,不想回头翻的,直接拿这份完整版。环境变量换成你实际审计的仓库路径和数据路径就行。
# audit_workflow.sh
set -e
# 1. 生成模拟数据(锁 seed)
python synth_data.py
# 2. 跑目标管线,模拟数据
cd "$VENDOR_EVAL_REPO"
pip install -r requirements.txt
python eval.py --input ../synthetic_style_a.jsonl
# 3. 跑目标管线,真实历史数据
python eval.py --input ../history_data.jsonl
# 4. 查模型依赖漂移
git log --oneline -- models.yaml
git diff HEAD~5 HEAD -- models.yaml
# 5. 交叉对照,数字自己看
到这里,这个工作流就搭完了。跑起来了吗,照这个清单自查:
- 模拟数据生成器锁了 seed,连跑两次 hash 一致
- 模拟数据、真实数据在同一条管线、同一组参数下各跑了一次
- 用 git diff 查过模型引用,确认有没有漂移
- 交叉对照矩阵四个数字齐了,每一个都能说清来路
都打勾了,这篇就算交付。接下来你可以试着把它接进 GitHub Actions——目标仓库每次更新评估脚本,自动触发一轮交叉验证;或者接一个 MCP server,把审计结果同步进项目看板,方便复核。都不难,属于这地基上随手加的装修。
最后说一句策略。报告出来之后怎么用,就不是技术问题了。审计报告压在手里没急着公开,让两家供应商继续互相攻击,拖出时间,最后跟两家各签了一纸不具约束力的意向书——实际打算自建内部能力。这是隔岸观火。咱们搭的这套工具就是那个“岸”,不是让供应商难看用的,是让决策的人手里有个自己的数字。想躺着看火就先看着,想出手的时候,手里有的是真东西。
