第 2 章:Harbor 的核心概念与运行模型
一次系统配置评测结束后,两个工程师得出了相反的结论。第一个人看到 Agent 的最后一句“修复完成”,便把任务记为成功;第二个人只看 Job 页面上的均值,便说每个任务都得了 0.67 分。两人都跳过了真正决定结论的对象:Agent 改动了哪个 Environment,Verifier 为哪一次 Trial 产生了什么 Reward,以及 Dataset 的 Metric 如何聚合多次 Reward。
这不是术语问题,而是排障问题。若不能沿着真实运行对象追踪数据,你就无法判断失败发生在模型、Agent、环境、评分器还是聚合层。本章固定使用 Harbor v0.18.0(提交 527d50deb63a5d279e8c20593c18a2cbc7f61f9e),建立一条能映射到源码类型与输出文件的执行链。1
读完本章,读者应当能够:
- 区分 Task、Dataset、Agent、Model、Environment、Trial 与 Job 的职责;
- 从 Verifier 一直追踪到 Trial Reward,再追踪到 Dataset Metric;
- 分清 Artifact、普通日志与 trajectory;
- 用无模型、无容器的示例核验 Reward、Metric 和 trajectory 的类型边界;
- 根据输出目录判断一次评测停止在哪个阶段。
2.1 先问三个问题
面对任何评测结果,先问:谁做了什么、在哪里做、谁判定结果。在 Harbor 中,“谁”不是 Model,而是使用 Model 和工具的 Agent;“哪里”是由 Task 定义、由某个 Provider 实现的 Environment;“谁判定”是 Verifier。Trial 把三者绑定成一次尝试,Job 再组织许多 Trial。
还要再问一个统计问题:这个数字描述“一次尝试”,还是“一组尝试”?前者是 Reward,后者通常是 Metric。Harbor 随附的核心概念文档把 Trial 定义为 Agent 对一个 Task 的一次尝试:通常在完成验证后产生 Reward;若验证被禁用、跳过或失败,则可能没有 Reward。Job 则是一组 Trial。2 这两句话已经给出最重要的对象边界。
本章不讨论如何安装 Harbor,也不运行真实模型或 Docker。我们先确保能阅读运行模型;第 3 章再把同一条链落到完整评测上。
2.2 两层输入、两层执行和两层结果
把 Harbor 看成“文件定义层”和“运行对象层”的组合,会比背诵名词更可靠。
| 概念 | v0.18.0 中的主要落点 | 它负责什么 | 它不是什么 |
|---|---|---|---|
| Task | Task、任务目录、任务侧 TaskConfig | 指令、环境定义、测试、可选 solution 与元数据 | 不是一次执行 |
| Dataset | DatasetManifest、Job 侧 DatasetConfig | 组织 Task,并可携带 Dataset 级文件或指标定义 | 不是 Trial 结果的容器别名 |
| Agent | BaseAgent、Trial 侧 AgentConfig | 接收指令,在 Environment 中行动并记录执行上下文 | 不等于底层 Model |
| Model | AgentConfig.model_name、ModelInfo | Agent 使用的模型身份 | 在 v0.18.0 中不是独立调度单元 |
| Environment | BaseEnvironment 及 Provider 实现 | 提供容器化状态、命令执行、文件传输与生命周期操作 | 不只是一个 Dockerfile |
| Trial | TrialConfig、Trial、TrialResult | 绑定一个 Task、一个 Agent 配置和一次环境执行 | 不是 Dataset 聚合 |
| Verifier | BaseVerifier、Verifier | 在 Agent 阶段后执行测试并解析评分文件 | 不负责跨 Trial 统计 |
| Reward | VerifierResult.rewards | 描述一次 Trial 的评分字典 | 不是均值、通过率等聚合值 |
| Job | JobConfig、Job、JobResult | 展开、并发运行、恢复并汇总 Trial | 不是一个更大的 Task |
| Metric | BaseMetric.compute() | 把一组 Reward 或缺失值聚合成指标字典 | 不是 Verifier 的单次判定 |
| Artifact | ArtifactConfig、ArtifactHandler | 从沙箱收集需要保留或交给独立 Verifier 的文件 | 不等于所有日志 |
| 日志 | job.log、trial.log、agent/、verifier/ | 保存运行诊断与各实现写出的文件 | 不保证具有统一语义结构 |
| trajectory | ATIF Trajectory | 结构化保存消息、动作、工具调用、观察和用量 | 不等于 trial.log,也不保证每个 Agent 都产生 |
2.2.1 Task 与 Dataset:定义一个问题,还是组织一组问题
Task 从目录加载 task.toml 和指令;使用共享 Verifier 的单步任务还要求环境目录与适合目标操作系统的测试脚本。独立 Verifier 可以把测试脚本预置在自己的镜像中,因此此时宿主机任务目录不一定有同名脚本。Task 把“要做什么”“在什么初始状态做”“怎样验收”放在同一可分发单元中。3
Dataset 则保存 Task 引用。发布格式的 DatasetManifest 包含 Dataset 元信息、Task 引用和文件引用;Task 引用用内容摘要固定具体内容,Dataset 文件也能用摘要参与内容哈希。4 运行 Job 时,Job 侧 DatasetConfig 将本地、包或仓库形式的 Dataset 解析成一组 Trial 侧 TaskConfig。因此一个 Task 可以被多个 Dataset 复用,而 Dataset 不会替 Task 执行任何动作。
2.2.2 Agent、Model 与 Environment:行动者、推理组件与世界
BaseAgent 的核心接口是 setup(environment) 与 run(instruction, environment, context)。run() 接收任务指令和 Environment,并将 token、成本或 rollout 细节写入 AgentContext。5 Model 只是 Agent 配置的一部分:model_name 被传给 Agent,结果中再解析为可选的 ModelInfo。由此可以推断,在 Harbor v0.18.0 的运行模型中,Agent 才是被 Trial 调用的行动者;Model 是 Agent 使用并被记录的推理组件,不能把“换模型”和“换 Agent 实现”视为同一个实验变量。
BaseEnvironment 是容器化环境的统一接口,允许由一个或多个容器组成,并负责启动、停止、执行命令和传输文件。Trial 通过 EnvironmentFactory 选择具体 Provider;Task 的 [environment] 描述任务要求,Trial 侧 Environment 配置描述本次运行选择与覆盖。6 在 Provider 支持 Task 所需能力、且运行配置没有改变任务定义的前提下,同一个 Task 可以在本地 Docker 或受支持的远程沙箱中保持相同任务语义。这是统一接口与能力校验支撑的设计目标,不是所有 Task 都能无条件跨 Provider 运行的保证。
2.2.3 Trial 与 Job:一次尝试和实验编排
TrialConfig 必须指向一个 Task,并包含 Agent、Environment、Verifier、Artifact 等运行配置;TrialResult 则保存任务身份、Agent/Model 信息、各阶段时间、Agent 上下文、Verifier 结果和异常。7 单步与多步 Task 会分别构造 SingleStepTrial 与 MultiStepTrial,但二者都由 Trial 基类负责准备、收尾、事件和结果持久化。
JobConfig 可以同时列出 Dataset、独立 Task、多个 Agent 及重复次数。Job 先解析 Task,再按“重复次数 × Task × Agent”生成 TrialConfig,交给 TrialQueue 并发执行;最终 JobResult 保存总 Trial 数、进度/成本统计、分组指标和 Trial 结果。8 所以“运行一个 Dataset”并不意味着只产生一个 Trial。例如 2 个 Task、1 个 Agent、3 次尝试会展开为 6 个 Trial——这是直接由 v0.18.0 的三层循环推得的数量,不是 Dataset 自身的字段。
2.2.4 配置对象、执行对象与结果对象
同一个名词附近往往有三种类型,阅读源码时不要混在一起。以 Trial 为例:TrialConfig 是尚未运行的计划,Trial/SingleStepTrial 是持有运行状态和方法的执行对象,TrialResult 是可以序列化的结果。Task 也有相似分层:任务目录里的 TaskConfig 描述作者定义的环境与评分规则,Trial 侧同名 TaskConfig 描述本次运行从本地路径、Git 或包注册表哪里取得 Task。它们位于不同模块,字段也不同。
结果对象不仅保存“分数”。TrialResult 还包含稳定的 UUID、Task 身份与摘要、Trial URI、完整运行配置、Agent/Model 信息、环境准备、Agent setup、Agent 执行和 Verifier 的时间区间,以及可选异常。7 这些字段让同一个低分可以被拆成“Agent 确实运行但没完成”“Agent 超时后仍被评分”“Verifier 自身失败”等不同事件。
Job 也有“返回值”和“磁盘摘要”的区别。Job.run() 返回的内存 JobResult 会装入组合后的 trial_results;但 v0.18.0 最终写 Job 根目录 result.json 时显式排除该字段,各 Trial 的完整结果仍分别保存在自己的 result.json 中。9 因而审计时要从 Job 摘要下钻到 Trial 文件,不能假定根结果文件复制了所有明细。
2.3 一次评测的真实生命周期
下面的伪图只使用 v0.18.0 中存在的对象和调用方向:
Job.create(JobConfig)
├─ DatasetConfig / TaskConfig → 解析并缓存 Task
├─ MetricConfig / Dataset 定义 → 构造 Metric
└─ attempts × tasks × agents → TrialConfig[]
│
▼
TrialQueue(并发、重试)
│ Trial.create
▼
Task → SingleStepTrial / MultiStepTrial
│
构造 Agent/Agent Environment,写 lock.json
│ Trial.run:初始化结果并写 config.json
▼
启动 Agent Environment → 健康检查 → skills → Agent.setup
│
install_only ├─ 是 → 跳过 Agent 与 Verifier ───────────┐
│ 否 │
▼ │
Agent.run → 同步 agent 日志/trajectory → 收集 Artifact │
│ │
verifier.disable ├─ 是 → 不发 VERIFICATION_START,且无 Reward ┤
│ 否 │
▼ │
解析 Verifier Environment 模式 │
┌───────┴──────────────────────┐ │
│ 共享 │ 独立 │
▼ ▼ │
在 Agent Environment 验证 停止 Agent Environment │
│ → 创建/启动 Verifier Env │
│ → 上传已收集 Artifact │
│ → 验证 → 停止 Verifier Env │
└───────┬──────────────────────┘ │
▼ │
VerifierResult.rewards(验证成功时) │
│ │
停止仍在运行的 Agent Environment ◀────────────────────┘
│
写 Trial result.json → END
│
▼
Job 按 Agent / Model / Dataset 分组 Reward
│
▼
Metric.compute(rewards[])
│
▼
Job result.json
图中从 Agent 执行到验证的分支展开的是单步 Trial。多步 Trial 会按 step 重复相应阶段、归档每步输出,再选择 Trial 级结果;这不改变 Job 最终聚合 Trial Reward 的边界。10
这条链可细分为十一步。
- 解析输入。
Job.create()解析 Agent skills、Dataset 与独立 Task,验证 Environment 资源策略,解析 Metric,并缓存 Task。11 - 展开实验。 Job 为每个“尝试 × Task × Agent”组合生成
TrialConfig。n_concurrent_trials只改变同时运行多少个 Trial,不改变逻辑组合数。 - 创建运行对象与锁。
Trial.create()下载或定位 Task,构造Task,再根据是否有 steps 选择单步或多步 Trial。Trial 构造期间会建立输出目录与lock.json,并构造 Agent、Agent Environment 和 Artifact handler。10 - 初始化本次运行。
Trial.run()先初始化TrialResult、写config.json,再发出START;这些动作发生在环境启动之前。 - 准备环境。 Trial 启动 Agent Environment,执行健康检查、注入 skills,再调用
Agent.setup()。 - 应用仅安装开关。 若
install_only=true,setup 完成后直接跳过 Agent run 与验证,进入收尾;它不是一条会产生 Reward 的正常评测路径。 - 运行 Agent。 Trial 把 Task 指令、Environment 和空的
AgentContext传给Agent.run();阶段时间和事件同时被记录。 - 同步证据。 Agent 结束后,Trial 同步
agent/下的日志;单步 Trial 随后收集约定目录与显式 Artifact。远程环境需要下载,本地挂载环境则准备宿主机上的日志。 - 按条件执行 Verifier。 若
verifier.disable=true,_run_verifier()直接返回,不发出VERIFICATION_START,也不产生 Reward。否则,共享模式在仍运行的 Agent Environment 中评分,评分后才停止它;独立模式先收集 Artifact 并停止 Agent Environment,再创建、启动独立 Verifier Environment,上传 Artifact,完成评分后停止该独立环境。默认 Verifier 执行测试脚本,然后优先解析reward.json,否则解析reward.txt;后者被包装成{"reward": 数值}。两者都不存在时会抛出RewardFileNotFoundError,而不是悄悄生成 0。12 - 持久化 Trial。 收尾阶段确保 Agent Environment 已停止,在
result.json中写入TrialResult并发出END;即使发生可记录异常,exception_info、exception.txt和已有日志也帮助区分基础设施错误与真实低分。10 - 聚合 Job。 Job 收集每个 Trial 的 Reward 或缺失值,按 Agent、Model、Dataset 分组调用 Metric,计算统计与可选的 pass@k,最后更新 Job 的
result.json。13
2.3.1 生命周期不是“成功或失败”两个状态
Trial 会发出 START、ENVIRONMENT_START、AGENT_START、AGENT_END、VERIFICATION_START、END 和 CANCEL 事件。Job 通过这些事件更新 pending、running、completed、errored、cancelled 与 retry 统计;事件中的 result 从 START 前就已初始化,到 END 时才完整。14 这比给整个进程贴一个布尔标签更有用,但事件不能被当作成功证明:AGENT_END 在 Agent 阶段的 finally 中发出,只说明该阶段已经离开。若已有 AGENT_END 却没有 VERIFICATION_START,应先查 exception_info 与 Agent 日志,因为普通 Agent.run() 异常或 hook 异常也可能形成这一组合;排除这些后,再查输出同步、Artifact 收集、install_only、verifier.disable 与进入 Verifier 前的转换。
单步 Trial 还有一个容易忽略的行为:AgentTimeoutError 或 Installed Agent 的非零退出码会先写入 exception_info,随后仍同步 Agent 输出并继续执行 Verifier。15 这允许 Verifier 检查超时前已经完成的部分工作,所以同一个 TrialResult 可以既有异常又有有效 Reward。Job 统计会把它计入错误,同时保留其评分。不要把“有 Reward”解释成“执行无异常”,也不要把“有异常”解释成“必然没有 Reward”。
若 Trial 遇到其他未在 Agent 阶段内部处理的异常,基类会记录异常,再调用具体 Trial 的 _recover_outputs(),最后在 finally 中停止环境、写结果并发出 END。恢复范围不是统一保证:单步 Trial 会尝试同步 Agent 输出并收集 Artifact;多步 Trial 的基类异常恢复只同步 Agent 输出并停止环境,不保证再次收集当前步骤 Artifact,不过已经完成的步骤可能早已归档各自输出。取消则额外发出 CANCEL 并把取消继续向上传播。16 这解释了为什么失败 Trial 仍可能留下大量有价值的文件,也解释了为什么不能仅凭目录存在就断言 Artifact 恢复完整。
注意:Agent 返回一句“完成”只属于交互内容;它既不是 Verifier 结果,也不会自动变成 Reward。反过来,Reward 为 0 表示 Verifier 给出有效的低分,和“Verifier 未运行成功、Reward 缺失”不是同一状态。
2.4 Reward 不是 Metric
这是本章最重要的边界。v0.18.0 的 VerifierResult 只有一个核心字段:rewards: dict[str, float | int] | None。它可以是一维 {"reward": 1.0},也可以是多维 {"correctness": 1.0, "format": 0.5}。无论有几个键,它都属于某一个 Trial。17
Metric 的输入则是 list[RewardDict | None],输出是另一份指标字典。内置 Mean、Sum、Min 与 Max 会跨列表聚合;默认没有显式 Metric 时,Job 使用 Mean。判定单位是整组输入的联合键集合:所有非空 Reward 合起来最多出现一个键时,均值输出键为 mean;整组出现两个或更多不同键时,才分别按键聚合。例如 [{"a": 1}, {"b": 1}] 中每个字典单独看都是单键,但联合键集合有两个,输出会保留 a、b,不会产生 mean。缺失的整个 Reward 或多维 Reward 中缺失的键,在这些内置聚合中按 0 处理。18
这里还有两个统计边界。第一,Reward 字典有多个键时,内置 Metric 先收集所有出现过的键,再逐键聚合;某个 Trial 没有 format,不会让整个 Trial 消失,而是为该键贡献 0。第二,None 表示没有 Reward,空字典 {} 表示存在一份不含评分维度的字典;在内置聚合里两者都可能贡献 0,但它们的运行语义和排障证据仍不同。Metric 只定义聚合规则,不会替你解释缺失原因。
Dataset 也不是 Metric 的数值本身。Dataset 决定哪些 Task 进入组,并可携带自定义 Metric;Job 还可以追加 Job 级 Metric。最终指标属于具体的 Agent/Model/Dataset 分组。13 因此比较两个实验时,必须同时核对 Dataset 版本、Task 集合和 Metric 定义,不能只比较同名数字。
2.4.1 一个可核验的最小示例
前置条件是已取得本书固定版本源码,并安装 uv。在 /private/tmp/harbor-framework-v0.18.0 运行下面的命令;它不会启动容器,也不会调用模型:
uv run python - <<'PY'
from harbor.metrics.mean import Mean
from harbor.models.trajectories import Agent, Step, Trajectory
from harbor.models.verifier.result import VerifierResult
a = VerifierResult(rewards={"correctness": 1.0, "format": 0.5})
b = VerifierResult(rewards={"correctness": 0.0, "format": 1.0})
metric = Mean().compute([a.rewards, b.rewards, None])
trajectory = Trajectory(
session_id="diagnosis-demo__agent",
agent=Agent(name="demo-agent", version="1.0", model_name="demo-model"),
steps=[
Step(step_id=1, source="user", message="修复服务配置"),
Step(step_id=2, source="agent", message="已完成", llm_call_count=0),
],
)
print("trial_a_rewards=", a.rewards)
print("dataset_metric=", metric)
print("trajectory_schema=", trajectory.schema_version)
print("step_ids=", [step.step_id for step in trajectory.steps])
PY
本章实际执行得到:
trial_a_rewards= {'correctness': 1.0, 'format': 0.5}
dataset_metric= {'correctness': 0.3333333333333333, 'format': 0.5}
trajectory_schema= ATIF-v1.7
step_ids= [1, 2]
第一行才是 Trial A 的 Reward;第二行是把两个有效 Reward 和一个缺失值聚合后的 Metric。correctness 为 (1 + 0 + 0) / 3,format 为 (0.5 + 1 + 0) / 3。第三、四行同时验证 v0.18.0 默认使用 ATIF-v1.7,且步骤从 1 连续编号。19
2.4.2 概念混淆反例
假设三个 Trial 中,A 的 Reward 是 {"reward": 1},B 是 {"reward": 0},C 因评分文件缺失而没有 VerifierResult。下面的说法是错的:
“C 的 Reward 是 0,因此 Dataset 的 Reward 是 1/3。”
正确表述是:“C 的 Reward 缺失;默认 Mean 在 Metric 聚合 时把这个缺失值按 0 计入,因此 Job 中该组的 mean Metric 为 1/3。”两种描述可能显示同一个最终数字,却对应完全不同的修复动作:真实 0 分应检查 Agent 行为或评分规则,缺失 Reward 应先检查 Verifier、环境与评分文件。
同样,“没有 agent/trajectory.json,所以 Agent 没有运行”也是错误推断。BaseAgent.SUPPORTS_ATIF 默认是 False,只有支持并实现导出的 Agent 才会生成标准 trajectory;运行状态应结合 TrialResult、阶段时间、异常和日志判断。5
2.5 Artifact、日志与 trajectory 各回答什么问题
这三者都可能是文件,但用途不同。
Artifact 回答“需要保留什么产物或证据”。 Agent 可把文件写入容器内 /logs/artifacts/ 约定目录,也可由 Task 或 Job 用 ArtifactConfig 指定其他路径。Harbor 将它们收集到 Trial 的 artifacts/,并写 manifest.json,其中记录来源、目标、类型、状态和 Compose service。Artifact 下载是 best-effort;收集失败会进入 manifest,而不会单独把 Trial 判为失败。20
日志回答“运行时发生了什么”。 Job 有 job.log,每个 Trial 有 trial.log;agent/ 与 verifier/ 还保存实现相关日志、测试标准输出和评分文件。日志适合查超时、命令错误和下载失败,但格式可能是纯文本、JSONL、终端录屏或其他实现自定格式。
trajectory 回答“Agent 如何与用户、工具和环境交互”。 Harbor v0.18.0 的 ATIF Trajectory 根对象包含 Agent、至少一个 Step,以及可选的最终用量与子 Agent 轨迹。Step 的来源只能是 system、user 或 agent,并可保存消息、推理内容、工具调用、Observation 和 token/cost 指标;模型会校验 step_id 必须从 1 连续递增,并校验 Observation 对工具调用的引用。19 因此 trajectory 比普通日志更适合回放、比较和训练数据处理,但它仍是 Agent 日志目录中的一个结构化产物,不是 Reward 的来源替代品。
三者可以引用同一事实,却不能互相替代。例如 Agent 在 trajectory 中记录“写入 report.json”,Artifact 保存真正的 report.json,Verifier 日志记录测试如何读取它并为何扣分。trajectory 证明 Agent 声称执行了动作,Artifact 提供动作结果,Verifier 证明确切评分过程。审计时把三条证据对齐,才能发现“Agent 声称成功但文件不存在”“文件存在但格式错误”或“文件正确而测试脚本错误”这三类完全不同的问题。
警告:trajectory 和普通 Agent 日志可能包含提示词、工具参数、环境返回值或显式推理内容;Artifact 也可能收集配置和数据文件。保存、上传或用于训练前,应按项目的数据治理规则检查凭据与敏感信息。Harbor 的类型校验只保证结构,不等于完成脱敏。
典型的单步 Trial 输出可以这样阅读:
jobs/<job-name>/
├── config.json
├── lock.json
├── result.json
├── job.log
└── <trial-name>/
├── config.json
├── lock.json
├── result.json
├── exception.txt # 仅在异常时出现
├── trial.log
├── agent/
│ └── trajectory.json # 仅当 Agent 生成时出现
├── verifier/
│ ├── reward.txt 或 reward.json
│ └── test-stdout.txt # 默认 Verifier 合并 stdout 与 stderr
└── artifacts/
└── manifest.json
该结构来自 Job 和 TrialPaths 的实际路径属性。TrialPaths 虽然还定义了 test_stderr_path,但默认 Verifier 只把 test_stdout_path 传给执行命令,并用 2>&1 将两个流都重定向到 test-stdout.txt;只有自定义 Verifier 或其他生产者显式写入时,才应期待 test-stderr.txt。多步 Trial 则把每一步的 agent/、verifier/ 和 artifacts/ 归档到 steps/<step-name>/。21
2.6 映射到系统配置与故障诊断 Benchmark
现在把对象放回贯穿项目。一个“分析错误日志并生成结构化故障报告”的 Task 可以包含:任务指令、带故障日志的 Environment、验证报告字段的测试脚本,以及声明要保留的报告 Artifact。把“日志分析”和“Python 服务修复”两个 Task 放进 Dataset,只表示它们属于同一能力集合。
实验时选择一个 Agent 和一个 Model,并把 n_attempts 设为 3。Job 将生成 6 个 Trial。每个 Trial 都获得独立的 Task/Agent 组合与结果:Agent 修改自己的 Environment,Verifier 读取最终状态并产生 Reward,trajectory 解释行动过程,Artifact 保留报告或环境证据。Job 最后对两个 Task 的多次 Reward 计算 Dataset 对应 Metric。
设计数据流时,可以给每层只分配一个问题。Task 的元数据说明“要测哪种诊断能力”;Trial 说明“某个 Agent/Model 对某个 Task 的这一次尝试发生了什么”;Reward 说明“这一次达到各验收维度的程度”;Metric 说明“同一实验分组整体表现怎样”。结构化故障报告本身应作为 Artifact 保留,生成过程进入 trajectory,评分细节进入 verifier/,最终数值进入 VerifierResult。这样即使平均 Metric 不变,也能追踪某个字段为什么从正确变成错误。
验收时不要只保存一个平均值。至少保留以下关联:
Job
└─ Agent + Model + Dataset 分组
├─ Trial(task=日志分析, attempt=1) → Reward + trajectory + Artifact
├─ Trial(task=日志分析, attempt=2) → Reward + trajectory + Artifact
├─ Trial(task=日志分析, attempt=3) → Reward + trajectory + Artifact
└─ ...
└─ Metric(rewards[]) → 分组统计
由此可以推断,Metric 适合回答“这一组实验总体如何”,而单个 Trial 的 Reward、异常、日志和 trajectory 才能回答“这个样本为什么失败”。
2.7 失败模式与验收方法
遇到“页面上没有分数”时,按数据产生顺序排查,而不是先怀疑聚合算法。
- 打开 Trial
result.json:是否有exception_info、agent_execution和verifier时间? - 查看
trial.log与exception.txt:环境、Agent 或 Verifier 是否超时或抛错? - 查看
verifier/test-stdout.txt:默认 Verifier 将测试的 stdout 与 stderr 合并写入这里;只有自定义实现确实生成test-stderr.txt时才另查该文件。 - 查看
reward.json或reward.txt:文件是否存在、非空、可解析?若两者都存在,v0.18.0 选择 JSON。 - 若评分依赖 Artifact,检查
artifacts/manifest.json的status,不要假定 best-effort 收集一定成功。 - 只有确认各 Trial 的 Reward/缺失状态后,才检查 Job
result.json中该 Agent/Model/Dataset 分组的 Metric。
可以把常见现象压缩成下面的判读表:
| 现象 | 先确认什么 | 不应直接得出的结论 |
|---|---|---|
rewards={"reward": 0} | 测试输出、任务最终状态与评分规则 | 基础设施一定故障 |
verifier_result=null | exception_info、Verifier 时间与评分文件 | Reward 等于 0 |
| 同时有异常和 Reward | 异常类型、Agent 结束时间、Verifier 是否随后执行 | 结果对象自相矛盾 |
| Metric 低于有效 Reward 的直观均值 | 是否有 None、缺失维度、额外尝试或自定义 Metric | 聚合实现一定算错 |
manifest.json 中 Artifact 为 failed | 来源路径、service、权限和环境能力 | Agent 一定没有生成文件 |
没有 trajectory.json | Agent 是否声明并实现 ATIF、其他 Agent 日志和阶段时间 | Agent 一定没有运行 |
这张表的原则是保留“不知道”的状态。Reward 缺失、Artifact 收集失败和 trajectory 未生成,都不能被方便地改写成 0、空文件或未运行。过早折叠状态会让 Job 层的统计看似整齐,却丢掉最重要的失败分类信息。先记录事实,再让 Metric 按明确规则处理缺失,评测才可审计。
时间字段还能帮助验证排查顺序:环境准备结束应早于 Agent 执行,Verifier 开始应在 Agent 阶段之后。某个阶段只有开始时间而没有结束时间,通常比一条笼统的错误消息更能缩小中断位置。不过时间只能说明阶段边界,不能证明任务质量;最终仍要把它与异常、日志、Reward 和 Artifact 对照,这一步不可省略。
本章的验收标准是:读者能指着源码或输出说明每个箭头,而不是只复述定义。可以在固定源码目录执行:
git rev-parse HEAD
git describe --tags --exact-match
uv run pytest -q \
tests/unit/test_metrics.py \
tests/unit/test_verifier.py::TestVerifierRewardParsing::test_verify_prefers_reward_json_over_reward_text \
tests/unit/models/test_trajectory.py
前两条应分别得到本章开头的提交与 v0.18.0。本章实际运行后三组测试共 19 项通过。再把最小示例中的第一个 step_id 改成 2;Pydantic 应拒绝该 trajectory,并报告期望从 1 连续编号。只要这些检查成立,就同时验收了版本、Reward/Metric 边界和 trajectory 的核心结构约束。
2.8 本章小结
- Task 定义一个可执行问题,Dataset 组织 Task;Trial 是一次尝试,Job 是多次尝试的编排与汇总。
- Agent 才是被 Trial 调用的行动者;Model 是 Agent 使用并记录的推理组件,Environment 是承载状态与工具的执行世界。
- Verifier 为单个 Trial 产生 Reward;Metric 在 Job 中跨 Reward 聚合,缺失 Reward 不等于 Trial Reward 为 0。
- Artifact 保存指定产物,日志用于运行诊断,ATIF trajectory 结构化描述 Agent 的交互过程,三者不可互换。
- Job 根
result.json是聚合摘要;完整 Trial 明细保存在各 Trial 目录,审计时应继续下钻。 - 排障应沿“环境 → Agent → Artifact/日志 → Verifier → Reward → Metric”的产生顺序进行。
2.9 练习
- 使用本章最小示例,把两份 Reward 都改成只含同一个
reward键,解释为什么整组输入的联合键集合只有一个键,以及Mean().compute()的输出键为什么是mean而不是reward。 - 构造一个三步 ATIF trajectory,其中第二步包含一个
ToolCall和对应ObservationResult;再故意把source_call_id改成不存在的值,记录校验错误。 - 假设一个 Job 有 4 个 Task、2 个 Agent、
n_attempts=3。计算 Trial 数;再解释把n_concurrent_trials从 4 改成 8 为什么不改变这个数。 - 为贯穿项目画一棵输出树,标出结构化故障报告应作为 Artifact、Verifier 输出、普通日志还是 trajectory 保存,并说明选择理由。
- 给定“两个 Trial Reward 为 1 和 0,第三个 Trial 缺失 Reward,默认 mean 为 1/3”的记录,分别写出排查真实 0 分与缺失 Reward 的前三步。