第 4 章:设计一个可评测的 Agent 任务
“给你一些服务日志,找出故障并写一份报告。”这句话适合交代工作,却不适合直接成为 Benchmark。哪个目录里有日志?“故障”指最早异常、直接失败点,还是可修复的根因?报告写到哪里、采用什么格式?如果两个候选原因都说得通,怎样完成?这些问题若只藏在测试代码里,低分测到的可能是猜题能力;若把正确组件名和关键事件写进示例,满分又可能只是复制答案。
本章把贯穿项目的第一项业务需求——“分析服务日志并生成结构化故障报告”——收紧为一个可评测的 Harbor Task。重点不是先写 Dockerfile 或 Verifier,而是建立测量契约:要测哪种能力,Agent 看见什么,必须留下什么结果,哪些事实应公开成接口,哪些答案必须只留在评分侧。后续第 5~7 章再实现环境、Solution 和 Verifier。
读完本章,读者应当能够:
- 把业务场景拆成能力目标、场景包装与控制变量;
- 编写不依赖隐藏假设、又不泄露标准答案的
instruction.md; - 定义输入、输出、完成条件与边界情况,并把它们映射到可观察事实;
- 识别答案泄露、关键词捷径、测试篡改和“靠猜常见根因”等失败设计;
- 用 Harbor v0.18.0 初始化并静态加载一个单步 Task;
- 使用可执行的评审清单决定任务能否进入环境实现阶段。
4.1 先定义能力,不要先写故事
一个 Task 同时承载两种东西:能力目标是希望从 Agent 行为中推断的能力;场景包装是让这种能力有具体输入和语境的题面。二者相关但不等价。
以本章任务为例,服务名叫 checkout-api、日志位于 /workspace/input/,都属于包装。真正要测的是:Agent 能否跨文件关联事件、排除干扰项、找出一条解释失败现象的主要因果链,并以机器可读结构引用证据。把服务名换成 billing-worker,目标能力不应改变。反过来,若只把日志从 100 行扩充到 10 万行,任务或许更慢,却不一定增加因果诊断难度。
先写一张能力—证据表:
| 能力维度 | Agent 必须产生的可观察事实 | 不应冒充该能力的代理指标 |
|---|---|---|
| 结构化读取 | report.json 能按公开契约解析 | 文件存在、字数很多 |
| 因果诊断 | root_cause 与作者侧事实一致 | 出现“数据库”“超时”等常见词 |
| 证据归因 | 引用的 event_id 确实存在,并共同支撑结论 | 复制整段日志或只引用最后一条 ERROR |
| 影响判断 | 唯一受影响的 request_id 集合正确 | 简单统计 ERROR 行数 |
| 输入完整性 | 分析前后输入日志内容一致 | 声称“只读分析” |
这里刻意没有把“报告文笔好”列入首个核心能力。摘要和建议仍可保留,便于人读;但若首版 Verifier 只能确认字段非空,就不能把该结果宣传为对写作质量的测量。第 11、12 章再为主观质量增加独立评分维度。
4.1.1 明确本章的测量边界
本章 Task 的能力目标可以写成一句可证伪的声明:
给定一组格式已知、证据充分且包含干扰项的本地服务日志,Agent 能识别唯一的主要根因,引用跨文件事件证据,计算受影响请求集合,并生成符合公开结构的 JSON 报告。
这句话同时排除了若干内容:不要求修复服务,不连接生产环境,不从互联网查故障案例,不评价服务恢复,不把工具调用次数作为成功条件,也不处理多个同等根因。它们不是不重要,而是会把首个 Task 变成几个能力的混合测量。
“唯一主要根因”和“证据充分”是任务作者必须保证的前置条件,不能留给 Agent 猜。若两个独立根因都能解释全部失败,测试却只接受一个,问题在 Task;若某个输入文件偶尔缺失,应先归为环境构建失败,而不是让 Agent 因不可见证据得 0 分。
4.1.2 从完成条件反推输入
先列出不可妥协的完成条件,再反推输入应提供什么:
- 输出必须是
/workspace/report.json中的单个 UTF-8 JSON 对象; root_cause.component和root_cause.category能与作者侧事实比较;- 每条证据通过“文件名 + 全局唯一
event_id”引用,而不依赖易变的行号; affected_request_ids去重并排序,使相同语义有稳定表示;- 输入日志在运行前后内容一致。
于是输入日志至少要采用明确编码,并为事件提供稳定 ID;用于计算影响的记录要有 request_id;构成因果链的时间戳需要同一时区;分类与因果关系还要有不依赖自然语言猜测的公开表示。所有这些字段必须在题面中解释。否则,Verifier 对它们的检查就是隐藏要求。
4.2 把 instruction 写成公开接口
Harbor 单步 Task 从 instruction.md 读取给 Agent 的任务文本;task.toml 保存配置与元数据。v0.18.0 的 Task 加载时解析 TOML,再读取单步指令;有效目录还需要 task.toml、environment/、instruction.md,默认共享 Verifier 场景还要有与目标操作系统匹配的测试入口。1 这说明“目录可加载”和“题目设计正确”是两层验收:前者能由代码检查,后者仍要做语义评审。
一份合格 instruction 至少回答五个问题:目标是什么、输入在哪里且如何解释、输出写到哪里且格式怎样、什么算完成、哪些动作禁止。Harbor v0.18.0 随附的 create-task 指南也要求明确目标、输出和约束,同时只描述完成状态,不泄露测试实现。2
4.2.1 输入契约
本任务把 /workspace/input/*.log 定义为 UTF-8 JSON Lines。每行是独立 JSON 对象,具有 timestamp、event_id、component、level 和 message;与请求关联的记录还具有 request_id、event_type 和 outcome。事件若由另一个事件直接导致,就含有 caused_by_event_id,其值是直接原因的 event_id。每个 ERROR 或 FATAL 事件还含有 failure_code。event_id 在全部输入文件中唯一。
timestamp 仅接受 RFC 3339 UTC 子集 YYYY-MM-DDTHH:MM:SS[.fraction]Z:T/Z 大写,YYYY 四位,Gregorian 日期合法,HH 为 00~23,MM/SS 为 00~59,小数秒省略或含一位以上数字。禁止数值偏移、空格、缺秒、逗号小数、小写 t/z 与 second = 60;环境和 Verifier 均排除 leap second。RFC 3339 §5.6~5.7 的一般语法容纳小数秒,并在规定条件下容纳小写字母与 60;这是 Task 明示的更窄子集,不是声称 RFC 3339 全面禁止 60。3
对每个 request_id,将 request.completed 按解析后的 UTC 时刻、同刻再按 event_id 排序,不得比较原始时间戳;末项为终态,failure 纳入目标集合 T,success 排除,即使此前失败过。
failure_code 采用穷举词表,每个代码只映射到一个类别,因此不再需要把“DNS 故障算依赖还是网络”之类的优先级藏进 Verifier:
root_cause.category | 允许映射到该类别的 failure_code |
|---|---|
network | dns_failure、route_failure、connection_failure、connection_reset、tls_transport_failure |
resource | cpu_exhausted、memory_exhausted、disk_exhausted、file_descriptor_exhausted、thread_exhausted、connection_pool_exhausted、quota_exhausted |
configuration | missing_setting、invalid_setting、deployment_misconfiguration |
dependency | dependency_unavailable、dependency_error、invalid_upstream_response |
application | unhandled_exception、code_defect、data_processing_error |
从任一 T 中的事件出发,反复沿 caused_by_event_id 回溯,直到遇到没有该字段的事件;得到的完整序列叫该目标的因果路径。作者侧必须保证:所有引用都存在;图中没有环;每条目标路径都终止;每个目标事件都有父事件,且 T 中任意两事件不存在祖先关系;所有目标路径的无父事件是同一个 ERROR 或 FATAL 事件;该事件具有合法 failure_code;路径并集跨越至少两个文件。这个共同无父事件就是唯一主要根因,不再使用无法机械判断的“最早可操作”措辞。
这些信息是接口,不是答案。公开日志格式不会告诉 Agent 哪个组件有错;反而能避免把解析格式、时区和去重规则变成无意的谜题。作者侧还要保证:
- 目标路径符合上述唯一性与跨文件约束;
- 有真实但不构成根因的 WARNING/ERROR 干扰项;
- 不包含真实或合成凭据、个人信息或可访问生产系统的标识;
- 不存在
expected.json、备份文件、注释或镜像层残留直接给出答案。
最后一项尤其容易误解。日志当然必须含有推导答案所需的证据;“不泄露答案”指的是不另外暴露已归纳好的结论、Gold patch、预期输出或评分规则,而不是删掉可解线索。
这里的因果指针与故障代码是首个教学 Task 的受控标注,不代表真实日志天然具有完整因果图,也不把开放世界中的根因诊断简化为普遍适用的唯一语义。它牺牲一部分写实性,换取首版评分契约的公开、唯一和可复现;后续若移除这些标注,就必须引入经过独立复核的人工 Gold 与相应的不确定性处理,不能沿用本章的精确判定承诺。
本版也不测脱敏。既然输入不含真实或合成秘密,Verifier 就不应临时发明“疑似凭据”正则。未来若要加入安全处理能力,应先在输入中定义显式的 sensitive 标记,声明值均为合成数据,再公开固定占位符、禁止原样出现的字段范围和逐值比较规则;在那之前,它只是环境数据治理要求,不是评分项。
4.2.2 输出契约
首版报告使用下列字段。公开字段、类型和枚举是契约;具体组件、类别、事件 ID 与请求 ID 是实例答案,不出现在 instruction、示例或元数据中。
| 路径 | 类型与约束 | 作用 |
|---|---|---|
schema_version | 字符串,固定为 "1.0" | 区分报告契约版本 |
incident_summary | 非空字符串 | 面向人的简要现象 |
root_cause.event_id | 字符串,精确等于共同无父事件的 event_id | 唯一指明根因事件 |
root_cause.component | 区分大小写,逐字等于根因事件的 component 字符串 | 避免别名与规范化猜测 |
root_cause.category | 由根因事件的 failure_code 按公开表唯一映射 | 稳定分类 |
root_cause.explanation | 非空字符串 | 解释因果联系,不要求固定措辞 |
evidence | 全部目标因果路径的事件并集 | 每项精确包含 file、event_id、role |
impact.affected_request_ids | T 中 request_id 的精确去重集合,按字典序排列 | 最终成功的请求明确排除 |
recommended_actions | 1~3 个非空字符串 | 记录建议;首版只验结构,不声称测量质量 |
evidence 不是“至少给几条”的开放集合,而是路径并集的唯一规范化表示:每个事件恰好出现一次,既不能遗漏,也不能加入路径外事件;file 必须逐字等于包含该事件的输入文件基本名;role 对共同无父事件取 root,对 T 中的事件取 target-failure,对其余严格中间事件取 causal-link。数组沿用上面的比较规则:先按解析后的 timestamp 时刻升序,时刻相同再按 event_id 字典序。输入保证目标事件都有父事件,所以三种角色不会冲突。
为避免“宽松解析器接受、严格 Verifier 拒绝”,键集合也公开固定:顶层只能有 schema_version、incident_summary、root_cause、evidence、impact、recommended_actions;root_cause 只能有 event_id、component、category、explanation;impact 只能有 affected_request_ids;证据项只能有 file、event_id、role。任何层级都不接受未知字段。JSON 对象键本就必须唯一;两个证据项也不得引用同一 event_id。摘要、解释和建议仍只检查类型、非空与数量,不把措辞质量算入首版核心分数。
完成条件既不能只写“生成 JSON”,也不应规定“先用 jq,再用 Python”。前者不足以验收语义,后者把实现过程误当结果。Agent 可以使用 rg、jq、Python 或其他可用工具;只要产物满足同一终态,方法不应影响正确性。
4.2.3 处理边界与隐藏假设
Harbor 的任务分析 rubric 会把“测试期待具体参数、格式或结构,而 instruction 没有精确定义”以及“测试检查需要 Agent 自行猜测的行为”视为任务规范问题。4 写完 instruction 后,可逐项问:
| 模糊问题 | 本任务的显式决定 |
|---|---|
| “全部日志”包括什么? | 只读取 /workspace/input/ 直属目录中的 *.log |
| 时间戳接受什么格式? | 仅上述 UTC 子集;大写 T/Z、合法日历、固定范围、拒绝 leap second |
| 时间顺序怎样比较? | 解析后比较 UTC 时刻,同刻再按 event_id;不按原字符串或文件顺序 |
| 重试怎样计数? | 每个请求只看最后一条完成记录;最终成功排除,最终失败计一次 |
| 什么是“主要根因”? | 从每个目标失败沿 caused_by_event_id 回溯得到的共同无父事件 |
| 类别冲突怎样解决? | failure_code 穷举且只属于一个类别,按公开映射表确定 |
| 哪些事件是证据? | 恰好是所有目标因果路径的并集,不多不少 |
| 行号变化怎么办? | 使用稳定 event_id,不要求物理行号 |
| 可以修服务或改日志吗? | 不可以;这是只读分析 Task |
| 证据不足怎么办? | 不作为本任务实例;作者应判定环境/数据不合格 |
| 建议写得多是否分更高? | 不会;首版只检查 1~3 项与非空 |
这个定义没有声称“最早出现的日志就是根因”。例如,某请求超时之前总会出现进程启动事件,但只有被因果指针纳入目标路径的事件才参与 Gold。环境设计者仍要检查指针是否忠实表达了题目故事;若两个独立评审者会为同一目标建立不同的父边,就应修改输入标注,而不是让 Verifier 猜自然语言因果。
4.3 最小可执行设计
下面只初始化并静态评审 Task,不运行 Agent,也不声称任务已经可解。Harbor v0.18.0 的公开 CLI 注册了 harbor task init [NAME],支持 -p/--tasks-dir、--description 与可重复的 --author;随附教程使用同一子命令,该版本测试确认默认会生成 instruction、配置、Dockerfile、pytest 测试入口和 Solution 模板。5
从 ~/harbor-lab 运行。前置条件是第 3 章已安装 harbor==0.18.0,目标目录尚不存在。命令会创建 ~/harbor-lab/tasks/log-incident-report/;成功时打印 Task initialized,目录冲突或名称不合法时应先清理设计而非覆盖已有任务。
mkdir -p ~/harbor-lab/tasks
cd ~/harbor-lab
harbor --version
harbor task init llmkb/log-incident-report \
-p tasks \
--description \
"Diagnose a service incident from local logs and write a structured report." \
--author "Book Team"
find tasks/log-incident-report -maxdepth 3 -type f | sort
在 v0.18.0 上,初始化结果包括:
log-incident-report/
├── .gitignore
├── README.md
├── instruction.md
├── task.toml
├── environment/
│ └── Dockerfile
├── solution/
│ └── solve.sh
└── tests/
├── test.sh
└── test_outputs.py
Harbor 官方 Cookbook 的 simple-task 也展示了 instruction、环境、测试和 Solution 四层最小结构。它是独立仓库、没有随 Harbor v0.18.0 标签锁定;本章核对的固定提交仍使用兼容旧名 version = "1.0",而 v0.18.0 初始化器实际输出 schema_version = "1.3"。因此下面以 v0.18.0 模型和实测 CLI 输出为准,不逐字复制 Cookbook 配置。67
把 task.toml 收紧为:
schema_version = "1.3"
[task]
name = "llmkb/log-incident-report"
description = "Diagnose a service incident from local logs and write a structured report."
authors = [{ name = "Book Team" }]
keywords = ["log-analysis", "incident-response", "json", "causal-diagnosis"]
[metadata]
difficulty = "medium"
difficulty_explanation = "Teaching hypothesis: requires cross-file event correlation and distractor rejection."
category = "system-administration"
tags = ["log-analysis", "incident-response", "structured-output"]
capability_target = "causal-log-analysis"
[verifier]
timeout_sec = 120.0
[agent]
timeout_sec = 600.0
[environment]
build_timeout_sec = 600.0
[task] 是包信息:名称必须采用 org/name,并可包含描述、作者和关键词。[metadata] 在 v0.18.0 的 Task 模型中是任意字典;Viewer 会专门读取其中字符串类型的 difficulty、category 和字符串列表 tags 作为筛选项。8 difficulty_explanation 和 capability_target 是本书的审计约定,不是 Harbor 自动校准出来的结论。更不能在元数据中写 root_cause = "...",因为元数据会随 Task 分发。
把 instruction.md 替换为下面的完整公开契约:
# 分析服务日志并生成结构化故障报告
`/workspace/input/` 直属目录中的 `.log` 文件包含完成本次诊断所需的全部证据。
这些文件采用 UTF-8 JSON Lines:每行都有 `timestamp`、`event_id`、
`component`、`level` 和 `message`;与请求相关的记录另有 `request_id`、
`event_type` 和 `outcome`。有直接原因的事件另有 `caused_by_event_id`,其值为
直接原因的 `event_id`;每个 `ERROR` 或 `FATAL` 事件另有 `failure_code`。
`event_id` 在所有输入文件中唯一。
`timestamp` 仅接受 `YYYY-MM-DDTHH:MM:SS[.fraction]Z`:`T/Z` 大写,`YYYY`
四位,Gregorian 日期合法,`HH` 为 `00`~`23`,`MM/SS` 为 `00`~`59`,
小数秒省略或含一位以上数字。拒绝数值偏移、空格、缺秒、逗号小数、小写
`t/z` 与 leap second `second = 60`;输入不生成 leap second。
每个 `request_id` 的 `request.completed` 按解析后的 UTC 时刻、同刻再按
`event_id` 排序,不比较原始字符串。末项为终态;最终 `failure` 才是目标失败,
最终 `success` 不受影响。
从每个目标失败事件开始反复沿 `caused_by_event_id` 回溯。输入保证引用存在、
图无环,而且所有路径终止于同一个没有 `caused_by_event_id` 的 `ERROR` 或
`FATAL` 事件;这个共同无父事件就是唯一主要根因。输入还保证每个目标事件
都有父事件,任意两个目标事件不存在祖先关系,且所有目标路径的事件并集
来自至少两个文件。
根因类别由根因事件的 `failure_code` 唯一确定:
- `network`:`dns_failure`、`route_failure`、`connection_failure`、
`connection_reset`、`tls_transport_failure`;
- `resource`:`cpu_exhausted`、`memory_exhausted`、`disk_exhausted`、
`file_descriptor_exhausted`、`thread_exhausted`、
`connection_pool_exhausted`、`quota_exhausted`;
- `configuration`:`missing_setting`、`invalid_setting`、
`deployment_misconfiguration`;
- `dependency`:`dependency_unavailable`、`dependency_error`、
`invalid_upstream_response`;
- `application`:`unhandled_exception`、`code_defect`、
`data_processing_error`。
把报告写到 `/workspace/report.json`。报告必须是单个 JSON 对象,不要使用
Markdown 代码围栏。顶层只能包含 `schema_version`、`incident_summary`、
`root_cause`、`evidence`、`impact`、`recommended_actions`,任何层级均不接受
未知字段。
报告必须包含:
- `schema_version`:字符串 `"1.0"`;
- `incident_summary`:非空字符串;
- `root_cause`:只能包含 `event_id`、`component`、`category`、`explanation`。
`event_id` 必须等于共同无父事件的 ID;`component` 必须区分大小写并逐字
等于该事件的 `component`;`category` 按上表映射;`explanation` 为非空字符串;
- `evidence`:恰好包含全部目标路径的事件并集,每个事件一次。每项只能包含
`file`、`event_id`、`role`;`file` 是事件所在输入文件的基本名;共同无父事件的
`role` 为 `root`,目标失败事件为 `target-failure`,其余中间事件为
`causal-link`。数组按解析后的时刻、同刻再按 `event_id` 排序;不得缺项、
加项或重复;
- `impact`:只能包含 `affected_request_ids`。该数组必须精确等于所有目标失败
事件中 `request_id` 的去重集合,并按字典序排列;
- `recommended_actions`:1~3 个非空字符串。
只读分析输入,不要修改 `/workspace/input/`,不要启动或修复服务。
无需使用外部信息。
注意:上面的类别枚举、字段名和排序规则是公开接口,不是答案泄露。组件名、正确类别、关键
event_id、受影响请求集合和因果解释才是实例答案。本章不会给出这些值。
注意:instruction 中“无需使用外部信息”描述的是任务语义,不会自动切断网络;网络策略由配置字段而不是自然语言指令实施。9 本章的配置骨架尚未宣称网络受限;真正的阶段网络策略和 Provider 边界留到第 8 章。在那之前,不要把该骨架当成已经抵抗外部查找的评测环境。
最后做一次无 Docker、无模型费用的静态加载。下面从 Task 根目录运行;若本机未缓存包,uvx 会访问包索引。它固定 Python 3.13,与第 3 章的安装策略一致。预期只打印 Task 名、schema 版本和 static design gate: PASS。
cd ~/harbor-lab/tasks/log-incident-report
uvx --python 3.13 --from "harbor==0.18.0" python - <<'PY'
from pathlib import Path
from harbor.models.task.task import Task
task = Task(".")
assert task.name == "llmkb/log-incident-report"
assert task.config.schema_version == "1.3"
assert task.config.metadata["capability_target"] == "causal-log-analysis"
instruction = Path("instruction.md").read_text()
for required in (
"/workspace/input/",
"/workspace/report.json",
"event_id",
"caused_by_event_id",
"failure_code",
"YYYY-MM-DDTHH:MM:SS[.fraction]Z",
"leap second",
):
assert required in instruction, f"missing public contract: {required}"
for leaked_interface in ("/solution", "/tests", "reward.txt"):
assert leaked_interface not in instruction, f"grading interface leaked: {leaked_interface}"
print(task.name, task.config.schema_version)
print("static design gate: PASS")
PY
这个检查只能证明目录和 TOML 可被 v0.18.0 加载,且几个公开接口存在;模板测试甚至还没有实现本任务的语义。它不能证明日志可解、答案未藏在镜像里、Verifier 无漏洞或难度合适。把静态加载写成“Task 已通过评测”会混淆设计门禁与运行验收。
4.4 答案泄露与任务捷径
Agent 的高 Reward 只有在“解决任务”是最便宜的可靠路径时才有解释力。Harbor v0.18.0 自带的 Adapter 评审规则把威胁压缩成两个不变量:在整个 Trial 生命周期中,Agent 既不能读取 Ground Truth,也不能直接或间接影响权威通过信号;检查范围包括 instruction、Dockerfile、运行时镜像、测试执行链和 Reward 写入。10
4.4.1 泄露面
逐面检查比只搜索 answer 更可靠:
| 泄露面 | 本任务的防线 |
|---|---|
| instruction 示例 | 只给结构和枚举,不给正确组件、类别、事件或请求 ID |
| Task 名称/描述/元数据 | 使用中性 log-incident-report,不编码根因或上游实例 ID |
| 环境文件 | 不放 expected.json、Gold patch、带答案注释的生成脚本或备份 |
| 镜像历史 | 不先 COPY 答案再删除;删除后的层仍可能保留内容 |
| Solution | 不复制进 Agent 镜像,不从 instruction 引用 |
| tests | Agent 主调用期间不暴露断言与期望值;全生命周期也不信任 Agent 可写的同名路径 |
| 网络/来源标识 | 不暴露可搜索到原题的 URL、Benchmark 名或原始 task ID |
在 Task 根目录可先做启发式搜索:
rg -n -i \
'gold|expected|answer|root.?cause|reward\.txt|/tests|/solution|https?://' \
instruction.md task.toml environment
预期不是机械地“零匹配”:root_cause 作为公开字段可以出现。每个匹配都必须人工判断它是接口名、证据,还是已经归纳好的答案;二进制、镜像层和构建缓存还需要后续单独审计。
4.4.2 把防篡改边界扩展到整个 Trial
“Agent 调用结束后才上传测试”只能减少直接读取,不能独自证明评分安全。v0.18.0 的单步 Trial 有两种 Verifier 环境模式:11
| 模式 | v0.18.0 的阶段关系 | 设计时必须保留的威胁判断 |
|---|---|---|
shared(默认) | Agent 运行、日志与 Artifact 收集完成后,Verifier 复用同一个 Agent 环境;测试在执行前上传,环境到验证结束后才停止 | Agent 主调用通常看不到稍后上传的测试,但它已控制该环境,可能留下后台进程、路径、符号链接、工具或环境状态 |
separate | 收集待传 Artifact 后先停止 Agent 环境,再在新的 Verifier 环境中评分;测试构建进评分镜像,不在运行时上传 | 隔离了同一环境中的持续进程与大部分路径污染,但转移的 /logs/artifacts/ 和显式 Artifact 仍完全由 Agent 控制 |
因此,任务设计记录必须写明选用 shared 还是 separate,并逐项回答下列问题;本章只定义门禁,第 7 章才实现和攻击具体 Verifier:
- 权威断言、期望值、评分脚本与 Reward 在所有阶段是否都位于 Agent 不可写的可信边界中?Reward 是否由评分流程在每条路径上覆盖写入,而不是接受预存值?
shared模式是否考虑 Agent 退出后仍存活的后台进程,以及预创建/tests、/logs/verifier、父目录或符号链接对后续上传和写入的影响?不能把“Agent 的前台命令已返回”当成环境已净化。- Verifier 是否会
source、exec、导入或以其他方式执行 Agent 可写文件?PATH、解释器、Shell 和辅助工具能否被 Agent 放置的同名程序遮蔽?权威执行链必须来自可信路径。 - 所有 Agent 产物是否都按敌对输入处理,并在解析前检查预期文件类型、路径、符号链接、大小与格式?即使使用
separate,也不能执行、导入或信任转移进来的 Artifact。
高隔离需求应优先进入 separate 设计,因为它缩小了共享状态和后台进程的攻击面;但“分离”不是“可信”。它只改变执行边界,不改变 Agent 产物的信任级别。反过来,若保留 shared,设计评审必须给出完整执行链的可信来源,而不能只写“tests 在 Agent 阶段不可见”。
4.4.3 不泄露仍可能太容易
关键词捷径。 若某行直接写 root_cause=cache,其余日志只是装饰,Task 测到的是字符串提取。应让症状、依赖错误和上游原因分布在不同文件,并加入真实但无关的高等级日志;正确证据必须构成因果链。
先验猜测。 若 Dataset 中每个事故的答案都是数据库连接问题,完全不读日志也可能得高分。实例化时应平衡根因类别,并让 event_id 与受影响请求集合每题不同;不得用同一静态报告通过多题。
过程伪装。 要求 Agent 输出“我已检查全部日志”不能证明它真的检查过。Verifier 应检查终态事实与引用证据,而不是相信自述或强制某条命令序列。
评分篡改。 把测试放进 /workspace,允许 Agent 预写一个 Verifier 只在缺失时才生成的 Reward,或让评分脚本从 Agent 可写路径加载辅助程序,都会把“影响评分”变成捷径。具体隔离、净化和覆写规则留到第 7 章,但设计评审现在就要标记权威评分材料、执行链与 Agent 可写状态的交集。
捷径也不要定义得过宽。Agent 用一条 jq 管道完成关联,只要结果来自输入事实,就是高效解法;任务不应为了“看起来更 Agentic”强迫它多轮调用工具。需要防的是绕过目标能力的路径,而不是短路径本身。
4.5 难度与元数据是实验假设
本章把 difficulty = "medium" 标为教学性暂定。这不是实测分级,也不是 Harbor 自动推导的属性。难度属于“Task × Agent/Model × 工具与预算”的关系:同一日志题对带 JSON 查询工具的 Agent 和只会基础 Shell 的 Agent 并不等价。
更可靠的难度旋钮直接作用于目标能力:
- 关联跨度:证据来自一个文件还是多个组件;
- 因果深度:从目标失败到共同根因需要沿多少层直接原因指针回溯;
- 干扰强度:有多少语义合理但与目标请求无关的异常;
- 影响计算:是否涉及重试、重复记录与部分成功;
- 表达自由度:只选类别,还是还需解释与引用证据。
单纯增加日志体积、使用生僻缩写、压缩超时时间或藏起格式说明,通常增加的是扫描成本、领域猜测或运气。它们会让任务更难,却不一定让目标能力的测量更有效。
正式标级前应做三类试跑:Oracle 证明至少一条路径可解;人工按公开 instruction 独立求解并记录分歧;多个目标 Agent 重复运行,观察成功、失败和错误类型。若所有 Agent 都稳定满分,应先检查捷径再考虑增加因果深度;若全部失败,应先检查隐藏假设、不可解输入和预算,而不是立即标成 hard。
元数据应支持检索和分层,而不是存答案。本书约定:keywords 放稳定的领域和形式词;category 取一个主类;tags 表达可多选的技能或工件;capability_target 用于审计“这题声称测什么”;difficulty_explanation 记录分级依据。团队还应维护受控词表,避免 log-analysis、logs、observability 被随意当成同一标签。
4.6 一个会误判 Agent 的失败设计
考虑下面这条 instruction:
分析 /logs 下的文件,找到问题并写一份详细报告。报告要正确、完整、专业。
假设隐藏测试却要求 /workspace/report.json,要求字段 cause_type 等于某个枚举,并按请求 ID 排序。这个 Task 同时有四个缺陷:输入路径与真实路径不一致;输出位置和结构未公开;“详细、专业”没有操作定义;测试接受的类别词表只存在于评分侧。即使 Agent 找到正确根因,也可能因为写成 Markdown 或使用同义词得 0 分。
另一个常见“修复”是把完整期望 JSON 放进 /workspace/example-report.json,让 Agent 照格式写。这消除了格式猜测,却把正确组件、证据 ID 和影响集合一并泄露。正确做法是公开 schema、类型、枚举和排序规则,只把本实例的事实值保留在不可见的评分数据中。
还有一种反例看似严谨:日志最后一行写着 FATAL root cause: dependency unavailable,Verifier 只检查报告中含 dependency。这会稳定通过,却没有测跨文件关联。应把验收追溯回能力表:至少检查正确组件、类别、跨文件证据 ID 和受影响请求集合,才能排除只抄一行的策略。
4.7 进入实现前的验收清单
下面是本章的设计门禁。先执行静态命令,再由一名没有看过 Gold 的评审者只阅读 Agent 可见材料。任一“否”都应回到设计,而不是靠更复杂的 Verifier 掩盖。
能力与契约
- 能力目标用“给定输入,产生可观察结果”的句式表达,而不是复述业务故事;
- 每个核心评分项都能映射到目标能力,没有仅因“方便测试”而加入的要求;
- instruction 明确输入范围、编码、字段语义、时区、输出路径、格式和完成条件;
-
timestamp语法已公开,环境和 Verifier 使用同一子集; - 测试将检查的字段、类型、枚举、排序和去重规则都已公开;
- 根因事件、组件、类别、受影响集合和证据并集都能从公开字段唯一计算;
- 没有把特定工具、命令顺序或自述过程误作正确性条件;
- 不评价的能力已经列为非目标,例如修复服务、脱敏和建议质量。
边界与可解性
- 因果引用存在且无环,所有目标路径收敛到唯一无父事件,并跨越至少两份文件;
- 时间按解析时刻、同刻再按
event_id比较;其余边界处理已明确; - “证据不足”“多个同等根因”等不在范围内的实例不会混入首版 Dataset;
- 独立评审者仅凭公开材料能描述同一完成状态,并无法提出第二个同样合理的 Gold;
- 环境缺文件、编码错误等构建问题不会被当成 Agent 能力失败。
泄露、捷径与难度
- instruction、名称、元数据、URL、文件名、注释和构建产物不暴露实例答案;
- Agent 在整个 Trial 生命周期中看不到 Solution、Ground Truth 与期望输出,也不能直接或间接控制权威通过信号;
- 设计记录明确选择
shared或separate,并按该模式审计完整执行链; -
shared模式已覆盖后台进程、预建评分路径、符号链接、PATH/解释器遮蔽和 Agent 可写代码执行; -
separate模式仍把所有转移 Artifact 视为敌对数据,不执行、不导入、不直接信任; - 静态关键词、固定类别先验或同一报告不能跨实例取巧;
- 最短合法解仍必须读取并关联任务证据;
- 难度来自关联、因果、干扰和影响计算,而不是纯体积、隐含规则或苛刻超时;
-
medium明确标为待试跑校准的假设,没有伪造成功率或人工耗时。
v0.18.0 静态验收
-
harbor --version精确输出0.18.0; -
Task(".")能加载目录,schema_version为1.3; -
task.name为合法org/name,描述、关键词和作者不含 Gold; -
[metadata]的difficulty、category、tags类型适合 Viewer 筛选; - 启发式泄露搜索的每个匹配均已人工判读;
- 评审记录明确写着“只通过设计/静态门禁,尚未完成环境、Oracle 和 Verifier 验收”。
4.8 本章小结
- 可评测 Task 从能力目标开始;服务名、文件名和故事只是场景包装。
- instruction 是公开接口:输入、输出、完成条件和边界规则必须与后续测试一致。
- 首个教学 Task 用受控因果指针和故障代码换取唯一判定;这不代表真实日志分析具有相同的开放世界语义。
- 公开 schema 不等于泄露答案;实例的正确事实、Gold 和评分实现必须隔离。
- 防评分篡改必须覆盖整个 Trial;
separate能缩小共享状态风险,却不会让 Agent Artifact 自动可信。 - 任务捷径包括关键词提取、类别先验、评分篡改和可搜索的来源标识;高效合法工具使用不是捷径。
- 难度标签是等待试跑验证的实验假设,应由目标能力的认知操作决定。
harbor task init与Task(".")只能完成结构和静态加载验收,不能证明任务可解或评分可靠。
4.9 练习
- 把“分析日志并写报告”改写成一条可证伪的能力声明,另列出三个场景包装变量和三个控制变量;解释为何日志行数不是能力本身。
- 为本章输出契约增加
timeline字段。定义元素类型、排序、重复事件和时区规则,但不要给出任何本实例的正确事件 ID。 - 设计一份含两个看似合理根因的教学性日志概要。说明它为什么会使现有 Task 不公平,并给出两种修订方式:修改输入,或扩展输出契约。
- 对一个现有 Task 执行本章的泄露搜索与人工评审,分别找出一个真实泄露、一个误报和一个
rg无法发现的镜像层风险。 - 设计
easy、medium、hard三个日志实例,只改变关联跨度、因果深度和干扰强度;写出校准计划,不预先虚构任何 Agent 成功率。