【Harness Engineering】07_多代理与验证:用分工和验证管理不稳定性

源码地址: GitHub - wquguru/harness-books: 📚 Two books on harness engineering --- the design philosophies behind Claude Code & Codex: constraints, query loops, context governance, multi-agent verification. harness-books.agentway.dev · GitHub

一、单代理的极限

1.1 问题从"会不会做"变成"怎么分工"

单代理的小矛盾常靠耐心遮过去,但任务一大,研究、实现、验证就挤在同一条上下文链上抢预算、抢注意力、抢叙事中心。多代理看上去是自然答案,实际并不便宜:不加约束的并行只会把单代理的混乱复制几份。真正困难的是隔离各 agent 的不稳定性,同时把结果组织回来。

Claude Code 的源码在这点上很清醒:它没有把 subagent 当成"另一个会说话的窗口",而是当成一段需要明确缓存边界、状态边界、验证职责和清理责任的受管执行流程。


二、Forked Agent 的第一原则:Cache-Safe

2.1 缓存优先

src/utils/forkedAgent.ts 开头有一段注释,非常能说明 Claude Code 对 subagent 的真实理解。它说 forked agent utility 的职责包括:

  1. 与父代理共享 cache-critical params(缓存关键参数),确保 prompt cache hit(提示词缓存命中)

  2. 跟踪整个 query loop 的 usage(用量)

  3. 记录指标

  4. 隔离可变状态,防止干扰主循环

这四条里,最先出现的是"共享 cache-critical params"。这并非偶然。它说明在 Claude Code 眼里,fork 是运行时层面的受控分叉。既然是分叉,就必须非常在意哪些参数必须和父请求保持一致,否则 prompt cache 共享就失效,成本和延迟会立刻变坏。

2.2 CacheSafeParams 清单

CacheSafeParams 里明明白白列了这些要素:

  • systemPrompt(系统提示词)

  • userContext(用户上下文)

  • systemContext(系统上下文)

  • toolUseContext(工具使用上下文)

  • forkContextMessages(分叉上下文消息)

还专门提醒:别随便改 maxOutputTokens(最大输出令牌数),因为 thinking config(思考配置)也会受影响,而 thinking config 又是 cache key 的一部分。

2.3 运行时经济学

这段设计说明,多代理首先是运行时经济学问题。一个子代理如果每次都把父上下文重新烧一遍 token,看上去像在并行提效,实际只是把浪费并行化。Claude Code 在这个环节先处理的是:怎么 fork 才不把缓存打烂。

2.4 骨架与不变式

python 复制代码
// 骨架: forkAgent() 核心骨架
params = CacheSafeParams {
    systemPrompt, userContext, systemContext,
    toolUseContext, forkContextMessages        // 必须与父一致,保 prompt cache hit
}

ctx = createSubagentContext(parent):
    readFileState  := clone(parent.readFileState)              // 默认隔离
    abortController := new ChildAbortController(parent)        // 子控制器挂在父上
    getAppState    := wrap(parent.getAppState, suppress_prompt)
    setAppState    := noop                                     // 默认只读
    nestedMemoryAttachmentTriggers := new Set()
    loadedNestedMemoryPaths         := new Set()

三条不变式:

复制代码
1. child.CacheSafeParams == parent.CacheSafeParams         // fork 不破坏 prompt cache
2. child.mutable_state 默认隔离,除非显式 opt-in 共享      // 共享必须显式声明
3. parent.abort ⇒ propagate(child.abort)                    // 父死子亡

三、状态隔离:子代理首先要减少污染

3.1 默认隔离

forked agent 的第二个关键,在 createSubagentContext()。源码里对它的默认行为写得很直白:默认情况下,所有 mutable state 都隔离,避免干扰 parent。

它默认会做这些事:

  • readFileState(已读取文件状态)先 clone(克隆)

  • abortController(中断控制器)生成 child controller(子级控制器),而不是直接共享

  • getAppState(获取应用状态)做包装,让子代理避免 permission prompt(权限提示)

  • setAppState(设置应用状态)默认 no-op(空操作)

  • nestedMemoryAttachmentTriggers(嵌套记忆附件触发器)、loadedNestedMemoryPaths(已加载的嵌套记忆路径)等集合都重新建

只有在明确 opt-in(选择加入)的情况下,才会共享某些 callback,例如 shareSetAppStateshareSetResponseLengthshareAbortController

3.2 隔离作为默认伦理

这套设计特别重要,因为它揭示了一个很多人做多代理时都会忽略的事实:子代理最宝贵的地方,在于它可以避免把自己的局部混乱污染主线程。

研究中的误判、临时读到的文件状态、一次性的推理枝杈、正在进行的工具决策,如果全都直接写回主上下文,你得到的只会是更快的脏化。

