模型评测最容易被低估的一步,不是用什么指标,也不是选哪个测试集,而是“整个评测过程本身有没有一套稳定、可复用、能自动跑的标准化测试框架”。很多人把模型跑通一个 Demo,看了几条输出,就得出“效果不错”的结论,等到真正要换模型、调参数、对比版本、做回归测试时,才发现之前的结果根本没法复现。这篇内容适合正在做模型选型、模型迭代、算法效果回归、或者需要把评测结果写进技术报告和项目总结的开发者。最值得关注的点是:评测不是一次性打分组装,而是一个需要像软件测试一样被工程化的过程。

先说一句判断:任何没有固定流程、没有版本记录、没有统一判定标准的模型评测,都只能叫“试了试”,不能叫“评测”。如果要把模型结果作为功能上线、技术选型或优化的依据,就必须把评测流程标准化,而且这套流程应该尽量用自动化测试框架来承载,而不是靠手工改 Prompt、手动标分、肉眼比对。

1. 先想清楚:模型评测为什么不能只靠“看效果”

1.1 看似能用的模型,上线后不一定稳

很多项目在接入模型时,第一步都是“拿几个典型问题试一下”。试完发现回答完整、逻辑清楚,就觉得可以上线。但上线之后,问题往往出在那些你没试过的场景上:输入格式稍微变了一点、问题换了一种说法、上下文变长、并发量上来,输出质量就开始波动。这一点在评测阶段不一定会暴露,因为手工试测的时候,样本太少,而且人脑会自动忽略不重要的错误。

我见过一个比较典型的案例:某个文本摘要模型的演示效果很好,给的三四篇样例都摘得干净利落。但换成真实业务里的长文档之后,摘要经常丢掉关键结论,有时候还会把表格内容混进正文。问题不在模型本身,而在于评测时根本没有覆盖“长文档、带表格、多段落”这些实际场景。 评测的第一原则,不是证明模型能跑,而是把模型放进真实输入分布里,看它到底哪些场景行、哪些场景不行。

所以我建议,不管项目大小,模型评测都要先做一件事:把你要处理的输入范围明确下来。包括文本长度、格式、语言、领域、语气、是否有噪音、是否需要多轮上下文。这个范围就是测试集的边界,也是标准化评测框架的起点。

1.2 没有被标准化的评测过程,几乎没法复盘

做模型迭代的人应该都有过这种体验:上一个模型跑了 0.82 的分数,换了一个版本之后变成 0.78,你根本不记得上次的评测数据是怎么来的。是同一个测试集吗?是不是有一批坏数据被中途替换掉了?评测脚本前后改过没有?随机种子是不是变了?如果这些信息没有记录,你不仅无法判断模型是变好了还是变差了,连定位问题都不知道从哪下手。

标准化测试框架真正解决的问题,不是“让评测更高级”,而是让评测过程具备可复现性。可复现的意思是说:任何人、任何时间、任何机器上,只要用同一份代码、同一个配置、同一个测试集,就能得到同一个评测结果。听起来很简单,但做到并不容易。光是“随机种子不一致导致推理结果不同”这一项,就能让两次评测差出好几个百分点。

1.3 评测标准化要解决的不是一个指标,而是一套流程

很多人把“标准化”理解为“统一指标”。比如大家都算准确率、F1、BLEU,就叫标准化了。但在实际生产里,这只是最表层的一步。真正的标准化评测框架,至少包含下面这些内容:

  • 测试集怎么来、怎么清洗、怎么切分、怎么更新、怎么避免测试集污染
  • 输入怎么构造:Prompt 模板是否统一,同一类任务是不是都走同一个入口
  • 模型运行环境怎么固定:模型版本、推理参数、随机数、批大小、精度模式是否一致
  • 指标怎么计算:是逐条算完再平均,还是先汇总再计算;不同的计算顺序结果可能不一样
  • 结果怎么记录:分数之外,还有哪些运行信息需要留档,方便之后复盘
  • 失败和不稳定情况怎么处理:某条推理超时、某条输出格式异常,是直接失败还是重试还是跳过

这些内容加在一起,才叫测试框架。只看一个指标,只能叫“算了个数”。

2. 标准化测试框架需要先拆出哪些模块

真正落地的时候,我不建议一上来就写代码,而是先把评测框架拆成几个模块,确定边界和职责。这样后面不管是加新测试集、新模型、新指标,还是换评测脚本,都不会变成一堆代码补丁。

2.1 测试集:固定、分层、可版本化管理

