复核一次模型升级:完整计划匹配从 15/24 升到 19/24,EvalArc 门禁为什么仍然拒绝

用固定版本的 evalarc 和校验过的发布记录重算一次模型升级判定:总匹配数提高了,却有 10 个检查阻断升级,根源是两个用例的格式或结构问题。另附两个退出码的读法。

四步流程图:固定 evalarc 0.15.0 和发布包,先核对 SHA-256 再解压;evalarc diff 比较已保存的结果,退出码 1,门禁拒绝;在原始回答中看到 already-closed 漏掉 actions 键;可选的 regrade 重跑检查,退出码 0,但 gate_passed 仍为 false。

一次模型配置升级把完整计划的匹配数从 15/24 提高到 19/24,升级门禁却拒绝了它:5 个检查回归,5 个可靠性下降。EvalArc 仓库的《Your first model-upgrade review》把这个判定整理成一条可复现的评审路径:装固定版本的工具,下载固定版本的记录,自己重算结论,全程不需要克隆仓库、GPU 或模型 API。本文按这份文档(固定在 commit 6af26bf)讲怎么做这次评审,退出码和报告字段各说明什么,以及结论能推到哪里为止。

本站的 EvalArc:分数变高了,智能体哪里退步了 已经介绍过这个工具和这组对照的结论。本文不再做工具介绍,只讲评审的操作步骤和读结果的方法。下文数字都来自项目自己的文档和它保存的记录([O],作者自报的开发示例),不是第三方的独立评测。

这次升级比较了什么

基线是 Qwen/Qwen3-8B(BF16),当前配置是 Qwen/Qwen3.8-27B-FP8。8 个公开的客服规划用例,每个用例、每种配置各新生成 3 次,48 个回答全部保留。计划只比对,不执行。每个回答跑 5 个 pytest 检查:JSON 结构,路由调用,备注内容与重试键,关闭条件,完整的有序计划。每一侧因此有 5 × 24 = 120 个检查结果。

按用例和检查配对,一共 40 项:10 项改善,20 项不变,5 项回归,5 项可靠性下降。后面这 10 项阻断了升级。它们只对应两个用例的格式或结构问题,不是 10 个独立原因:

  • already-closed:5 个检查全部从 3/3 降到 0/3。以 seed 17 为例,基线返回 {"actions":[]},当前模型返回 {},漏掉了必需的 actions 键。
  • resolved-bug:5 个检查从 3/3 降到 1/3,原因是有两个回答把 JSON 包在了 Markdown 代码块里。

检查之间并不独立:JSON 一旦无效,这个回答的所有检查都会失败。所以阻断检查的个数不能直接当作问题的严重程度,要回到原始回答里看具体出了什么错。

不用 GPU 复现这个判定

需要 Python 3.11+ 和一个新目录,文档给出的命令用 Linux/macOS 上的 Bash。先装发布版:

mkdir evalarc-model-first-review && cd evalarc-model-first-review
python3 -m venv .venv
.venv/bin/python -m pip install evalarc==0.15.0

接着下载 v0.15.0 发布附件 evalarc-model-upgrade.zip。文档里的下载脚本先核对 SHA-256,一致才解压,并保留原始文件;哈希不一致就退出,不解压。期望值是 6b1b9212d5b25da281c54f192489298af7a0b0f4524f807ea6e2821bfa6f515c,发布页的 SHA256SUMS 里也有这个值。解压后打开 recorded-review/index.html,可以离线浏览全部 48 个原始回答。

.venv/bin/python - <<'PY'
import hashlib
import io
from pathlib import Path
from urllib.request import urlopen
from zipfile import ZipFile

url = "https://github.com/noteflowai/evalarc/releases/download/v0.15.0/evalarc-model-upgrade.zip"
expected = "6b1b9212d5b25da281c54f192489298af7a0b0f4524f807ea6e2821bfa6f515c"
with urlopen(url, timeout=60) as response:
    data = response.read()
if hashlib.sha256(data).hexdigest() != expected:
    raise SystemExit("Archive checksum differs; do not extract it.")