Claude Code 在这里的态度是:共享要靠明确同意,隔离才是默认伦理。 这种伦理很像数据库事务设计,不像聊天玩具。它不假定"大家都是自己人,状态可以随便串",而是假定"只要是可变状态,就必须先隔离,再决定共享哪些部分。"


四、协调者模式:Synthesis 才是稀缺能力

4.1 Coordinator 的职责

如果只看 src/coordinator/coordinatorMode.ts,你会发现 Claude Code 对 coordinator 的要求很有分寸。它明确说 coordinator 的工作包括:

  • 帮用户达成目标

  • 指挥 worker 做 research(研究)、implementation(实现)、verification(验证)

  • 综合结果并和用户沟通

  • 能直接回答的问题就直接回答,不要滥委派

4.2 最关键的一句:Always Synthesize

最关键的一句,在第 5 节 prompt 里:Always synthesize(始终综合)。当 worker 回报研究结果后,协调者必须先读懂,再写出具体 prompt;不要说"based on your findings"(基于你的发现),不要把理解继续外包给 worker。

这句话几乎是多代理的命门。真正稀缺的不是并行输出,而是有人把 worker 的局部知识重新压成清晰、可执行、可验证的下一步;缺这一层,多代理很快退化成礼貌的任务转发机。

Claude Code 在 prompt 设计上要求 research / synthesis 分开、协调者对结果负责,后续 prompt 必须出现具体文件、具体位置、具体变更------研究可以分布式,但理解必须重新收束


五、验证必须独立成阶段

5.1 四阶段分工

coordinatorMode.ts 还有一段特别值得抄下来。它把常见任务分成:

阶段 职责
Research(研究) 探索和收集信息
Synthesis(综合) 理解并整合结果
Implementation(实现) 写代码改代码
Verification(验证) 证明代码有效

5.2 验证的目标

并且专门强调:verification 的目标是证明代码有效 ,而不只是确认代码存在。源码里甚至写得近乎不留情面:

  • run tests with the feature enabled(启用功能后运行测试)

  • investigate errors, don't dismiss as unrelated(调查错误,不要当作无关问题)

  • be skeptical(保持怀疑)

  • test independently, don't rubber-stamp(独立测试,不要走形式盖章)

5.3 实现 ≠ 交付

这说明 Claude Code 把验证当成第二层质量关,而不是实现 worker 顺手带的附属动作:prompt 里明确分层为"implementation 自证 + verification 作为独立 QA"。

为什么重要?"我改了代码"和"代码因此正确"之间隔着一条很宽的河,模型尤其擅长在上面搭纸桥------它会给出改动、解释、甚至像样的测试输出,但这些都不等于功能真的在系统里站住。

把 verification 单列,正是为了防止"会改代码"冒充"能交付结果":实现专注于改,验证专门怀疑这些改动配不配活着。


六、Hooks 和任务生命周期:子代理不是扔出去就算了

6.1 生命周期 Hook

多代理系统还有一个很容易被忽略的地方:spawn(生成)只是开头,收尾同样重要。

src/utils/hooks/hooksConfigManager.ts 里定义了 SubagentStart(子代理启动)和 SubagentStop(子代理停止)两类 hook。前者在 subagent 启动时触发,输入里有 agent_idagent_type;后者在 subagent 即将结束时触发,输入里还带 agent_transcript_path(代理转录路径),并允许 exit code 2 把 stderr 反馈给 subagent,继续让它跑。

这说明子代理在 Claude Code 里是显式暴露生命周期节点的系统对象。启动时可以观测,停止前可以介入,转录路径可追踪。这里的重点在于,"子代理结束"也是需要被管理的事件。

6.2 Cleanup 机制

与此同时,src/tasks/LocalAgentTask/LocalAgentTask.tsxregisterAsyncAgent() 又展示了另一个层面:每个 async agent 都会注册 cleanup handler(清理处理器),父 abort 可以自动传播给子 abort controller。任务结束后还要 evict output(驱逐输出)、更新状态、解除 cleanup 注册。

这套机制非常像操作系统,不像聊天面板。它关心的核心问题是:

  • 这个 agent 是否仍在运行

  • 父任务死了它是否该跟着死

  • 它的输出文件是否还要保留

  • 它的 cleanup callback 有没有泄漏(没有及时清理导致内存或资源无法释放)

很多多代理 demo 都只做到"我能再起一个 agent",Claude Code 至少多做了一步:它把 agent 当作会泄漏资源、会残留状态、会在父进程结束后变成孤儿的运行实体来看待。 这才像是在把代理当系统组件处理。

6.3 子代理生命周期失败矩阵