测试集是标准化评测的核心资产。它不能是临时拼出来的几十条样例,也不能是网上随手下载的一个 CSV 文件,而应该是被明确管理的数据集合。

固定是第一步。每轮评测用的测试集必须是一致的,至少在同一版本内部保持一致。就算要调整测试集,也应该通过新建版本的方式,而不是在原文件上直接改。这样前后两次评测结果之间才有可比性。

分层是第二步。不要只准备一个“全部样本”的测试集,要按业务维度切分。比如一个客服意图识别评测,可以按意图类型分成售前、售后、投诉、催单等子集;一个代码生成模型评测,可以按题目难度分成简单、中等、困难,或者按是否涉及多文件依赖来划分。分层评测的好处是,你能直接看出模型到底在哪个子集上变好,在哪个子集上退化。

可版本化管理是第三步。测试集文件要放在 Git 或数据版本管理工具里,文件命名要带上版本号或日期,不能出现“test_final_v3_really_final.csv”这种名字。每次评测时,要把测试集版本记录到结果文件里。

2.2 任务定义:输入输出、判定逻辑和可重复性要求

一个模型可能同时承担多种任务,比如既能做摘要,又能做问答,还能做分类。不同任务必须单独定义评测方式。不要把三类任务混在一个评测脚本里,否则某个任务效果不好时,很难单独定位。

任务定义需要写清楚四件事。第一,输入结构是什么,是一个字符串、一组字段、还是一段对话历史。第二,输出结构是什么,是纯文本、JSON、还是包含置信度分数的结构化结果。第三,判定逻辑是什么,是字符串完全匹配、关键词匹配、靠另一个模型打分、还是人工审核。第四,可重复性要求是什么,比如固定随机种子,确保同一批输入每次推理输出一致。

这里特别提醒一下:判定逻辑是很多人偷懒的地方。有人用“输出里包含目标词”就算对,但业务场景里经常出现明明意思完全不对、只是碰巧包含目标词的情况。如果判定标准本身不可信,评测框架再规范也没用。

2.3 指标层:不只选指标,还要规定怎么算

选指标是相对简单的一步,难的是把计算过程规定清楚。以分类任务为例,准确率好理解,但如果测试集本身类别不平衡,你还要决定是算宏平均还是微平均。宏平均先算每个类别的准确率再取平均,微平均把所有样本归到一起再算,两者结果差异可能相当大。

生成类任务更复杂。BLEU 是 n-gram 级别的匹配指标,ROUGE 更关注召回,BERTScore 用语义向量也算相似度,但不同指标对同一批结果可能给出不同排序。评测框架要做的是“先固定指标集合,再固定计算方法”,不能这次用 ROUGE-L,下次用 ROUGE-1,换指标的同时还换了模型,那就永远定位不了是模型变差了还是指标换了。

指标层还需要考虑聚合方式。逐条计算指标再取平均值,和把全部预测结果汇总后计算,在很多情况下结果不一样。特别是像 F1 这种基于混淆矩阵的指标,两种计算方式的差异会更明显。所以评测框架里一定要写明聚合方式。

2.4 基线与对照:没有参照物,分数没有意义

单独看一次评测分数,很难判断模型到底处于什么水平。标准化评测框架里,一定要配置基线。基线可以是上一个线上版本,也可以是一个规则系统,还可以是开源社区公认的基准结果。

有了基线之后,每次模型迭代的评测报告就变成了“新模型 vs 基线”的对比。这样你不仅能看出涨分还是掉分,还能看出涨分到底发生在哪个子任务、哪个分层数据集上。很多时候全局指标没有明显变化,但某个关键子集的指标已经掉了很多,如果没有基线对比,这种情况很容易被忽略。

3. 用 pytest 搭一个可落地的模型评测框架

在技术实现层面,我推荐用 pytest 来承载模型评测流程,而不是自己写一个 Python 脚本循环跑。pytest 是 Python 生态里最常用的自动化测试框架,它天然支持测试发现、断言、参数化、插件扩展、失败重试、结果报告,这些正好是模型评测需要的能力。

3.1 为什么选 pytest 而不是自己写评测脚本

很多人第一次做模型评测时,习惯写一个 eval.py ,用 for 循环遍历测试集,把结果算完,打印一个分数。这种方式对于一次性任务问题不大,但一旦进入持续迭代阶段,问题就出来了:没有统一的测试入口,没有失败隔离,没有参数化,没有统一的报告格式。每条测试用例如果中间有一行报错,整个脚本就挂掉,前面跑完的结果也丢了。

