Agent 长程任务的完成度信任问题,以及 Zagens 如何用组合式 Harness 解决它
- 模型自建的 checklist 有欠分解上限——
open_items: 0不等于真完成。 - Zagens 用三层可组合门禁:L1 防早停 → L2 退出码 Oracle → L3 交付物对账,均在
graph_complete候选时由 Harness 主动裁决。 - 裁决信号必须是机器可回放的(退出码、路径命中、事件日志),禁止 LLM 当法官。
- 对抗审计只做缺口枚举,最终绿灯仍由机器门决定;超大任务叠加 Cycle / Macro Loop。
术语速查
| 术语 | 含义 |
|---|---|
| LHT | Long-Horizon Task,长程代码任务模式;Layer 1 的强制继续机制 |
| CRAFT | 多代理 QA 段(Implementer + Reviewer + Verifier),与 LHT 实现段交替 |
| Machine Oracle | 机器裁决:退出码、路径/glob 命中等可回放信号 |
graph_complete | 模型自产 plan + checklist 均无可办项时的完成候选状态 |
audit_unmet | 有界轮次耗尽后门仍未全绿时的诚实停机,不产生假绿灯 |
| MicroStack02 | Go 微服务框架压测用例:24 类交付物 + 四门验收,暴露欠分解假绿 |
| DEMO3–6 | Monkey 解释器等长程实证压测序列,逐轮修补早停泄漏与假绿路径 |
1. 引言:Agent 的「声称完成」陷阱
2025–2026 年,LLM Agent 的编码能力已经很强——能写数千行代码、做多文件重构、跑测试。但实践中暴露出一类深层问题:模型经常过早「声称完成」,而大量交付物实际上并未落地。
这不是模型在说谎。根因在于:
- 模型自建的 checklist 存在欠分解(under-decomposition)——某些必要交付物从未进入它自己的任务图;
- 模型检查自己的 checklist 全部打勾 → 任务图显示
open_items: 0→ 模型合法地宣布完成; - 但检视才发现:gzip 中间件没写,router trie 重构没做,e2e 脚本没跑过。
用 Zagens 团队的内部术语说:完成度的天花板 = 模型自分解 checklist 的完整性。模型自己画的作战地图画漏了,按图索骥也找不到漏掉的目标。
Zagens 作为面向 DeepSeek V4 的开源 Agent Harness,核心工程命题是:如何构建一套不依赖模型自我声明的、可审计的、可回放的完成度信任机制。
本文从工程架构角度,拆解 Zagens Harness 系统的设计与实现。
2. 核心理念:Harness,不是聊天壳
Zagens 的判断很明确:不能信任模型自己说「做完了」。需要一套独立于模型的完成度判定系统。
这套系统的三条设计原则(源自 Zagens Harness 设计文档,经 DeepSeek Agent + Harness 负责人 Deli Chen 的持续学习研究印证):
- 信号质量 = 机器裁决(Machine Oracle)—— 最终判断依据是退出码(exit code)、路径命中(path/glob hit)等可精确回放的信号,不是 LLM 的散文判断;
- 信号独立性 —— 需要两层独立:规则独立(不靠模型推理,靠正则/文件扫描)和 Agent 独立(不是同一个 Agent 的自我确认);
- 独立性不能退化为「换一个 LLM 盖章」 —— 用 LLM 审计 LLM 的输出,只会把「建设者自我确认」变成「审计者-建设者共谋确认」,独立性名存实亡。
这些原则落实到工程上,就是 Zagens 的组合式 Harness(Composable Harness)—— 三层可插拔门禁系统。
3. 引擎基础:Kernel V3 —— 事件溯源的 Turn 引擎
Harness 门禁要可审计、可回放,离不开底层引擎的事件持久化。Zagens v0.8.2 的 Kernel V3 是事件溯源 turn 循环——下文只保留与 Harness 相关的要点;完整 turn 机状态机见源码 LiveTurnMachine / ReplayTurnMachine。
每次 turn 的完整生命周期被记录为 KernelEvent,存储在 SQLite 的 kernel_events 表:
CREATE TABLE kernel_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
seq INTEGER NOT NULL,
ts_ms INTEGER NOT NULL,
kind TEXT NOT NULL,
turn_id TEXT,
payload TEXT NOT NULL -- JSON KernelEvent
);
事件溯源对 Harness 的价值:
- 可回放(Replayable):
ReplayTurnMachine能从事件日志重建状态,Harness 的金色夹具(golden fixtures)可在 CI 中精确验证门禁行为; - 可审计:所有 turn 生命周期事件(模型请求、工具调用结果、门禁判定)都有结构化持久化记录;
- 可恢复:
log-first会话恢复优先从事件日志重建消息,不依赖 session JSON。
基于这个引擎,Zagens 的 Harness 各层门禁得以可靠地挂载在 turn 循环的各个退出点上。
4. 三层组合式 Harness 架构
Zagens 的完成度门禁系统是三层可组合的:
组合方式(可独立开关):
- Layer 1 仅 = 原始效果(模型自驱 + 防早停);
- Layer 1+2 = 纯粹的退出码门禁(最确定、最便宜);
- Layer 1+2+3 = 完整组合,能捕获欠分解场景。
4.1 门禁在何时触发
§4 的流程图容易误读为「L1 跑完再跑 L2」。实际时序如下:
要点: Layer 1 贯穿全程;Layer 2/3(及 Stub 门)仅在 graph_complete 候选时原子执行,同轮内 L2 全绿后才进入 L3。
4.2 案例:MicroStack02 欠分解假绿
MicroStack 框架压测(代号 MICROSTACK)规定 24 类交付物、四门验收 + [verify:] 纪律。一次典型失败跑(MicroStack02):
| 观测 | 模型侧 | 实际产物 |
|---|---|---|
| UI / 节点 | checklist 100%、incomplete_stop=0、gate_skip graph_complete ×3 | — |
| 规模 | 声称完成 | 仅 ~7045 行(目标 15–40K) |
| 覆盖率 | checklist 项已勾 | app 16.3%、orm 4.7% |
| 欠分解 | 任务图全绿 | gzip 中间件、cmd/microstack/main.go、Router trie 重构从未进 checklist |
各层分别拦住了什么?
| 层级 | 仅 Layer 1 时 | 加上 Layer 2+3 后 |
|---|---|---|
| Layer 1 | ✅ 无早停;但 checklist 全绿 → 放行候选 | 同左 |
| Layer 2 | — | ✅ 覆盖率门、gofmt 真门可拦「测试红/未格式化」 |
| Layer 3 | — | ✅ middleware/gzip.go、**/*trie*.go 等 manifest 项抓到「从未分解进清单」的交付物 |
根因代码定位:maybe_continue_incomplete_code_task 只看模型自产 plan + checklist——完成度上界 = 模型自拆清单完整度。Layer 3 把「模型欠分解」平移为「操作者预写 manifest」,但 manifest 可离线审查、可回归,远比每次现场分解稳定。
下面逐层深入。
5. Layer 1:强制继续(LHT)—— 不让模型用散文结束
Layer 1 解决的核心问题:模型在多步骤任务中途以文字总结结束,不再调用任何工具。
5.1 问题根因
源码定位在 crates/runtime-server/src/long_horizon/mod.rs 的 maybe_continue_incomplete_code_task:
graph = CodeTaskGraph::from_snapshots(plan, checklist)
if graph.is_empty() -> Skip("graph_empty")
if !graph.incomplete() -> Skip("graph_complete")
完成判定完全依赖模型自产的 plan + checklist 快照。如果模型在初始分解时就遗漏了关键交付物,「open_items == 0」就是一个虚假的完成信号。
5.2 强制继续机制
maybe_continue_incomplete_code_task 被挂载在 no_tool_uses.rs 的分支链中(audit 之后、Break 之前):
当模型在工具调用后只输出文字总结,引擎走 no_tool_uses 路径 → 第 6 分支触发 → 注入一条强制继续的 nudge 消息:
长程代码任务尚未完成 — 请勿仅用文字总结结束本轮。
目标:将 auth 模块重构为基于 trait 的后端
进度:████░░░░░░ 42%(plan 1/3 阶段;checklist 2/5 项未完成)
仍待完成:
- [plan ◎] 引入 AuthBackend trait
- [todo ○] 更新集成测试
请继续用工具完成当前 in_progress 项,验证(如 cargo check/test),再 checklist_update / update_plan。
5.3 NudgeTracker:防死循环
为防止模型在同一个 item 上无限循环,Zagens 实现了 NudgeTracker:
上图为状态机概览;阈值如下:
| 规则 | 行为 |
|---|---|
同一 in_progress_id 被 nudge 3 次而无实质进展 | long_horizon_blocked |
| 同一 item 被 nudge 5 次(硬上限) | 停止 nudge,等待用户消息恢复 |
in_progress_id 变化 | 重置计数 |
| 连续 8 个助手 turn 无工具调用 | nudge 变为「请用户引导」,不再机械继续 |
5.4 进展判定
「有进展吗?」需要客观信号,而非正则的玄学判断:
| 信号 | 判断方式 | 性质 |
|---|---|---|
edit_file / write_file / apply_patch 成功 | 工具返回 success | 客观 |
exec_shell + 退出码 0 + 匹配验证正则 | success + 匹配 | 启发式 |
checklist_update / update_plan 成功 | 状态机迁移 | 客观 |
| git working-tree 变化 | git status --porcelain 签名变化 | 客观,语言无关 |
其中 git working-tree 信号(Phase 2.x 引入)解决了非主流语言项目中验证正则列表不全的问题——只要工作区文件发生了实际变化,就算进展。
5.5 演进中的关键修补
LHT 并非一次性设计完成。经过 DEMO3–DEMO6 四次实证压力测试,团队修复了多个隐蔽的早停泄漏路径:
| 泄漏路径 | 症状 | 修复 |
|---|---|---|
| Step budget 耗尽 | 模型在 100 step 后 break,无任何 nudge | maybe_continue_at_step_limit 给予新的预算窗口(最多 3 次续命) |
| loop_guard 截停 | 同一工具连续失败 8 次,直接 Break | maybe_continue_after_loop_guard_halt 清除失败计数 + 建议切换方案 |
| 上下文溢出 | 上下文超出模型窗口,强制 Failed | maybe_cycle_handoff_on_context_overflow 强制一次 cycle 交接 |
| 欠分解导致的虚假完成 | checklist 全绿但关键交付物未写 | 交给 Layer 2+3 解决(见 §4.2) |
每个泄漏路径都对应一个 run.rs 中的 break 出口。团队做了系统性审计:每个 turn 退出点都必须经过 LHT 门禁,否则就是一个新的早停泄漏。
6. Layer 2:硬验收门 —— Harness 主动执行,退出码当法官
Layer 1 只能保证「模型没提前停」,但不能保证「做对了」。Layer 2 扭转了判决权力:不再相信模型说「我跑过了,通过了」,Harness 重新跑一遍,看退出码。
6.1 设计铁律
不能用 LLM 当法官。 用 LLM 审计者「读一遍然后说 LGTM」是一个软的、非确定性的、可说服的、不能离线回放的裁决。
正确的分工(下图);硬验收门与交付物覆盖均由机器 Oracle 裁决,不由 LLM 散文判断:
6.2 验收 Manifest
操作者在 ~/.zagens/config.toml 中声明验收门列表:
[long_horizon.completion_gate]
mode = "enforce"
max_manifest_rounds = 5
[[long_horizon.completion_gate.verify]]
id = "build"
cmd = "go build ./..."
[[long_horizon.completion_gate.verify]]
id = "contracts_stable"
cmd = "git diff --exit-code contracts/"
[[long_horizon.completion_gate.verify]]
id = "coverage_app"
shell = "none"
argv = ["zagens", "coverage-gate", "--pkg", "app", "--min", "75"]
关键约束:
shell = "none"+argv:跨平台(Windows 无 bash),避免引用语义漂移;- 退出码不是 0 就判不通过,强制模型返工;
- 区分断言失败与基础设施错误:
exit_class分assertion(测试红)和infra(命令找不到、超时),连续 N 次infra则诚实宣布audit_unmet; - 只信任用户全局配置/内置测试夹具中的可执行命令,工作区配置/模型生成文件只能走
observe模式(记录但不阻塞)。
6.3 任务无关的通用验收门
Layer 2 有三类 verify 来源;其中 操作者 manifest(§6.2)适合回归夹具与强约束任务,另有两类零 per-task 配置、全局开关即覆盖所有代码任务的通用来源:
| 来源 | 配置开关 | 命令从哪里来 | 信任层级 |
|---|---|---|---|
| 操作者 manifest | completion_gate.mode | 手写的 [[verify]] 列表 | 受信任全局配置 |
模型 [verify:] 回放 | auto_verify_replay | 扫描已完成 checklist 项,Harness 主动重跑 | 无需额外信任 |
| 工具链探测门 | toolchain_gate | 探测 go.mod/Cargo.toml/package.json 等 | 内置固定命令 |
三类来源按归一化命令合并去重(优先级 operator > toolchain > model),一次跑完。日常任务可在 ~/.zagens/config.toml 中只开全局开关,无需 per-task manifest:
[long_horizon.completion_gate]
auto_verify_replay = "enforce" # 主动复跑模型声明的 [verify:]
toolchain_gate = "observe" # 工具链探测 build/test
如此设计后,Layer 2 的价值核心不再是「每个任务写一份 manifest」,而是**「Harness 主动执行、退出码当法官」**——大多数日常任务打开上述开关即可。
6.4 Stub 门(规则独立的接地信号)
Green build 可能掩盖缺失的功能实现——todo!() 编译能通过,但运行就崩。Zagens 在 Layer 2 之前插入一个纯文件扫描的 stub 门:
- 阻塞级(
enforce):todo!()、unimplemented!()、NotImplementedError、throw/panic!/raise+ “not implemented” 句式 → 立即阻止完成; - 记录级:纯
TODO/FIXME注释 → 从不阻塞,仅计 telemetry。
这层门在 graph_complete 候选时先于 Layer 2/3 执行——纯文件扫描几乎零开销,如果发现 stub 就没必要花几分钟跑构建了。
7. Layer 3:交付物对账 —— 抓到模型从未分解进 checklist 的项
Layer 2 能阻止「测试没过但模型声称过了」,但它不能捕获「模型从未写在 checklist 里的交付物」——这才是 MicroStack02 的核心失败模式。
Layer 3 在 Layer 2 全绿后,用纯 Rust 同步扫描(非 agent_spawn、非 LLM)对工作区做 path/glob/tracked 对账,可选每项跑 optional_verify_cmd:
操作者在 ~/.zagens/config.toml 中声明 [[long_horizon.completion_gate.deliverable]](完整回归夹具见 fixtures/harness/microstack-completion-gate.toml):
[[long_horizon.completion_gate.deliverable]]
id = "gzip_middleware"
path = "middleware/gzip.go"
[[long_horizon.completion_gate.deliverable]]
id = "deliverable_24_refactor"
glob = "**/*trie*.go"
optional_verify_cmd = "go test ./... -run Refactor"
[[long_horizon.completion_gate.deliverable]]
id = "contracts_frozen"
path = "contracts/server.go"
tracked = true # 须 git 仓库且已 commit,否则无基线
扫描完成后输出结构化 JSON,例如:
{
"pass": false,
"missing_deliverables": [
{"id": "gzip_middleware", "what": "middleware/gzip.go does not exist", "evidence": "workspace path missing"},
{"id": "deliverable_24_refactor", "what": "Router trie refactor not executed", "evidence": "glob router/*trie* zero hits"}
],
"manifest_round": 2
}
pass 只取决于退出码和路径命中——不依赖任何 LLM 的「感觉」,纯函数输出,离线可回放,可做快照回归。
诚实上限
三层门禁各自有界计数器:max_manifest_rounds、max_audit_rounds。如果耗尽而门仍未全绿,不会无限循环,而是记录 audit_unmet + 未达标门列表,不产生虚假绿灯。
8. Cycle & Handoff:可信的回合间交接
长程任务往往单次会话的上下文窗口不够。Zagens 通过 **Cycle(循环)**机制实现上下文无缝交接。
8.1 触发条件
| 条件 | 默认 |
|---|---|
| 活动输入 token ≥ 阈值 | 768K(约 1M 窗口的 75%) |
| 无飞行中的工具/流/审批 | in_flight: false |
| LHT 增强 | 在 75–85% 警告带内,优先在 checklist item 完成时触发,避开编辑中途 |
8.2 交接内容
每次 cycle 交接有两层:
层 1(StructuredState)由引擎确定性保留;层 2(carry_forward)由模型在 cycle 边界手写。Carry forward 模板示例:
Also include in <carry_forward>:
- Long-horizon objective (one line)
- Open checklist/plan item ids or labels still pending
- Last verification command and outcome (pass/fail/not run)
- Files currently being edited (paths only)
- Failed approaches / constraints
8.3 可视化的 Cycle 时间线
下图仅为示意,不代表真实耗时。
用户在 LongHorizonPanel 的 Cycle 标签页中可以看到:
- 当前 cycle 编号 + 时间线;
- 每轮 carry_forward 摘要(可展开预览);
- 768K 警告线与当前上下文使用量的对比。
9. 宏观循环(Phase 4):LHT ↔ CRAFT 交替
何时启用: 默认长程任务 Layer 1–3 + Cycle 即可。Macro Loop 面向 15K–20K 行级超大重构,在实现与 QA 之间交替多轮;回归基线见
fixtures/harness/lht-eval-arms/lht_long_refactor.toml。
CRAFT(Implementer + Reviewer + Verifier 多代理段)负责质量闭环与缺口识别,没有 graph_complete 放行权——最终仍由 Layer 2/3 机器门裁决。
Zagens 实现了 Phase 4 Macro Loop——在 LHT 实现段和 CRAFT QA 段之间交替:
每轮 LHT 段使用 Layer 1–3 门禁推进代码实现;CRAFT 段使用多代理(Implementer + Reviewer + Verifier)做质量闭环。Macro Loop 的 auto_continue 机制让这个过程可以无人值守地跑多个回合。
这个宏循环的回归基线固定在 fixtures/harness/lht-eval-arms/lht_long_refactor.toml 中,作为 CI 的定期验证夹具。
10. 对抗审计器:Agent 独立的缺口枚举器
可选扩展: 默认 Stub 门 + Layer 2/3 已覆盖大多数场景。对抗审计在
config.toml中 opt-in 启用,用于 Stub 扫不到的「无标记占位实现」或「规格有、清单无」的缺口枚举。
Layer 2 的 stub 门(正则扫描)只能捕获标记明确的残缺实现。对「函数体返回 return Ok(()) 占位但没有标记」或「整个模块从未进入 checklist」这类情况,需要另一个 Agent 来发现。
但这里有一个微妙的设计边界:「法官」vs「缺口枚举器」。
| 法官型(禁止) | 缺口枚举器型(允许) | |
|---|---|---|
| 权力 | 直接判 pass/fail,放行或阻塞 | 没有放行/否决权;只输出「疑似缺口」候选 |
| 输出 | “LGTM/不通过” 散文 | 机器可测试的断言:{file:line, 缺什么, 建议的 [verify: cmd]} |
| 谁最终裁决 | 自身(软的、可说服的、不可回放) | 仍然是机器 Oracle——候选注入后,stub 门/退出码/路径对账来裁决 |
| 失败模式 | 审计者-建设者共谋盖章 | 最坏情况「报几个假缺口」,机器门一跑就驳回了 |
对抗审计器的输出永远不直接进入 graph_complete 的放行/阻塞判断;它只能扩展机器的门禁检查面。最终绿灯仍然由退出码和路径命中决定。
11. 可视化:让完成度可见
Harness 产生事实,可视化让事实可见。
Zagens 的桌面 UI 中有一个 2×2 的 Harness Grid:
LongHorizonPanel 有三个子标签页:Task Graph(计划/checklist/门禁摘要)、Cycle(时间线与 carry_forward)、Context(768K 阈值 vs 上限)。
读屏场景:如何判断「真完成」?
| 你看到的 | 含义 | 建议 |
|---|---|---|
| Task Graph 100% + 门禁全绿 | Layer 2/3 已通过 | ✅ 可信任完成 |
| Checklist 100% + 门禁琥珀色「有条件完成」 | checklist 全勾但 manifest 未绿或 observe 缺口 | 继续让模型返工或检查 config |
Nodes 有 gate_skip graph_complete 但无 manifest_gate_result | 未启用 completion_gate | 考虑打开 Layer 2/3 |
Nodes 有 audit_unmet | 有界轮次耗尽 | 人工介入,不假绿 |
Nodes 标签页记录 Harness 决策流:
背景:DEMO3–6 压测序列
| 代号 | 载体 | 主要发现 |
|---|---|---|
| DEMO3 | ~2W 行 Go Monkey 解释器 | unverified_acceptance 软提示→软门禁;[verify:] 纪律 |
| DEMO4 | 同上 | step budget 耗尽无声早停 → maybe_continue_at_step_limit |
| DEMO5 | 20K 行解释器 | plan/checklist 双计数、verify_gate 文本匹配误报、cycle 阈值 mid-turn 不评估 |
| DEMO6 | Monkey 双后端 | checklist_write 批量更新绕过 verify_gate |
12. 经验教训与演化
Zagens Harness 不是纸面设计——它在 DEMO3–DEMO6 四次实证压力测试中暴露出许多隐蔽问题,并逐一修补。以下是几个有普适性的教训:
12.1 所有 turn 退出点都必须是 LHT 门禁
最初 LHT 只挂载在 no-tool-uses 路径上。但 step budget 耗尽、loop_guard 截停、上下文溢出——每个外循环 break 路径都可能产生虚假的 Completed,必须系统性审计每个退出点。
12.2 进展判定不能依赖文本匹配
最初用 result_contains_success 检查 "exit code: 0" 子串来判定验证命令是否成功,但 exec_shell 成功路径返回的是裸 stdout(ok monkey/lexer 0.078s),根本不包含退出码信息。修复很简单:删除多余的文本检查,success 布尔值就是退出码 0 的权威信号。
12.3 checklist 和 plan 不能算两份工作量
DEMO5 中,模型创建了 12 个 plan 项但全部放弃(pending),同时用 checklist 推进了 19 项全部完成。原来的进度算法将两方面相加,结果 12 + 19 = 31 总项、12 项 pending,进度卡在 61%。修复:当 checklist 非空时,以 checklist 为完成度的权威来源。
12.4 [verify:] 纪律是基础
模型的 checklist 项经常把「可运行的验收」退化为「创建文件」项——“create example scripts” 在 checklist 中标记完成只是创建了文件,但从未真正跑过。Zagens 在 base.md 中强化了 checklist 写作纪律:任何需要运行的验收项必须写作 [verify: <command>] <label>,并在引擎层做了判断强化。
12.5 门禁系统的天花板 = manifest 的完整性
这是一个诚实的边界声明。Layer 3 保证「manifest 中的交付物不会漏掉」,但不能保证「规范中写了但 manifest 中没写的」。这本质上是把「模型欠分解」变成了「操作者欠写 manifest」。但 manifest 可以离线审查、可回归、可复用——比模型每次现场分解稳定得多。
13. 测试与回归架
Zagens Harness 的验证体系包括:
夹具路径均在 fixtures/harness/ 下;设计规格见 docs/harness/COMPOSABLE_HARNESS.md。
14. 结语
Zagens 的 Harness 工程回答了一个核心问题:当 AI Agent 说「我做完了」时,你怎么知道它是真的做完了?
答案不是信任模型,而是构建一整套独立于模型的机器门禁系统:
这套系统的设计哲学可以浓缩为一句话:Harness 不替模型做事,它只负责检查模型有没有把事做完。检查的手段是机器可回放的、可审计的、不依赖模型自身表达的。
本文基于 Zagens v0.8.2 的公开设计文档与源码。作品名称:Zagens(MIT 协议),开源地址:github.com/didclawapp-ai/zagens

873

被折叠的 条评论
为什么被折叠?



