跳到主要内容
从零搭一套AI测试工具的覆盖率审计工作流

从零搭一套AI测试工具的覆盖率审计工作流

Kevin
Kevin

· 阅读约 7 分钟

供应商又发新博客了,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,把审计结果同步进项目看板,方便复核。都不难,属于这地基上随手加的装修。

最后说一句策略。报告出来之后怎么用,就不是技术问题了。审计报告压在手里没急着公开,让两家供应商继续互相攻击,拖出时间,最后跟两家各签了一纸不具约束力的意向书——实际打算自建内部能力。这是隔岸观火。咱们搭的这套工具就是那个“岸”,不是让供应商难看用的,是让决策的人手里有个自己的数字。想躺着看火就先看着,想出手的时候,手里有的是真东西。