pytest 恰好能解决这些问题。它的测试发现机制让我们可以把每条测试样例当作一个测试用例来执行,单条失败不会影响其它用例。参数化可以把不同输入组合批量注入测试函数。conftest 可以统一加载模型、配置、测试集,避免重复代码。pytest 插件生态里还有超时控制、失败重试、HTML 报告等能力,几乎不用自己造轮子。

更关键的一点是,pytest 的架构让评测流程和业务逻辑分离。测试用例只负责“给输入、拿输出、做判定”,模型加载、数据读取、指标计算都可以放到 fixture 或工具模块里。这样评测框架的可维护性会高很多。

3.2 环境准备和目录结构

在本地复现这套评测框架,需要先准备好基本环境。下面是建议的目录结构:

model_eval/
├── conftest.py              # 存放 fixture,统一加载模型和配置
├── pytest.ini               # pytest 配置,注册插件、设置公共参数
├── requirements.txt         # 依赖文件
├── eval_config.yaml         # 模型路径、测试集路径、指标参数等配置
├── data/
│   └── test_sets/           # 测试集文件,建议按版本归档
│       ├── v1.0/
│       └── v1.1/
├── cases/
│   ├── test_summarization.py
│   ├── test_classification.py
│   └── test_qa.py
├── metrics/
│   ├── __init__.py
│   ├── rouge_metrics.py
│   └── accuracy_metrics.py
└── reports/
    └── eval_results/        # 每次评测的结果目录

这里的核心思路是:测试集、测试用例、指标计算、结果报告彼此独立。这样即使某一天完全更换模型,也不需要大改评测用例。

依赖文件方面,一般会包含这些基础依赖,但具体版本要以你项目实际运行环境为准:

pytest
pytest-timeout
pytest-repeat
pyyaml
transformers  # 如果评测的是 Transformer 模型
torch         # 按实际推理框架选择

需要注意,原始项目材料没有给出明确版本号,落地时不要盲目安装最新版,先确认你使用的模型框架和 Python 版本兼容关系。

3.3 最小可运行示例:单任务、单指标

先从最小规模开始。这里以文本分类模型评测为例,演示 pytest 怎么组织一条评测用例。

先写一个 conftest.py,把模型加载放到 fixture 里,保证所有测试用例复用同一个模型实例,不会每条样例都重新加载一次:

import pytest
from model_wrapper import load_model, predict

@pytest.fixture(scope="session")
def model():
    model = load_model("model_path")
    return model

@pytest.fixture()
def sample_inputs():
    # 实际项目中这里会从测试集文件读取
    return [
        {"text": "我想退货", "label": "售后"},
        {"text": "你们有什么优惠", "label": "营销咨询"},
        {"text": "我的订单还没送到", "label": "物流查询"},
    ]

然后写测试用例,把每个样本都变成一个可独立执行的测试:

import pytest

@pytest.mark.parametrize(
    "text, expected_label",
    [
        ("我想退货", "售后"),
        ("你们有什么优惠", "营销咨询"),
        ("我的订单还没送到", "物流查询"),
    ],
)
def test_classification_case(model, text, expected_label):
    result = predict(model, text)
    assert result == expected_label, f"输入: {text}, 预期: {expected_label}, 实际: {result}"

运行方式很简单:

pytest cases/test_classification.py -v

跑完以后,pytest 会告诉你每条用例是 passed 还是 failed。这个阶段的目标只有一个:让模型在新的测试框架里正常启动、正常推理、正常判定。不要一上来就把 1000 条测试集全灌进去,先跑通这 3 到 5 条,确认链路没问题再说。

3.4 批量评测:参数化、并发和超时控制

单条链路跑通之后,就要处理批量评测。批量评测和单条评测不一样,不只是把样例数量变多,还要考虑几个工程问题:失败隔离、超时处理、并发控制、结果记录。

pytest 的参数化天然支持批量输入。你可以把测试集文件读取出来,动态生成参数列表。常见做法是在测试模块里读取数据文件,然后构造 parametrize 参数:

import json
import pytest

def load_test_cases(json_path="data/test_sets/v1.0/cls_test.json"):
    with open(json_path, "r", encoding="utf-8") as f:
        data = json.load(f)
    return [(item["text"], item["label"]) for item in data]

@pytest.mark.parametrize(
    "text, expected_label",
    load_test_cases(),
    ids=lambda x: str(x)[:20],
)
def test_classification_batch(model, text, expected_label):
    result = predict(model, text)
    assert result == expected_label

批量跑的时候,建议加上超时控制,避免某条输入让推理卡死:

pytest cases/ -v --timeout=30 --timeout-method=thread

这里的 --timeout=30 表示单条用例超过 30 秒就标记为失败,避免整个评测卡在一个坏样本上。

并发方面,pytest-xdist 插件可以用 -n auto 参数自动开启多进程。但需要注意,模型推理本身会占用显存或内存,多进程并行时如果每个进程都加载一次模型,资源占用会成倍增长。如果机器配置不够,不要盲目开并发。

注意:解析批量评测任务时,我建议先跑 50 条、单进程、开超时,确认没有问题了,再逐步增加进程数和样本量。低配置机器能跑通 100 条不代表能压住 1000 条的并发任务。

批量评测的另一个核心问题是输出隔离。每条用例是独立执行的,如果某条失败,不能影响其它条目。pytest 天然满足这一点:一条 failed 并不会让整个进程退出,跑完所有用例后会汇总失败列表。

3.5 结果输出:日志、报告和可复用的摘要

评测不能只看命令行终端里的 “3 passed, 1 failed”,因为终端输出会滚动,结果很快就被冲掉了。标准做法是把评测结果落盘。

pytest 的 JUnit XML 报告可以直接生成结构化结果,适合后续合并进持续集成或数据看板。先安装 pytest 自带的 junitxml 支持(不需要额外插件),运行:

pytest cases/ -v --junitxml=reports/eval_results/20250210_classification_v1.0.xml

如果你需要更直观的 HTML 报告,可以用 pytest-html 插件:

pytest cases/ -v --html=reports/eval_results/20250210_classification_v1.0.html

但光有 pytest 报告还不够,模型评测还需要记录“谁在什么配置下跑的、测试集是哪个版本、模型是哪个版本、推理参数是什么”。这些信息建议写进一个单独的 JSON 摘要文件。简化版如下:

{
  "eval_time": "2025-02-10 18:30:00",
  "testset_version": "v1.0",
  "model_version": "bert-base-chinese",
  "framework": "pytorch 2.1.2",
  "metrics": {
    "accuracy": 0.92,
    "per_class_accuracy": {
      "售后": 0.94,
      "物流查询": 0.91,
      "营销咨询": 0.89
    }
  },
  "total_cases": 300,
  "passed": 276,
  "failed": 24
}

这份摘要文件建议和代码一起提交到版本仓库,或者至少归档到评测结果目录。这样下次评测时,可以直接对比两份 JSON,快速定位变化。

4. 判断评测结果是否可信,先看这几个参数

评测框架搭好之后,最怕的不是跑不起来,而是跑出来一份“看起来正确但实际不可信”的结果。要让评测结果有意义,必须对几个关键参数做约束。

4.1 样本量、按层切分和随机数种子

小样本跑出来的分数波动很大。比如 20 条样本里错了 1 条,准确率就直接掉 5 个百分点,这个波动可能根本不是模型变化引起的,而是样本噪音。所以评测结果里必须写清楚样本量,不能只写准确率 0.95 不写是在多少条样本上算出来的。

按层切分也很重要。如果评测结果只有一个全局准确率,你看不出模型在用户投诉这种高价值场景里的表现如何。所以建议至少按业务类别或难度等级切分,每个子集单独出指标。

随机数种子是很多人最容易忽略的一点。像基于神经网络的模型,推理阶段虽然通常确定性较高,但某些实现里如果开启了采样、或者某些算子本身有随机性,两次评测结果就会不一样。评测前,要把模型推理相关的随机种子固定下来。如果是自回归生成模型,还要把 do_sample=False 或固定 temperature ,否则输出不会稳定。

4.2 重复运行、稳定性:不能只看一次分数

模型评测的结果不应该基于单次运行就下结论。尤其是生成类任务,同一条输入在同一次运行里可能都会产出不同结果。更稳妥的做法是:把同一轮评测重复跑 2 到 3 次,观察指标波动范围。

如果两次运行分数差在 0.5 个百分点以内,基本可以认为评测过程是稳定的。如果波动超过 2 个百分点,先不要急着比较模型版本,优先排查评测链路本身是不是有随机性。常见原因包括:温度参数没固定、GPU 算子有随机性、并发导致批次顺序变化、输出解码策略不一致。

4.3 时间与资源:评测本身也要可预期

模型评测不只是“算分数”,它本身是一个计算任务。如果评测集很大,或者模型推理很慢,一次全量评测可能要跑几个小时。评测框架里要记录总耗时和平均单条耗时,这样后续换模型时可以预估成本。

