系列专栏:测试工程师每日一博 · Day 26(2026-08-10,周一,8 月第二周)
上一篇:[Day 25 · 性能压测的容量规划:把"找拐点"上升为"找容量曲线方程"]
这是系列的翻面时刻 🌟—— 前 25 篇都在讲"如何用 LLM 帮测试工程师"(Day 13/22/24/25),Day 26 第一次回答"如何测试 LLM 自身"。也就是说,被测对象不再是 Java 代码、不再是 HTTP 接口,而是模型本身。
目标:给一份 eval harness + golden set + drift 三件套的工程化清单,读完能直接照搬落地。这是测试工程师迎接 LLM 时代的核心素养。
一、为什么 Day 26 是翻面时刻
工业里把 LLM 接进自家产品的团队一夜变多——客服 / 智能搜索 / 代码助手 / 内容生成。他们的核心交付物就是 LLM 输出。所以测试工程师必须翻面,被测对象从代码翻到模型。先回看过去 25 篇 LLM 出现的场景:
| Day | LLM 的角色 |
|---|---|
| Day 13 | LLM 生成测试用例 / selector 自愈 / flaky 根因 |
| Day 16 | LLM 故障复盘自动归类 |
| Day 17 | LLM 候选风险点提 PR review |
| Day 22 | LLM 视觉自愈 selector |
| Day 24 | LLM 安全报告解读 |
| Day 25 | LLM 容量曲线翻译 |
也就是说,Day 26 之前 LLM 都是"工具",被测对象是产品代码。Day 26 翻面 —LLM 自身是被测对象。
这一翻面工业意义重大。今天把 AI 接进自家产品的团队越来越多(RAG 客服、智能搜索、代码助手…),这些团队的核心交付物本身就是 LLM 输出。如果你的"测试工程师"对 LLM 自身毫不了解,等于对一个全新测试维度视而不见。
二、测试 LLM 跟测试代码的本质区别
正常的 Java 代码:输入确定 + 内部状态确定 → 输出确定。assertThat(actual).isEqualTo(expected)能精确断言。
LLM 完全反着:
| 维度 | 传统代码 | LLM |
|---|---|---|
| 确定性 | 强(同输入同输出) | 弱(temperature / 采样随机) |
| 输出形式 | 强类型对象 | 自由文本 |
| 错误定义 | 异常 / 错误值 | 主观判断"答得对不对" |
| 覆盖率 | 行 / 分支 / 字节码 | 没有公认的覆盖率概念 |
| flaky | 罕见(多为并发 / 时钟) | 常态(每次 inference 都可能不同) |
| 回归测试 | 单测断言 | N-shot 标注 + 评分矩阵 |
| 工具链 | JUnit / pytest / coverage.py | promptfoo / LangSmith / DeepEval |
注意"覆盖率"那一栏——Day 26 不能拿 JaCoCo 那套来回答"LLM 测了多少"。LLM 测试的"覆盖"不是一个百分比,是一组维度(准确率 / 召回 / 安全 / 偏见 / 多语言 / 长文 / 上下文…),每个维度都有它自己的 golden set。
三、eval harness:LLM 测试的工具集
LLM 测试最核心的工具是eval harness——它给一段 LLM 输出自动打分,产生数据化的"对错判断"。
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ golden set │───→│ LLM under │───→│ scoring │───┬→ 分数 + 报告 │ (N 条样本) │ │ test │ │ function │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ ┌─ scorer 分类 ─────────────────────────┤ │ │ exact match │ semantic match(LLM-as-judge) │ regex / structured-validation │ human eval spot check │四个常见 scoring 方法:
3.1 exact match(精确匹配)
只适合结构稳定输出,如分类标签:
defexact_match_score(llm_output:str,expected:str)->float:return1.0ifllm_output.strip().lower()==expected.lower()else0.0优点:可复现、零歧义;
缺点:对自由文本输出几乎不能用作主指标。
3.2 semantic match(LLM-as-judge)
让另一个 LLM 当裁判:
defllm_judge_score(question:str,reference:str,candidate:str)->float:""" 用 GPT-4 之类强模型当裁判,在很多工业里是 SOTA 做法 """prompt=f""" 给定问题:{question}标准答案:{reference}候选答案:{candidate}用 1-5 给候选答案打分,只输出数字。 """score=call_judge_llm(prompt)returnfloat(score)/5.0# 归一化到 0-1优点:能判断"语义对错";
缺点:judge LLM 也有 bias,且 evaluate 自身的成本不低。
3.3 regex / structured-validation
对期望 JSON / 表格 / 代码的输出:
importjson,redefschema_score(output:str)->float:"""输出必须是合法 JSON 且包含必填字段"""try:data=json.loads(output)except:return0.0required={"test_name","assertions","expected"}return1.0ifrequired.issubset(data.keys())else0.5优点:严格,可复现;
缺点:只校验"形状",不校验"内容质量"。
3.4 human eval spot check
定期抽 N 条样本给人 review,这是最后一道质量门。这是工业里**「绝对必要」**的一步(再强的自动评估也代替不了人评)。
3.5 promptfoo / DeepEval 的角色
它们都是开源 eval harness 框架,把上面 §3.1-3.4 全部封装为 YAML 配置:
# promptfooconfig.yaml(已有用例就一份 yaml)prompts:-"给客户问题「{{question}}」{describe:测试:「订单查询」场景下,模型能正确给出订单状态}/{测试:{question西班牙语 / 问无意义问题 /...}}providers:-openai:gpt-4o-openai:gpt-4o-mini-local:my-inhouse-model:v2tests:-vars:{question:'我的订单 ABC123 状态?'}assert:-type:containsvalue:'已发货'-type:llm-rubricvalue:'答案应该说明预计送达时间'-assert:-type:javascriptvalue:'output.length < 200'跑一条promptfoo eval,对每个 provider × 每个 test 跑一套评分,输出对比表。这就是 LLM 测试的"最佳实践骨架"。
3.6 一个综合 scorer:加权融合
实际工业里没有单一 scorer 能解决一切。
成熟的 eval harness 会把上面 4 类 scorer 加权融合,
权重跟业务场景对齐:
defcomposite_score(llm_output,expected,**ctx):weights=ctx.get('weights',{'exact':0.2,# 结构化输出底线'semantic':0.5,# 语义对错(主指标)'schema':0.2,# 形状合规'safety':0.1,# 不输出危险内容})scores={'exact':exact_match_score(llm_output,expected),'semantic':llm_judge_score(ctx['question'],expected,llm_output),'schema':schema_score(llm_output),'safety':safety_score(llm_output),}# 任一维度 0 分 → 复合分直接 0(强约束,避免平均掩盖)ifany(s==0forsinscores.values()):return0.0returnsum(scores[k]*weights[k]forkinweights)关键设计是任一维度 0 分 → 复合分直接 0。
这条强约束避免了「语义优秀但安全维度挂了,
总分 90% 假象通过」的常见坑——
这种坑放出去就是给客服 LLM 留 PII 输出的后门。
四、golden set:LLM 的回归测试基线
跟 Day 16 §7"生产故障转红灯"类似,LLM 测试也需要golden set—— 一组精心挑选的样本,代表系统"应该答对"的所有维度。
4.1 golden set 不是随便 N 条样本
工业里很容易踩一个坑:随机采 1000 条 prompt 作为 golden set。结果是:复杂样本占比太少,跑出来分数看着高,实际上系统在边角案例上挂成狗。
golden set必须按维度分桶:
# golden-set.yamldimensions:-name:basic_qa# 基础问答samples:50# 占比 30%sample_proportion:0.3-name:multi_turn# 多轮对话samples:30sample_proportion:0.18-name:long_context# 长上下文(> 4K tokens)samples:15sample_proportion:0.09-name:multilingual# 多语言samples:20sample_proportion:0.12-name:edge_cases# 边角:错别字 / 错引上下文 / 矛盾输入samples:25sample_proportion:0.15-name:safety# 安全:不能输出 PII / 歧视 / 危险信息samples:25sample_proportion:0.15total:165关键设计:
- 每个维度独立计分;
- 报告里看,单维度分数 < 阈值就是红灯(不要看总分);
- 维度配比跟业务场景相关(客服系统 safety 占比要更高,搜索系统应召回优先)。
4.2 golden set 何时升级
工业真实经验:
- 每月新增 10 条:从生产日志里挑"模型答错了人补回的",或"用户投诉"案例;
- 季度大 review:重审维度配置,配比改了就重跑全集(旧数据失效);
- 大模型版本切换前:必跑全套,且保留历史作为对比 baseline。
4.3 golden set 进 git
跟 Day 14 §6.2 fixture 户口同源 — golden set必须进 git,带 .meta 户口:
# golden-set/safety-001.yaml.metaid:safety-001description:模型应拒绝提供信用卡盗刷建议created_by:"@qa_engineer"created_at:2026-07-15tags:[safety,p0,content_mod]last_passed_at:2026-08-09fail_rate_30d:0owner:"@security_team"任一条 case 30 天失败率 > 0,触发 review。
4.4 跟 Day 16 红灯回扣的具体衔接
Day 16 §7 把生产故障转成「@Tag(prod_incident) 红灯 case」,
LLM 时代也有完全对位的动作 ——
把线上 LLM 答错的 case转成 golden set 新条目:
# tools/incident_to_golden.pydefincident_to_golden_sample(incident_log:dict)->dict:""" 输入:线上记录的「用户问题 + 模型答错 + 真实正确答案」 输出:符合 §4.1 维度结构的 golden set 新条目 """return{'id':f'prod_incident_{incident_log["id"]}','dimension':classify_dimension(incident_log),# 自动归类到 6 维'question':incident_log['question'],'expected':incident_log['corrected_answer'],'tags':['prod_incident',incident_log['severity']],'source':'production','created_at':now(),}这条流程就是 Day 16 §7 在 LLM 测试体系的镜像:
生产红灯反向灌入 golden set,循环改善。
工业实践里,一条 P1 投诉的 LLM 答错对应一条 golden 新样本,
golden set 每周 10-30 条新样本,
其中 60% 来源于生产反思(Day 16 镜像),
另 40% 来源于探索性测试准备(回扣 Day 11 左移)。
五、模型 drift:LLM 的 flaky 等价物
LLM 测试最大的"流动性"问题:同样的 prompt、同样的 golden set,模型版本一变,行为就漂移。这是 Day 10 flaky 在 LLM 时代的等价物。
5.1 drift 类型分类
defdetect_drift(history:list[dict],window_days:int=30)->dict:""" history: [{"ts", "score", "dimension"}] 返回各维度近 N 天的漂移 """today=now()recent=[hforhinhistoryifh["ts"]>today-window_days]baseline=[hforhinhistoryiftoday-2*window_days<h["ts"]<=today-window_days]drift={}fordimindimensions:recent_score=avg([r["score"]forrinrecentifr["dimension"]==dim])baseline_score=avg([b["score"]forbinbaselineifb["dimension"]==dim])drift[dim]={"recent_score":recent_score,"baseline_score":baseline_score,"drift_pct":(baseline_score-recent_score)/baseline_score*100ifbaseline_scoreelse0,}returndriftdrift 阈值:
- 各维度 drift > 5% → 黄灯警告;
- drift > 15% → 红灯,立刻停止新版本发版,排查原因。
5.2 drift 的常见 3 种根因
1. 模型版本切换(gpt-4o-2026-05 → gpt-4o-2026-08) ─ 即使是新版本"更好",它输出更啰嗦 / 更严格 / 更短也可能让你的 golden set 短期失败 2. 数据 drift(RAG 系统的索引更新) ─ 知识库内容变了,旧答案不再"最新" ─ 解法:把"时效敏感"和"时效不敏感"的 case 分桶评估 3. prompt drift ─ 业务方调整了 prompt(把"简洁"改成"详细"),所有 case 输出量级变 ─ 解法:prompt 进 git(回扣 Day 13 §8.3 prompts/ YAML)每类 drift 的解法不同,不能"看到红灯就重测"——必须先分类根因再决定怎么改。
六、LLM 测试的金字塔
回到 Day 1 测试金字塔的精神,LLM 测试也能套用三角形:
┌─────────────┐ │ human eval │ ← 1%(每周抽 10 条人评) ├─────────────┤ │ 大规模样本 │ ← 9%(几百条 random sample 评估 recall/precision) ├─────────────┤ │ golden set │ ← 30%(每版本跑,N=165 标注样本) ├─────────────┤ │ invariants │ ← 60%(schema / 安全 / 不输出 PII 等结构化断言) └─────────────┘4 层由下到上:
- invariants(60%):自动化的 schema / 安全断言,跑得最快(CI 每次);
- golden set(30%):每次模型版本切换跑;
- 大规模样本(9%):月度评估,用 LLM-as-judge;
- human eval(1%):每周抽 10 条人评,最重要但不频繁。
每层的"代价跟价值成反比":invariants 跑得快但价值低;human eval 跑得慢但价值最高。比例60/30/9/1就是这种反比的最佳折中。
6.1 跟 Day 1 测试金字塔的根本差异
Day 1 经典金字塔 70/20/10,「单测越多越好」;
Day 26 LLM 金字塔 60/30/9/1,自动 invariants 占 60%,人评只占 1%。
这两个比例不可类比 ——
传统单测是「严格断言」,LLM invariants 也是严格断言;
传统集成测是「多组件协作」,LLM golden set 是「多维样本评估」;
传统 E2E 是「真实用户路径」,LLM random sample 是「线上随机采样」。
金字塔是精神同源,具体执行完全异类。
这次回到 §2 那张表的核心意义:
不能用 JaCoCo 覆盖率思路测 LLM,但能借测试金字塔的精神。
七、把 LLM 测试接进 CI
# .github/workflows/llm-eval.ymlname:LLM Evalon:pull_request:paths:['prompts/**','golden-set/**','model-config/**']schedule:[{cron:"0 4 * * 1"}]# 每周一.depth evaljobs:eval:runs-on:llm-runnersteps:-uses:actions/checkout@v4-name:Run invariants(CI 每次)run:promptfoo eval tests/invariants/--threshold 0.99-name:Run golden set(PR + 每周)if:github.event_name == 'schedule'run:promptfoo eval golden-set/--threshold 0.92-name:Drift detection(每周)if:github.event_name == 'schedule'run:python tools/drift_detect.py--window 30d-name:Notify on regressionif:failure()run:|gh issue create --title "[LLM Eval regression] ${{ github.run_id }}" \ --body "@qa-leads,本周 LLM eval 退步,请参 §5.2 三种根因分类排查"这条 pipeline三次红线:- invariants < 99% → PR 红(CI 拦下),prompt / golden 改动 PR 红灯;
- golden set < 92% → 拦新版上线(Day 23 卡发布决策树 §4 Q5 中的应用);
- drift 红灯 → 自动 issue,人分类根因。
八、回扣系列
| Day | 在 Day 26 它接到的线 |
|---|---|
| Day 1 测试金字塔 | §6 LLM 测试金字塔(60/30/9/1) |
| Day 3 覆盖率 | §2 LLM 无 JaCoCo 概念,用多维度替代 |
| Day 8 TDD | §7 invariants 在 PR 同 TDD 红绿一致 |
| Day 10 flaky 治理 | §5 drift 是 LLM 时代的 flaky |
| Day 13 prompt 工程就是新单元测试 | §3 eval harness 是它的扩展,全篇是 Day 13 §8 的延伸 |
| Day 14 fixture | §4.3 golden set 进 git + .meta 户口 |
| Day 16 红灯回扣 | §4 golden set 来源是生产发现错答 |
| Day 17 代码评审 | §7 prompt + golden set 改动 PR review |
| Day 19 三方协作 | §7 LLM 测试需要 QA + MLE + DevOps 三方 |
| Day 20 OKR 度量 | §5 drift_pct 进度量体系 |
| Day 23 微服务测试矩阵 | LLM 作为后端服务时,它该进 Q5(外部表现型) |
| Day 24 安全测试 | golden safety 维度 |
12 条回扣。Day 26 是系列第二次"翻面时刻"—— 第一次是 Day 11 左移、Day 12 右移把时间轴展开;Day 26 把"被测对象"从代码翻到模型。
九、给测试工程师的实操入门路径
如果你团队刚把 LLM 接上,不知道从哪儿下手:
- 第一周:写 20 条最关键的 case(覆盖核心业务路径美),手跑,不接 CI;
- 第二周:把 20 条用 §3.1 exact match / §3.3 schema validation 接到 promptfoo,写跑 invariants;
- 第三周:把第 1 周的 20 条扩到 100 条,按 §4.1 维度分桶,接 CI;
- 第 4-8 周:加 drift 监控 + LLM-as-judge + 月度大 eval;
- 第 9+ 周:开始研究 RAG / agentic 评估(下一个高度)。
关键:不要追求一上来就 200 条 case + LLM-as-judge——前 4 周先把 invariants 和 golden set 落地,让团队建立"LLM 也能测"的工程感;然后再加深度。
十、反模式清单
| 反模式 | 表现 | 干掉的办法 |
|---|---|---|
| golden set 随机采样 | 复杂场景占比太少,分数虚高 | §4.1 按维度分桶 |
| 只看总分 | 单维度挂但被总分掩盖 | 报告看维度,不看总分 |
| 不接 CI | 提交 prompt / model 升级就跑不了 | §7 三次红线接进 CI |
| 没有 human eval | LLM-judge 也错,人评缺失 | 每周抽 10 条,1% 的工作量 |
| drift 红灯"重测吧" | 没分类根因直接重跑,根本问题没解 | §5.2 三种根因分类 |
| golden set 半年不更新 | 边角案例永远没补 | §4.2 月增 10 条案例 |
| 用 JaCoCo 思路算覆盖率 | LLM 无对应概念 | §2 维度替代 |
| 模型版本升级不跑 eval | 上线后 unknown regression | §7 schedule 跑全套 |
十一、原则三句话
- LLM 测试不是断言,是评分矩阵——exact / semantic / schema / human 四档各有适用场景;
- golden set 按维度分桶,不看总分看单维度——5% drift 就告警,15% drift 就拦发版;
- invariants + golden set + random sample + human eval 60/30/9/1—— LLM 测试金字塔。
十二、思考题
留给今晚:
- 你团队目前在用 LLM 吗?如果有,"测试 LLM 自身"做过吗?用什么方法?
- 团队上次升级 prompt / 模型版本有没有跑 eval?如果没跑,要不要在下次升级前补上?
- 你的 LLM 系统每条样本都生成 PII 风险分析吗?如果没,§7 invariants 加一条;
- drift 红灯亮了第一个动作是什么?不要立刻重测,先用 §5.2 三种根因分类;
- eval harness 进 CI 后,谁在维护 golden set?
12.1 一句口诀(贴墙上)
读过本篇后,团队做 LLM 上线决策时可套用 ——
「invariant 拦断、golden 分维、random 抽样、human 把关」:
CI 自动 invariants 拦 P0 红线(60%);
golden set 按维度分桶看分数不看总分(30%);
每周大规模 random sample 跑 recall/precision(9%);
每周 10 条人工人评补盲点(1%)。
三层自动 + 1% 人工,LLM 测试金字塔就此落地。
十三、TL;DR
- Day 26 是系列翻面时刻:前 25 篇 LLM 是"工具",Day 26 LLM 是"被测对象";
- eval harness= golden set + scoring function;
- 4 种 scoring:exact match / semantic(LLM-as-judge)/ schema-validate / human eval;
- golden set 按维度分桶,6 维(基础 / 多轮 / 长文 / 多语 / 边角 / 安全)推荐配比;
- drift 是 LLM 的 flaky:5% 黄灯,15% 红灯拦发版;
- LLM 测试金字塔 60/30/9/1(invariants / golden / random sample / human);
- 12 条对位前文,Day 13 §8(“prompt 当单元测试”)→ Day 26(“prompt + golden set 升级版”)。
下一篇:Day 27 ·测试工程师的开源参与:从提 issue 到核心贡献者的实操路径
Day 6 LLM 测试是个深度话题,Day 27 转向工程师外延 — 把测试工程师的影响力延伸到公司之外的 OSS圈。
拆 issue → PR → reviewer → maintainer 四阶段,以及如何让公司支持这种社区投入。
下篇见 👋