事件顺序 前置状态 触发 下一步
parent abort + child in-flight 子任务未完成 parent abort 传播 child.abort,等 cleanup,不可留孤儿
child crash 子代理异常退出 SubagentStop exit ≠ 0 evict output,fire stop hook
child timeout 子运行超限 registerAsyncAgent 超时 abort child,写 synthetic result
cache key drift CacheSafeParams 被改 fork 拒绝 fork 或重算 cache
shared state 未声明 opt-in 未开 child 试图写 setAppState no-op,不污染父
cleanup 泄漏 task 结束未取消 handler finalize 强制 unregister + evict

七、验证不仅针对代码,也针对记忆和建议

7.1 记忆也会过时

多代理与验证并不只发生在 code change 之后。Claude Code 在 memory 体系里也埋了一条很值得注意的原则。

src/memdir/memoryTypes.ts 里专门提醒:memory records can become stale (记忆记录可能过时);在基于 memory 给用户建议之前,要先 verify current state(验证当前状态);如果记忆与现状冲突,要相信眼下读到的真实状态,并更新或删除 stale memory(过时记忆)。

7.2 验证是抵抗时间漂移的习惯

这句话放在多代理章节里,恰好能说明一个更一般的事实:验证是整个系统用来抵抗时间漂移和上下文漂移的基本习惯。

一个系统如果只验证新写下去的代码,却不验证旧记忆、旧假设、旧索引,那它仍然会被历史信息带偏。

从这个角度看,verify 既是一项 skill,也是一种组织纪律。你可以把工作分出去,可以把信息存起来,可以让其他 agent 先跑在前面,但在用户准备据此行动之前,总要有人回到当前现实,重新确认这些东西还是真的。


八、多代理真正解决的是不确定性的分区

8.1 分区的好处

Claude Code 的多代理设计围绕一个朴素目标:给不确定性分区。

  • research worker 在局部上下文里探索

  • implementation worker 专注修改

  • verification worker 专门怀疑

  • coordinator 在中间收束、综合、接口用户

8.2 职责清晰,错误可定位

这种分区的最大好处是职责清晰,错误可定位

  • research 漏线索

  • synthesis 没吃透

  • implementation 写错

  • verification 放水

每一种都能被指认。反过来,一锅浓汤式的单代理方案出问题只能整体返工。多代理真正有价值的地方,不是并发,而是把不同种类的不确定性关进不同容器,再由 coordinator 组织回来。


九、总结

9.1 核心原则

这一章可以归纳成一句话:

多代理依赖清晰分工:研究、实现、验证和综合各自处在不同约束容器里,最后由协调者把结果重新缝合成可交付结果。

9.2 六个源码位置指向同一结论

源码位置 结论
forkedAgent.ts cache-safe 参数、usage tracking 与状态隔离放在第一位
createSubagentContext() 默认隔离 mutable state,只允许显式 opt-in 共享
coordinatorMode.ts coordinator 必须 synthesize,综合理解不能外包
coordinatorMode.ts verification 独立成阶段,实现与验证必须角色分离
hooksConfigManager.ts SubagentStart / SubagentStop 生命周期 hook
LocalAgentTask.tsx parent abort、cleanup、output eviction,生命周期需要回收机制

9.3 最终结论

抽成工程原则:

fork 先看 cache 与状态边界,再谈"人格分工";子代理默认隔离,可共享必须显式;研究可委派、综合不可外包;验证必须与实现解耦,否则系统会奖励自证正确;agent 生命周期必须可观测、可中止、可清理;并行的真正价值在于职责更清楚。

相关推荐
LayZhangStrive3 小时前
提示词沉淀 - 使用豆包时平时提问题
面试·职场和发展·prompt·提示词·豆包
ZGi.ai4 小时前
ZGI:工作流分支失控,先把规则拆出 Prompt
prompt·prompt工程·workflow·aiagent·zgi
赵大仁5 小时前
Prompt 缓存与上下文压缩:把 Token 账单砍一刀的实操清单
ai·大模型·prompt·token·成本优化
Elias不吃糖14 小时前
Langfuse 入门:Trace、Prompt、Dataset、Experiment、Evaluator
前端·python·prompt·langfuse
宇擎智脑科技16 小时前
DeepSeek Harness 架构解析:MCP 和 Skill 如何被统一为 Cordis 插件
架构·deepseek·harness·dsh
李兆龙的博客1 天前
从一到无穷大 #82:Agent Memory Eval 中的 Harness 选择
harness
默 语1 天前
Prompt 不是即兴发挥:结构化设计与版本化评测的工程化路径
prompt
机构师1 天前
AI编程实战:把需求讲给 AI——Prompt 基础与模板
开发语言·人工智能·prompt·ai编程
yingyuecom1 天前
Seedance 2.5正式发布:映悦AI迎来“更长、更可控、更极致”的视频生成时代
人工智能·gpt·chatgpt·prompt·aigc