资源占用也是一样。评测脚本跑的时候,要关注显存和内存占用曲线。如果评测过程出现显存溢出,pytest 会报错,但不会告诉你具体是哪条用例触发的。排查方式是把测试集按规模切小,逐步定位。

如果你打算把评测周期做得很频繁,比如每次提交代码都跑一次模型评测,那就要考虑时间成本是否能接受。评测集可以区分冒烟集和全量集,冒烟集只跑几十条高代表性样本,用于快速反馈;全量集放到夜间或定时任务里跑。

4.4 指标一致性:不同轮次、不同环境、不同人

指标计算的一致性非常容易出问题。同一个模型、同一个测试集,可能因为指标计算代码版本不一致,算出两个不同的分数。标准化框架里,指标模块一定要单独维护,并且记录版本。

我遇到过最典型的情况:某个同事觉得“稍微改一下 ROUGE 的 tokenizer 分词方式”,结果全局分数涨了 3 个百分点。但这不是模型变好了,而是指标口径变了。所以评测框架里必须把指标模块的版本和配置写进结果文件,确保每次评测用的指标计算逻辑一致。

5. 常见坑与排查顺序

评测跑起来之后,一定会遇到各种问题。这里把我实际踩过的几个典型场景整理一下,给一个通用排查链路。

5.1 分数忽高忽低,先查什么

如果两次评测分数波动很大,排查顺序如下:

  1. 先看测试集有没有变化:是不是有人改了测试文件,或者读取顺序变了。
  2. 再看模型权重有没有变化:模型文件路径是不是被替换了,CACHE 目录有没有被清掉。
  3. 再看推理参数:temperature、top_p、do_sample、随机种子。
  4. 再看指标计算逻辑:指标模块代码是否有改动。
  5. 最后看运行环境:GPU 是不是被别人占用了,导致批次顺序或显存调度变化。

其中测试集变化和推理参数是最容易出的问题。建议每次评测把测试集的文件哈希记录到摘要里,一旦分数变化,先比对哈希,确认数据没变。

5.2 测试集污染、数据泄漏这类问题怎么防

模型评测最容易犯的重大错误,是测试集泄漏。比如你要做模型微调,但测试集里的样本和训练集有重合,或者测试集里包含写进过 Prompt 的示例文本。这种情况下测出来的分数会虚高,而且上线后立刻打回原形。

标准化框架里要做三件事:第一,测试集必须独立于训练集,至少在业务层面严格分离;第二,测试集不能从公开的通用数据集里直接截取一部分当业务评测集,除非你确认它没有出现在预训练数据里;第三,测试集要定期做相似度去重,防止某些模型在训练时已经见过了这些数据。

5.3 评测脚本报错时,不要急着调模型

很多人一看到评测里出现 failed,第一反应就是“这个模型效果不行”,然后马上调 Prompt、换模型。但实际大量 failed 并不是能力问题,而是流程问题。常见原因包括:

  • 输入格式不匹配,比如模型接口要求 JSON,你传了纯文本
  • 判空逻辑不严谨,模型输出了空串或格式异常,被误判为错误
  • 超时设置过短,长文档推理时间超限
  • 并发进程过多,显存或内存溢出,属于环境问题而非模型问题

所以排查时要先看 failed 的类型,把失败分类,再决定下一步。我的经验是,先输出每条 failed 用例的输入、预测、期望、错误信息,人眼扫一遍,往往能找到共同点。如果一批失败都是“超时”,那就不是模型能力问题,而是资源与超时配置问题。

5.4 从单机脚本到持续评测,需要补什么

当评测框架在单机上稳定下来以后,下一步通常是接到持续集成或定时任务里。要做到这一点,还需要补几件事:

  • 命令行入口要支持传参,比如测试集版本、模型路径、输出目录,不能硬编码在代码里
  • 评测命令要能返回非零退出码,用于流水线判断是否失败
  • 结果文件要有唯一命名,建议用评测时间加测试集版本加模型名
  • 要有一套“冒烟评测”和“全量评测”的切换机制,把快速反馈和完整回归区分开

如果团队里有多人参与,评测框架的代码和结果摘要都要纳入版本管理。评测不是一个人的私活,标准化框架的意义正在于:不管谁跑,怎么跑,结果都能对齐。

最后留几个我自己排查时会优先看的点:失败样本是真的被模型判错,还是输入数据本身标注错误;分数的变化能不能在子集指标里定位到具体业务方向;评测过程有没有把某个步骤的随机性压到最低。如果这三点都能回答清楚,模型评测就算真正进入了可用状态。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