不确定性系统的测试哲学
两个层面的正确性
LLM 输出的"正确"有两个独立的层面。
结构正确:输出符合预期的格式——JSON 可解析、字段齐全、类型匹配、值在合法范围内。语义正确:输出的含义符合预期——摘要准确反映了原文、分类结果符合业务定义、推理过程逻辑自洽。
一个输出可以结构正确但语义错误(JSON 格式完美,但情感分类把正面评价归为负面)。也可以结构错误但语义正确(回答的内容正确,但不是预期的 JSON 格式)。两个层面需要不同的策略,两个层面的独立性很强。
结构测试是已解决问题
第四章已经论证了一个关键判断:类型系统是 LLM 应用中最有效的结构约束工具。Pydantic 模型在解析阶段执行的 model_validate_json,本身就是一次完整的结构测试——字段存在性、类型正确性、值域范围、枚举约束、跨字段一致性,全部在一次解析调用中完成。
这意味着:如果你的 Pydantic 模型定义得足够精确,结构测试在运行时已经发生了。在 CI 中再写一组 pytest 来验证"输出是否符合 Schema",验证的是 Pydantic 本身的可靠性。结构测试的工程重心应该放在模型定义的精确性上(第四章的主题)。
唯一需要额外结构测试的场景是结构合规率的统计度量:同一个 prompt 运行 100 次,有多少次通过了 Schema 验证?这个数字是一个有价值的质量指标,它的实现方式是"多次调用 + 统计"。
语义测试的策略
语义测试的核心困难在于:没有编译器可以告诉你"这个摘要是否准确"。语义测试仍然可以工程化。以下几种策略各有侧重。
基于规则的断言。 某些语义约束可以被转化为可程序化检查的规则。例如,摘要应该提及原文中的关键实体——用 NER 工具提取原文和摘要的实体集合,计算召回率,设定阈值。这类规则的优势是确定性、低成本、可在 CI 中自动运行。局限是覆盖面窄:只能检查能写成规则的语义约束,而很多语义判断("这个摘要是否抓住了要点")无法被还原为规则。
参考答案对比。 构建一组"黄金标准"输入-输出对,将 LLM 输出与参考答案做相似度比较。不要求完全匹配——使用 embedding 相似度或 ROUGE 等文本相似度指标,设定合理的阈值。这种方法的价值在于它提供了一个锚点,但阈值本身就需要靠经验来定。
LLM-as-Judge。 用另一个 LLM(通常是更强或不同的模型)来评估第一个 LLM 的输出质量。这就有了套娃问题——评判者本身也不确定。但实践中,精心设计 prompt 的 Judge 模型,一致性可以做到够用。关键是将 Judge 的评估也纳入统计验证——看多次评估的分布,而非单次评估的结论。
规则断言最简单直接,但只能管住可形式化的硬约束。参考答案对比扩展到了已知场景的质量基线。LLM-as-Judge 能处理开放性判断,但成本最高、可靠性也最低。
属性测试的真正战场
传统单元测试断言具体值:assert f(3) == 9。LLM 应用中这种断言失效——同一输入不会产生相同输出。基于属性的测试(Property-based Testing)提供了不同的思路:不断言具体值,而是断言输出必须满足的性质。
但这个思路需要一条关键的分界线:哪些性质值得用属性测试来验证,哪些已经被类型系统覆盖了?
| 属性类型 | 示例 | 类型系统能否覆盖 | 是否需要属性测试 |
|---|---|---|---|
| 字段存在与类型 | sentiment 字段是字符串 | Pydantic 字段定义 | 不需要 |
| 值域约束 | confidence 在 0 到 1 之间 | confloat(ge=0, le=1) | 不需要 |
| 枚举约束 | sentiment 只能是 positive/negative/neutral | Literal[...] | 不需要 |
| 跨字段一致性 | confidence 高时 reasoning 应包含证据 | model_validator | 视复杂度而定 |
| 输入-输出关系 | 摘要长度短于原文 | 无法表达 | 需要 |
| 扰动不变性 | 换人名不改变情感判断 | 无法表达 | 需要 |
| 跨调用一致性 | 同一输入多次调用的核心判断一致 | 无法表达 | 需要 |
表格上半部分的属性——字段存在、值域、枚举、跨字段一致性——是第四章讨论的类型系统和验证器的职责。用 PBT 框架随机生成输入来测试这些属性,本质上是在测试 Pydantic 的正确性。
属性测试的真正增量价值在表格下半部分:输入与输出之间的关系、对输入扰动的不变性、跨多次调用的一致性。这些性质无法在单次输出的 Schema 验证中表达,因为它们涉及多个输入或多次调用之间的对比。
输入-输出关系属性要求了解输入和输出之间应满足的约束。摘要短于原文、翻译保留原文的实体、分类结果不依赖于输入中不相关的变化——这些约束只有在同时看到输入和输出时才能检验。
扰动不变性属性要求系统对特定变换保持稳定。将相同内容用不同措辞表达,情感分析结果应该一致。将文本中的人名替换为其他人名,摘要的结构应该不变。这类属性直接检测模型的鲁棒性和公平性。
跨调用一致性属性要求系统的随机性不影响核心判断。同一份合同,多次提取的关键条款应该一致。同一个问题,多次回答的核心结论应该一致。不一致的输出不一定意味着哪次是错的,但一致性本身就是一个有价值的质量信号。
属性测试的实践约束
LLM API 调用有成本和延迟。PBT 框架默认对每个测试函数生成大量随机输入——对确定性函数合理,但对需要调用 LLM API 的测试,可能耗时过长、成本过高。
实践中的调整策略:减少采样数量(CI 中用少量样本做冒烟测试,定期用大量样本做全面测试);缓存 LLM 响应以避免重复调用(但要意识到缓存会掩盖输出的随机性——缓存模式测试的是属性是否在"某一次"输出上成立);分层测试——输入-输出关系属性需要实际调用 LLM,但可以对已有的输出样本离线运行,将 API 成本与测试频率解耦。
测试的统计性质
LLM 应用的测试结果天然是统计性的。同一个测试用例多次运行可能产生不同结果。这是被测系统的本质特征。
工程应对方式是接受统计性质,建立统计化的质量标准:结构合规率(100 次运行中通过 Schema 验证的比例)、语义准确率(在标注测试集上的评估得分)、一致性(同一输入多次运行,核心判断一致的比例)。这些阈值取决于业务场景的容错度——医疗建议和营销文案的容错度截然不同。关键在于:有一个明确的、可量化的质量标准,好过没有标准的"看起来不错"。
测试金字塔的适配
传统测试金字塔(大量单元测试 → 适量集成测试 → 少量端到端测试)在 LLM 应用中需要适配。
底层:类型系统验证。 大量、自动化、确定性。由 Pydantic 模型在运行时执行,CI 中通过结构合规率统计监控。这一层的成本接近零——它是应用代码的一部分。
中层:组件级属性与语义测试。 适量,针对单个 LLM 调用的属性和语义质量。使用标注测试集、属性断言和自动化评估指标。这一层是测试工程投入的主要方向。
顶层:端到端语义测试。 少量,覆盖完整业务流程。通常需要人工评估或高质量的 LLM-as-Judge。
底层保证系统不会以结构错误的方式失败。中层保证单个 LLM 调用的质量可接受。顶层保证整个系统满足业务需求。与传统金字塔的精神一致:覆盖面和成本呈金字塔分布。但底层从"大量单元测试"变成了"类型系统本身"——这是第四章的声明式约束在测试领域的直接回报。