Path("recorded-review").mkdir()
with Path("review.zip").open("xb") as output:
    output.write(data)
with ZipFile(io.BytesIO(data)) as archive:
    archive.extractall("recorded-review")
PY

然后重算升级判定:

.venv/bin/evalarc diff recorded-review/reports/baseline.xml \
  recorded-review/reports/current.xml --output my-review

结果看 my-review/index.html 或 my-review/diff.json。在记录的浏览器视图里选 already-closed、seed 17,就能看到上面那对回答。这一步只比较保存下来的原生测试结果(JUnit XML),既不重新生成回答,也不重跑检查。

退出码和 gate_passed 分别说明什么

这份文档里有两个容易读错的退出码:

命令做了什么预期退出码含义
evalarc diff比较已保存的 JUnit 结果1比较正常完成,门禁拒绝了升级
regrade.py对每个保存的回答重新执行 5 个检查,写出新的 JUnit,再重算比较0脚本做完了;regraded/comparison.json 里仍是 "gate_passed": false

也就是说,diff 的退出码 1 是一个结论,不是运行出错。regrade.py 的退出码 0 只说明脚本跑完了,不说明升级通过。这份入门文档只给出预期的退出码 1 和 0;门禁规则和输入格式见 examples/results-diff/README.md。重新评分后要读 gate_passed 字段,不能只看进程有没有报错。

可选的重新评分要装记录时用的 pytest 版本,每次换一个新的输出目录:

.venv/bin/python -m pip install pytest==8.4.2
.venv/bin/python recorded-review/regrade.py --output regraded

这一步同样不调用任何模型。评分器只脱敏两类元数据:JUnit 里的主机名,以及 stdout 里的绝对报告目录。脱敏前后的哈希都有记录,模型回答和检查结果一律不改。

把它用到自己的升级评审

文档对“换成自己的变更”只提了几条要求:两次运行的用例 ID、检查名和被评估的契约要对齐;每一次尝试都要保留;把原生结果交给 evalarc diff。输入格式和门禁规则在仓库的 examples/results-diff/README.md。这个示例的实验说明里还有几条做法可以照搬:

  • 先定协议,后出结果。protocol.json 在任何完成的回答产出之前就已提交,期望计划独立于生成的回答单独保存。
  • 两侧条件一致。两个模型用同样的客服规则(policy)、用例文本、采样设置和 seed:seed 17、29、43,temperature 0.6,top-p 0.9,top-k 20,关闭 thinking,输出上限 512 token。录制硬件是 NVIDIA L40S。
  • 把加载时做过的修正写进记录。Transformers 5.17.0 的正则前缀匹配会把检查点里并不存在的 dense mlp.gate 路由排除项,误当成 gate_proj 的排除项,于是丢掉它的 scale 张量。FP8 记录器在内存里去掉这些路由条目,并要求检查点的每个键都有着落。具体调整和加载报告写在 identity 文件里,没有产出回答的初始化失败记在 runtime-failures.json。
  • 失败的回答也留着。10 个阻断检查背后的那些回答全部保存在记录里,没有清洗。

局限 / 注意

  • 这是一个小型开发示例,不是模型排行榜。两个配置的架构、参数量和量化都不同,差异不能归因于某一个因素,也不能用来推算模型的总体能力。
  • 用例是挑选出来的公开开发用例,可能出现在训练数据里;没有做去污染,也不给出总体准确率。
  • 完整计划相等只是一项很窄的契约检查,不证明真实的客服任务已经完成;计划也没有执行。
  • 每个用例的 3 次生成是 3 次独立采样,不是对同一个回答评了 3 次,作者也没有把它当作一般意义上的 Pass^k 估计。
  • 默认的复核步骤只能证明“从保存的结果算得出这个判定”,regrade.py 也只是对保存的回答重跑检查,都不是重新生成回答的独立复现。换了硬件、内核或库版本,采样结果可能不同。记录里的耗时只是诊断用的观测值,不能当吞吐量比较。
  • 本文依据的是固定在 commit 6af26bf 的文档和 evalarc==0.15.0,更新的版本在命令或输出上可能不同。

链接