第 10 章:设计公平、可重复的评测实验
一次内部评审会上,团队展示了两个系统的“成功率”:系统 A 为 75%,系统 B 为 67%。追问后才发现,A 原计划运行 6 个 Trial,其中两个因认证错误被删除,75% 是剩余 4 个 Trial 中成功 3 次;B 的 67% 则是 6 个计划 Trial 中成功 4 次。两个百分数都算对了,却不能放在一起比较。
本章解决的不是怎样把 Job 跑得更多,而是怎样在看到结果之前写清楚实验规则。我们将锁定第 9 章的 Dataset digest,对两个 Agent 与两个 Model 做全因子比较,每个 Task、每个组合重复三次。这个规模足以演示完整流程,但不足以给能力差异下稳定结论;三次重复是教学性冒烟设计,不是推荐的生产样本量。
本章仍固定 Harbor Framework v0.18.0,提交为 527d50deb63a5d279e8c20593c18a2cbc7f61f9e。1
读完本章,你应该能够:
- 把“哪个系统更好”改写成可检验的问题、指标与停止规则;
- 区分 Harbor 能记录的配置、Agent 接受的参数和 Provider 实际执行的参数;
- 正确解释 Pass@1、成功率、Reward、成本、延迟与缺失结果;
- 从固定 Dataset digest 生成 Job,核对锁文件,并保存逐 Trial 审计记录;
- 识别失败排除、自动重试和事后挑选指标造成的选择性报告。
10.1 先定义被比较的系统
“比较 Model A 与 Model B”通常不够精确。Trial 中真正行动的是 Agent,Model 是 Agent 使用的推理组件;Prompt、Skill、工具、采样参数、超时和网络也会改变行动。更诚实的实验对象是一个完整配置单元:
系统 = Agent 实现与版本
+ Model/Provider 精确标识
+ Agent kwargs、Prompt、Skill、MCP 与工具权限
+ Task/Dataset 与 Verifier
+ Environment、资源、网络和超时
本章的比较问题是:“在固定的 llmkb/system-diagnosis Dataset 内容上,四个 Agent×Model 组合的宏平均 Pass@1、平均 Reward、Agent 阶段耗时和已报告模型成本有何差异?”这里 Agent 和 Model 是两个交叉因素:两个 Agent 都与两个 Model 配对。NIST 将这种在实验前确定因素、水平和响应变量的详细计划称为实验设计;重复用于观察随机误差,区组与随机化用于处理干扰因素。2
先把主要估计量写成唯一可执行的规则。对 Agent a、Model m,先在每个 Task t 上计算 p[a,m,t]=c/n,再对两个 Task 等权平均得到 macro[a,m]。四个 macro[a,m] 是本章的四个主要描述量,不把它们再跨 Model 合成单一“Agent 主效应”。事前固定两个分层对比:在 Model A 下报告 macro[Agent A,Model A]−macro[Agent B,Model A],在 Model B 下报告对应的 A−B;交互单列为这两个差值之差。平均 Reward、成本和延迟是次要响应量;每 Task 结果和失败类型是诊断量。这样,研究者不能在看到四格结果后只选有利 Model,也不能用次要指标替换主要描述量。
实验单位是某一 Task 上的一次 Trial,不是 Job 汇总行。Harbor v0.18.0 按 n_attempts × Task × AgentConfig 展开 Trial。3 本章有 2 个 Task、4 个 AgentConfig 和 3 次尝试,因此计划数是 2 × 4 × 3 = 24。若最终没有 24 份可分类的 Trial 记录,首先报告缺失,而不是重新定义分母。
10.2 控制变量,而不是追求表面相同
公平比较不等于让两种 Agent 的每个内部数字相同。max_turns=50 和另一 Agent 的 max_iterations=50 未必代表相同工具机会。应固定能共享的外部约束,记录无法统一的内部机制,再限定结论为“这两个完整系统在该协议下的比较”。
| 类别 | 本章固定或记录的内容 | 容易漏掉的边界 |
|---|---|---|
| Harbor | v0.18.0 与精确提交 | 只记 harbor 命令名不够 |
| Dataset | Registry digest、Task 名、Task digest、Metric | latest 和可移动标签不是正式锚点 |
| Agent | 名称、运行时版本、kwargs、Skill/Prompt 摘要 | 同名 Agent 的安装版本可能不同 |
| Model | Provider、完整 Model ID、可用的快照/发布日期 | 营销名称可能继续指向变化的服务 |
| 采样 | 请求的 temperature、top-p、seed 等 | 请求被接受不等于 Provider 执行 |
| 环境 | Task digest、镜像/平台、CPU、内存、网络 | Dockerfile 固定不保证可移动基础镜像不变 |
| 调度 | 并发数、运行顺序、时间区组、重试规则 | 并发会改变限流和延迟 |
| 判定 | 成功条件、Reward 键、缺失政策 | 不能看完结果再改通过阈值 |
Harbor 的通用 JobConfig 没有顶层 temperature、top_p 或随机种子字段。v0.18.0 的 AgentConfig 记录 name、model_name、kwargs、环境、Skill 和 MCP 等配置;--ak key=value 也只是把值放入具体 Agent 的 kwargs。4 Terminus-2 与 OpenHands SDK 在本书版本中都声明了 temperature 参数,前者把显式值交给 LLM backend,后者把值写为容器内 LLM_TEMPERATURE;但这只能证明 Harbor 侧 Agent 尝试传递该参数,不能证明任意 Model/Provider 接受或严格执行它。5
因此实验记录应并列保存三列:requested、agent_forwarded、provider_effective。前两列可以由 config.json、lock.json、Agent 源码或日志核验;第三列必须由所用 Provider 的固定版本官方文档或返回元数据核验。无法确认时写“未知”,不能把 temperature=0 宣称为确定性保证。若某 Provider 拒绝该参数,应为所有被比较系统采用事前声明的共同策略,例如都不显式发送,而不是只为失败的一格换参数。
安装型 Agent 还需要单独固定依赖。OpenHands SDK 适配器接受 version,非空时会为 openhands-sdk 与 openhands-tools 使用相同的精确版本约束;留空则没有这两项约束。6 但即使填写版本,v0.18.0 的安装命令仍会下载移动的 uv 安装器、按 3.12 而非 Python patch 安装解释器,并安装未固定版本的 fastapi。严格实验应使用受控镜像和制品源,保存镜像 digest、安装器摘要、Python 完整版本与完整依赖锁;Harbor 提交和两个 SDK 包的版本锁不能替代这些证据。
时间、共享机器负载、Provider 限流和缓存也是干扰因素。NIST 的随机区组原则是在相对同质的区组内包含各处理水平,再对剩余顺序随机化。7 Harbor v0.18.0 的 Job 展开顺序来自确定的嵌套循环,并不提供“随机运行顺序”字段。由此可以推断,若时段漂移不可忽略,应在 Harbor 之外预先生成平衡的运行区组或轮换 agents 顺序,并保存该计划;不要把默认顺序称为随机化。本章示例把 n_concurrent_trials 设为 1,以减少本机资源竞争,代价是总耗时更长、顺序效应更明显。若延迟是主要指标,应单独制定负载协议,不与高并发吞吐实验混用。
10.3 主要指标与失败政策
10.3.1 Pass@1、成功率与 Reward
对二元成功条件,某 Task 的 Pass@1 点估计就是 c/n:n 是按协议计入的尝试数,c 是 Reward 恰为 1 的次数。再对 Task 的 Pass@1 取等权平均,得到宏平均 Pass@1,避免 Task 因重试或缺失较多而意外获得较小权重。它不是“一次 Trial 是否成功”的别名;一次观察只能给出 0 或 1,不能稳定估计成功概率。
Chen 等人在 HumanEval 中给出的 pass@k 无偏估计量为 1 - C(n-c,k) / C(n,k),前提是每题生成 n ≥ k 个样本,并以至少一个样本通过作为成功。8 当 k=1 时该式化为 c/n。Harbor v0.18.0 使用相同的逐项乘积形式,但自动结果只对单键、数值为 0/1 的 Reward 生效;缺失 Reward 计作失败,遇到多键或非二元 Reward 时不生成该组 pass@k。它选择的 k 从 2 开始,为不超过每 Task 最小尝试数的 2 的幂与 5 的倍数,所以三次尝试只会自动产生 Pass@2,不会产生 Pass@1 或 Pass@3。9
Harbor 的自动 pass@k 只读取 Reward,不检查同一 Trial 的 exception_info。由此可以推断,“异常但仍得到 Reward 1”的 Trial 会被内置 pass@k 当作成功;本章事前政策则把它的 Pass@1 记为失败并保留部分 Reward。两者出现差异时应解释口径,不能把内置字段当作本章主要指标的无条件复算结果。
这一区别决定了报告方式:本章从逐 Trial 记录计算宏平均 Pass@1;Harbor 的 stats.evals[*].pass_at_k 作为补充核对。不要用 1-(1-pass@1)^k 替代有限样本估计,原论文说明这种代入会产生偏差。8
Reward 允许表达部分完成。本章两个 Task 约定单一 reward 且范围为 0 到 1:平均 Reward 保留部分得分,Pass@1 只把 reward == 1 记为成功。缺失 Reward 在主要分析中按 0 计,同时单列缺失原因。Harbor 默认 Mean 也会让缺失 Reward 为聚合贡献 0,但报告仍必须区分有效 0 与 None。10
10.3.2 成本、延迟和不确定性
TrialResult 保存 Trial 起止时间及环境、Agent setup、Agent execution、Verifier 的阶段时间;AgentContext 可选保存输入、缓存、输出 token 和 cost_usd,Job 只对存在的值求和。11 因此至少报告三种时间:Trial 端到端耗时、Agent 阶段耗时、Job 墙钟时间。排队时间、并发和重试会让它们不同。成本也要分开:Harbor 报告的 Agent 模型成本、Provider 账单、沙箱/计算/存储成本;cost_usd=null 是未知,不是 0。
成功率还应带不确定性区间。本章只对每个“系统×Task”的二项比例 c/n 报告双侧 Wilson 95% 区间:令 p̂=c/n、z=1.959963984540054,上下界为 (p̂+z²/(2n) ± z√(p̂(1−p̂)/n+z²/(4n²)))/(1+z²/n)。该方法来自 Wilson 对二项成功概率的区间推断。12 四个系统的跨 Task 宏平均只给点估计、两个 Task 的 Pass@1 向量和原始 c/n,不对宏平均套用 Wilson 区间,也不把两个 Task 合并成一个同质二项样本。区间只描述在“尝试近似独立、生成机制稳定”等假设下的采样不确定性,不覆盖 Dataset 代表性、Provider 漂移或 Task 间异质性。每个系统×Task 只有三次尝试时区间会很宽;这正是本章反复强调“三次只能演示流程”的原因。正式实验应在运行前根据希望检测的最小差异、允许的区间宽度和预算确定样本量。
10.3.3 失败与重试先写政策
本章主要分析采用以下规则:
- 认证、限流、环境构建、Agent 超时、Verifier 异常和 Reward 缺失都占用计划 Trial 的分母;主要 Pass@1 记为 0,同时按失败类型分层。
- 有效的部分 Reward 按原值进入平均 Reward,但只有 1 进入成功计数。
- 不因“看起来是基础设施问题”事后删除 Trial;可以另做一个事前定义的敏感性分析,但必须与主要结果并列。
retry.max_retries=0,避免原始失败被自动替换。若整组因已确认的公共基础设施事故重跑,旧 Job 仍保留,并记录替代关系。
这条重试规则针对 v0.18.0 的具体行为:TrialQueue 对可重试异常复用同一 TrialConfig,重试前删除该 Trial 目录;Job 在新尝试开始时移除前一次结果的统计贡献,最终保留最后一次结果并累计 n_retries。13 因而自动重试适合提高作业完成率,却不足以保存完整实验失败史;最终 cost_usd 也可能不含被移除尝试已经发生的费用。若必须启用重试,应另存 Provider 账单和 Job 日志,并把“首次尝试成功率”作为 Harbor 之外的审计数据。
10.4 写下实验清单,再启动 Job
先在沿用第 9 章的项目根目录创建本章使用的目录:
mkdir -p configs records scripts
然后把下面的 JSON 保存为 configs/system-diagnosis-2x2.json。它能被 v0.18.0 JobConfig 解析。sha256: 后的 64 个 0 是构造的格式占位摘要,provider/model-a 与 provider/model-b 是构造的 Model ID;运行前必须替换为第 9 章发布记录中的真实 Dataset digest 与 Provider 官方精确 ID。两个 OpenHands 配置中的 version 也是必须在运行前替换的相同占位值:应换成已验证存在且符合协议的精确版本,并在审计脚本中同步填写。配置中的两个 Agent 都在 v0.18.0 注册,且各自源码接受这里使用的 temperature。14
{
"job_name": "system-diagnosis-2x2-r3",
"jobs_dir": "jobs",
"n_attempts": 3,
"n_concurrent_trials": 1,
"timeout_multiplier": 1.0,
"retry": {"max_retries": 0},
"agents": [
{
"name": "terminus-2",
"model_name": "provider/model-a",
"override_timeout_sec": 900,
"kwargs": {"temperature": 0.2}
},
{
"name": "terminus-2",
"model_name": "provider/model-b",
"override_timeout_sec": 900,
"kwargs": {"temperature": 0.2}
},
{
"name": "openhands-sdk",
"model_name": "provider/model-a",
"override_timeout_sec": 900,
"kwargs": {
"temperature": 0.2,
"version": "<运行前替换为已验证的-openhands-sdk-版本>"
}
},
{
"name": "openhands-sdk",
"model_name": "provider/model-b",
"override_timeout_sec": 900,
"kwargs": {
"temperature": 0.2,
"version": "<运行前替换为已验证的-openhands-sdk-版本>"
}
}
],
"datasets": [
{
"name": "llmkb/system-diagnosis",
"ref": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"task_names": [
"llmkb/log-incident-report",
"llmkb/python-port-repair"
]
}
]
}
先在项目根目录验证意图:
harbor --version
harbor run --config configs/system-diagnosis-2x2.json --print-config \
> records/system-diagnosis-2x2.print-config.json
--print-config 只证明 JSON 与 Pydantic 配置可构造,不会解析远程 Dataset、核验凭据或实例化 Model。确认真实 digest、四个 AgentConfig、n_attempts=3、n_concurrent_trials=1 与 max_retries=0 后,才去掉 --print-config。Registry 包 Dataset 在解析时会把 ref 更新为服务端返回的内容版本;实际 Job 还会按每个 Task 和 AgentConfig 生成 Trial。15
harbor run --config configs/system-diagnosis-2x2.json \
--env-file ~/.config/harbor/credentials.env
这条命令会产生模型费用。执行前应完成一次不计入正式比较的 Oracle smoke Job,确认 Dataset、Environment 和 Verifier 可运行;还要核对 Provider 配额与预算停止线。正式 Job 创建后保存三层证据:输入 config.json、解析后的 lock.json、每个 Trial 的 result.json。v0.18.0 的 JobLock 记录并发、重试规则和各 Trial 的 Task digest、Agent、Skill、环境与 Verifier 配置,也尝试记录 Harbor 发布版本和可获得的 Git 提交;普通非 VCS 安装的 git_commit_hash 可以为空,所以本书仍要求单独保存精确提交。它比命令历史更接近实际运行输入,但仍不替代 Provider 快照、账单、受控镜像和制品源记录。16
Job 根 result.json 最终写盘时不包含 trial_results;完整 Trial 结果保留在各 Trial 子目录。17 验收时检查:Job 的 n_total_trials 为 24,n_completed_trials 为 24,n_retries 为 0;每个 Agent×Model×Task 恰有 3 个 Trial;同一 Agent 的全部 Trial 都有唯一且符合事前预期的 agent_info.version;每个 Trial 都能归类为有效 Reward 或具体失败。OpenHands 任一 Trial 的版本不等于配置中的精确值,或同一 Agent 在 24 个 Trial 中出现多个版本,都必须把实验标为“不完整实验”。
10.5 用统一模板记录结果
不要让 Viewer 截图成为唯一记录。下面的 YAML 是结果记录模板;其中数值均为空,不是实测结果。
experiment_id: system-diagnosis-2x2-r3
status: incomplete # complete / incomplete / superseded
analysis_plan: "四格主要描述量、两个按Model分层的Agent A-B与交互"
primary_descriptives: four_system_macro_pass_at_1
planned_agent_contrasts:
model_a: "macro[Agent A,Model A] - macro[Agent B,Model A]"
model_b: "macro[Agent A,Model B] - macro[Agent B,Model B]"
interaction: "model_a_contrast - model_b_contrast"
secondary_metrics: [mean_reward, agent_latency_sec, reported_agent_cost_usd]
harbor: {version: "0.18.0", commit: "527d50deb63a5d279e8c20593c18a2cbc7f61f9e"}
dataset:
name: llmkb/system-diagnosis
digest: "sha256:<真实摘要>"
task_digests: {}
protocol:
attempts_per_task_cell: 3
planned_trials: 24
concurrency: 1
retries: 0
missing_reward_policy: "primary_as_failure"
stop_rule: "完成24个计划Trial;预算或安全门槛触发时停止并报告不完整"
interval_policy: "双侧Wilson 95%仅用于系统×Task;宏平均仅报点估计、Task向量和原始c/n"
expected_agent_versions:
terminus-2: "2.0.0"
openhands-sdk: "<与Job配置相同,运行前替换>"
sampling:
requested: {temperature: 0.2}
agent_forwarded: "待日志核验"
provider_effective: "待官方文档或响应元数据核验"
results_by_cell: []
failures_by_type: {}
deviations: []
artifacts: {job_dir: "", provider_bill: "", analysis_script: ""}
results_by_cell 的每一项至少包含 Agent、Agent 版本、完整 provider/name、Task、计划数、完成数、成功数、Pass@1、双侧 Wilson 95% 区间、平均 Reward、Reward 缺失数、各阶段延迟分布、已知成本之和与成本缺失数。四个系统的 Dataset 宏平均应另存点估计、Task 向量和原始计数,不写 Wilson 区间;再按事前公式报告每个 Model 下的 Agent A−B 与交互,不给跨 Model 的单一 Agent 主效应。对 Dataset 汇总时先在每个 Task 内计算,再等权平均,不要让某个 Task 因多出重试而获得更高权重。
10.5.1 从逐 Trial 证据重建分母
不要直接把 Job 汇总中的 evals[*].n_trials 当成计划分母。v0.18.0 只有在 verifier_result.rewards 存在时才增加这个字段;异常数另存在 n_errors,一个既有异常又有 Reward 的 Trial 还会同时进入两类统计。11 内置 evals 与 pass@k 的分组键还只使用 Model 的 name,忽略 provider;provider-a/same-model 与 provider-b/same-model 会合并为同组。18 因此实验单元身份必须来自逐 Trial 的完整 provider/name,不能从 evals 键反推。若两个 Provider 的 basename 相同,内置 pass@k 与 n_trials 只可视为合并后的诊断值;主要分析始终从 Job 的 n_total_trials 和全部 Trial result.json 重建。
下面的审计脚本只使用 Python 标准库。它不计算最终排名,而是先验证 24 份证据、Agent 版本和 Reward 契约是否齐全,并按 Agent、完整 Model、Task 输出最小原始计数。保存为 scripts/audit_experiment.py,从项目根目录运行;先把 EXPECTED_AGENT_VERSIONS 中的 OpenHands 占位替换为与 Job 配置相同的精确值。若一个 Trial 有异常,即使 Verifier 仍给出 Reward,脚本也按本章政策把 Pass@1 记为失败,但保留该 Reward 进入部分得分均值;这两个口径有意不同。
import json
import math
import sys
from collections import defaultdict
from pathlib import Path
EXPECTED_AGENT_VERSIONS = {
"terminus-2": "2.0.0",
"openhands-sdk": "<与Job配置相同,运行前替换>",
}
def require(condition, message):
if not condition:
raise ValueError(message)
job_dir = Path(sys.argv[1])
job = json.loads((job_dir / "result.json").read_text())
require(job["n_total_trials"] == 24, ("n_total_trials", job["n_total_trials"]))
require(job["stats"]["n_completed_trials"] == 24,
("n_completed_trials", job["stats"]["n_completed_trials"]))
require(job["stats"]["n_retries"] == 0,
("n_retries", job["stats"]["n_retries"]))
paths = sorted(job_dir.glob("*/result.json"))
require(len(paths) == 24, f"expected 24 trial results, got {len(paths)}")
cells = defaultdict(lambda: {
"n": 0, "success": 0, "reward_sum": 0.0,
"exceptions": 0, "missing_rewards": 0,
})
versions = defaultdict(set)
for path in paths:
trial = json.loads(path.read_text())
agent_name = trial["agent_info"]["name"]
versions[agent_name].add(trial["agent_info"]["version"])
model_info = trial["agent_info"].get("model_info") or {}
model = model_info.get("name")
if model_info.get("provider"):
model = f'{model_info["provider"]}/{model}'
key = (agent_name, model, trial["task_name"])
row = cells[key]
row["n"] += 1
exception = trial.get("exception_info")
rewards = (trial.get("verifier_result") or {}).get("rewards")
if exception is not None:
row["exceptions"] += 1
if rewards is None:
row["missing_rewards"] += 1
value = 0.0
else:
require(set(rewards) == {"reward"}, (path, rewards))
value = rewards["reward"]
require(isinstance(value, (int, float)) and not isinstance(value, bool),
(path, "reward must be int/float but not bool", value))
require(math.isfinite(value), (path, "reward must be finite", value))
require(0.0 <= value <= 1.0, (path, "reward outside [0, 1]", value))
row["reward_sum"] += value
row["success"] += int(exception is None and value == 1.0)
require(set(versions) == set(EXPECTED_AGENT_VERSIONS),
("unexpected agent names", versions))
for agent_name, expected in EXPECTED_AGENT_VERSIONS.items():
require(versions[agent_name] == {expected},
("agent version mismatch", agent_name, versions[agent_name], expected))
require(len(cells) == 8, f"expected 4 systems x 2 tasks, got {len(cells)}")
require(all(row["n"] == 3 for row in cells.values()), cells)
print(json.dumps({"|".join(map(str, key)): row for key, row in cells.items()},
ensure_ascii=False, indent=2))
python3 scripts/audit_experiment.py jobs/system-diagnosis-2x2-r3 \
> records/system-diagnosis-2x2.raw-counts.json
脚本通过只说明结构、版本与计数契约成立,不说明四个系统的差异有统计或业务意义。它在读取 JSON 原始值时拒绝 bool、字符串、NaN、Infinity 和越界 Reward;但 v0.18.0 的 VerifierResult 会通过 Pydantic 把 bool 与数值字符串归一化为数值,写入 Trial result.json 后已无法恢复原始类型。19 因而最早、最严格的门禁仍必须放在自定义 Verifier 或第 9 章 Metric 聚合入口;本脚本只能发现尚未被模型归一化、或被手工写入原始 JSON 的非法值。
下一步应由固定版本的分析脚本计算每个系统×Task 的 success/n、双侧 Wilson 95% 区间和 reward_sum/n,再为四个系统输出宏平均点估计、Task 向量、原始计数、两个按 Model 分层的 Agent A−B 以及交互。延迟至少输出中位数和完整分位点所需的逐 Trial 数据,不宜只留平均数;成本同时输出“有成本记录的 Trial 数”,避免把部分可见的费用当作完整总账。
10.5.2 四层验收
一份可发布实验至少通过四层检查。第一层是输入身份:配置里的 Dataset ref 与 Job 解析后的 ref 一致,每个 Trial 的 Task digest 与发布记录一致,受控镜像和制品源有不可变身份。第二层是设计完整性:八个 Agent×Model×Task 单元各有三次尝试,没有额外 Trial、静默过滤或自动重试;同一 Agent 的所有 Trial 版本唯一且符合事前值。第三层是测量完整性:每份 Reward、异常、阶段时间与成本缺失状态都可定位,不能只看到聚合均值。第四层是解释完整性:报告同时给出主要、次要和敏感性分析,任何事后变化进入偏离记录。
如果第一层失败,应停止运行并修复引用;如果第二层失败,保留 Job 但标记 incomplete;如果第三层失败,先查基础设施和 Agent 日志;如果只有第四层失败,不应重跑来“修正数字”,而应修正报告。把这四层分开,可以避免为了补一份文档而改变数据,也可以避免把 Dataset 解析错误误诊成模型退化。
下面是一个教学性构造反例,不是模型实测:系统 A 计划 6 次,得到 3 次成功、1 次有效失败和 2 次认证异常。只保留有 Reward 的四次会报告 3/4=75%;按事前主要政策则是 3/6=50%,并单列认证异常率 2/6。若再从每个 Task 只挑最好一次,还能制造 100%。三个数字描述的是三种不同的选择规则,不能把最漂亮的一个写成“成功率”。
在数据出现前冻结假设、主要指标、排除规则、停止规则和分析代码,可以把确认性分析与事后探索区分开。预注册方法的核心价值也在于把数据收集前的计划与数据出现后的分析区分开,而不是禁止探索。20 在工程团队里不必建立公开注册平台;带时间戳、只追加变更记录的 experiment-plan.yaml 已能显著改善审计。任何偏离都可以发生,但必须在 deviations 中说明时间、原因和影响。
10.6 停止规则与结论边界
本章的固定样本停止规则是:“完成 24 个计划 Trial;若触发事前预算、安全或 Provider 配额门槛,则停止整个 Job,保留已产生记录,并把实验标为 incomplete。”不要在某一组合领先时提前停止,也不要因为差异不明显便追加三次,直到出现想要的排序。若需要序贯分析,应在实验前选择相应方法和误差控制;本章不展开该统计主题。
四格比较还要保留交互关系。若 Model A 在 Terminus-2 上领先、在 OpenHands SDK 上落后,结论应是“Model 效应依赖 Agent”,而不是先把两个 Agent 的结果平均成一个 Model 排名。反过来,比较 Agent 时也不能只挑各自最有利的 Model。本章固定报告四格矩阵与每 Task 明细、每个 Model 下的 Agent A−B,以及两个 A−B 之差所表示的交互;不提供跨 Model 的单一 Agent 主效应。某个 Agent 无法使用某个 Model 时,该格应写“不兼容/未运行”,整项实验也不再是完整的 2×2 设计;不能临时给它换一个不同 Model 后仍声称全因子公平比较。
质量、成本和速度也不是可以随意相加的单一分数。先分别报告宏平均 Pass@1、平均 Reward、成本与延迟,再把产品约束写成选择规则,例如“Pass@1 不低于门槛时选择成本较低者”。门槛必须在结果出现前确定。若事后才发明权重,使偏好的系统获胜,这只是决策解释,不是预先定义的实验结论。
三次重复还有三个不能修饰掉的限制。第一,成功率区间很宽;第二,只有两个 Task,Dataset 代表性无法由重复次数补足;第三,远程 Model 服务可能随时间变化,即使 Model ID 不变也需要记录时间和 Provider 元数据。因此本章结果适合验证实验管道、暴露大故障和估算后续预算,不适合写“系统 A 普遍优于系统 B”。
最终报告应同时给出四个宏平均点估计及其 Task 向量、系统×Task 的 Wilson 95% 区间、原始计数、失败分类和完整配置。排序可以作为导航,不能代替不确定性。若两个系统点估计不同但区间宽、失败结构也不同,最有用的结论往往是“当前实验不能区分”,以及下一轮需要增加哪些 Task、重复或控制。
10.7 本章验证边界
本章写作时已用 v0.18.0 的 Pydantic 模型解析示例 Job JSON,并复跑 pass@k、Metric、Terminus-2/OpenHands SDK temperature、TrialQueue 重试和 JobResult 相关的 102 项单元测试,全部通过。当前机器没有可用 Docker,也没有为写作提供 Registry 凭据或两个真实 Model 的 API Key,因此没有启动 24 个付费 Trial;本章所有系统名称占位、百分比和失败记录都已标为构造示例,不能作为真实 Agent 比较结果。详细命令与边界记录在本章验证报告中。21
10.8 本章小结
- 比较对象是 Agent、Model、Prompt/Skill、工具与运行约束组成的完整系统,不是孤立的 Model 名称。
- 固定 Dataset digest、Task digest、Harbor 版本和实验政策,再运行
2×2×2×3=24个计划 Trial。 - Pass@1 来自重复的二元结果;一次 Trial 不是稳定能力估计,三次重复也只适合教学性冒烟。
- Harbor 能记录 Agent kwargs,不保证任意 Provider 执行 temperature 或 seed;OpenHands 包版本也不等于完整依赖锁,未知状态必须保留。
- 缺失、超时、认证错误和重试都必须事前定义计入方式;主要结果不能只统计“有分数”的 Trial。
- 成本、延迟和不确定性应与质量并列,Job/Trial 结果、锁文件、Provider 账单和偏离记录共同构成审计证据。
10.9 练习
- 把示例 JSON 中的构造 digest 换成第 9 章真实 Dataset digest,用
--print-config验证四个 AgentConfig;解释为什么这一步仍不能证明 Provider 接受temperature。 - 构造每 Task 三次二元 Reward,手算 Pass@1 和 Pass@2,再与
harbor.utils.pass_at_k比较;说明为什么 Harbor 不显示 Pass@1。 - 为“认证错误计为失败”和“认证错误只进入敏感性分析”分别写结果模板,指出哪一个是主要结果、哪一个不能单独发布。
- 将
n_concurrent_trials从 1 改为 4,列出对质量、Provider 限流、延迟和成本审计的潜在影响,并设计一个不混淆吞吐与延迟的验证方案。 - 设计下一轮样本量与停止规则:写出最小关注差异、希望的区间精度、最大预算和提前停止条件;不得查看本轮系统排名后再定义这些值。