在项目代号 Z AI 的内部大模型评测工作中,“Benchmaxxing”被用来指代围绕 AI benchmark 反复测量、比对、调优的过程。这个词听起来像在追求刷分,真正的工程含义却完全不同:它要求团队在每次模型迭代前后,用一套固定、可复现的基准评测,回答“这次改动到底有没有变好、变好了多少、在哪个维度上变好”这件事。
Z AI 早期遇到过很典型的问题:模型负责人觉得这一版回答更自然,产品经理觉得上一版更稳定,测试同学反馈同一道题目连续调用三次结果都不一样。没有基准评测,讨论就只能停留在主观感受上。下面用 Z AI 作为项目代号,说明一套 AI 模型基准评测体系从零到落地时需要考虑的问题、需要写的代码、需要避开的坑,以及最后如何把单次测试变成可持续的工程规范。如果你正在给自己的模型、智能体或 AI 应用搭建评测流程,可以把 Z AI 替换成你自己的项目名。
1. 先把 Z AI 的评测目标拆成可验证的任务
1.1 基准测试要解决的是“上线决策”,不是“证明自己”
很多团队做评测是从“跑一遍公开榜单”开始的。公开榜单当然有参考价值,但它解决的问题和内部评测要解决的问题并不完全一样。公开榜单比的是“模型相对强弱”,内部评测要回答的是“当前候选版本能不能上线”。后者要求测试集贴近真实使用场景、指标能区分好坏、运行成本和回归成本可接受。
Z AI 的定位是一个同时处理中文问答、代码生成和工具调用的智能助手。如果只给它做一道数学题集合的测试,即使分数很高,也无法判断它在实际客服场景里会不会胡编乱造。因此第一步不是找数据集,而是把产品需求拆成可验证的任务,再为每个任务配上数据来源、打分方式和最低样本量。
1.2 评测维度按“核心风险”拆分
建议从风险角度划分维度,而不是从技术角度。Z AI 团队第一版评测表可以按下面这样设计:
| 评测维度 | 核心风险 | 观测指标 | 建议最小样本量 |
|---|---|---|---|
| 事实问答 | 回答是否准确、编造比例是否可控 | 答案命中率、引用命中率 | 300 - 500 |
| 中文指令遵循 | 是否按要求输出格式、长度、语气 | 格式正确率、人工盲评均分 | 200 - 300 |
| 代码生成 | 生成的代码能否通过单元测试 | 单元测试通过率 | 100 - 200 |
| 工具调用 | 是否调用正确 API、参数是否完整 | 端到端子任务成功率 | 100 - 150 |
| 安全合规 | 面对违规请求是否按规则拒绝 | 拒绝符合率 | 按风险类型各 50 - 100 |
表中的样本量是经验值,不是科学结论。但有一个原则是通用的:一个维度如果只有十条数据,任何分数波动都可能来自题目本身,而不是模型变化。低于五十条的维度,不建议把分数当作上线门槛,更适合作为人工抽检清单。
1.3 评测流水线由五个环节组成
一个可复用的评测不是写一个脚本调用一次模型。从工程上看,Z AI 评测流水线至少包含五个环节:
- 数据准备:整理测试题目、标准答案、难度标签、分类标签。
- 评测执行:统一调用模型或 API,记录原始输出。
- 结果打分:按题型使用不同打分器,包括规则匹配、代码执行、裁判模型。
- 指标计算:分维度统计通过率、均分、稳定性、置信区间。
- 报告归档:生成给团队看的评分报告,同时把数据集版本、模型版本、代码版本一起归档。
这五步里最容易出错的是第二步和第三步。很多人为了省事,让模型输出结果后直接人眼对比,这样无法批量做回归测试。更推荐的做法是把“原始输出”和“最终分数”分层保存:先保存模型回答的原文,再通过独立打分脚本算出分数。这样如果打分逻辑出了问题,不需要重新调用模型,只需要重新跑打分脚本。
2. 搭好评测环境与数据集,避免“换个机器结果变了”
2.1 环境版本要固定,尤其是 Python、框架和模型服务端
评测代码对运行资源的要求取决于模式。如果 Z AI 已经通过 API 方式提供服务,评测机不需要大显存;如果需要在本地加载模型权重做离线评测,则要单独准备 GPU 环境。两种环境的依赖清单应该分开维护。
下面是一份适合 API 评测的 Python 依赖示例,实际版本号需要结合你的安装环境确认:
# requirements-eval.txt requests>=2.31,<3 PyYAML>=6.0,<7 numpy>=1.26,<2 pandas>=2.0,<3如果后续要接开源评测框架,例如 lm-evaluation-harness 或 OpenCompass,建议使用项目自己的虚拟环境,不要装在系统 Python 里。因为框架升级频繁,不同版本的 prompt 模板、指标实现方式都可能变化,一旦混装,评测结果很难追根因。
2.2 评测模式按部署阶段区分
| 评测模式 | 适用阶段 | 优点 | 主要成本 |
|---|---|---|---|
| 纯 API 评测 | 模型已部署到测试环境 | 无需本地 GPU,配置简单 | 依赖网络和 API 稳定性 |
| 本地离线评测 | 模型权重刚产出 | 不依赖外部服务,可控性强 | 需要 GPU,显存管理复杂 |
| Docker 内评测 | 多人共享、CI 集成 | 环境隔离,可复现 | 需要维护镜像和资源调度 |
Z AI 第一版可以先用 API 模式跑通流程。理由很简单:评测代码的核心是“数据循环 + 请求封装 + 打分”,而不是模型加载。先把链路打通,后续再根据需求切换到离线模式,改动成本不大。
2.3 数据集目录结构要提前确定
数据集不应该是散落在个人电脑里的一堆 Excel 文件。建议使用下面的目录结构:
z-ai-bench/ ├── configs/ │ └── zai_eval.yaml ├── data/ │ ├── eval_sets/ │ │ ├── general_zh.jsonl │ │ ├── code_gen.jsonl │ │ └── tool_call.jsonl │ ├── snapshots/ │ └── manifest.json ├── results/ │ ├── raw/ │ └── reports/ ├── src/ │ ├── client.py │ ├── runner.py │ ├── scorers.py │ └── report.py └── README.md其中 data/eval_sets 存放当前使用的评测集,results/raw 存放模型原始输出,snapshots 存放按时间归档的旧版本数据集。每条评测记录建议采用 JSON Lines 格式,一行一个对象,便于追加和增量处理。
{"id": "general_zh_0001", "category": "general_zh", "difficulty": "easy", "prompt": "请用三句话解释数据库索引的作用,不超过80字。", "reference": "", "check_type": "judge"}字段划分需要满足三个要求:
- id 全局唯一,方便结果对齐。
- category 和 difficulty 用于分维度统计。
- check_type 决定使用哪种打分器,可选值包括 choice、contains、judge、executable。
这里的 prompt 是直接发给模型的用户消息。如果评测目标是多轮对话或 Agent 场景,请在数据结构中增加 messages 字段,而不是把多轮内容全部拼进一个字符串里。
3. 用一套最小代码跑通“调用—记录—打分—出报告”
3.1 先写一个统一的模型调用层
假设 Z AI 提供了一个兼容 OpenAI Chat Completions 风格的 HTTP 接口。这样的话,评测脚本只需要关心 base_url、模型名和 API Key,不需要关心服务端实现细节。
# src/client.py import requests class ZAIClient: """Z AI 评测用的统一模型调用客户端。""" def __init__(self, base_url: str, model: str, api_key: str = "EMPTY"): self.base_url = base_url.rstrip("/") self.model = model self.api_key = api_key def chat(self, messages, temperature: float = 0.0, max_tokens: int = 1024): url = f"{self.base_url}/chat/completions" headers = {"Authorization": f"Bearer {self.api_key}"} payload = { "model": self.model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": False, } resp = requests.post(url, headers=headers, json=payload, timeout=60) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]这里不建议把多个模型的调用逻辑堆在一个函数里。当 Z AI 需要对比候选版本 A 和基线版本 B 时,调用层应该接收 model 作为参数,让同一个评测脚本可以跑两个模型,而不是复制两份评测代码。
3.2 用配置管理模型名、采样参数和并发数
采样参数对评测结果影响非常大。为了可比性,Z AI 在做精度类任务时通常把 temperature 设为 0.0;但温度设置在不同模型服务端的实现并不完全一致,所以必须把参数写进配置而不是藏在代码里。
# configs/zai_eval.yaml model: name: z-ai-latest base_url: "https://z-ai.example.internal/v1" api_key_env: "Z_AI_API_KEY" generation: temperature: 0.0 max_tokens: 1024 timeout_seconds: 60 retry_times: 3 run: concurrency: 1 judge_model: z-ai-judge关键参数的含义可以这样理解:
- temperature:越低越稳定。做选择题、代码生成、事实问答时建议设为 0;做创意类任务时再单独评估。
- max_tokens:设置过小会让长回答被截断,导致代码或长文任务误判。
- retry_times:网络抖动时自动重试,但重试次数不宜过多,避免把服务端故障误判成模型能力问题。
- concurrency:第一版建议从 1 开始,后续根据 API 限流情况调大。并发过高会触发服务端的限流,反而引入大量失败请求。
3.3 评测执行脚本要逐条保存原始输出
一个常见错误是等所有题目跑完再一次写入结果文件。真实评测中经常出现第十题开始服务端超时、进程被中断,结果前面九个答案全部丢失。更稳妥的做法是每完成一条就追加写入一行。
# src/runner.py import json from pathlib import Path import yaml from src.client import ZAIClient def load_lines(path: Path): with open(path, "r", encoding="utf-8") as f: return [json.loads(line) for line in f if line.strip()] def write_result(path: Path, record: dict): path.parent.mkdir(parents=True, exist_ok=True) with open(path, "a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") def run_dataset(config, dataset_path: Path, output_path: Path): client = ZAIClient( base_url=config["model"]["base_url"], model=config["model"]["name"], ) records = load_lines(dataset_path) parameters = config["generation"] for record in records: messages = [{"role": "user", "content": record["prompt"]}] try: answer = client.chat( messages=messages, temperature=parameters.get("temperature", 0.0), max_tokens=parameters.get("max_tokens", 1024), ) item = { "id": record["id"], "category": record.get("category", "uncategorized"), "difficulty": record.get("difficulty", "unknown"), "prompt": record["prompt"], "reference": record.get("reference", ""), "check_type": record.get("check_type", "contains"), "raw_output": answer, } except Exception as exc: item = { "id": record["id"], "category": record.get("category", "uncategorized"), "difficulty": record.get("difficulty", "unknown"), "prompt": record["prompt"], "reference": record.get("reference", ""), "check_type": record.get("check_type", "contains"), "raw_output": "", "error": str(exc), } write_result(output_path, item)这个脚本的设计重点是“失败也要写成一条结果”。如果某条请求失败,最好保留 error 字段,这样后续统计时可以单独统计请求失败率。如果把失败请求静默丢弃,模型故障会被误读为分数低。
3.4 打分器按 check_type 分开实现
规则型打分适合选择题和包含关系判断。下面是一个选择题结果的规范化示例:
# src/scorers.py import re def normalize_choice_text(text: str): """把各种形式的答案文本规范化为单个 A/B/C/D。""" if text is None: return None text = text.strip() match = re.match(r"^[\((]?([A-Da-d])[\))]?[\.、.::]?", text) if match: return match.group(1).upper() candidates = [ch.upper() for ch in text if ch.upper() in "ABCD"] return candidates[0] if candidates else None def score_record(record: dict): check_type = record.get("check_type", "contains") raw_output = record.get("raw_output", "") reference = record.get("reference", "").strip() if not raw_output: return {"score": 0.0, "error": record.get("error", "empty_output")} if check_type == "choice": prediction = normalize_choice_text(raw_output) return {"score": 1.0 if prediction == reference else 0.0, "prediction": prediction} if check_type == "contains": return {"score": 1.0 if reference in raw_output else 0.0} return {"score": 0.0, "error": "unsupported_check_type"}包含关系判断在中文评测中容易误判。比如 reference 是“拒绝”,模型回答是“不应拒绝”,字符串匹配会给出错误结果。使用 contains 打分只适用于答案关键词非常固定的场景,生产环境建议先人工校验三十条。
3.5 开放性问题使用裁判模型,但必须给评分标准
对于没有唯一答案的题目,Z AI 评测可以使用一个独立的裁判模型做打分。裁判模型的评分标准应该写在提示词里,不能只让模型“判断好坏”。
JUDGE_PROMPT = """你是评测裁判。请根据下面的任务描述和评分标准打分。 任务描述: {instruction} 待评分的模型输出: {model_output} 评分标准: 1. 内容是否符合任务要求。 2. 是否包含明显的事实错误。 3. 格式是否满足题目要求。 4. 是否用中文简洁作答。 请先输出一句话理由,然后输出 JSON,格式如下: {{"reason": "理由", "score": 整数1到5}} """裁判模型有两个风险:一是会对长答案给更高分,产生长度偏好;二是对某些观点存在偏见。使用裁判模型前,建议准备一个二十条左右的小样本集合,同时做人工打分和裁判模型打分,计算一致率,一致率低就说明裁判提示词需要调整。
3.6 生成汇总报告
所有题目跑完后,按照 category 和 check_type 统计通过率。最简单的报告可以是 Markdown 表格,后续可以升级成 JSON 或 HTML。
# src/report.py import json from collections import defaultdict from src.scorers import score_record def build_report(raw_path): scores_by_category = defaultdict(list) with open(raw_path, "r", encoding="utf-8") as f: for line in f: if not line.strip(): continue record = json.loads(line) result = score_record(record) category = record.get("category", "unknown") scores_by_category[category].append(result["score"]) rows = [] for category, scores in sorted(scores_by_category.items()): total = len(scores) passed = sum(scores) rows.append({ "category": category, "total": total, "pass_rate": round(passed / total, 4) if total else 0.0, }) return rows运行命令也可以固定下来:
export Z_AI_API_KEY="your-api-key" python -m src.runner \ --config configs/zai_eval.yaml \ --dataset data/eval_sets/general_zh.jsonl \ --output results/raw/general_zh_20250401.jsonl建议 output 文件名带日期或带模型版本。这样后续对比回归时,只需要看不同日期的文件,不需要重新运行模型。
4. 读数时最该关注的不是总分,而是分数稳定性与污染点
4.1 指标不是越多越好,而是能支撑决策
Z AI 评测报告里可以出现多类指标,但每个指标都要能回答一个问题:
| 指标 | 计算方式 | 回答的问题 | 使用注意 |
|---|---|---|---|
| 通过率 | 正确题目数 / 总题数 | 这一维度是否达标 | 数据量少时波动大 |
| 平均分 | 裁判分数求和 / 题数 | 开放问题质量如何 | 受裁判模型偏好影响 |
| 请求失败率 | 失败条数 / 总条数 | 推理服务是否稳定 | 不能与模型能力混淆 |
| 分难度通过率 | 按难度分组统计 | 是难度问题还是能力问题 | 需要每个难度都有足够样本 |
| 方差或置信区间 | 对多条结果做重采样 | 分数是否可信 | 样本少时范围会很大 |
最典型的问题是“总分差不多,怎么判断能不能上线”。假设候选版本总分提高了 0.5%,但安全合规维度下降了 8%,这时总分没有任何参考意义。上线决策应该看“最差维度”而不是“平均维度”。
4.2 用自助法估算分数波动范围
评测集是抽样出来的,不是全量线上请求,所以分数天然有误差。直接用“通过率 72%”来对比“通过率 70%”并不严谨。一个简单做法是用 bootstrap 重采样得到置信区间。
import numpy as np def bootstrap_confidence_interval(values, n_bootstrap=2000, seed=42): rng = np.random.default_rng(seed) arr = np.asarray(values, dtype=float) means = [] for _ in range(n_bootstrap): sample = arr[rng.integers(0, len(arr), size=len(arr))] means.append(sample.mean()) means.sort() return float(np.percentile(means, 2.5)), float(np.percentile(means, 97.5))如果候选版本在 300 道题目上的通过率是 72%,重采样后的区间可能是 67% 到 77%。只要区间与基线版本重叠,就不能宣称有显著提升。这个步骤虽然简单,却能在团队里避免大量无意义争论。
4.3 基准污染是最隐蔽的失效方式
这里需要特别提醒一点:
评测集一旦出现在训练语料或系统提示词中,分数就会失真。Z AI 的私有评测集不应该进入任何训练任务,并且每次发布前都要比对题目与训练语料的相似度。
具体做法包括:
- 设置私有保留集,只有评测管理员能访问,并且不进入预训练、微调或偏好优化数据。
- 公开数据集改用动态模板,换说法、换数字、换名称,防止模型靠记忆答题。
- 定期轮换评测集,旧题可以保留作为回归集,但不能长期只依赖同一套题。
- 记录数据集哈希。运行评测前在 manifest.json 中写入 sha256,后续回溯时能确认结果对应的题目版本。
污染检测不一定是复杂算法。最简单的方式是抽取题目中的长 n-gram 片段,在训练语料里做子串匹配,命中数量高就说明风险偏高。这个检查应该在每次使用公开数据训练前执行。
5. 从现象倒推根因:Z AI 评测常见的几类问题
5.1 从评测执行到根因分析的排查表
评测结果异常时,不要先怀疑模型,建议按下面顺序排查:配置是否生效、数据集版本是否正确、请求参数是否符合预期、服务端日志是否正常、打分逻辑是否正确、最后再评估模型本身。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 不同日期同一模型分数变化大 | 数据集被改过或追加了新题 | 比较 manifest.json 哈希 | 将数据集打快照,禁止原地修改 |
| 选择题得分异常低 | 打分器没有处理“答案是B”开头 | 抽样打印 prediction 字段 | 增加 normalize 逻辑,人工校验三十条 |
| 请求大量超时 | 并发过高或单请求响应过长 | 查看服务端限流日志和响应耗时 | 降低 concurrency,增加重试 |
| 长代码题得分低 | max_tokens 太小导致输出被截断 | 检查 raw_output 是否以截断符结尾 | 调大 max_tokens 或设置 truncate 标记 |
| 裁判分数与人工不一致 | 裁判 prompt 没有给明确标准 | 统计一致率 | 重写评分标准,固定裁判版本 |
| 本地离线评测显存不足 | batch size 设置过大 | 查看 GPU 显存日志 | 降低 batch size,使用梯度无关推理 |
| 候选版本分数提升但线上体验变差 | 评测集与线上分布不一致 | 检查评测集来源和难度分布 | 采集真实脱敏请求补充评测集 |
5.2 一个很典型的偏差:把失败请求算成模型答错
评测脚本在请求失败时如果返回空字符串,打分器通常会给 0 分。这样服务端故障会被误判为模型能力下降。对比两个版本的分数时,如果其中一个版本恰好遇到网络抖动,失败率升高,最后分数就会被系统性拉低。
解决办法是把请求失败率和能力通过率分开统计。在报告中明确写出本轮失败了多少条,并且只有失败率低于 5% 时,通过率才适合用于版本对比。如果失败率过高,先修复评测环境,再重新评测。
5.3 另一个隐蔽问题:评测集顺序影响结果
如果评测脚本没有打乱顺序,并且并发请求排队执行,服务端缓存、上下文长度分布不均匀都会影响结果。虽然模型推理通常和顺序无关,但很多外部 API 服务会做缓存,同样的请求第二次调用可能走缓存,导致结果偏差。
更规范的做法是每条请求记录发送时间、模型输出长度、响应耗时。这些字段会在后续排查中提供很大帮助。评测不是为了证明模型能答对,而是为了在模型答错时能定位到是数据问题、参数问题、服务问题还是模型问题。
6. 把单个 Benchmark 变成可持续的评测体系
6.1 数据集和代码要进入版本管理
很多团队会把模型权重放在版本管理里,却把评测集散落在共享盘。正确做法是给评测数据建立与代码同等的版本管理流程。每条数据要能回答三个问题:
- 这道题是谁在什么时候加的?
- 这道题对应的标准答案是什么?
- 这道题有没有进入过训练数据?
manifest.json 可以记录这些信息:
{ "dataset": "general_zh_20250401.jsonl", "sha256": "8f2a1c9e4d6f0a3b7c95e2d4a6b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b", "schema_version": 2, "created_by": "eval-owner", "created_at": "2025-04-01T10:00:00+08:00", "note": "Z AI 候选版本评测集,禁止进入训练语料。" }生成哈希只需要一行命令:
sha256sum data/eval_sets/general_zh_20250401.jsonl每次执行评测时,把 manifest 拷贝到 results 对应目录下,结果报告才能追溯。
6.2 建立回归门禁,而不是只做一次性评测
Z AI 评测体系建议区分三层:
- 快速冒烟层:每次模型变更后跑 20 到 50 条,验证接口可用、输出格式基本正确。
- 标准回归层:每次准备发版前跑全量评测集,覆盖主要能力维度。
- 深度评估层:模拟真实用户和 Agent 场景,采样后人工抽检。
快速冒烟层最好接入持续集成。模型后端有新的部署构建时,自动调用二三十条核心题目,一旦通过率低于阈值,就直接拒绝进入下一步。这个机制可以把大问题挡在评测早期,避免每次都发版后才发现严重回归。
6.3 上线决策看“最差维度”,不看“平均分数”
Z AI 每次候选版本发布前可以设定规则:
- 事实问答通过率不得低于线上版本 1 个百分点。
- 安全合规维度的拒绝符合率不允许下降。
- 请求失败率高于阈值时不允许上线,必须回滚或修复。
- 任何维度下降超过阈值,都需要提交原因说明,不能因为总分类似就发布。
这条规则的价值是防止优化分数时牺牲特定能力。很多模型在刷分过程中会把安全规则调松,短期内能力分数提升,长期风险极高。评测体系的职责就是在高风险维度设置不可逾越的底线。
6.4 从单轮问答延伸到 Agent 和工程链路
Z AI 并不只做单轮回答,它还会被集成到不同业务系统里,例如通过 Spring AI 接入企业应用,或在 Agent 场景中生成工具调用参数。这个时候评测难度会明显上升,因为不能再只看最终文本。可扩展的方向包括:
- 多轮对话评测:评估上下文理解、长对话记忆、话题切换恢复能力。
- 工具调用评测:给模型一个任务列表和可用工具,检查参数是否完整、调用顺序是否正确。
- 端到端任务评测:模拟用户从提问到完成任务的全流程,统计任务完成率而不是单轮回答质量。
- 故障注入测试:故意构造 API 返回异常、工具超时,看模型能否正确处理。
这些扩展比单轮问答评测更贴近生产环境,也是“benchmaxxing”真正进入应用价值的部分。
6.5 把“刷分冲动”关进制度笼子里
最后回到 Benchmaxxing 这个词。做评测的目标不是让分数无限上涨,而是让分数稳定地反映真实能力。长期维护评测体系时,团队最容易犯的错误是在测试集上反复修正提示词、修正打分规则,直到分数好看为止。这样训练出来的不是更强模型,而是更会应付测试题目的模型。
一个可落地的约束是:任何评测集更新,都必须在更新前先定义为独立任务,并由不参与模型调优的人审核。提示词模板的修改要记录原因,打分器改动要重新跑旧结果做校验,数据集轮换要保留完整快照。
真正能长期使用的 Z AI 评测体系,往往不是那个能刷出最高分的体系,而是那个能让团队在每次迭代时都信任评测结果、知道分数为什么变、也知道分数什么时候不可信的体系。