第 13 章:分析结果、诊断失败并审计 Benchmark
一次评测结束后,你在 Viewer 里看到三个 reward=0:第一个 Trial 在环境启动时失败,第二个 Agent 真的提交了错误答案,第三个 Verifier 没有产生合法的奖励文件。它们的最终分数相同,工程含义却完全不同。如果直接把三个零都算成“模型失败”,不仅会低估能力,还会把修复基础设施和修复评分器的问题错误地推给 Agent 团队。
本章把结果分析看成一条可复核的证据链,而不是一张排行榜。我们会从 Viewer 定位异常,再回到每个 Trial 的结构化结果、阶段日志、Artifact、trajectory 和评分明细;随后建立失败归因、波动排查、泄漏与 gaming 审计、能力覆盖和难度报告。所有统计继续遵守第 10 章的边界:以计划中的每个 Trial 为分母,以完整的 provider/name 标识模型,以 Harbor 重试边界解释数据。质量维度和 Judge 则沿用第 11、12 章的契约:主奖励、维度分与评分异常必须分开报告。
13.1 学习目标
完成本章后,你应该能够:
- 用 Viewer 快速导航 Job、Task 与 Trial,同时不把界面聚合值当成审计真相;
- 从
TrialResult、日志、Artifact、trajectory 和reward-details.json建立可追溯证据链; - 明确区分基础设施失败、Agent 失败、Verifier 失败和 Judge 失败,不从最终 Reward 反推根因;
- 识别 flaky Task、数据泄漏与 scorer gaming,并设计能够阻断发布的审计门;
- 同时报告质量、成本、速度与不确定性,并说明缺失数据和未执行边界。
13.2 Viewer 是入口,不是账本
假设 Job 输出位于 jobs/2026-07-16__audit-demo/。可以用下面的命令打开本地 Viewer:
harbor view jobs --jobs
harbor view 默认监听本机地址,并在一组本地端口中寻找可用端口。它会扫描 Job 根目录和直接的 Trial 子目录;因此尚未写出 result.json、但已有配置的 Trial 也可能显示为进行中。Viewer 很适合回答“哪个 Task 异常”“哪个 Trial 值得打开”“Artifact 在哪里”,却不应替代独立审计脚本。1
原因有三点。第一,Job 根目录的磁盘版 result.json 为了控制体积会排除 trial_results;完整结果位于各 Trial 子目录。第二,Viewer 的部分列表以主键 reward 展示平均值:已完成但缺少主奖励的 Trial 可能按零进入任务均值,而进行中的 Trial 又可能暂不产生均值。第三,Harbor 内置 JobStats 的评测分组键只使用 Agent、模型名和数据集,不包含 Provider;同名模型跨 Provider 时可能碰撞。正式报告必须重新读取每个 Trial 的 result.json,保留 agent_name + provider/name + task。2
一个典型 Trial 目录可抽象为:
trial-name/
├── result.json
├── trial.log
├── exception.txt
├── agent/
│ └── trajectory.json
├── artifacts/
│ └── manifest.json
└── verifier/
├── reward.txt 或 reward.json
├── reward-details.json
├── test-stdout.txt
└── test-stderr.txt(仅在实现或任务明确产生时存在)
多步 Trial 的逐步 Artifact 位于 steps/<step>/artifacts/。不要把“文件树中列出了某文件”理解为“每个 Trial 都必然存在该文件”。默认 Verifier 脚本将标准错误通过 2>&1 合并到 test-stdout.txt,因此缺少 test-stderr.txt 本身不是故障。Agent 日志下载也是尽力而为:远端日志下载失败会被记录,但不能单凭本地日志缺失断言 Agent 没有运行。3
13.2.1 五层证据栈
审计时按从结构化到原始材料的顺序取证:
| 层 | 主要问题 | 关键材料 | 不能单独证明什么 |
|---|---|---|---|
| 结果层 | Trial 是否完成、在哪个阶段异常 | TrialResult、ExceptionInfo、阶段 TimingInfo | Reward 不能证明根因 |
| 评分层 | 主奖励和维度分是否合法 | VerifierResult.rewards、reward-details.json | 分数不能证明测试覆盖完整 |
| 日志层 | 执行过什么、错误首先出现在哪里 | trial.log、Agent/Verifier 日志 | 日志文本可能缺失或由不可信进程写入 |
| Artifact 层 | 工作区最终状态和指定证据是什么 | manifest、下载文件、哈希 | 文件存在不等于内容可信 |
| trajectory 层 | Agent 声称的步骤、工具调用和观测是什么 | ATIF trajectory.json | 格式有效不等于记录真实或完整 |
TrialResult 同时允许 exception_info 与 verifier_result 存在;JobStats 也会分别累计已完成数、奖励与异常。因此“有 Reward”不能推出“没有异常”,“有异常”也不保证“没有 Reward”。阶段时间只表示某段流程开始或结束;例如 AGENT_END 在 Agent 阶段的 finally 中写入,它表示阶段退出,不表示答案正确。4
Artifact 也不是可信输入。Harbor 会按配置从服务中下载文件,并把 ok、failed、empty 或 skipped 状态写入 manifest;下载异常通常转成 manifest 的失败条目,而不是自动让整个 Trial 失败。同名目标发生冲突时,先到的文件可保留,后到的条目被跳过。主服务 Artifact 还可能在主服务仍存活时采集。把 Artifact 当作对抗性证据:校验 manifest 状态、来源服务、路径、大小和内容哈希,再解释内容。5
Viewer 的 trajectory 接口只读取 agent/trajectory.json 并执行 JSON 解析。正式审计还要运行 Harbor 的 ATIF 校验器:
python -m harbor.utils.trajectory_validator \
jobs/2026-07-16__audit-demo/trial-name/agent/trajectory.json
ATIF 模型会检查版本、连续 step ID、消息来源、工具调用引用和子 Agent 关系等约束。校验通过只说明结构符合契约;trajectory 仍是 Agent 或适配器产生的材料。反过来,缺失或无效 trajectory 是可观测性失败,不能据此声称 Agent 没有使用工具。6
13.3 不要让一个分类字段承担所有含义
最稳健的结果表不是单一的 status,而是至少三条轴:
- 执行轴:
complete、infrastructure_failure、verifier_failure、unknown; - 目标轴:
pass、fail、unscored,仅由通过契约校验的客观主奖励决定; - Judge 轴:
ok、failure、not_run,由评分明细中的错误和完整性决定。
这样可以表达一个容易被单字段抹掉的状态:客观测试已通过,但用于代码质量的 Judge 超时。该 Trial 的目标轴仍是 pass,Judge 轴则是 failure;质量维度不能把 Judge 的超时零分当成真实质量零分。
四类失败的最低证据要求如下:
| 类别 | 定义 | 最低证据 | 常见误判 |
|---|---|---|---|
| 基础设施失败 | 环境、Provider、网络或宿主资源使 Trial 未获得有效机会 | 环境/Provider 异常类型,加阶段日志或同批次共因证据 | 把所有 AgentTimeoutError 都算基础设施 |
| Agent 失败 | 环境与 Verifier 健康,Agent 产物被有效、确定的客观检查拒绝 | 无未解释异常;主奖励契约有效;测试日志指向候选产物 | 看到 reward=0 就归因 Agent |
| Verifier 失败 | 测试执行、奖励文件生成/解析或评分契约本身失败 | Verifier 异常、缺失/空/非法奖励、测试装载失败或确定性重放不一致 | 把没有 Reward 当零分 |
| Judge 失败 | Judge 请求、解析、超时或校准失败,不能代表候选质量 | 明细中的 error、缺失维度、解析重试耗尽或校准失败 | 把超时生成的 value=0 当质量差 |
异常名称只是线索,不是完整根因。例如 Agent 非零退出可能来自 Agent 自身,也可能来自 Provider 故障或错误的启动配置;超时可能是策略低效,也可能是宿主机拥塞。基础设施类最好有同时间窗口的多 Task 共因、Provider 错误码或环境健康检查证据。无法满足证据要求时标记 unknown,不要强迫分类。
13.3.1 一组相同零分的反例
考虑下面四个 Trial。它们在只看最终 Reward 时都可以显示为零或没有有效奖励:
A: EnvironmentStartTimeoutError;Verifier 未启动
B: 无异常;reward=0;测试显示候选程序输出错误
C: VerifierOutputParseError;reward.json 不是合法契约
D: 客观 reward=1;quality Judge 超时并写入 value=0、error="judge timed out..."
正确结论分别是:A 为基础设施失败且目标未评分;B 才是 Agent 失败;C 为 Verifier 失败且目标未评分;D 的客观目标通过,但 Judge 失败,质量维度未知。Reward Kit 的 Judge 超时路径确实可能同时产出数值零与 error,所以审计器必须读取错误字段,而非只取 value。7
一条实用的取证顺序是:
- 对照计划清点 Trial,确认结果数、完整 Provider、Task 校验和与重试数;
- 读取
exception_info和四段阶段时间,定位首先失败的阶段; - 检查主奖励及全部维度是否满足数值、有限、范围和必需键契约;
- 递归检查
reward-details.json中的 Judgeerror、warning 与缺失维度; - 再用 Verifier 日志、Artifact 与 trajectory 交叉验证;
- 只有前述执行链健康且客观检查有效时,才把失败归到 Agent。
13.4 一个可复现的结果分析例子
下面的脚本只使用 Python 标准库。它读取一个 Job 根目录的直接 Trial 子目录,保留 provider/name,把所有计划 Trial 留在主分母中,并把主奖励、执行失败和 Judge 状态分开。它不是通用根因推理器:异常映射是本次评测预注册的策略,未知异常必须人工复核。
#!/usr/bin/env python3
import json, math, statistics, sys
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path
INFRA = {
"EnvironmentStartTimeoutError", "SandboxBuildFailedError",
"HealthcheckError", "ApiRateLimitError", "ApiOverloadedError",
"ApiConnectionClosedError", "NetworkConnectionError",
}
VERIFIER = {
"VerifierTimeoutError", "RewardFileNotFoundError",
"RewardFileEmptyError", "VerifierOutputParseError",
"AddTestsDirError", "DownloadVerifierDirError",
}
def require(ok, message):
if not ok:
raise ValueError(message)
def strict_loads(text, where):
def no_duplicates(pairs):
result = {}
for key, value in pairs:
require(key not in result, f"{where}: 重复 JSON 键 {key!r}")
result[key] = value
return result
def no_constant(token):
raise ValueError(f"{where}: 非法非有限常量 {token}")
return json.loads(text, object_pairs_hook=no_duplicates,
parse_constant=no_constant)
def load(path):
return strict_loads(path.read_text(encoding="utf-8"), str(path))
def finite_score(value, where):
require(type(value) in (int, float), f"{where}: 非数值奖励")
require(math.isfinite(value) and 0 <= value <= 1,
f"{where}: 奖励必须有限且在 [0,1]")
return float(value)
def nested_errors(value, path="$"):
found = []
if isinstance(value, dict):
for key, child in value.items():
here = f"{path}.{key}"
if key == "error" and isinstance(child, str) and child.strip():
found.append((here, child))
found.extend(nested_errors(child, here))
elif isinstance(value, list):
for index, child in enumerate(value):
found.extend(nested_errors(child, f"{path}[{index}]"))
return found
def seconds(timing, where):
if not timing or not timing.get("started_at") or not timing.get("finished_at"):
return None
value = (datetime.fromisoformat(timing["finished_at"]) -
datetime.fromisoformat(timing["started_at"])).total_seconds()
require(value >= 0, f"{where}: 结束时间早于开始时间")
return value
def trial_cost(result, where):
contexts = ([result["agent_result"]] if result.get("agent_result") else
[step.get("agent_result") for step in result.get("step_results") or []
if step.get("agent_result")])
values = []
for context in contexts:
value = context.get("cost_usd")
if value is not None:
require(type(value) in (int, float), f"{where}: cost_usd 非数值")
require(math.isfinite(value) and value >= 0,
f"{where}: cost_usd 必须有限且非负")
values.append(float(value))
return {"known_total_usd": sum(values) if values else None,
"observed_contexts": len(values), "expected_contexts": len(contexts),
"complete": bool(contexts) and len(values) == len(contexts)}
def objective_status(primary, rule):
if rule["mode"] == "binary":
require(primary in (0.0, 1.0), "二元主奖励只能是 0 或 1")
return "pass" if primary == 1.0 else "fail"
require(rule["mode"] == "threshold", "未知的主奖励判定模式")
threshold = finite_score(rule["pass_threshold"], "pass_threshold")
return "pass" if primary >= threshold else "fail"
root = Path(sys.argv[1])
plan = load(root / "audit-plan.json")
job = load(root / "result.json")
require(job.get("n_total_trials") == plan["expected_trials"],
"Job 计划数与 audit-plan 不符")
require(job.get("finished_at") is not None, "Job 尚未结束")
require(job.get("stats", {}).get("n_retries", 0) == plan["expected_retries"],
"重试数与预注册计划不符;旧 Trial 目录可能已被替换")
paths = sorted(p for p in root.iterdir() if (p / "result.json").is_file())
require(len(paths) == plan["expected_trials"], "Trial 结果数与计划不符")
contracts = plan["trials"]
require(len(contracts) == plan["expected_trials"], "Trial 身份清单长度不符")
allowed_scored_exceptions = plan.get("allowed_scored_exceptions", {})
require(set(allowed_scored_exceptions).issubset(contracts), "异常评分豁免包含未知 Trial")
rows, dimensions, expected_dimensions, seen = [], defaultdict(list), Counter(), set()
for trial_dir in paths:
r = load(trial_dir / "result.json")
trial_name = r["trial_name"]
require(trial_name not in seen and trial_name in contracts,
f"{trial_dir}: 未计划或重复的 Trial")
seen.add(trial_name)
contract = contracts[trial_name]
require(r["task_name"] == contract["task_name"], f"{trial_dir}: Task 名不符")
require(r["task_checksum"] == contract["task_checksum"],
f"{trial_dir}: Task checksum 不符")
require(r.get("started_at") and r.get("finished_at"),
f"{trial_dir}: Trial 时间不完整")
model = r["agent_info"]["model_info"]
provider = model.get("provider")
require(isinstance(provider, str) and provider, f"{trial_dir}: 缺 Provider")
model_name = model.get("name")
require(isinstance(model_name, str) and model_name, f"{trial_dir}: 缺模型名")
agent_version = r["agent_info"].get("version")
require(isinstance(agent_version, str) and agent_version,
f"{trial_dir}: 缺 Agent 版本")
system = f'{r["agent_info"]["name"]}@{agent_version}|{provider}/{model_name}'
require(system == contract["system"], f"{trial_dir}: 系统身份不符")
exc = (r.get("exception_info") or {}).get("exception_type")
rewards = (r.get("verifier_result") or {}).get("rewards")
required_reward_keys = contract["required_reward_keys"]
judge_reward_keys = contract.get("judge_reward_keys", [])
judge_detail_keys = contract.get("judge_detail_keys", [])
for label, keys in (("required", required_reward_keys),
("judge_reward", judge_reward_keys),
("judge_detail", judge_detail_keys)):
require(isinstance(keys, list) and
all(isinstance(key, str) and key for key in keys),
f"{trial_dir}: {label} 键清单非法")
require(len(keys) == len(set(keys)), f"{trial_dir}: {label} 键重复")
require("reward" in required_reward_keys, f"{trial_dir}: 契约未要求主奖励")
require(set(judge_reward_keys).issubset(required_reward_keys),
f"{trial_dir}: Judge Reward 键不在必需键中")
require(set(judge_reward_keys) == set(judge_detail_keys),
f"{trial_dir}: Judge Reward 与明细键未成对声明")
require("reward" not in judge_reward_keys,
f"{trial_dir}: 客观主奖励不得声明为 Judge Reward")
detail_path = trial_dir / "verifier" / "reward-details.json"
detail = load(detail_path) if detail_path.exists() else None
judge_errors = []
if judge_detail_keys and detail is None:
judge_errors.append(("$", "缺必需的 reward-details.json"))
elif detail is not None:
for key in judge_detail_keys:
if key not in detail:
judge_errors.append((f"$.{key}", "缺预注册 Judge 键"))
else:
judge_errors.extend(nested_errors(detail[key], f"$.{key}"))
judge = ("failure" if judge_errors else
"ok" if judge_detail_keys else "not_run")
if exc in INFRA:
execution = "infrastructure_failure"
elif exc in VERIFIER:
execution = "verifier_failure"
elif exc:
execution = "unknown"
elif rewards is None:
execution = "verifier_failure"
else:
execution = "complete"
allowed_type = allowed_scored_exceptions.get(trial_name)
metric_eligible = (execution == "complete" or
(rewards is not None and exc is not None and
allowed_type == exc))
raw_objective = "unscored"
primary = None
if rewards is not None:
require(set(rewards).issubset(required_reward_keys),
f"{trial_dir}: Reward 出现未声明键")
require("reward" in rewards, f"{trial_dir}: 缺主奖励 reward")
primary = finite_score(rewards["reward"], str(trial_dir))
raw_objective = objective_status(primary, plan["primary_reward"])
for key, value in rewards.items():
finite_score(value, f"{trial_dir}:{key}")
if metric_eligible:
for key in required_reward_keys:
expected_dimensions[(system, key)] += 1
if key not in rewards:
require(key in judge_reward_keys and judge == "failure",
f"{trial_dir}: 缺必需 Reward 键 {key}")
continue
if key not in judge_reward_keys or judge == "ok":
dimensions[(system, key)].append(float(rewards[key]))
objective = raw_objective if metric_eligible else "unscored"
# “Agent 失败”是一个派生结论,不覆盖执行和 Judge 两条轴。
agent_failure = metric_eligible and objective == "fail"
primary_success = metric_eligible and objective == "pass"
cost = trial_cost(r, str(trial_dir))
rows.append({
"trial": trial_name, "task": r["task_name"], "system": system,
"execution": execution, "objective": objective,
"raw_objective": raw_objective,
"agent_failure": agent_failure, "primary_success": primary_success,
"metric_eligible": metric_eligible,
"residual_reward": rewards is not None and not metric_eligible,
"judge": judge, "judge_errors": judge_errors,
"primary": primary, "cost": cost,
"trial_seconds": seconds(r, str(trial_dir)),
"agent_seconds": seconds(r.get("agent_execution"), f"{trial_dir}:agent"),
})
require(seen == set(contracts), "计划身份清单未全部出现")
grouped = {}
for system in sorted({row["system"] for row in rows}):
sample = [row for row in rows if row["system"] == system]
costs = [row["cost"] for row in sample]
complete_cost = [c["known_total_usd"] for c in costs if c["complete"]]
partial_cost = [c["known_total_usd"] for c in costs
if not c["complete"] and c["known_total_usd"] is not None]
trial_latency = [row["trial_seconds"] for row in sample
if row["trial_seconds"] is not None]
agent_latency = [row["agent_seconds"] for row in sample
if row["agent_seconds"] is not None]
grouped[system] = {
"denominator": len(sample),
"primary_success": sum(row["primary_success"] for row in sample),
"objective": dict(Counter(row["objective"] for row in sample)),
"execution": dict(Counter(row["execution"] for row in sample)),
"agent_failures": sum(row["agent_failure"] for row in sample),
"judge": dict(Counter(row["judge"] for row in sample)),
"cost": {
"complete_total_usd": sum(complete_cost),
"complete_trial_count": len(complete_cost),
"partial_known_total_usd": sum(partial_cost),
"partial_trial_count": len(partial_cost),
"missing_trial_count": sum(c["observed_contexts"] == 0 for c in costs),
"observed_contexts": sum(c["observed_contexts"] for c in costs),
"expected_contexts": sum(c["expected_contexts"] for c in costs),
},
"speed": {
"median_trial_seconds": statistics.median(trial_latency),
"trial_known_count": len(trial_latency),
"median_agent_seconds": (statistics.median(agent_latency)
if agent_latency else None),
"agent_known_count": len(agent_latency),
},
}
report = {
"policy": {"denominator": "all planned trials",
"model_identity": "agent@version|provider/name",
"retry_boundary": plan["expected_retries"]},
"overall": {
"planned_denominator": plan["expected_trials"],
"result_count": len(rows),
"primary_success": sum(row["primary_success"] for row in rows),
"residual_reward_trials": sum(row["residual_reward"] for row in rows),
},
"groups": grouped,
"dimension_means": {
f"{system}|{key}": {
"mean": (sum(dimensions[(system, key)]) /
len(dimensions[(system, key)]))
if dimensions[(system, key)] else None,
"n": len(dimensions[(system, key)]),
"missing": expected_dimensions[(system, key)] -
len(dimensions[(system, key)]),
}
for (system, key) in sorted(expected_dimensions)
},
"rows": rows,
}
print(json.dumps(report, ensure_ascii=False, indent=2))
配套的 audit-plan.json 不只固定 expected_trials 和 expected_retries,还要逐 Trial 固定 task_name、task_checksum、agent@version|provider/name 和 required_reward_keys,用 primary_reward 声明二元门禁或通过阈值,并用 judge_reward_keys、judge_detail_keys 声明哪些分数和明细来自 Judge。这样某维度即使全部缺失,也会以 mean: null, n: 0, missing: ... 出现在报告中;Judge 失败只排除对应 Judge 维度,不会误删仍有效的确定性维度。仅有 reward-details.json 不代表 Judge 已运行:文件可能只含确定性 criterion。异常后仍允许采纳分数的极少数场景必须在 allowed_scored_exceptions 中逐 Trial 精确写出允许的 exception_type,默认映射为空。
在一个六 Trial 的契约夹具中,我们可以安排:一个客观通过、一个有效零分、一个环境启动失败、一个“客观通过但 Judge 超时”、一个带残留 Reward 的 Verifier 异常和另一个客观通过。这里的 JSON 是人工构造并用 Harbor TrialResult/JobResult 加载验证的结果契约,不是模型实跑结果。
脚本应得到这些关键事实:overall.planned_denominator 为六;每个 Provider 组各有自己的分母,不能把组内三误写成全局六。环境失败没有评分;有效零分是 Agent 失败;Judge 超时的 Trial 仍可在目标轴上通过,但 Judge 质量未知;带异常的残留 Reward 单列为 residual_reward,既不进入主要成功数,也不污染正式维度均值。两个同名模型若来自不同 Provider,会进入两个不同分组。这与第 10 章“异常即主要分析失败、但保留原始 Reward 证据”的事前口径一致。
成本不能只判断 Trial 是否出现过一个数。脚本把成本分成完整已知、部分已知和完全缺失,并报告已观察/预期 Agent context 数;多步 Trial 只要有一个实际 context 缺成本,就进入 partial_trial_count,其已知部分不会冒充完整总账。速度同样分别报告 Trial 与 Agent 阶段的有效样本数。严格 JSON 读取还拒绝重复键、NaN 和 Infinity,避免解析器宽容性绕过 Reward 契约。
这个例子还揭示了一个容易忽略的限制:Harbor 重试会删除旧 Trial 目录,最终结果替代旧结果进入统计,根 result.json 主要留下 n_retries。如果审计计划要求“不允许重试”,任何非零值都应阻断发布;如果允许重试,就必须在运行前把每次尝试复制到外部不可变存储,否则不能声称分析了全部尝试。8
13.5 诊断 flaky,而不是把随机性都叫作 flaky Task
同一 Task 在五次 Agent 运行中两次成功、三次失败,不能直接证明 Task flaky。Agent 本来就可能随机,Provider 也可能改变采样或限流。要定位波动来源,使用分层重放:
- Verifier/Judge 重放:冻结同一候选 Artifact,在隔离环境中多次运行客观 Verifier;Judge 单独固定模型、提示、温度和输入重放。若候选不变但分数变化,问题在 Verifier、Judge 或其依赖。
- Task/环境重放:用确定性 Oracle 或合约夹具在全新环境中多次执行。Oracle 失败是发布阻断信号,但根因仍可能是 Environment、Solution 或 Verifier,必须继续分解。
- Agent 重放:只有前两层稳定后,才把剩余差异视为 Agent/Provider 随机性,并用多次 Trial 的成功计数和不确定性表达。
重放记录至少固定 Task 校验和、环境镜像、Solution/Verifier 版本、完整 Provider/模型名、Agent 版本、Judge 配置、种子和重试策略。对二元成功率报告 s/n,并按第 10 章的方法给出区间;小样本不要只报百分比。例如 2/3 不是“稳定 66.7%”,而是高度不确定的三个观测。
建议为每个 Task 预注册三类门槛:Oracle 重放次数及必须通过的比例、冻结候选的 Verifier 一致性、负例必须拒绝的比例。阈值要在看到正式模型结果前确定。否则团队很容易在结果出现后调整规则,把真正不稳定的 Task 保留下来。
13.6 泄漏与 scorer gaming 是两类不同风险
泄漏是 Agent 得到了不应可见的信息;gaming 是 Agent 在没有真正解决任务的情况下影响或绕过评分。两者可能同时发生,但修复路径不同。
泄漏审计关注 Agent 可见面:solution/、隐藏测试、gold patch、Judge rubric、数据集答案、可搜索的唯一 Task 文本、仓库 Git 历史、环境变量、挂载和网络。运行前生成可见文件清单及哈希;运行后检查 trajectory、shell 历史、网络日志和 Artifact 是否访问了禁止位置。不要把“trajectory 没记录访问”当作未访问的证明,因为 trajectory 可能不完整。
Gaming 审计关注评分边界:Agent 是否能写 reward.txt 或 reward.json,能否修改 tests/、test.sh、Verifier 依赖或 Judge 配置,Verifier 是否执行了 Agent 可写目录中的程序,是否只检查易伪造的输出文本,是否存在用基准 ID 直接返回答案的捷径。Harbor 的 adapter review 清单也要求检查 Solution、测试与指令泄漏、Git 历史、评分器写权限以及重写奖励文件等风险。9
审计用的日志、Artifact 和 trajectory 自身也可能包含提示注入文本。自动摘要可以帮助导航,但不能成为失败归因依据;任何“忽略审计规则”的工件内容都应当作数据,而不是指令。
13.7 能力覆盖与难度:标签不是测量
TaskConfig.metadata 是任意字典。Viewer 会按约定读取字符串 difficulty、category 和标签,但不会证明这些值语义正确。作者写的 difficulty="hard" 只是声明难度,不能直接当作经验难度。10
能力覆盖报告应先建立矩阵:行是预注册能力,例如依赖诊断、代码修改、测试设计、工具使用和长程恢复;列至少包含 Task 数、计划 Trial 数、各 Provider 的有效评分数、执行失败数、成功计数和置信区间。每个 Task 最好有一个主能力,附加标签用于交叉分析。若一个 Task 同时计入三个能力,报告必须说明覆盖数会重复,不能把列相加得到总 Task 数。
经验难度则来自固定的 baseline panel,而不是作者印象。对每个 Task,使用相同预算和协议运行预注册的多个基线,按完整 Provider/模型分开保存每个 Trial。可以报告“所有基线都通过”“仅强基线通过”“所有 Agent 基线都失败但 Oracle 稳定通过”等可观察模式;若需要 easy/medium/hard 分档,阈值必须事先确定,并同时给出原始 s/n 与区间。不存在跨任务、跨预算都有效的通用阈值。
几个信号值得进入人工复核队列:所有基线始终通过的 Task 可能区分度过低;所有基线都失败的 Task 可能过难,也可能说明说明书、环境或 Verifier 有问题;Oracle 失败则直接阻断发布,但不能自动宣布“Task 数据有问题”,因为 Solution、环境和 Verifier 都可能是根因。
13.8 一份可发布的评测报告应包含什么
报告首先给身份和分母,再给指标。下面是一份精简结构:
provenance:
harbor_version: 0.18.0
harbor_commit: 527d50deb63a5d279e8c20593c18a2cbc7f61f9e
task_checksums: "逐项列出"
agent_and_models: "agent@version + provider/name"
retry_policy: "max_retries、实际 n_retries、旧尝试保存策略"
denominators:
planned_trials: 120
result_files: 120
objectively_scored: 113
quality:
primary_success: "82/120,含区间"
dimensions: "每个维度的均值、n、缺失数;Judge 错误不填零"
failures:
infrastructure: 3
agent: 31
verifier: 4
judge: 2
unknown: 0
cost:
complete_total_usd: 46.70
complete_trials: 112
partial_known_total_usd: 1.50
partial_trials: 4
missing_trials: 4
agent_contexts: "已观察成本数/实际 context 数"
speed:
trial_latency: "中位数、分位数、有效 n"
agent_latency: "中位数、分位数、有效 n"
uncertainty:
binary_metrics: "s/n 与区间"
judge_repeatability: "重放协议与差异"
coverage_and_difficulty:
capability_matrix: "Task 数、Trial 数、Provider 分层"
declared_vs_empirical: "分别报告"
audit:
artifacts: "manifest 状态、哈希与缺失"
trajectories: "有效、无效、缺失"
leakage_and_gaming: "检查项、负例、发现与修复"
deviations: "未执行项、环境限制、已知不确定性"
主成功率的分母仍是所有计划 Trial,所以基础设施失败和缺失结果不会悄悄消失;但失败表把它们单独列出,读者不会把它们误认成 Agent 错答。维度均值则按该维度有有效观测的 n 报告,并显式列缺失。这正是“严守主分母”和“不伪造维度零分”可以同时成立的方式。
13.8.1 发布审计门
正式发布前,至少执行以下硬门:
- 计划 Trial 数、实际
result.json数和 Task 校验和完全一致;每个结果都能按固定 Harbor 模型加载; - 身份包含 Agent 版本和完整
provider/name;同名模型不跨 Provider 合并; - 实际重试数符合预注册策略;若要分析所有尝试,旧尝试已进入不可变存储;
- 主 Reward 和维度 Reward 满足非布尔、有限、范围、必需键契约;缺失评分不改写为零;
- 基础设施、Agent、Verifier、Judge 和
unknown分开;正式发布不允许未解释的unknown; - 必需 Artifact 的 manifest 状态为
ok,内容哈希已记录;冲突、空文件、下载失败已处置; - 必需 trajectory 通过 ATIF 校验;缺失与无效数单独报告,且不把 trajectory 当真值;
- Oracle、冻结候选 Verifier 重放、负例和 gaming 夹具达到预注册门槛;Judge 完成重复性或校准检查;
- 泄漏面、评分写权限、网络与 Git 历史已检查;发现的问题已有复测证据;
- 质量、成本、速度均报告有效样本数和缺失数;二元指标给出
s/n与不确定性; - 所有未执行检查、外部依赖和协议偏离都在报告中列出。
探索性内部报告可以保留 unknown 并继续调查,但不能用“暂按 Agent 失败”填满表格。发布门的作用不是让数字更漂亮,而是确保每个数字都能回到原始 Trial 和明确契约。
13.9 必须承认的权衡
更丰富的 Artifact、日志和 trajectory 提高可诊断性,也增加存储、隐私和泄漏风险;应按最小必要原则采集,对秘密与个人数据做脱敏,并为原始证据设置访问控制和保留期。
重试能提高作业完成率,却会模糊第一次失败并删除本地旧 Trial。用于产品可用性时可以重试;用于科学比较时,要么禁用重试,要么在删除前外部保存全部尝试,并分别报告首试和最终尝试。
Judge 能补充客观测试难以表达的质量,但增加成本、延迟与非确定性。关键正确性优先使用可执行客观检查;Judge 适合独立质量轴,并需要固定配置、错误字段、重复性和人工校准。能力覆盖和 baseline 难度面板也会增加预算,但比用少量标签制造确定性更诚实。
13.10 小结
- Viewer 用于定位,正式统计来自逐 Trial 结果;Job 根结果和界面聚合不能替代审计账本。
- Reward 是结果,不是根因。执行轴、目标轴和 Judge 轴应分开,尤其不能把 Judge 超时零分当作质量零分。
- 日志、Artifact 和 trajectory 是互相校验的证据;它们都可能缺失、不完整或不可信。
- 主成功率保留所有计划 Trial 为分母;失败归因、维度有效样本、成本与速度覆盖数另行披露。
- Flaky Task 需要分层重放证明;普通 Agent 波动不等于 Task flaky。
- 泄漏与 gaming 要分别审计;作者难度标签也必须与固定 baseline panel 的经验难度分开。
- 发布审计门应阻断缺失结果、未知根因、评分契约错误、未校验工件和未披露协议偏离。
13.11 练习
- 构造四个
TrialResult契约夹具,分别表示环境失败、有效零分、奖励解析失败和“客观通过但 Judge 超时”。让分析脚本输出三条状态轴,并写出每个判断所需的证据。 - 选择一个确定性候选 Artifact,设计五次 Verifier 重放和五次 Judge 重放。预先写出一致性阈值、随机种子、依赖固定方式,以及波动出现后的归因流程。
- 为一个自定义 Task 画出 Agent 可见文件/网络面与 Verifier 信任边界,设计一个泄漏探针和两个 scorer-gaming 负例;说明它们为什么不会破坏正式答案。
- 为十个 Task 建立能力覆盖矩阵:每个 Task 指定一个主能力和可选交叉标签,按 Provider 报告计划数、有效评分数、基础设施失败和
s/n。解释为什么标签列不能简单求和。 - 把本章审计门实现为 CI 检查:在缺 Provider、非有限 Reward、Artifact manifest 为
failed、trajectory 无效、出现重试或存在unknown时返回非零退出码,并保存机器可读报告。