第 17 章:设计长周期与多步骤任务
凌晨两点,告警显示订单 API 的数据库超时率突然升高。一个合格的事故处理 Agent 不应只改掉一个数字:它要先保存证据,再提出可证伪的根因,实施有回滚路径的修复,完成回放验证,最后交付复盘。若把这些要求塞进一段超长 instruction,最终的 1 分只能说明“结尾看起来正确”,却无法回答 Agent 是在哪一步偏离了事实。
Multi-step Task 把同一 Trial 切成有序步骤,并让这些步骤操作同一个 Environment。Harbor v0.18.0 会为每一步分别运行 Agent 和 Verifier、保存 StepResult,再把已执行步骤的 Reward 聚合成 Trial 级 Reward。1 本章把贯穿项目中的生产事故拆成“发现、诊断、修复、验证、复盘”五步,重点不是增加流程感,而是建立可定位、可阻断、可审计的长周期评测。
读完本章,你应当能够:
- 建立一个至少三步、可被 Harbor 解析的 Multi-step Task;
- 解释共享文件状态、Agent 会话状态和分步 Artifact 的不同生命周期;
- 准确选择
mean或final,并预测提前终止后的 Trial Reward; - 用
min_reward、setup、healthcheck 和严格 Verifier 阻断无效的后续步骤; - 根据
step_results区分候选失败和评测基础设施失败。
17.1 先决定什么值得成为一步
切分步骤的标准不是“操作数量”,而是是否存在一个值得单独验证的状态边界。本章事故的五个边界如下。
| 步骤 | Agent 交付物 | Verifier 回答的问题 | 后续依赖 |
|---|---|---|---|
detect | evidence.json | 证据是否来自原始日志且字段完整 | 诊断只能引用已保存证据 |
diagnose | diagnosis.json | 因果链是否与证据和配置一致 | 修复必须针对已确认根因 |
repair | 新 config.json 与 rollback.json | 参数是否安全、回滚是否可执行 | 回放读取修复后的配置 |
validate | validation.json | 固定回放是否通过且未绕过验证器 | 复盘引用验证结果 |
postmortem | postmortem.md | 全链路产物是否一致 | 无 |
如果两个动作之间没有可验证状态,把它们拆开只会增加模型调用、日志和超时面。反过来,若后一步的合法输入取决于前一步的输出,就应在边界处设置 Verifier;否则,一个错误根因可能在最后一步被一份措辞漂亮的复盘掩盖。
Multi-step 也不是工作流引擎。它适合在一次 Trial、一个持续环境、同一个评测目标中观察规划和修正能力;跨团队审批、持续数天的人工等待和不可回放的生产变更,不应直接放进 Benchmark。
第一次改造时,先保留三个最有信息量的边界通常更稳妥:证据、变更、验收。只有当你能说明新增一步会隔离哪一种失败、产生什么独立产物以及由谁消费时,再把诊断或复盘拆出。步骤越多,模型调用、超时入口和 Artifact 数量越多;这些成本未必带来新的能力信号。评审每个边界时可以追问:“去掉这一步后,我是否仍能定位第一个错误状态?”如果答案是肯定的,它大概率只是流程装饰。
17.2 v0.18.0 的执行链
只要 task.toml 含非空的 [[steps]],Trial.create() 就会选择 MultiStepTrial;Task 根目录不再需要 instruction.md,每个声明的步骤必须有同名目录和 instruction.md,且必须能找到步骤级或共享的 OS 兼容测试脚本。23
目录的最小骨架是:
incident-long-horizon/
├── task.toml
├── environment/
│ ├── Dockerfile
│ ├── replay.py
│ └── incident/
│ ├── incident.log
│ └── config.json
├── tests/
│ ├── verify.py
│ └── replay_reference.py # Verifier 阶段才上传的可信回放实现
└── steps/
├── detect/
│ ├── instruction.md
│ ├── tests/test.sh
│ └── solution/solve.sh
├── diagnose/ # 同样包含 instruction/tests/solution
├── repair/
├── validate/
│ ├── workdir/
│ │ ├── validation-request.json
│ │ └── setup.sh
│ └── ...
└── postmortem/
└── ...
Environment 在 Trial 开始时建立一次,五步顺序使用同一容器文件系统。每一步开始时,Harbor 先清理本步 Agent 日志,把 steps/{name}/workdir/ 上传到容器的 WORKDIR,执行其中保留名称 setup.sh 的脚本,再运行可选的步骤 healthcheck;之后才依次执行 Agent、Artifact 收集、Verifier 和本步输出归档。4 这形成了下面的顺序:
共享 Environment
└─ step N: 上传 workdir → setup.sh → healthcheck
→ Agent → 收集 Artifact → Verifier → 归档
↓
判断是否继续
这里有三个容易混淆的事实。
第一,workdir/ 是覆盖式输入阶段,不是只读附件。后一步上传与前一步同名的文件时,可能改写 Agent 已产生的状态。官方实现直接把整个目录上传到当前 pwd 返回的 WORKDIR,因此应该使用不冲突的文件名,或让 setup 明确校验并迁移旧状态。5
第二,共享环境提供的是可靠的外部状态连续性,并不等价于可靠的对话记忆。Harbor 在 Trial 初始化时创建一个 Agent 实例,随后每步都调用该实例的 run();但是具体 Agent 可以在每次调用时重置对话。例如 v0.18.0 的 Terminus 2 明确重置每次运行的 trajectory、计数器和 Chat。67 因此跨 Agent 公平比较时,应把必要事实写入 /app/incident/ 这类可验证文件,而不能假定模型记得上一步对话。
第三,每步有独立的 agent_result、verifier_result、异常和计时信息。Multi-step Trial 不在顶层 agent_result 保存一次汇总对话;token 与成本汇总会遍历各 StepResult.agent_result。8 这也是失败定位时应从 step_results 开始,而不是只看最终 Reward 的原因。
17.2.1 用状态账本替代隐式记忆
长周期任务最危险的设计是让后一步“根据你刚才的判断继续”,却没有规定判断存在哪里。不同 Agent 对多次 run() 的会话处理不同,模型还可能在长上下文压缩时丢掉细节。更稳妥的做法是为每一步定义一个状态契约:谁创建、谁只能读取、谁允许修改、Verifier 校验哪些不变量。
本章示例把 /app/incident/ 当作状态账本。incident.log 是密封输入,任何一步都不得修改;evidence.json 由 detect 创建,之后只读;diagnosis.json 必须引用 evidence 中已有的 trace;rollback.json 保存修复前的值;validation.json 只能由受信任回放器生成。这样,后一步可以重新读取事实,Verifier 也能发现“诊断引用了不存在的 trace”或“回滚记录的是修改后的值”这类跨步骤不一致。
状态契约还要规定失败后的可观察性。若 repair 先覆盖配置、后写 rollback,并在两者之间超时,环境会留下半完成状态。Solution 和理想 Agent 都应采用“读取旧值 → 写临时文件 → 严格解析 → 原子替换 → 写 rollback”的次序;Verifier 则同时检查新配置和旧值副本。对数据库任务可以使用事务,对多容器任务可以用 collect hook 导出只读快照,但原则相同:让中间状态可以被辨认,不要让异常看起来像一次完整修复。
17.2.2 Artifact 是快照,不是新的共享存储
共享状态位于 Environment;Artifact 是把某一时刻的状态复制到 Trial 结果中,供审计和独立 Verifier 使用。v0.18.0 的 Runner 在 Agent 结束后、Verifier 开始前执行配置化 Artifact 收集,然后在 Verifier 结束后把本步日志与 Artifact 目录归档。9 同一提交随附文档把收集描述为发生在 verification 之后,与 Runner 顺序不一致;本书按锁定版本的可执行代码与测试解释,并把这一差异列入发布回归。10 由此可以推断,若希望快照反映候选提交状态,应让 Agent 在返回前落盘;不要指望共享 Verifier 修改业务文件后再由同一轮配置化收集捕获。
Task 级 Artifact 会在每个已执行步骤重复收集,适合观察持续演化的 incident/;Step 级 Artifact 只在对应步骤加入收集列表,适合 validation-request.json 这类阶段输入。相互包含的路径会造成含义和目标路径冲突,v0.18.0 的配置模型会检查 Task 级与 Step 级 Artifact 的碰撞。11 因此本例把复盘放在 /app/postmortem.md,而不是既收集 /app/incident 又声明 /app/incident/postmortem.md。
17.3 配置五步事故任务
动手前,先用锁定源码自带的三步 Task 做无 Docker 的结构预检。hello-multi-step-advanced 完整包含 scaffold、implement、document 三步,以及分步 instruction/tests/solution、第二步 workdir/setup、healthcheck、门禁和 Artifact;它是验证本机源码与本章目录心智模型是否一致的最小现成夹具。12
cd /path/to/harbor-v0.18.0
uv run --frozen python - <<'PY'
from pathlib import Path
from harbor.models.task.task import Task
task = Task(Path("examples/tasks/hello-multi-step-advanced"))
assert task.has_steps
assert [s.name for s in task.config.steps or []] == [
"scaffold", "implement", "document"
]
print("three-step task contract: ok")
PY
这个检查只解析配置和目录契约,不构建容器,也不证明 Solution 能通过。完整验证仍要在 Docker 可用时运行 harbor run -p examples/tasks/hello-multi-step-advanced -a oracle。先通过这个官方三步基线,再把同一机制扩展到下面的五步事故,而不是一开始同时调试目录、Runner 和业务 Verifier。
以下配置锁定本书的 Harbor v0.18.0 基线。[[steps]] 的出现顺序就是执行顺序;步骤名必须与 steps/ 下的目录名一致。multi_step_reward_strategy 位于 TOML 根部,不属于 [task] 或 [environment]。其 schema 在 v0.18.0 中只接受 mean 与 final。13
schema_version = "1.3"
multi_step_reward_strategy = "final"
artifacts = ["/app/incident"]
[task]
name = "book/incident-long-horizon"
description = "调查、修复并复盘一次由重试参数触发的事故"
keywords = ["multi-step", "incident-response"]
[environment]
build_timeout_sec = 600.0
network_mode = "no-network"
workdir = "/app"
cpus = 1
memory_mb = 2048
storage_mb = 4096
[agent]
timeout_sec = 300.0
[verifier]
timeout_sec = 60.0
[[steps]]
name = "detect"
min_reward = 1.0
[steps.agent]
timeout_sec = 120.0
[steps.verifier]
timeout_sec = 30.0
[steps.verifier.env]
STEP_NAME = "detect"
[[steps]]
name = "diagnose"
min_reward = 1.0
[steps.agent]
timeout_sec = 180.0
[steps.verifier.env]
STEP_NAME = "diagnose"
[[steps]]
name = "repair"
min_reward = 1.0
[steps.agent]
timeout_sec = 180.0
[steps.verifier.env]
STEP_NAME = "repair"
[[steps]]
name = "validate"
min_reward = 1.0
artifacts = ["/app/validation-request.json"]
[steps.agent]
timeout_sec = 180.0
[steps.verifier.env]
STEP_NAME = "validate"
[steps.healthcheck]
command = "test -f /app/incident/config.json && test -f /app/validation-request.json"
interval_sec = 1.0
timeout_sec = 5.0
retries = 3
[[steps]]
name = "postmortem"
artifacts = ["/app/postmortem.md"]
[steps.agent]
timeout_sec = 180.0
[steps.verifier.env]
STEP_NAME = "postmortem"
步骤级 timeout 和 user 在未设置时回退到 Task 顶层配置;步骤级 healthcheck 则是在 setup 之后、Agent 之前额外执行,不会替代 Environment 的顶层 healthcheck。14 这里给前四步都设置 min_reward = 1.0,因为任何中间状态不满足契约时,继续运行都会把“恢复前序状态”的负担混入下一项能力。
基础环境只含固定事故输入和确定性回放器:
FROM python:3.12-slim@sha256:c3d81d25b3154142b0b42eb1e61300024426268edeb5b5a26dd7ddf64d9daf28
WORKDIR /app
COPY incident/ /app/incident/
COPY replay.py /app/replay.py
RUN chmod 0444 /app/incident/incident.log /app/replay.py \
&& chmod 0644 /app/incident/config.json
该 OCI index digest 已通过 Docker 官方 Registry 的鉴权 HEAD 请求核对;digest 锁定内容,但发布者仍应按组织策略扫描镜像,而不能把“可复现”误当成“无漏洞”。15
初始 /app/incident/config.json 中 timeout_ms 为 50、max_retries 为 9;incident.log 则包含三个相同 trace 的 db_timeout 与一次 retry_budget_exhausted。这是教学性构造,不是实测事故数据。replay.py 读取配置,当 200 <= timeout_ms <= 500、1 <= max_retries <= 3 时写出 passed: true,否则写 passed: false;它不联网,也不调用模型。文件模式 0444 只能减少误改,不能阻止 root Agent 篡改,因此 Verifier 必须使用 Agent 阶段尚未上传的 /tests/replay_reference.py 重算结果,并校验原始日志的固定 SHA-256;不能把 /app/replay.py 自称为信任边界。
17.3.1 Instruction 只公开本步契约
五份 instruction 不应泄露 tests 的具体断言,但必须给出动作、输入、输出和边界。例如:
<!-- steps/detect/instruction.md -->
阅读 `/app/incident/incident.log` 和当前配置。把证据写入
`/app/incident/evidence.json`,必须包含 incident_id、受影响组件、
观察到的错误类型,以及非空 trace_ids。不要修改 config.json。
<!-- steps/repair/instruction.md -->
依据 evidence.json 与 diagnosis.json 修复 config.json。timeout_ms 必须在
200~500,max_retries 必须在 1~3;不得修改 incident.log 或 replay.py。
在 rollback.json 中保存修改前的两个值和恢复命令。
<!-- steps/postmortem/instruction.md -->
读取前四步产物,在 `/app/postmortem.md` 写复盘。至少包含 Evidence、
Root Cause、Change、Rollback、Validation 五个二级标题;事实与 JSON
产物保持一致,不要声称执行了未记录的操作。
diagnose 要求从 evidence.json 写出 root_cause 与因果链;validate 要求执行 python3 /app/replay.py --request /app/validation-request.json --output /app/incident/validation.json,而不是自行编造通过结果。每一步都显式引用前序文件,Agent 即使没有对话记忆,也能恢复工作上下文。
五份 solution/solve.sh 不是一份大脚本拆成五个文件,而是五个可独立解释的状态迁移。建议按下表编写,并让每个脚本使用 set -euo pipefail:
| Solution | 必做动作 | 不得做的事 |
|---|---|---|
detect | 从日志提取 incident、组件、错误和 trace,原子写入 evidence | 修改配置或预写根因 |
diagnose | 读取 evidence 与旧配置,写因果链及 retry-storm 根因 | 删除与假设冲突的证据 |
repair | 先保存 50/9,再把配置改到允许区间,写恢复命令 | 修改日志、回放器或测试 |
validate | 调用受信任回放器并保留其真实输出 | 直接写 passed: true |
postmortem | 从四份机器可读产物生成五节报告 | 声称执行产物中不存在的动作 |
每个 Solution 都应能在前序 Oracle Solution 完成后运行,并且重复运行不会破坏状态。例如 repair 第二次执行时不能把“已经修复的值”覆盖进 rollback;它应在 rollback 已存在且内容合法时保留原始 50/9。这个幂等性要求不是 Harbor 的隐式保障,而是 Task 作者为可调试性建立的契约。
17.3.2 用 setup 暴露阶段输入,而不是答案
validate/workdir/validation-request.json 保存固定回放样例数和允许的错误预算。配套 setup.sh 只做前置检查,并在完成后删除自己:
#!/usr/bin/env bash
set -euo pipefail
test -f /app/incident/evidence.json
test -f /app/incident/diagnosis.json
test -f /app/incident/rollback.json
python3 -m json.tool /app/incident/config.json >/dev/null
rm -- "$0"
setup 以本步 Agent user、在 WORKDIR 中执行;非零退出会记录本步异常,跳过本步 Agent、Artifact 收集和 Verifier,并终止后续步骤。16 因此 setup 适合验证评测前置条件,不适合把候选答案补齐。若 setup 会修改业务状态,应保证幂等,并把修改纳入 Artifact,否则重跑时难以解释状态来源。
17.3.3 每一步都要有可独立攻击的 Verifier
Task 根部的 tests/verify.py 提供严格 JSON 读取和五个验证函数;每个步骤的 tests/test.sh 只选择对应函数。共享 tests/ 会先上传到 /tests,步骤级 tests 随后覆盖同名文件,所以步骤级 test.sh 优先。17
#!/usr/bin/env bash
# 五个步骤都可使用这一内容;STEP_NAME 来自各步 verifier.env
set -u
exec python3 /tests/verify.py "${STEP_NAME:?STEP_NAME 未设置}"
验证器应把两类失败分开:
import json
import sys
from pathlib import Path
class CandidateFailure(Exception):
pass
class InfrastructureFailure(Exception):
pass
def strict_object(path: Path) -> dict:
if not path.exists():
raise CandidateFailure(f"候选产物缺失: {path}")
try:
value = json.loads(
path.read_text(encoding="utf-8"),
object_pairs_hook=lambda pairs: _no_duplicates(pairs),
parse_constant=lambda value: (_ for _ in ()).throw(
ValueError(f"非有限数: {value}")
),
)
except (OSError, UnicodeError, ValueError, json.JSONDecodeError) as exc:
raise CandidateFailure(f"候选 JSON 非法: {exc}") from exc
if not isinstance(value, dict):
raise CandidateFailure("顶层必须是 object")
return value
def _no_duplicates(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"重复键: {key}")
result[key] = value
return result
def verify_repair() -> None:
config = strict_object(Path("/app/incident/config.json"))
rollback = strict_object(Path("/app/incident/rollback.json"))
timeout = config.get("timeout_ms")
retries = config.get("max_retries")
if type(timeout) is not int or not 200 <= timeout <= 500:
raise CandidateFailure("timeout_ms 越界或类型错误")
if type(retries) is not int or not 1 <= retries <= 3:
raise CandidateFailure("max_retries 越界或类型错误")
if rollback.get("timeout_ms") != 50 or rollback.get("max_retries") != 9:
raise CandidateFailure("rollback 没有保存原始值")
def main(step: str) -> int:
try:
dispatch = {
"detect": verify_detect,
"diagnose": verify_diagnose,
"repair": verify_repair,
"validate": verify_validate,
"postmortem": verify_postmortem,
}
if step not in dispatch:
raise InfrastructureFailure(f"未知步骤: {step}")
if not Path("/tests/replay_reference.py").is_file():
raise InfrastructureFailure("可信回放实现未被打包")
if not Path("/app/incident/incident.log").is_file():
raise CandidateFailure("候选删除了事故输入")
dispatch[step]()
except CandidateFailure as exc:
Path("/logs/verifier/reward.txt").write_text("0\n")
print(f"candidate failure: {exc}", file=sys.stderr)
return 0
except InfrastructureFailure as exc:
print(f"infrastructure failure: {exc}", file=sys.stderr)
return 20
Path("/logs/verifier/reward.txt").write_text("1\n")
return 0
这里省略的 verify_detect 先核对原始日志 SHA-256,再检查 trace 是否确实存在;verify_diagnose 交叉检查证据、根因和原配置;verify_validate 运行 /tests/replay_reference.py 重算并比较输出,而不是只相信 Agent 写的 passed;verify_postmortem 会先调用前四个验证函数,再检查五个标题及关键值。这样,选择 final 时,最后一步的 Reward 才真正代表端到端状态。
候选输出缺失、格式错误、原始输入被 Agent 改写或断言不满足时,Verifier 写 0 并正常结束;Verifier 自己的可信回放实现没有被打包或测试分发损坏时,不写 Reward 并以非零码结束。前者是被测对象的能力结果,后者会让本步缺少 verifier_result 并记录异常,不应混入模型失败率。若无法判断文件是被 Agent 删除还是打包时就缺失,应先用 Oracle 与启动前 healthcheck 建立基线,再分类,不能仅凭“文件缺失”猜原因。
17.4 mean、final 与提前终止
17.4.1 mean 是按 Reward key 分别求均值
未设置 multi_step_reward_strategy 时,多步骤 Task 默认使用 mean。对每个至少出现一次的 Reward key,Harbor 以“产生了 verifier_result 的步骤数”为分母;某一步有 verifier_result 但缺少该 key 时,该步为该 key 贡献 0;完全没有 verifier_result 的步骤不进入分母。18
假设三个已执行步骤的 Reward 是:
detect: {correctness: 1.0, evidence: 1.0}
diagnose: {correctness: 0.5}
repair: 无 verifier_result
则这是教学性计算,不是实测输出:correctness = (1.0 + 0.5) / 2 = 0.75,evidence = (1.0 + 0) / 2 = 0.5。分母不是声明的五步,也不是为每个 key 单独统计出现次数。由此可以推断:各步骤使用完全不同的 key 会互相稀释,除非这种 0 分正是你的评分意图。
mean 适合每一步都测量同一尺度、每一步的过程质量都应计分的任务。它不支持步骤权重;需要权重时,应让各步输出同尺度后在自定义 Verifier/Metric 中实现并测试,而不是把 key 名当作权重机制。
17.4.2 final 指最后实际执行的一步
final 原样采用 step_results[-1].verifier_result,不会合并更早步骤。正常跑完时,它是 postmortem;若在 diagnose 因 min_reward 提前停止,它就是 diagnose 的结果,而不是配置中名为 postmortem 的步骤。19 因而 final 只适用于最后一步 Verifier 会重新验证全链路的设计。本章的 verify_postmortem 正是为此存在。
17.4.3 min_reward 是控制流,不是聚合权重
标量 min_reward = 1.0 检查 rewards["reward"];字典形式如 min_reward = { correctness = 0.8, safety = 1.0 } 会逐一检查声明的 key。任一值低于阈值或 key 缺失都停止,缺失按负无穷处理;全局关闭 verification 时,阈值检查被忽略。20
还要注意 v0.18.0 的一个细节:Runner 只有在“本步记录了异常且没有 verifier_result”时无条件停止。如果 Agent 已记录异常,但 Verifier 仍成功产生结果,是否继续将由 min_reward 决定;未设置阈值时甚至可能继续。21 这与“任何 Agent 异常都必然停止”的直觉不同。因此,对有前序依赖的步骤,应同时做到:Verifier 对不完整状态稳定给出 0,且该步骤设置 min_reward。setup 或 healthcheck 失败发生在 Agent 之前,不运行 Verifier,所以会因“异常且无结果”直接终止。
把传播规则写成决策表更容易审查:
| 本步结果 | 有 min_reward 时 | 无 min_reward 时 | Trial 级 final |
|---|---|---|---|
| Reward 达阈值、无异常 | 继续 | 继续 | 暂指向本步,若后续执行则被替换 |
| Reward 低于阈值 | 停止 | 继续 | 使用本步低 Reward |
| Verifier 无结果并记录异常 | 停止 | 停止 | 为 None |
| Agent 有异常,但 Verifier 仍给达标结果 | 继续 | 继续 | 使用本步结果或后续结果 |
| setup/healthcheck 异常 | 停止 | 停止 | 本步无 VerifierResult,因此为 None |
表中的最后一列说明一个重要边界:final 不会自动为未运行步骤补 0。若基础设施错误导致最后实际步骤没有 VerifierResult,Trial 级结果是 None;下游统计应把它放入错误分母,而不是擅自改成候选 0 分。若只是候选状态不合格,Verifier 应尽量稳定地产生 0,使提前终止仍保留可解释的能力信号。
17.4.4 在锁定源码上运行语义探针
下面的探针只用于核对 v0.18.0 行为,调用了带下划线的内部方法,不能当作扩展 API。它构造三个步骤,直接验证缺 key 补 0、无 VerifierResult 不进入 mean 分母,以及 final 选择最后实际结果:
from types import SimpleNamespace
from harbor.models.task.config import MultiStepRewardStrategy
from harbor.models.trial.result import StepResult
from harbor.models.verifier.result import VerifierResult
from harbor.trial.multi_step import MultiStepTrial
trial = object.__new__(MultiStepTrial)
a = StepResult(
step_name="detect",
verifier_result=VerifierResult(
rewards={"correctness": 1.0, "evidence": 1.0}
),
)
b = StepResult(
step_name="diagnose",
verifier_result=VerifierResult(rewards={"correctness": 0.5}),
)
c = StepResult(step_name="repair", verifier_result=None)
trial._result = SimpleNamespace(step_results=[a, b, c])
mean = trial._aggregate_step_rewards()
assert mean is not None
assert mean.rewards == {"correctness": 0.75, "evidence": 0.5}
trial.task = SimpleNamespace(
config=SimpleNamespace(
multi_step_reward_strategy=MultiStepRewardStrategy.FINAL
)
)
assert trial._select_multi_step_reward() is None
trial._result.step_results = [a, b] # 模拟在 diagnose 后提前停止
assert trial._select_multi_step_reward() is b.verifier_result
print("multi-step reward semantics: ok")
在锁定源码根目录保存为临时脚本并执行 uv run --frozen python probe.py。预期只打印最后一行;任何断言失败都表示当前源码不是本章基线,或语义已经发生变化。
17.5 运行、验收与失败定位
先在 Harbor v0.18.0 源码环境或安装了同版的项目环境中做静态加载:
cd /path/to/harbor-v0.18.0
uv run --frozen python - <<'PY'
from pathlib import Path
from harbor.models.task.task import Task
task = Task(Path("/path/to/incident-long-horizon"))
assert task.has_steps
assert [step.name for step in task.config.steps or []] == [
"detect", "diagnose", "repair", "validate", "postmortem"
]
assert task.config.multi_step_reward_strategy.value == "final"
print("task contract: ok")
PY
然后运行 Oracle,逐步证明 Solution 与 Verifier 能在共享状态上闭环。Oracle Agent 在 Multi-step Task 中按步骤索引寻找 steps/{name}/solution/solve.sh;每次 run() 后索引前移。22
harbor run -p /path/to/incident-long-horizon -a oracle -e docker
这条命令会真实构建和启动 Docker 环境。成功标准不是在书中预设某段控制台文字,而是检查生成的 Trial:共有五个 step_results;每步有 verifier_result.rewards.reward == 1;最终 Reward 为 1;steps/{name}/artifacts/ 中能看到 /app/incident 的阶段快照;最终快照中的配置、回放和复盘互相一致。v0.18.0 在每步收集 Task 级、Trial 级和 Step 级 Artifact,并将本步归档到 steps/{name}/artifacts/。23
对真实 Agent 再运行:
harbor run \
-p /path/to/incident-long-horizon \
-a "<agent>" \
-m "<provider/model>" \
-e docker
模型名、凭据和费用取决于所选 Agent/Provider,本章不提供虚构结果。长周期任务的预算应按已执行步骤累计:每步都有独立 Agent timeout 和上下文消耗,Trial 的 token/cost 汇总则来自实际存在的各步 agent_result。先用 Oracle 和低成本模型跑少量 Trial,再决定并发和重复次数。
排查时按下面顺序,不要从最终 Reward 倒猜:
- 找到最后一个
step_results,确认 Trial 是正常结束还是提前停止; - 查看该步
exception_info,判断失败发生于 setup、healthcheck、Agent 还是 Verifier; - 若有
verifier_result,读取 Reward 和本步 verifier 日志;若没有,优先排查测试、Artifact、容器和超时; - 对比当前步与前一步的 Artifact 快照,找出第一个状态偏差;
- 再阅读该步 trajectory,判断是未观察证据、推理错误、工具错误还是时间耗尽。
例如,repair 得到 Reward 0 且没有异常,是候选修复不符合边界;validate 的 setup 报 JSON 解析错误,是前序候选留下了非法状态,但失败由 setup 暴露;Verifier 报“密封的事故输入缺失”且没有 Reward,则是基础设施或任务打包问题。不要把第三种情况记为模型 0 分。
注意:禁用 verification 会同时失去
min_reward门禁。它适合检查 Agent 是否能启动,不适合验证长周期控制流。
警告:不要让 Agent 直接操作真实生产凭据或真实告警端点。长周期 Benchmark 应使用密封日志、模拟服务和最小权限凭据;Artifact 还要在发布前做密钥与个人信息扫描。
17.6 发布门与回归矩阵
Multi-step Task 至少要通过四类回归:
| 用例 | 注入 | 期望 |
|---|---|---|
| Oracle 全通过 | 逐步执行五份 Solution | 五步都运行,最终 Reward 为 1 |
| 前序候选失败 | detect 输出空 trace_ids | detect Reward 为 0,只产生一个 StepResult |
| setup 失败 | 删除 diagnosis.json 后进入 validate | 本步有异常、无 VerifierResult,后续不运行 |
| Verifier 基建失败 | 从 Task 包中删除 tests/replay_reference.py | 不写 Reward,标为基础设施失败 |
| 聚合语义 | 构造缺 key 与无结果的 StepResult | mean 分母和补 0 行为符合源码 |
| 终点语义 | 中间步骤触发门禁且策略为 final | 使用最后实际执行步骤的结果 |
本章书稿生成时可以运行不依赖 Docker 的 Task 解析、Runner 单元测试和 Verifier fixture;但完整 Oracle 闭环必须有可用 Docker daemon。若当前机器没有 Docker,这项必须明确留为发布门,不能把静态通过描述成容器实测。
17.7 本章小结
- Multi-step 的价值是建立可验证的状态边界,而不是把长 instruction 机械切段。
- Environment 文件系统跨步骤持续;对话记忆取决于 Agent 实现,关键状态应落盘并验证。
mean对有 VerifierResult 的步骤按 key 求均值,缺 key 补 0;final采用最后实际执行步骤的完整结果。min_reward控制是否继续,不改变聚合公式;依赖前序状态的步骤应配合严格 Verifier 使用。- 分步 Artifact、
StepResult和 trajectory 共同提供从状态偏差到推理偏差的定位链。 - 候选失败应产生确定性 0 分;测试、密封输入或容器故障应保留为基础设施异常。
17.8 练习
- 把示例缩成
detect、repair、validate三步,分别使用mean与final,手工列出[1, 0, 1]时的 Trial Reward,并用 v0.18.0 单元测试验证。 - 为
diagnose增加多维 Reward{correctness, evidence},故意让detect缺少evidencekey,解释mean结果为什么被补 0 稀释。 - 在
validate/workdir/加入与前一步同名的config.json,观察覆盖风险;随后改成无冲突文件名并让 setup 校验 SHA-256。 - 注入三种失败:候选 JSON 重复键、setup 非零退出、Verifier 受信任文件缺失。根据
StepResult写出自动分类规则。 - 设计一个不依赖对话历史的“复盘”步骤:只允许读取前四步 Artifact,验证它仍能生成与事实一致的报告。