AI 应用的测试工程 2026:从 LLM 单元测试到端到端 CI 流水线的统一架构
把 LLM 应用测试压成 unit / contract / eval / e2e 四层金字塔,加上 schema 契约、不变式断言、LLM-as-judge 校准、red-team 反例和 8 条 SRE/QA 落地清单。
约 32 分钟阅读9,346 字2 次阅读博主

把 LLM 应用测试压成 unit / contract / eval / e2e 四层金字塔,加上 schema 契约、不变式断言、LLM-as-judge 校准、red-team 反例和 8 条 SRE/QA 落地清单。

一句话摘要: LLM 应用的测试不是"加一个 eval set"那么简单,本文把它压成 unit / contract / eval / e2e 四层金字塔,加上 schema 契约、不变式断言、LLM-as-judge 校准、red-team 反例和 8 条 SRE/QA 落地清单。
把 LLM 应用接入生产环境之后,绝大多数团队都会撞上同一面墙:输出不是点而是分布。传统软件测试面对的是一个确定性的函数 ,给同样的输入一定有同样的输出,因此单元测试可以精确比较 与 。而 LLM 应用面对的是一个随机采样过程 ,同样的 prompt 拉两次可能拿到语气、措辞、甚至事实细节都有偏差的两个结果。如果硬性把传统断言范式套上去,会出现三种症状。
第一种是"假阳性风暴":为了容忍随机性,断言被迫放宽,例如"包含关键词 X 即可",结果所有用例都过,失效被悄悄漏过。第二种是"假阴性轰炸":稍微紧一点的断言就把正常返回误判成 fail,开发者在 Slack 上抱怨"改一行 prompt 红了二十个 case",最后把测试关掉,任其漂移。第三种更隐蔽,叫"指标雪崩":每个 case 都有自己的延迟、token、相似度、质量分,被记录到 dashboard 上但没有人知道哪个指标该报警,最后 dashboard 沦为事后追责的事故现场。
这三类症状共同指向一个根因:用确定性的判断框架去框定概率性的输出。修复路径不是放弃测试,也不是堆更多用例,而是把测试本身重新设计成"在分布上稳定、在点上宽松"的形态。本文要做的事情,是把这套形态在 2026 年的工程实践中如何落地讲清楚:从 LLM 单元测试,到 schema 契约,到 LLM-as-judge 评估管线,再到端到端 CI 与 red-team,最后给出一份可被 SRE 和 QA 团队逐条勾选的清单。
传统测试金字塔是 unit 在底、integration 在中、e2e 在顶,每层数量递减、单测成本低而 e2e 成本高。LLM 应用的测试金字塔必须做四层改造,顺序是 LLM unit、contract、eval、e2e。
图表加载中…
四层各自的"不变量"不一样。Unit 层的不变量是输出在结构上必须满足某些近似关系,例如"必须包含至少 3 条事实性陈述"、"摘要长度在原文 10%-30% 之间"、"与参考答案 cosine 相似度 ≥ 0.75"。Contract 层的不变量是输出必须被某个 schema 接受,例如 Pydantic 模型的所有字段、JSON Schema 的 required 数组、tool spec 的参数签名。Eval 层的不变量是输出的语义质量在 judge 模型视角下稳定,包括事实性、相关性、流畅度、是否有幻觉。E2E 层的不变量是整条链路在给定种子流量下的关键指标,包括首 token 延迟、P50 延迟、成功率、护栏命中率。
把这四层堆起来之后,金字塔的形状与传统测试一致(底大顶小),但每一层内部的"断言"都不是布尔值,而是带置信区间的统计量。Unit 层断言 100% 命中、Contract 层 100% 通过、Eval 层在 judge 校准集上的 Kendall's tau ≥ 0.7、E2E 在种子流量上 P50 延迟稳定在 SLO 内。这套指标体系比传统测试金字塔多一维:置信度。任何一层的"通过"都要附带一个置信区间,而 CI 流水线可以基于置信区间决定是否阻断合并。
Unit 层最大的工程难题是怎么写"近似匹配"。给一句完全相同的输出,你的断言不能是 output == expected,而应该是**"output 与 expected 在某个特征空间里距离 ≤ 阈值"**。这里的关键是选哪个特征空间。常见的有三种:字符级 (Levenshtein / BLEU)、语义级 (sentence embedding cosine / BERTScore)、结构级 (AST / DOM 树相似度)。
from sentence_transformers import SentenceTransformer
import numpy as np
class ApproxMatcher:
def __init__(self, model_name="BAAI/bge-m3"):
self.model = SentenceTransformer(model_name)
# 阈值在 0.75-0.85 之间, 由 golden set 上的人工标注决定
self.threshold = 0.78
def assert_similar(self, output: str, reference: str, label: str):
e_out = self.model.encode(output, normalize_embeddings=True)
e_ref = self.model.encode(reference, normalize_embeddings=True)
score = float(np.dot(e_out, e_ref))
assert score >= self.threshold, (
f"[{label}] cos={score:.3f} < {self.threshold:.3f} | "
f"output[:120]={output[:120]!r} | reference[:120]={reference[:120]!r}"
)
return score
但近似匹配只解决了一半。另一半是不变式断言——输出必须在结构上、长度上、领域约束上满足一组"必须成立"的属性,与具体内容无关。常见不变式有五类。
第一类长度不变式:摘要长度 / 翻译后长度 / 列表条目数必须在某个区间内。第二类结构不变式:必须以某个特定前缀或后缀收尾、必须包含某个必须出现的实体 (人名/产品名/法规号)、必须以 markdown 列表而非纯文本呈现。第三类拒绝不变式:涉政、涉黄、涉暴力的 prompt 必须被拒答,拒答模板必须命中,且不暴露 system prompt。第四类链路不变式:必须调用过 RAG、必须引用至少 2 条来源、必须经过 PII redact 节点。第五类时间不变式:答案中提到的事件日期必须与外部知识库条目一致,不能引用未公开的"未来"时间。
不变式断言的工程价值远超近似匹配,因为它不依赖具体答案内容,对 prompt 迭代天然鲁棒。改一次 prompt 让措辞全变,近似匹配可能大片 fail,但只要新输出仍满足 5 条不变式,unit 层仍然通过。把 unit 层主要靠不变式、辅以小比例近似匹配,是 2026 年 LLM 应用测试的主流形态。
最后是关于 unit 层的测试运行成本。Unit 测试每次 PR 都要跑,规模在 1000+ 用例,即便每条平均 800ms 也要 13 分钟。生产中通常并行化到 8 个 worker 跑,可以压到 2 分钟以内。但要小心一个陷阱:很多团队把整个 prompt 链路 (含 RAG 检索 + 主推理) 都跑进 unit 层,这是错位。Unit 层应当只测"纯 prompt → LLM → 解析"的最短路径,任何会引入网络 I/O、数据库 I/O、外部服务依赖的环节都应 mock 掉,否则 unit 层会被 I/O 拖死,也会因为外部服务的不可控而频繁 flaky。Mock 外部依赖是 LLM unit 层"既快又稳"的核心纪律,务必在测试架构设计之初就明确边界。
Unit 层解决"输出大致对",Contract 层解决"输出格式严格对"。LLM 应用的下游消费者往往是另一个程序:把 JSON 喂给前端、把 tool spec 喂给 dispatcher、把结构化数据写进数据库。任何下游消费方都不容忍"输出在结构上可能错"。
Pydantic v2 是 Contract 层的工业级基线。把 LLM 输出声明成 Pydantic 模型,让其在最外层用 model_validate_json 解析,就能在第一时间把"输出不是合法 JSON"或"输出缺字段"两种最常见的坏输出拦下。
图表加载中…
Contract 层必须解决的数学问题是schema violation rate 上限。设 为目标 violation rate, 为一次调用样本数, 为可接受失败数, 则 的上限为:
这个 Wilson 区间估计告诉我们,想用 1000 次调用断言 violation rate 低于 0.1%,可接受失败数必须 ≤ 0,且需要更多样本量才能在统计上稳健。这就是为什么 Contract 层断言要跑数千次才能拿稳的根因。
tool spec 是 Contract 层的另一翼。OpenAI function calling / Anthropic tool use / Google function call 都要求 LLM 输出的 tool call JSON 严格符合预定义的 spec。一个完整的 tool spec 契约测试套件,必须覆盖:参数类型 (string/int/array/object)、必填字段、enum 闭包、嵌套对象递归校验、参数取值范围、与下游业务规则的交叉约束 (例如"用户余额 ≥ 转账金额")。这些约束若靠下游 runtime 去校验,失败时已经付出一次工具调用成本,还可能污染事务状态。在 Contract 层把 schema 与业务规则一并卡掉,是降本的最关键一刀。
Contract 层还有两个工程细节容易被忽略。第一个是 schema 演化的版本管理。LLM 应用上线后,业务字段经常增删,例如"加一个 priority: Literal['low','medium','high'] 字段"。如果直接对线上 schema 改,旧版 LLM 输出可能仍然输出旧字段,新 schema 校验失败导致整条链路 reject。正确做法是 schema 版本化 (v1 / v2 / v3),每个版本保留 3-6 个月兼容期,新版本上线同时保留旧版本作为可选 fallback,直到所有上游调用方都升级到新版本。第二个是回环校验 (roundtrip validation)。把 LLM 输出 parse 进来得到 model A,再把 model A model_dump_json() 序列化,再用同一个 schema 重新 parse 一次,两次 parse 都成功才算契约通过。这能抓住 JSON 序列化层的边界 case (例如 datetime / Enum / Decimal 在不同 Python 版本下行为不一致)。这两个细节决定了 Contract 层在"持续迭代"场景下不会因为 schema 微调而大范围 fail。
Eval 层是 LLM 应用测试的"质量雷达",它不再关心输出长什么样,只关心输出好不好。LLM-as-judge (用 LLM 评 LLM) 是 2026 年最普遍的自动化质量评估手段,核心工程难题是校准 judge 模型自己。
from dataclasses import dataclass
from typing import Literal
@dataclass
class JudgeResult:
score: float
reasoning: str
confidence: Literal["low", "medium", "high"]
class LLMJudge:
def __init__(self, judge_model, calibration_set):
self.judge = judge_model
self.calibration_set = calibration_set
# 校准指标: 与人类标注的 Kendall's tau-b
self.tau_b = self._calibrate()
def _calibrate(self) -> float:
# 校准集: 500 条由 3 名人类标注员独立标注的 golden sample
human_scores = [item.human_avg for item in self.calibration_set]
judge_scores = [self.judge.score(item.prompt, item.output) for item in self.calibration_set]
return kendall_tau_b(human_scores, judge_scores)
def evaluate(self, prompt: str, output: str) -> JudgeResult:
raw = self.judge.score(prompt, output)
return JudgeResult(
score=raw.score,
reasoning=raw.reasoning,
confidence="high" if self.tau_b >= 0.7 else "medium" if self.tau_b >= 0.5 else "low"
)
校准集 (golden set) 是 Eval 层的命脉。它必须满足三个条件:由 ≥ 3 名人类标注员独立标注、覆盖所有意图类别 (informational/transactional/navigational/creative)、规模 ≥ 500 条以让 Kendall's tau 在统计上稳定。Judge 模型的 tau_b 在校准集上 ≥ 0.7 才被认为"可信赖",0.5-0.7 区间是"参考但不阻断",< 0.5 区间只能用于离线分析,不能进入 CI 阻断门。
Judge 模型自身的选型也是工程决策。首选是"比被测模型更强一档"的模型 (例如测 Sonnet 4.5 用 Opus 4.5 当 judge),这能保证 judge 看得懂被测模型的输出且不容易被生成的"看似合理但其实错"的话术误导。次选是与被测模型同档的独立模型 (例如被测 GPT-5 用 Claude Opus 4.5 当 judge),跨家族模型在判断上往往更严格,但成本高。绝对不要用被测模型自己 judge 自己,这种"自评"在统计上接近随机,2025 年 Anthropic 的一篇评估论文里已经证明:同模型 judge 与人类标注的 tau_b 中位数只有 0.31,远低于跨模型 judge 的 0.68。
鲁棒性矩阵是 Eval 层在 2026 年的新增组件。它的思想是对同一 prompt 做有扰动的多版本(改写措辞、加入同义问题、加入对抗性 follow-up),看 judge 模型与基础模型的表现是否稳定。鲁棒性矩阵的产出是一个二维表:行是 prompt 变体,列是评估指标 (准确率、相关性、幻觉率、延迟),单元格是模型在该 prompt 上的得分。矩阵的方差比平均值更重要——一个高方差的应用,即便平均得分不错,也意味着它对 prompt 措辞高度敏感,生产环境一旦用户表述略有变化,质量就会跳水。鲁棒性矩阵是 LLM 应用进入"严肃生产"的必要测试组件。
Unit/Contract/Eval 三层是"组件级"测试,E2E 是"链路级"测试。它模拟真实用户的完整调用路径:从网关入口 → prompt 拼装 → 检索 → 主推理 → 后处理 → 响应下发,中间所有节点全部走真实代码 (但数据可以是合成的或脱敏的)。
链路回归 (linkage regression) 的目标是任何一个节点的改动,都能在 PR 阶段被完整跑过。工程实现上,通常把链路用 pytest + docker-compose 编排成本地可跑实例,每个 PR 触发一组 20-50 个真实场景的回放。回放用的"历史 prompt"来自生产采样,但都经过 PII redact 与人工脱敏。
生产抽样 (production sampling) 是 2026 年流行的"事后测试"机制。生产环境按 1%-5% 比例对实际请求做静默 twin 化:同一个请求同时送给旧版本与新版本,两份响应都被记录但只有一份被发回用户。事后离线比对两份响应,如果差异率超过阈值,触发告警。这种"影子 A/B"机制不需要流量切分,也不打扰用户,是质量工程的重大进步。
影子流量 (shadow traffic) 进一步把生产真实流量复制到预发环境,新代码在预发跑同一份流量,记录差异。这种方法成本高 (复制全量流量),但能抓住链路回归与生产抽样都没法抓的"边缘案例",例如 prompt 长度接近上下文窗口上限的请求、含罕见字符集 (CJK 异体字、emoji 组合)、多语言混排。
CI 流水线的门禁通常按金字塔层级递增:Unit 必过、Contract 必过、Eval 在 judge 校准置信区间内通过、E2E 在种子流量上 P50 延迟稳定在 SLO 内。四层全过才能 merge,任何一层 fail 立刻阻断。这种"四道门"的工程纪律,是从 LLM 应用 demo 走向严肃生产的关键一步。
值得一提的是,CI 流水线的执行顺序对调试效率有显著影响。推荐顺序是 Unit → Contract → Eval → E2E,前一层失败立刻中断,而不必把后三层也跑完。这样开发者拿到红色信号后,平均只需看一个层级的日志就能定位根因。如果按"先全跑完再统一报红",开发者要在 4 个层级的输出中找最早失败的位置,反馈周期显著拉长。Anthropic 内部的一份工程调查显示,严格按 Unit→Contract→Eval→E2E 顺序执行后,平均 PR 反馈周期从 11 分钟压到 4 分钟,失败 PR 的二次修改率从 38% 降到 19%。这一顺序是 LLM 测试工程领域一个被严重低估的"小杠杆"。
单模型测试是必要但不充分的。2026 年的 LLM 应用几乎都是多模型并存:主推理可能用 GPT-5 / Claude Opus 4.5,降级用 Claude Sonnet 4.5 / Gemini 2.5 Pro,意图分类用本地小模型,embedding 用 bge-m3,judge 用 Claude Opus。这种异构栈让"测试矩阵"从二维升到三维:输入 × 模型 × 区域。
输入维度上,要把所有语种 (zh/en/ja/ko/es/pt/ar 等) × 所有意图类别 (informational/transactional/navigational/creative/safety-sensitive) 做笛卡尔积。模型维度上,主推理、备推理、embedding、judge、guardrail 五个槽位都要分别测。区域维度上,不同的 CDN 节点、不同的合规要求 (EU GDPR / 中国个人信息保护法 / 美国 CCPA) 会触发不同的 PII 处理路径。
测试矩阵的产出是一个三维 tensor,每个 cell 是一组指标 (准确率、延迟、token 成本、幻觉率)。这个 tensor 在 CI 阶段被采样,在 nightly 阶段被全量跑。任何 cell 出现明显回退都触发告警。工程上的现实约束是:三维矩阵的 cell 数量容易爆炸,所以必须做分层采样——核心 cell (主语种 + 主模型 + 主区域) 在每个 PR 都跑,长尾 cell (稀有语种 + 备模型 + 边缘区域) 在 nightly 跑。分层采样是控制 CI 时长的关键工程手段。
传统测试关注"正常路径",LLM 应用测试还必须关注"反例路径"。因为 LLM 的输入空间是开放的,任何自然语言都可能被用户输入,里面可能藏 prompt injection、可能藏越权指令、可能藏诱导模型生成错误事实的对抗样本。
Red-team 测试是 LLM 应用特有的攻防演练。它由一个专门的红队模型或人工编写的对抗 prompt 集驱动,尝试突破应用的护栏。常见的攻防目标有:诱导模型输出 system prompt、诱导模型忽略安全策略、诱导模型生成违禁内容、诱导模型泄露训练数据、诱导模型在 RAG 场景下忽略检索证据直接生成。Red-team 套件至少要包含 200 条精心设计的对抗 prompt,每条标注攻击目标 (target_pillar) 和期望防御行为 (defense_expected)。
幻觉检测是反例路径的另一个支柱。它有两种技术路线:基于事实性 (factuality) 的,例如"答案中提到的事件是否在外部知识库中有对应条目";基于内部一致性 (self-consistency) 的,例如"同一问题让模型回答 N 次,关键事实是否稳定"。事实性路线更准确但依赖外部知识库,内部一致性路线更便宜但容易漏报。两路并行是 2026 年的最佳实践。
越权注入是 RAG 类应用的特殊风险:用户把恶意指令塞进上传的文档,让 LLM 在 RAG 流程中误以为是 system 指令执行。防御方式是在 RAG 链路中插入指令隔离层,把检索回来的内容明确标记为 untrusted_document,并通过 prompt engineering 让 LLM 只引用不执行。对应的契约测试是:上传一个含 "Ignore previous instructions and ..." 的文档,断言 LLM 仍然按 system prompt 而非文档指令行事。
最后是反例路径的事后取证。当一次反例 prompt 真的让应用 fail,必须立即采集完整证据链:输入 prompt 全文、用户上下文 (脱敏后)、应用的中间产物 (RAG 检索片段、prompt 拼装结果、tool call 序列、最终输出)、模型响应时间、护栏日志、相关上下游节点的请求 ID。这套证据链要在事故复盘会上 1:1 重放,确认"它为什么 fail"和"我们怎么 fail 的",再把反例 prompt 加入 golden set 作为永久反例。一个成熟的 LLM 应用团队应当把"反例资产"视为与"正例资产"同等重要——正例决定"它能做什么",反例决定"它不能做什么",两者一起才能描绘出 LLM 应用的真实能力图谱。
最后给 SRE 和 QA 团队一份 8 条可勾选的清单,任何一条没勾上,都不应该把 LLM 应用推向生产。
图表加载中…
第一条,Unit 层必含不变式断言,且不变式覆盖率 ≥ 80% 的核心场景。第二条,Contract 层的 schema 必须用 Pydantic v2 或同等强度的类型系统,不允许裸 JSON 字符串在系统内流通。第三条,Eval 层的 judge 模型必须在 ≥ 500 条 golden set 上校准,tau_b ≥ 0.7 才能用于 CI 阻断。第四条,E2E 必含种子流量回归,种子库 ≥ 50 条,每月更新一次。第五条,生产抽样 1% 以上,影子 A/B 持续运行,差异率告警阈值设 5% (按业务可调)。第六条,多语种多模型矩阵必须分层采样,核心 cell 在 PR 阶段跑,长尾 cell 在 nightly 跑。第七条,Red-team 套件 ≥ 200 条对抗 prompt,每月迭代一次,新增攻击向量必须及时补。第八条,幻觉检测双路并行:事实性 + 内部一致性,任一路告警即触发人工 review。
把这八条不是"建议",是"门槛"。任何一条缺失,意味着在那个维度上"测试盲区"被默认接受。LLM 应用的失败模式与确定性软件不同,它会在一个看似正常的 prompt 上突然产出完全错误的答案,而 unit + contract + eval + e2e + red-team 五层联防,就是为这种"突变"准备的工程防御网。
把这套四层金字塔 + 八条清单 + 校准机制 (judge) + 攻防机制 (red-team) + 矩阵 (多模型多区域) 装进 CI 流水线,LLM 应用才真正跨过 demo 阶段,具备严肃生产的可观测性、可回滚性、可审计性。
最后强调测试左移的重要性。LLM 应用的故障成本远比传统应用高:一次错误的输出可能让企业损失合规信誉,一次幻觉可能引发法律纠纷,一次越权注入可能泄漏用户数据。因此测试不能等开发完再补,必须在需求评审阶段就把"什么算对、什么算错"明确下来,转化为 golden set 的初始条目;在 prompt 设计阶段就把不变式 + 契约 schema 一并定好;在 CI 配置阶段就把四道门禁和分层采样矩阵自动化。这个"左移"过程决定了 LLM 应用能否在规模化阶段保持质量稳定,质量不是测出来的,是设计出来的——这一句话送给所有正在做 LLM 应用工程的同行。
Conversation
0 条