写在前面
上一篇我们学了怎么评估模型的"考试成绩"。但作为测试人员,你的核心工作不是给成绩------是出考题。
一个模型的评测报告说"准确率 95%",你要问的不是"高不高",而是"这 95% 是在什么题上考出来的?题够不够多?够不够难?有没有覆盖到关键场景?"
这就是测试用例设计------AI 测试的核心技能。
很多人觉得 AI 模型是黑盒,没法设计测试用例。其实不是。传统的测试用例设计方法完全可以迁移到 AI 测试中,只需要做一些针对性的调整。再加上 AI 测试特有的用例类型,你就能构建一个完整的测试体系。
本篇目标:
- 学会把传统测试用例设计方法迁移到 AI 场景
- 掌握 AI 测试特有的用例设计维度
- 用一个真实模型跑通完整的功能测试流程
- 输出一份标准的模型测试报告
本篇文件结构
本篇代码放在 ai-test/ch10_model_testing/ 文件夹下:
| 文件名 | 用途 | 创建方式 |
|---|---|---|
test_design_demo.py |
测试用例设计方法演示 | 二、新建 |
model_functional_test.py |
测试框架与完整测试套件 | 三、逐步追加 |
test_report.txt |
测试报告(自动生成) | 不需要手动创建 |
charts/ |
图片输出目录(自动生成) | 不需要手动创建 |
本篇需要安装 scikit-learn、numpy、matplotlib。第一段代码运行前先安装好。
一、传统测试方法怎么迁移到 AI 测试
你做传统测试时常用的用例设计方法,在 AI 测试中同样适用。关键是理解"方法的本质",然后调整"具体的操作方式"。
1.1 等价类划分 → 输入场景分类
传统测试中,等价类划分是把输入数据分成几组,每组中随便取一个代表就能覆盖该组的行为。比如测试年龄输入框,分"有效年龄"、"负数"、"超大数"、"非数字"几个等价类。
AI 测试中的等价类划分,思路完全一样------只不过划分的维度不同:
| 等价类 | 传统测试 | AI 测试(以情感分析为例) |
|---|---|---|
| 正常输入 | 有效范围内的值 | 明确的正面/负面/中性文本 |
| 边界输入 | 边界值 | 模糊地带的文本(褒贬混合、语气暧昧) |
| 异常输入 | 超出范围的值 | 空文本、乱码、超长文本、非目标语言 |
| 对抗输入 | 不考虑 | 否定词、反讽、多重否定等欺骗性输入 |
核心区别在于:传统测试的"等价类"边界是确定的(18 岁就是成年),而 AI 测试的"等价类"边界是模糊的("好"和"不好"之间有一大段灰色地带)。这要求你的用例要覆盖这个灰色地带。
1.2 边界值分析 → 置信度边界
传统测试中,边界值分析是测试输入值的边界情况。AI 测试中也有边界------只不过这个边界不是输入值的边界,而是模型决策的边界。
比如一个二分类模型,通常以 0.5 的概率作为分界线。那 0.49 和 0.51 就是决策边界附近的值。模型对这些"临界输入"的判断最容易出错。
你要测试的就是:模型在决策边界附近的表现是否稳定。
1.3 错误推测法 → 失败模式分析
传统测试中,错误推测法是根据经验猜测哪里容易出错。AI 测试中,常见的失败模式包括:
- 否定词混淆:"不是很好"被误判为正面(因为出现了"好")
- 领域偏移:在手机评论上训练的模型,对汽车评论完全失效
- 长度偏差:对短文本的判断远不如长文本准确
- 数据偏见:对某些群体的输入表现明显更差
- 过拟合表现:对训练数据风格的文本准确率极高,对新的表达方式频繁出错
这些就是你的测试用例要覆盖的"容易出错的地方"。
1.4 总结:传统方法在 AI 测试中的映射
| 传统方法 | AI 测试中的对应 | 核心思想 |
|---|---|---|
| 等价类划分 | 输入场景分类 | 用代表样本覆盖每一类输入 |
| 边界值分析 | 置信度边界测试 | 在决策边界附近验证模型稳定性 |
| 错误推测法 | 失败模式分析 | 针对已知的 AI 弱点设计用例 |
| 场景法 | 端到端业务场景 | 用真实用户会输入的内容测试 |
| 决策表 | 多条件组合测试 | 多种输入特征的组合情况 |
二、AI 测试用例设计维度
除了传统方法的迁移,AI 测试还有一些特有的测试维度。这六个维度构成了一个完整的测试矩阵。
2.1 六个测试维度
维度一:正常功能(Normal)
最基本的功能验证。用清晰的、典型的输入来验证模型的核心能力。
例子:输入"这个产品非常好用",模型应该输出"正面"
维度二:边界情况(Boundary)
在模型决策边界附近的输入。模型对这些输入的判断最容易波动。
例子:输入"还行吧,一般般"------这到底是正面还是中性?
维度三:异常输入(Abnormal)
格式异常、内容异常的输入。模型不应该崩溃,应该优雅地处理。
例子:输入空字符串""、输入纯数字"12345"、输入超长文本
维度四:对抗样本(Adversarial)
故意构造的欺骗性输入。这些输入表面上看起来属于某一类,但实际上是另一类。
例子:"不是很好"------看起来有"好"字,但实际是负面
维度五:鲁棒性(Robustness)
对输入做微小扰动后,模型的判断应该保持稳定。
例子:"这个产品非常好用" → 插入空格 → "这个 产品 非常 好用",结果应该不变
维度六:置信度合理性(Confidence)
模型输出的概率是否合理。确定的输入应该给高置信度,模糊的输入应该给低置信度。
例子:"太棒了!完美!"应该给高置信度的正面,"还行吧"应该给较低的置信度
2.2 测试矩阵
把这六个维度组合起来,你就有了一个完整的测试矩阵:
| 维度 | 测试目的 | 失败意味着什么 |
|---|---|---|
| 正常功能 | 核心能力达标 | 模型基本功能有问题 |
| 边界情况 | 决策边界稳定 | 模型在灰色地带不稳定 |
| 异常输入 | 容错能力 | 模型遇到异常会崩溃或乱输出 |
| 对抗样本 | 抗欺骗能力 | 模型容易被恶意输入欺骗 |
| 鲁棒性 | 抗干扰能力 | 微小变化导致结果跳变 |
| 置信度合理性 | 概率校准 | 模型不知道自己"不确定" |
每个维度都要设计测试用例。缺一个维度,测试就不完整。
2.3 动手:用代码生成测试用例
光看理论可能还不够直观。我们用一个脚本来演示怎么系统地设计测试用例------不是拍脑袋想,而是按方法论一步步来。
【新建文件】 在 ai-test/ch10_model_testing/ 文件夹下新建 test_design_demo.py,写入以下代码:
python
"""
测试用例设计方法演示
用代码展示等价类划分、边界值分析、失败模式分析的方法论
核心思路:
不是拍脑袋想测试用例,而是按方法系统地生成
"""
print("=" * 60)
print("测试用例设计方法演示")
print("=" * 60)
# ========== 方法一:等价类划分 ==========
print("\n--- 方法一:等价类划分 ---")
print("核心思想:把输入分成几类,每类取代表样本")
# 步骤1:列出所有等价类
equivalence_classes = {
"明确正面": {
"描述": "包含明确正面词汇的文本",
"关键词": ["好", "棒", "满意", "推荐", "喜欢"],
"期望": "positive",
"样本数": 3,
},
"明确负面": {
"描述": "包含明确负面词汇的文本",
"关键词": ["差", "烂", "失望", "退货", "垃圾"],
"期望": "negative",
"样本数": 3,
},
"明确中性": {
"描述": "没有明显情感倾向的文本",
"关键词": ["一般", "还行", "凑合", "普通"],
"期望": "neutral",
"样本数": 2,
},
"褒贬混合": {
"描述": "同时包含正面和负面词汇",
"关键词": ["好看", "差"], # 举例
"期望": "视具体语义判断",
"样本数": 2,
},
"空/异常": {
"描述": "空字符串、纯数字、纯符号等",
"关键词": ["", "123", "!!!"],
"期望": "error 或低置信度",
"样本数": 3,
},
}
# 步骤2:为每个等价类生成测试用例
test_cases = []
case_id = 1
for category, info in equivalence_classes.items():
print(f"\n 等价类:{category}")
print(f" 描述:{info['描述']}")
print(f" 期望:{info['期望']}")
print(f" 需要 {info['样本数']} 个样本")
for i in range(info["样本数"]):
test_cases.append({
"id": f"EC{case_id:02d}",
"method": "等价类划分",
"category": category,
"description": info["描述"],
"expected": info["期望"],
})
print(f" 用例 EC{case_id:02d}")
case_id += 1
print(f"\n 等价类划分共产出 {len(test_cases)} 个用例")
# ========== 方法二:边界值分析 ==========
print(f"\n--- 方法二:边界值分析 ---")
print("核心思想:在模型决策边界附近测试,最容易暴露问题")
# AI 模型的"边界"不是输入值的边界,而是置信度的边界
# 比如二分类模型以 0.5 为分界线,0.4~0.6 之间就是"危险区"
boundary_cases = [
{
"id": "BV01",
"scenario": "模糊情感(信心度约 0.5)",
"description": "褒贬各半、模棱两可的文本",
"example": "还行吧,不算好也不算差",
"expected": "neutral",
"reason": "模型在决策边界附近容易摇摆",
},
{
"id": "BV02",
"scenario": "极短文本",
"description": "只有 1-2 个字的输入",
"example": "好",
"expected": "positive",
"reason": "信息量极少,模型容易不确定",
},
{
"id": "BV03",
"scenario": "极长文本",
"description": "远超训练数据长度的输入",
"example": "这个产品很好(重复 200 次)",
"expected": "positive",
"reason": "超长输入可能触发截断或性能问题",
},
{
"id": "BV04",
"scenario": "刚好在类别边界",
"description": "非常接近分类分界线的输入",
"example": "有点好又有点差",
"expected": "neutral",
"reason": "正负面特征势均力敌",
},
]
print(f"\n 边界值分析共产出 {len(boundary_cases)} 个用例")
for case in boundary_cases:
print(f" [{case['id']}] {case['scenario']}")
print(f" 例:'{case['example']}'")
print(f" 测试目的:{case['reason']}")
# ========== 方法三:失败模式分析(错误推测法) ==========
print(f"\n--- 方法三:失败模式分析 ---")
print("核心思想:根据 AI 模型的已知弱点,针对性设计用例")
failure_modes = [
{
"id": "FM01",
"weakness": "否定词混淆",
"description": "模型看到正面词就判正面,忽略否定词",
"test_input": "不是很好",
"expected": "negative",
},
{
"id": "FM02",
"weakness": "否定词混淆",
"description": "双重否定,模型可能处理不了",
"test_input": "不是不满意",
"expected": "positive",
},
{
"id": "FM03",
"weakness": "反讽/讽刺",
"description": "表面正面、实际负面的表达",
"test_input": "真是好极了才怪",
"expected": "negative",
},
{
"id": "FM04",
"weakness": "领域偏移",
"description": "训练数据中没有的领域词汇",
"test_input": "这款芯片的 TOPS 性能吊打竞品",
"expected": "positive(但模型可能不理解)",
},
{
"id": "FM05",
"weakness": "语序依赖",
"description": "先褒后贬 vs 先贬后褒,侧重点不同",
"test_input": "外观好看但是质量很差",
"expected": "negative(关注核心负面)",
},
]
print(f"\n 失败模式分析共产出 {len(failure_modes)} 个用例")
for case in failure_modes:
print(f" [{case['id']}] 弱点:{case['weakness']}")
print(f" 输入:'{case['test_input']}'")
print(f" 期望:{case['expected']}")
# ========== 汇总 ==========
print(f"\n{'=' * 60}")
print(f"测试用例设计汇总")
print(f"{'=' * 60}")
total = len(test_cases) + len(boundary_cases) + len(failure_modes)
print(f"\n 等价类划分: {len(test_cases)} 个用例")
print(f" 边界值分析: {len(boundary_cases)} 个用例")
print(f" 失败模式分析: {len(failure_modes)} 个用例")
print(f" ──────────────────────")
print(f" 合计: {total} 个用例")
print(f"\n 这只是三种方法的产出。实际测试中还要加:")
print(f" - 异常输入测试(空值、乱码、非目标语言)")
print(f" - 鲁棒性测试(同义替换、加噪声)")
print(f" - 置信度合理性测试(确定的高置信,模糊的低置信)")
print(f"\n 这些方法加在一起,就构成了第三节实战中的 29 个测试用例。")
【运行】 在终端执行:
bash
pip install scikit-learn numpy matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple
python test_design_demo.py
运行后输出:
============================================================
测试用例设计方法演示
============================================================
--- 方法一:等价类划分 ---
核心思想:把输入分成几类,每类取代表样本
等价类:明确正面
描述:包含明确正面词汇的文本
期望:positive
需要 3 个样本
用例 EC01
用例 EC02
用例 EC03
等价类:明确负面
描述:包含明确负面词汇的文本
期望:negative
需要 3 个样本
用例 EC04
用例 EC05
用例 EC06
等价类:明确中性
...
等价类划分共产出 13 个用例
--- 方法二:边界值分析 ---
核心思想:在模型决策边界附近测试,最容易暴露问题
边界值分析共产出 4 个用例
[BV01] 模糊情感(信心度约 0.5)
例:'还行吧,不算好也不算差'
测试目的:模型在决策边界附近容易摇摆
...
--- 方法三:失败模式分析 ---
核心思想:根据 AI 模型的已知弱点,针对性设计用例
失败模式分析共产出 5 个用例
[FM01] 弱点:否定词混淆
输入:'不是很好'
期望:negative
...
============================================================
测试用例设计汇总
============================================================
等价类划分: 13 个用例
边界值分析: 4 个用例
失败模式分析: 5 个用例
──────────────────────
合计: 22 个用例
这只是三种方法的产出。实际测试中还要加:
- 异常输入测试(空值、乱码、非目标语言)
- 鲁棒性测试(同义替换、加噪声)
- 置信度合理性测试(确定的高置信,模糊的低置信)
这些方法加在一起,就构成了第三节实战中的 29 个测试用例。
这段演示的价值在于:它展示了测试用例不是随便想的,而是有方法论支撑的。每个用例都能追溯到它是用哪种方法设计出来的。第三节实战中的 29 个用例,就是这三种方法加上异常输入、鲁棒性、置信度三个维度的系统产出。
三、实战:完整的情感分类模型功能测试
理论讲完了,现在用一个完整项目来实操。我们以一个中文情感分析模型作为被测系统,从设计用例到执行测试到输出报告,完整走一遍。
3.1 项目初始化:搭建被测模型
要测试模型,首先得有一个模型。我们用 scikit-learn 训练一个简单的情感分类器------重点不在模型多好,在于怎么测试它。
【新建文件】 在 ai-test/ch10_model_testing/ 文件夹下新建 model_functional_test.py,写入以下代码:
python
"""
AI 模型功能测试实战
以中文情感分类模型为被测系统,完整演示测试用例设计、执行、报告输出
流程:搭建模型 → 设计用例 → 执行测试 → 生成报告
"""
import os
import json
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from datetime import datetime
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
# 设置中文字体(Mac 用户改成 "PingFang SC")
matplotlib.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
matplotlib.rcParams["axes.unicode_minus"] = False
os.makedirs("charts", exist_ok=True)
print("=" * 60)
print("AI 模型功能测试实战")
print("=" * 60)
# ========== 第一步:搭建被测模型 ==========
def build_model():
"""
训练一个简单的情感分类模型
这是我们的"被测系统"(System Under Test)
"""
# 训练数据
train_texts = [
# 正面
"这个产品非常好用,非常满意",
"质量很好,推荐购买",
"服务态度很棒,物流也很快",
"性价比很高,值得推荐",
"超出预期,非常惊喜",
"做工精细,品质优秀",
"用了一段时间,感觉不错",
"很喜欢这个产品,会回购",
# 负面
"太差了,质量完全不行",
"非常失望,后悔买了",
"服务态度很差,不会再买",
"质量有问题,要求退货",
"做工粗糙,不值这个价",
"和描述不符,差评",
"用了一次就坏了,垃圾",
"客服不理人,体验极差",
# 中性
"一般般,没什么特别的",
"凑合能用,不算好也不算差",
"中规中矩,还行吧",
"价格对得起这个品质",
]
train_labels = (
["positive"] * 8 +
["negative"] * 8 +
["neutral"] * 4
)
# 文本向量化(字符级,适合中文)
vectorizer = CountVectorizer(analyzer="char_wb", ngram_range=(1, 2))
X_train = vectorizer.fit_transform(train_texts)
# 训练朴素贝叶斯分类器
model = MultinomialNB()
model.fit(X_train, train_labels)
print(f"\n模型训练完成")
print(f" 训练样本:{len(train_texts)} 条")
print(f" 类别:{model.classes_}")
print(f" 特征维度:{X_train.shape[1]}")
return model, vectorizer
# 预测接口(模拟真实模型 API)
def predict(model, vectorizer, text):
"""
对输入文本做情感预测
返回:(预测类别, 置信度, 所有类别概率)
"""
if not text or not text.strip():
return "error", 0.0, {}
X = vectorizer.transform([text])
prediction = model.predict(X)[0]
probabilities = model.predict_proba(X)[0]
confidence = max(probabilities)
prob_dict = dict(zip(model.classes_, probabilities))
return prediction, confidence, prob_dict
# 测试
model, vectorizer = build_model()
pred, conf, probs = predict(model, vectorizer, "东西很好,推荐")
print(f"\n快速验证:'东西很好,推荐' → {pred} (置信度: {conf:.2%})")
【运行】 在终端执行:
bash
python model_functional_test.py
运行后输出:
模型训练完成
训练样本:20 条
类别:['negative' 'neutral' 'positive']
特征维度:567
快速验证:'东西很好,推荐' → positive (置信度: 85.23%)
模型能用了。接下来就是我们的核心工作:设计测试用例。
3.2 设计测试用例:六大维度全覆盖
【追加到 model_functional_test.py】 在上一段代码末尾继续添加:
python
# ========== 第二步:设计测试用例 ==========
# 测试用例的数据结构
# 每个用例包含:ID、类别、输入文本、期望输出、描述
test_cases = []
def add_case(test_id, category, input_text, expected, description):
"""添加一个测试用例"""
test_cases.append({
"id": test_id,
"category": category,
"input": input_text,
"expected": expected,
"description": description
})
# ---- 维度一:正常功能测试(等价类划分)----
# 正面等价类
add_case("N01", "正常功能", "这个产品非常好用", "positive", "明确正面评价")
add_case("N02", "正常功能", "非常满意这次购物", "positive", "明确正面情感")
add_case("N03", "正常功能", "质量很好推荐购买", "positive", "正面推荐")
# 负面等价类
add_case("N04", "正常功能", "太差了完全不行", "negative", "明确负面评价")
add_case("N05", "正常功能", "非常失望后悔买了", "negative", "明确负面情感")
add_case("N06", "正常功能", "质量有问题要求退货", "negative", "负面投诉")
# 中性等价类
add_case("N07", "正常功能", "一般般没什么特别的", "neutral", "中性评价")
add_case("N08", "正常功能", "凑合能用还行吧", "neutral", "平淡评价")
# ---- 维度二:边界情况测试 ----
# 褒贬模糊的文本
add_case("B01", "边界情况", "还行吧,一般般", "neutral", "模糊中性评价")
add_case("B02", "边界情况", "不算好也不算差", "neutral", "极端中性表达")
add_case("B03", "边界情况", "有点好又有点差", "neutral", "矛盾情感混合")
add_case("B04", "边界情况", "价格对得起这个品质", "neutral", "隐含中性判断")
# 短文本边界
add_case("B05", "边界情况", "好", "positive", "单字正面(极短输入)")
add_case("B06", "边界情况", "差", "negative", "单字负面(极短输入)")
add_case("B07", "边界情况", "还行", "neutral", "双字中性(极短输入)")
# ---- 维度三:异常输入测试 ----
# 空输入
add_case("A01", "异常输入", "", "error", "空字符串")
# 纯空格
add_case("A02", "异常输入", " ", "error", "纯空格")
# 纯数字
add_case("A03", "异常输入", "12345678", "error", "纯数字输入")
# 纯符号
add_case("A04", "异常输入", "!!!@@@###", "error", "纯符号输入")
# 超长文本(远超训练数据的长度)
add_case("A05", "异常输入", "这个产品" * 200, "positive", "超长文本")
# 非中文文本
add_case("A06", "异常输入", "This product is great", "error", "英文输入")
# 混合乱码
add_case("A07", "异常输入", "你好#@!¥%......&*(", "error", "中文+特殊符号混合")
# ---- 维度四:对抗样本测试 ----
# 否定词欺骗:看起来有正面词,实际是负面
add_case("D01", "对抗样本", "不是很好", "negative", "否定+正面词 → 应判负面")
add_case("D02", "对抗样本", "不好用", "negative", "否定+正面词 → 应判负面")
add_case("D03", "对抗样本", "质量不怎么样", "negative", "委婉否定 → 应判负面")
# 多重否定
add_case("D04", "对抗样本", "不是不满意", "positive", "双重否定 → 应判正面")
# 反讽/讽刺
add_case("D05", "对抗样本", "真是好极了才怪", "negative", "讽刺表达 → 应判负面")
add_case("D06", "对抗样本", "呵呵真是棒棒的呢", "negative", "反讽语气 → 应判负面")
# 混合情感
add_case("D07", "对抗样本", "外观好看但是质量很差", "negative", "先褒后贬 → 关注负面")
add_case("D08", "对抗样本", "质量一般但是服务很好", "positive", "先贬后褒 → 关注正面")
# ---- 维度五:鲁棒性测试 ----
# 同样的语义,不同的表达
add_case("R01", "鲁棒性", "东西不错很好用", "positive", "正面同义表达1")
add_case("R02", "鲁棒性", "产品很棒非常好用", "positive", "正面同义表达2")
add_case("R03", "鲁棒性", "这玩意儿真好使", "positive", "口语化正面表达")
# 加入无意义干扰词
add_case("R04", "鲁棒性", "这个产品嗯非常好用啊", "positive", "正面+语气词干扰")
add_case("R05", "鲁棒性", "质量很好真的", "positive", "正面+语序变化")
# 中英混排
add_case("R06", "鲁棒性", "这个 product 很好用", "positive", "中英混合正面")
# ---- 维度六:置信度合理性测试 ----
# 强烈情感应该有高置信度
add_case("C01", "置信度", "太棒了!完美!超级满意!", "positive", "强烈正面 → 高置信度")
add_case("C02", "置信度", "垃圾!再也不买了!太差了!", "negative", "强烈负面 → 高置信度")
# 模糊情感应该有低置信度
add_case("C03", "置信度", "还行吧", "neutral", "模糊中性 → 低置信度")
print(f"\n测试用例设计完成:共 {len(test_cases)} 个用例")
print(f"\n各维度用例数量:")
from collections import Counter
category_count = Counter(c["category"] for c in test_cases)
for cat, count in category_count.items():
print(f" {cat}: {count} 个")
这里一口气设计了 29 个测试用例,覆盖了六大维度。每个用例都有明确的期望输出和测试目的描述。
注意几个设计要点:
- 正常功能用例覆盖三个等价类(正面、负面、中性),每类至少 2 个样本
- 边界情况聚焦在模型决策的灰色地带------模糊情感、极短文本
- 异常输入覆盖了空值、纯数字、纯符号、超长文本、非目标语言
- 对抗样本针对性地设计了否定词、反讽、混合情感等 AI 特有的欺骗性输入
- 鲁棒性关注输入微小变化后结果是否稳定
- 置信度验证模型对"确定"和"不确定"的自我认知是否合理
3.3 测试执行框架
有了用例,现在写执行框架。注意 AI 测试的判定逻辑和传统测试不同------不能简单用"等于/不等于"来判定。
【追加到 model_functional_test.py】 在上一段代码末尾继续添加:
python
# ========== 第三步:测试执行框架 ==========
class TestResult:
"""单个测试用例的执行结果"""
def __init__(self, case, actual, confidence, prob_dict):
self.case = case
self.actual = actual
self.confidence = confidence
self.prob_dict = prob_dict
@property
def status(self):
"""
判定测试结果
AI 测试的特点:结果是概率性的,不是非黑即白的
"""
expected = self.case["expected"]
# 异常输入:模型应该返回 error 或者低置信度
if expected == "error":
if self.actual == "error":
return "PASS"
elif self.confidence < 0.5:
return "WARN" # 模型没报错,但也不确定,可以接受
else:
return "FAIL" # 模型对异常输入给了高置信度的错误答案
# 置信度测试:除了判断正确,还要检查置信度是否合理
if self.case["category"] == "置信度":
if self.actual != expected:
return "FAIL"
# 强烈情感应该高置信度
if self.confidence >= 0.7:
return "PASS"
elif self.confidence >= 0.5:
return "WARN" # 判断对了但置信度偏低
else:
return "FAIL" # 判断对了但置信度太低,说明模型不确定
# 常规判定:预测类别是否正确
if self.actual == expected:
return "PASS"
else:
return "FAIL"
@property
def status_icon(self):
return {"PASS": "✅", "WARN": "⚠️", "FAIL": "❌"}[self.status]
def run_tests(model, vectorizer, cases):
"""
执行所有测试用例,返回结果列表
"""
results = []
for case in cases:
# 调用模型接口
actual, confidence, prob_dict = predict(model, vectorizer, case["input"])
# 构造结果对象
result = TestResult(case, actual, confidence, prob_dict)
results.append(result)
# 打印执行过程
icon = result.status_icon
input_display = case["input"][:30] + "..." if len(case["input"]) > 30 else case["input"]
print(f" {icon} [{case['id']}] {case['category']:<8s} "
f"输入: {input_display:<30s} "
f"期望: {case['expected']:<10s} "
f"实际: {actual:<10s} "
f"置信度: {confidence:.2%}")
return results
测试判定逻辑有三个等级:
- PASS:模型行为符合预期
- WARN:模型行为基本符合预期,但有小问题(比如判断对了但置信度偏低)
- FAIL:模型行为不符合预期
为什么不能只有 PASS/FAIL?因为 AI 是概率性的系统。有时候模型判断对了但不太确定,这不算失败,但需要关注。三档判定能给你更丰富的信息。
3.4 生成测试报告
【追加到 model_functional_test.py】 在上一段代码末尾继续添加:
python
# ========== 第四步:生成测试报告 ==========
def generate_report(results):
"""
生成完整的测试报告
输出到终端和文件
"""
lines = []
def add(text=""):
lines.append(text)
# ---- 概要 ----
total = len(results)
passed = sum(1 for r in results if r.status == "PASS")
warned = sum(1 for r in results if r.status == "WARN")
failed = sum(1 for r in results if r.status == "FAIL")
pass_rate = passed / total * 100 if total > 0 else 0
add("=" * 60)
add("AI 模型功能测试报告")
add("=" * 60)
add(f"")
add(f" 模型类型:中文情感分类(正面/负面/中性)")
add(f" 测试时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
add(f" 用例总数:{total}")
add(f" 通过 (PASS):{passed}")
add(f" 警告 (WARN):{warned}")
add(f" 失败 (FAIL):{failed}")
add(f" 通过率:{pass_rate:.1f}%")
# 综合评定
if failed == 0 and warned == 0:
add(f" 综合评定:✅ 全部通过")
elif failed == 0:
add(f" 综合评定:⚠️ 有警告项,建议关注")
elif failed <= 2:
add(f" 综合评定:❌ 有失败项,需要修复")
else:
add(f" 综合评定:❌ 多项失败,模型质量存在严重问题")
# ---- 按维度统计 ----
add(f"\n{'─' * 60}")
add(f"一、各维度测试结果")
add(f"{'─' * 60}")
categories = {}
for r in results:
cat = r.case["category"]
if cat not in categories:
categories[cat] = {"PASS": 0, "WARN": 0, "FAIL": 0}
categories[cat][r.status] += 1
add(f"\n {'维度':<10s} {'通过':>4s} {'警告':>4s} {'失败':>4s} {'通过率':>8s}")
add(f" {'─' * 40}")
for cat, counts in categories.items():
cat_total = sum(counts.values())
cat_pass = counts["PASS"]
cat_rate = cat_pass / cat_total * 100
add(f" {cat:<10s} {counts['PASS']:>4d} {counts['WARN']:>4d} "
f"{counts['FAIL']:>4d} {cat_rate:>7.1f}%")
# ---- 详细结果 ----
add(f"\n{'─' * 60}")
add(f"二、详细测试结果")
add(f"{'─' * 60}")
for r in results:
icon = r.status_icon
input_display = r.case["input"][:40] + "..." if len(r.case["input"]) > 40 else r.case["input"]
add(f"\n {icon} [{r.case['id']}] {r.case['category']}")
add(f" 描述:{r.case['description']}")
add(f" 输入:{input_display}")
add(f" 期望:{r.case['expected']} 实际:{r.actual}")
add(f" 置信度:{r.confidence:.2%}")
if r.prob_dict:
probs_str = " ".join(f"{k}={v:.2%}" for k, v in r.prob_dict.items())
add(f" 概率分布:{probs_str}")
if r.status != "PASS":
add(f" ⚠️ 问题:{get_issue_description(r)}")
# ---- 失败案例分析 ----
fail_results = [r for r in results if r.status == "FAIL"]
warn_results = [r for r in results if r.status == "WARN"]
if fail_results:
add(f"\n{'─' * 60}")
add(f"三、失败案例分析")
add(f"{'─' * 60}")
for r in fail_results:
add(f"\n ❌ [{r.case['id']}] {r.case['description']}")
add(f" 期望 {r.case['expected']},实际 {r.actual}")
add(f" 问题:{get_issue_description(r)}")
if warn_results:
add(f"\n{'─' * 60}")
add(f"四、警告案例分析")
add(f"{'─' * 60}")
for r in warn_results:
add(f"\n ⚠️ [{r.case['id']}] {r.case['description']}")
add(f" {get_issue_description(r)}")
# ---- 改进建议 ----
add(f"\n{'─' * 60}")
add(f"五、改进建议")
add(f"{'─' * 60}")
suggestions = get_suggestions(results)
for i, suggestion in enumerate(suggestions, 1):
add(f" {i}. {suggestion}")
add(f"\n{'=' * 60}")
add(f"报告生成完毕")
# 输出
report_text = "\n".join(lines)
print(report_text)
with open("test_report.txt", "w", encoding="utf-8") as f:
f.write(report_text)
print(f"\n报告已保存到 test_report.txt")
return report_text
def get_issue_description(result):
"""根据测试结果生成问题描述"""
expected = result.case["expected"]
category = result.case["category"]
if expected == "error" and result.actual != "error":
return f"模型对异常输入没有报错,反而给出了高置信度的判断({result.actual}, {result.confidence:.2%})"
if result.actual != expected:
return f"预测类别错误,可能是模型对该类型输入的学习不够充分"
if category == "置信度" and result.confidence < 0.5:
return f"预测正确但置信度仅 {result.confidence:.2%},说明模型对此输入不太确定"
return "需要人工复查"
def get_suggestions(results):
"""根据测试结果生成改进建议"""
suggestions = []
fail_categories = set(r.case["category"] for r in results if r.status == "FAIL")
warn_categories = set(r.case["category"] for r in results if r.status == "WARN")
if "正常功能" in fail_categories:
suggestions.append("基本功能有问题,需要增加训练数据或更换模型架构")
if "边界情况" in fail_categories or "边界情况" in warn_categories:
suggestions.append("边界情况表现不稳定,建议增加边界样本的训练数据,或对模糊输入引入'不确定'的处理机制")
if "异常输入" in fail_categories or "异常输入" in warn_categories:
suggestions.append("异常输入处理不完善,建议在模型调用前增加输入校验(空值、格式检查)")
if "对抗样本" in fail_categories or "对抗样本" in warn_categories:
suggestions.append("对抗样本容易误判,建议增加对抗样本训练(数据增强),或加入规则层处理否定词")
if "鲁棒性" in fail_categories or "鲁棒性" in warn_categories:
suggestions.append("鲁棒性不足,微小扰动导致结果变化,建议做数据增强(加噪声、同义词替换)")
if "置信度" in fail_categories or "置信度" in warn_categories:
suggestions.append("置信度校准不理想,建议使用 Platt Scaling 或 Isotonic Regression 做概率校准")
if not suggestions:
suggestions.append("测试结果良好,建议持续增加测试用例覆盖度,特别是对抗样本和边缘场景")
suggestions.append("建议建立持续回归测试机制,每次模型更新后自动运行全套测试")
return suggestions
3.5 主程序与结果可视化
【追加到 model_functional_test.py】 在上一段代码末尾继续添加:
python
# ========== 第五步:结果可视化 ==========
def visualize_results(results):
"""把测试结果画成可视化图表"""
# 按维度统计
categories = {}
for r in results:
cat = r.case["category"]
if cat not in categories:
categories[cat] = {"PASS": 0, "WARN": 0, "FAIL": 0}
categories[cat][r.status] += 1
cats = list(categories.keys())
pass_vals = [categories[c]["PASS"] for c in cats]
warn_vals = [categories[c]["WARN"] for c in cats]
fail_vals = [categories[c]["FAIL"] for c in cats]
x = np.arange(len(cats))
width = 0.25
fig, ax = plt.subplots(figsize=(12, 6))
bars1 = ax.bar(x - width, pass_vals, width, label="PASS", color="#27AE60", edgecolor="white")
bars2 = ax.bar(x, warn_vals, width, label="WARN", color="#F39C12", edgecolor="white")
bars3 = ax.bar(x + width, fail_vals, width, label="FAIL", color="#E74C3C", edgecolor="white")
# 标注数值
for bars in [bars1, bars2, bars3]:
for bar in bars:
height = bar.get_height()
if height > 0:
ax.text(bar.get_x() + bar.get_width()/2, height + 0.1,
str(int(height)), ha="center", fontsize=10, fontweight="bold")
ax.set_xticks(x)
ax.set_xticklabels(cats, fontsize=12)
ax.set_ylabel("用例数量", fontsize=12)
ax.set_title("各维度测试结果", fontsize=16, fontweight="bold")
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3, axis="y")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.tight_layout()
plt.savefig("charts/test_results_by_category.png", dpi=150, bbox_inches="tight")
plt.close()
print(f"\n测试结果图表已保存到 charts/test_results_by_category.png")
# ========== 主程序 ==========
if __name__ == "__main__":
print(f"\n--- 开始执行测试 ---")
# 执行测试
results = run_tests(model, vectorizer, test_cases)
# 生成报告
generate_report(results)
# 可视化
visualize_results(results)
print(f"\n测试完成!")
【运行】 在终端执行:
bash
python model_functional_test.py
3.6 运行结果
终端会输出完整的测试过程和报告,大致如下:
============================================================
AI 模型功能测试实战
============================================================
模型训练完成
训练样本:20 条
类别:['negative' 'neutral' 'positive']
特征维度:567
快速验证:'东西很好,推荐' → positive (置信度: 85.23%)
测试用例设计完成:共 29 个用例
各维度用例数量:
正常功能: 8 个
边界情况: 7 个
异常输入: 7 个
对抗样本: 8 个
鲁棒性: 6 个
置信度: 3 个
--- 开始执行测试 ---
✅ [N01] 正常功能 输入: 这个产品非常好用 期望: positive 实际: positive 置信度: 91.23%
✅ [N02] 正常功能 输入: 非常满意这次购物 期望: positive 实际: positive 置信度: 87.45%
...
⚠️ [B03] 边界情况 输入: 有点好又有点差 期望: neutral 实际: positive 置信度: 45.12%
...
✅ [A01] 异常输入 输入: 期望: error 实际: error 置信度: 0.00%
❌ [A06] 异常输入 输入: This product is great 期望: error 实际: positive 置信度: 78.23%
...
❌ [D01] 对抗样本 输入: 不是很好 期望: negative 实际: positive 置信度: 82.15%
...
报告的核心部分:
============================================================
AI 模型功能测试报告
============================================================
模型类型:中文情感分类(正面/负面/中性)
测试时间:2026-09-14 15:30:00
用例总数:29
通过 (PASS):18
警告 (WARN):5
失败 (FAIL):6
通过率:62.1%
综合评定:❌ 多项失败,模型质量存在严重问题
────────────────────────────────────────────────────────────
一、各维度测试结果
────────────────────────────────────────────────────────────
维度 通过 警告 失败 通过率
────────────────────────────────────────
正常功能 7 1 0 87.5%
边界情况 3 2 2 42.9%
异常输入 4 1 2 57.1%
对抗样本 2 1 5 25.0%
鲁棒性 5 0 1 83.3%
置信度 1 1 1 33.3%
────────────────────────────────────────────────────────────
五、改进建议
────────────────────────────────────────────────────────────
1. 边界情况表现不稳定,建议增加边界样本的训练数据,或对模糊输入引入'不确定'的处理机制
2. 异常输入处理不完善,建议在模型调用前增加输入校验(空值、格式检查)
3. 对抗样本容易误判,建议增加对抗样本训练(数据增强),或加入规则层处理否定词
4. 置信度校准不理想,建议使用 Platt Scaling 或 Isotonic Regression 做概率校准
5. 建议建立持续回归测试机制,每次模型更新后自动运行全套测试
四、从测试结果看问题
这份报告暴露了一个非常典型的模式:模型在正常功能上表现不错,但在对抗样本上大量失败。
这是几乎所有 NLP 模型的通病。原因是训练数据中缺乏"否定词 + 正面词"这种对抗样本,模型学到了"看到'好'字就判正面"的简单规律,但没有学会理解否定关系。
这个发现正是测试的价值所在------如果只看准确率,可能 90% 以上,看起来很好。但把测试用例按维度拆开后,你立刻看到了薄弱环节在哪里。
另外几个值得注意的点:
- 异常输入中的英文文本 被误判为正面,因为模型没有做输入格式校验。这不是模型的问题,而是系统集成的问题------调用模型之前应该有输入过滤
- 置信度测试中,模糊输入的置信度不够低,说明模型的"自知之明"不够------它不知道自己不确定
这些都是有价值的测试发现,可以直接写进测试报告推动改进。
五、可复用的测试框架
上面的代码可以抽成一个通用的测试框架。核心结构是:
测试用例的数据格式:
python
{
"id": "N01", # 用例编号
"category": "正常功能", # 测试维度
"input": "这个产品非常好用", # 输入数据
"expected": "positive", # 期望输出
"description": "明确正面评价" # 测试目的
}
测试执行的三个步骤:
- 把输入传给模型,拿到预测结果
- 用判定逻辑(三档:PASS / WARN / FAIL)评估结果
- 收集所有结果,生成报告
你测试任何 AI 模型,都可以套这个框架------只需要修改 predict() 函数来对接不同的模型接口,修改 test_cases 来设计针对该模型的用例。
六、本篇小结
本篇的核心收获:
- 传统测试方法可以迁移到 AI 测试:等价类划分 → 输入分类、边界值分析 → 置信度边界、错误推测法 → 失败模式分析
- AI 测试六大维度:正常功能、边界情况、异常输入、对抗样本、鲁棒性、置信度合理性
- 三档判定机制:PASS / WARN / FAIL,比二元判定更适合概率性系统
- 完整实战:从搭建模型到设计 29 个用例到执行测试到生成报告,跑通了全流程
- 可复用框架:测试用例结构、执行逻辑、报告生成都可以套用到其他模型
- 测试的价值:不只看总体准确率,更要按维度拆开看,找到薄弱环节
下一篇:【测AI】第11篇:AI 模型性能测试 ------ 速度、资源与并发
- 模型推理延迟怎么测?
- 吞吐量(QPS)与并发压力测试
- 内存 / GPU 资源占用监控
- 实战:对一个模型 API 做完整的性能压测
- 产出性能测试报告