【测AI】第10篇:AI 模型功能测试实战 —— 测试用例怎么设计?

写在前面

上一篇我们学了怎么评估模型的"考试成绩"。但作为测试人员,你的核心工作不是给成绩------是出考题。

一个模型的评测报告说"准确率 95%",你要问的不是"高不高",而是"这 95% 是在什么题上考出来的?题够不够多?够不够难?有没有覆盖到关键场景?"

这就是测试用例设计------AI 测试的核心技能。

很多人觉得 AI 模型是黑盒,没法设计测试用例。其实不是。传统的测试用例设计方法完全可以迁移到 AI 测试中,只需要做一些针对性的调整。再加上 AI 测试特有的用例类型,你就能构建一个完整的测试体系。

本篇目标:

  1. 学会把传统测试用例设计方法迁移到 AI 场景
  2. 掌握 AI 测试特有的用例设计维度
  3. 用一个真实模型跑通完整的功能测试流程
  4. 输出一份标准的模型测试报告

本篇文件结构

本篇代码放在 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": "明确正面评价"  # 测试目的
}

测试执行的三个步骤:

  1. 把输入传给模型,拿到预测结果
  2. 用判定逻辑(三档:PASS / WARN / FAIL)评估结果
  3. 收集所有结果,生成报告

你测试任何 AI 模型,都可以套这个框架------只需要修改 predict() 函数来对接不同的模型接口,修改 test_cases 来设计针对该模型的用例。


六、本篇小结

本篇的核心收获:

  • 传统测试方法可以迁移到 AI 测试:等价类划分 → 输入分类、边界值分析 → 置信度边界、错误推测法 → 失败模式分析
  • AI 测试六大维度:正常功能、边界情况、异常输入、对抗样本、鲁棒性、置信度合理性
  • 三档判定机制:PASS / WARN / FAIL,比二元判定更适合概率性系统
  • 完整实战:从搭建模型到设计 29 个用例到执行测试到生成报告,跑通了全流程
  • 可复用框架:测试用例结构、执行逻辑、报告生成都可以套用到其他模型
  • 测试的价值:不只看总体准确率,更要按维度拆开看,找到薄弱环节

下一篇:【测AI】第11篇:AI 模型性能测试 ------ 速度、资源与并发

  • 模型推理延迟怎么测?
  • 吞吐量(QPS)与并发压力测试
  • 内存 / GPU 资源占用监控
  • 实战:对一个模型 API 做完整的性能压测
  • 产出性能测试报告
相关推荐
Reload.1 小时前
深度学习-极验叠字点选-yolo-模型调优-高阶数学-opencv
人工智能·深度学习·opencv·yolo·计算机视觉·视觉检测
半亩码田1 小时前
C#转Python第5.1篇:异步编程:asyncio vs async/await,语法相似但机制不同
开发语言·python·c#
救救孩子把1 小时前
AI面试八股文-01_1-LLM基础概念(Token-上下文窗口-幻觉)
人工智能·面试·职场和发展·llm·token
ADAI_Bowen1 小时前
渲境AI实测:当AI尝试扛起建筑效果图的交付压力
大数据·人工智能
yi0111 小时前
DAY27: LeetCode 110. 平衡二叉树 | 在递归中判断是否平衡
人工智能·笔记·python·算法·leetcode·二叉树·递归
Sammyyyyy1 小时前
jev-ultrafast 实战:浏览器操作代理加本地 AI 网关,把云控制台配置做成自动化流水线
运维·人工智能·自动化·ai编程·servbay
狂师1 小时前
爆火!新一代e2e AI测试框架,单日狂揽1500 star
前端·人工智能·测试
米小虾1 小时前
随机基线都打不过:为什么一整代视觉 Token 压缩方法在混合线性注意力上集体失效
人工智能
米小虾1 小时前
一条 rollout 的两种死法:RLVR 的学习信号漏在哪,怎么补
人工智能