让 AI 带 AI:Fork-and-Delegate 多Agent协作架构全解
2026/7/1 15:58:25 网站建设 项目流程

让 AI 带 AI:Fork-and-Delegate 多Agent协作架构全解

《Claude Code 架构解密》读书笔记 · 第09篇 · 对应第6章前半(6.1-6.5)


导语

当一个 Agent 面对复杂任务时,单打独斗力不从心——代码重构需要同时研究多个文件、实现变更、运行测试;大型项目分析需要并行探索不同模块。Claude Code 没有引入一个复杂的工作流引擎,而是通过七个精心设计的模式,在现有 QueryEngine 循环之上渐进式叠加多Agent能力。本篇聚焦前五个模式:Fork-and-Delegate、Agent Type Registry、Tool Sandboxing、Scoped Memory,以及贯穿其中的"不引入新执行模型"这一最关键的架构洞察。


一、为什么需要多Agent编排?

从一个真实场景说起

用户对 Claude Code 说:"帮我把项目中所有的 JavaScript 文件迁移到 TypeScript,同时保证测试通过。"这个看似简单的请求包含了四个独立的子任务:调研阶段(扫描+分析依赖)、类型推断、迁移执行、验证修复。

单个 Agent 串行完成这一切,会面临两个困境:

困境具体表现
上下文膨胀随着处理文件增多,对话历史迅速膨胀,重要信息被淹没
效率瓶颈明明可以并行处理的独立文件,却不得不逐个排队等待

但多Agent引入了新的挑战——上下文共享、安全隔离、成本控制、生命周期管理、递归防护——每一个都是生产级系统必须回答的问题。

最关键的架构洞察

Claude Code 没有选择引入复杂的工作流引擎(如 LangGraph 的有向图、或 AutoGen 的对话协议),而是通过七个模式在现有 QueryEngine 循环之上渐进式叠加多Agent能力。

这种"不引入新的执行模型"的策略,是本章最重要的架构洞察。每个子Agent仍然运行同样的 while(true) 循环,同样的工具注册表,同样的权限检查——只是通过精细的配置和约束,让同一个执行引擎呈现出不同的行为模式。


二、Fork-and-Delegate——轻量级分叉委派

从 Unix Fork 到 Agent Fork

命名来自 Unix 的fork()——父进程创建子进程,子进程继承完整内存空间但拥有独立执行路径。Claude Code 借用了同样的隐喻:

  • 继承:子Agent获得父Agent的完整对话历史
  • 独立:子Agent拥有独立的 QueryEngine 循环
  • 隔离:子Agent的执行结果不会自动合并回父Agent

但与 Unix fork 不同的是,Agent Fork 面临一个独特约束——LLM API 的成本。每次 Fork 意味着向 API 发送一份完整的对话历史,N 个 Fork 的成本就是 N 倍。后文的 Context Cache Sharing(6.8节)将展示如何巧妙解决这个问题。

消息构建:占位符统一化

Fork 的核心在于buildForkedMessages()函数。它将父Agent的对话历史精心改造后传递给子Agent:

// 三步构建 Fork 消息 1. 保留父Agent的 assistant 消息(包含所有 tool_use 块) 2. 为每个 tool_use 创建统一占位的 tool_result 3. 追加本 Fork 子Agent特有的指令

最精妙的设计是占位符统一化。当父Agent同时发起3个Fork子Agent时:

Fork A: [系统提示][对话历史][tool_result:"Fork started..."][指令A] Fork B: [系统提示][对话历史][tool_result:"Fork started..."][指令B] Fork C: [系统提示][对话历史][tool_result:"Fork started..."][指令C]

前三部分字节级相同!这意味着 LLM Provider 的 Prompt Cache 可以被三个子Agent共享——只有最后的"指令"部分不同,需要单独计算。这一设计将 Fork 的边际成本从"完整上下文 × N"降低到接近"增量指令 × N"。

递归防护:深度为1的硬限制

多Agent系统的经典风险是无限递归——子Agent创建孙Agent,孙Agent再创建曾孙Agent。Claude Code 通过两道防线阻止:

防线机制可靠性
第一道消息历史标记扫描(<fork_boilerplate>标签)可能在上下文压缩时丢失
第二道querySource属性检查(不可变元数据)始终可靠

为什么需要两道防线?因为消息历史标记可能在 autocompact 时被重写丢失,而querySource是不可变的元数据。双重检查确保即使一道防线失效,另一道仍然有效。

三重互斥与功能开关

isForkSubagentEnabled()实现了三重守卫:

  1. Feature Flag 总开关:通过 GrowthBook 控制灰度发布
  2. 与 Coordinator 互斥:两者同时启用会产生混乱的任务层级
  3. 非交互式会话禁用:SDK/API 模式没有终端UI,Fork子Agent的权限请求无法"冒泡"

这种互斥设计体现了一个重要原则:同一系统中不应存在两种语义重叠的编排机制。Fork 和 Coordinator 解决的都是"任务并行化"问题,但方式不同——如果同时启用,开发者和 LLM 都无法判断何时使用哪种方式。

严格角色约束:10条不可协商规则

Fork 子Agent在创建时收到一组严格的行为规则:不得创建子Agent、不得向用户提问、必须按固定格式输出结果、不得修改自身系统提示……共10条不可协商规则。

这些规则与代码层的递归防护形成了纵深防御——即使 LLM 在极端情况下绕过了代码检查,system prompt 中的硬性规则仍然会约束它的行为。这种"代码+提示"双层防御策略贯穿全书。


三、Agent Type Registry——代理类型注册表

四层加载源与优先级覆盖

Agent定义来自四个层级,优先级从低到高:

内置Agent(代码硬编码) ← 被覆盖 插件Agent(通过插件系统注册) ← 被覆盖 用户自定义Agent(~/.claude/agents/*.md) ← 被覆盖 策略Agent(企业管理策略下发) ← 最高优先级

加载和合并的核心算法使用 Map 的"后写覆盖"语义——同名 Agent 只保留最后一次set的定义。由于数组按优先级从低到高排列,最高优先级的定义总是最后被写入,自然覆盖了低优先级的同名定义。

一行代码的优雅:无需显式的优先级比较逻辑,Map 的天然去重特性就完成了覆盖。

Markdown Frontmatter 定义格式

Claude Code 做了一个出人意料的格式选择:用 Markdown 文件定义 Agent

---name:code-reviewerdescription:Reviews code for quality and security issuestools:-Read-Grep-Globmodel:sonnet---You are a code reviewer. Focus on:1. Security vulnerabilities 2. Performance issues 3. Code style consistency Never modify files. Only report findings.

YAML frontmatter 包含元数据,Markdown 正文就是 Agent 的 system prompt。这种设计有三个显著优势:

  1. 对人类可读:非开发者可以直接编写和编辑 Agent 定义
  2. 对程序可解析:标准 YAML 解析器即可提取元数据
  3. 版本控制友好:Markdown 文件的 diff 清晰可读

与 LangChain/AutoGen 使用 Python/TypeScript 代码定义 Agent 相比,Markdown 方式将 Agent 定义"外部化"为配置文件,使得创建和修改可以在不改代码的情况下完成。

6种内置Agent的Feature Flag矩阵

Agent特性开关可用工具
general-purpose无(始终可用)全部工具
ExploreEXPLORE_SUBAGENT只读工具
PlanPLAN_SUBAGENT只读工具
statusline-setupRead + Edit
verificationVERIFICATION_SUBAGENT只读 + Bash
claude-code-guideCLAUDE_CODE_GUIDEWebFetch

Coordinator 模式下使用require()而非import加载 Worker Agent 定义——这打破了循环依赖链builtInAgents.ts → workerAgent.ts → coordinatorMode.ts → builtInAgents.ts,同时通过as typeof import(...)类型断言保留了完整的类型信息。

MCP动态扩展

除了文件系统中的静态定义,Agent 还可以通过 MCP 动态注册。MCP 服务器通过listAgents()协议方法暴露新的 Agent 类型,系统将它们与本地定义合并。

这种"静态定义+动态扩展"的双轨模式,使得 Agent Type Registry 既有稳定的核心能力,又有灵活的扩展空间。


四、Tool Sandboxing——工具沙箱隔离

三层过滤架构

有了 Agent Type Registry 管理"谁是谁",下一步是解决"谁能做什么"。Tool Sandboxing 实现了三层工具过滤:

层1: MCP工具始终放行(信任边界委托) 层2: 全局禁止列表(所有子Agent禁用 TaskOutput/ExitPlanMode/AskUserQuestion 等) 层3a: 自定义Agent额外禁止(AgentTool本身,防止未授权嵌套) 层3b: 异步Agent白名单限制(只允许可自主完成的工具)

黑名单/白名单的混合策略是核心设计哲学:

风险级别策略适用场景
低风险黑名单排除少量危险工具同步内置Agent,保持最大灵活性
高风险白名单明确列出允许工具异步后台Agent,最小化攻击面

工具权限矩阵

Agent文件读写BashAskUser说明
general-purpose最宽松的子Agent
Explore只读纯只读,快速探索
Plan只读纯只读,设计方案
Coordinator只能编排,不能执行
Worker (async)可执行但不可嵌套

核心原则:编排者不执行,执行者不编排。Coordinator 只能分派任务给 Worker,Worker 只能执行具体操作,不能再创建子Agent。这种职责分离防止了复杂的嵌套层级,也简化了权限推理。

MCP工具的特殊地位

过滤算法第一行if (tool.name.startsWith('mcp__')) return true——MCP 工具始终放行,不受任何过滤规则限制。

这是一个务实的设计决策。MCP 工具由外部服务器提供,Claude Code 无法预知其名称和功能,无法纳入静态的白/黑名单体系。同时,MCP 服务器本身负责自己工具的安全控制。这种"信任边界委托"策略避免了在两个系统之间重复实现安全检查。


五、Scoped Memory——分层记忆作用域

Agent的长期记忆需求

多Agent系统的关键挑战是记忆。不同于单次对话中的短期记忆(由上下文窗口承载),Agent还需要跨会话的长期记忆——记住用户偏好、项目约定、之前的学习成果。

但不同类型的记忆有不同的共享需求:

记忆类型共享范围示例
全局偏好所有项目共享“用户喜欢简洁的代码风格”
项目约定与项目团队共享“本项目使用 Prettier,缩进2空格”
本地配置仅本机保留“本地开发环境的数据库端口是5433”

三级作用域设计

Claude Code 通过目录层级自然映射三种共享需求:

User作用域: ~/.claude/agent-memory/<type>/ ← 跨项目共享 Project作用域: .claude/agent-memory/<type>/ ← 通过 VCS 与团队共享 Local作用域: .claude/agent-memory-local/<type>/ ← 仅本机(.gitignore排除)

无需引入任何数据库或远程存储服务,就实现了三种不同粒度的记忆持久化。这是"利用现有基础设施"的典范——User 作用域天然跨项目,Project 作用域天然随 Git 共享,Local 作用域约定俗成被忽略。

路径安全防护

记忆目录路径涉及文件系统操作,必须防止路径遍历攻击(如 Agent 类型名包含../../):

// normalize() + startsWith() 经典路径安全模式constnormalizedPath=normalize(absolutePath)if(normalizedPath.startsWith(join(memoryBase,'agent-memory')+sep))returntrue

先规范化路径(消除...等相对元素),再检查前缀匹配。这与第8章安全纵深防御的路径遍历防护策略一脉相承。

Fire-and-Forget目录创建

记忆加载函数需要确保目录存在,但目录创建是异步IO操作,而记忆加载是同步函数(它是 system prompt 构建流程的一部分,不能阻塞)。

voidensureMemoryDirExists(memoryDir)// 异步创建,不等待结果returnbuildMemoryPrompt({memoryDir,...})

void关键字明确表示"有意忽略 Promise 结果"。成立的前提是:即使第一次加载时目录尚未创建完成,Agent 也能正常工作——因为真正的记忆读写发生在后续的工具调用中,到那时目录早已创建完毕。

自动会话记忆提取

双阈值触发判断:

条件1: token增量超过阈值(说明对话有了新的实质性内容) 条件2: 工具调用次数超过阈值(说明Agent执行了有意义的操作)

两个条件必须同时满足,避免了在用户只是闲聊或Agent在思考时频繁触发无意义的记忆提取。

记忆提取本身通过 Fork 子Agent完成,且该子Agent被严格限制为只能编辑指定的记忆文件

// 最小权限原则的典范if(tool.name===FILE_EDIT_TOOL_NAME&&input.file_path===memoryPath)return{behavior:'allow',updatedInput:input}return{behavior:'deny',message:'...'}

记忆提取Agent只能编辑一个特定文件,不能读取其他文件、不能执行命令、不能创建子Agent。这是最小权限原则的教科书式应用。


横向对比

维度Claude CodeLangGraphAutoGen
编排模型隐式(Fork)或 Prompt 引导(Coordinator)显式有向图对话协议
灵活性LLM可动态决定任务分解方式图结构编译时确定Agent间协商
可预测性较低(LLM决策不确定)较高(图结构确定)中等
角色隔离三层工具过滤+权限矩阵节点级隔离对话上下文隔离
成本优化Prompt Cache 共享(Fork边际成本趋近于零)无特殊优化额外LLM调用开销
递归防护代码+Prompt双层防御图结构天然无递归协议层约束
Agent定义Markdown外部化Python代码Python代码
扩展方式静态文件+MCP动态注册代码修改代码修改

Claude Code 的选择反映了它的定位:通用 AI 编程助手,任务类型不可预知,需要 LLM 自主判断如何分解。而 LangGraph 更适合预定义工作流,AutoGen 更适合多Agent对话场景。


实战启示

1. 不引入新执行模型

多Agent能力不需要新的执行引擎。Claude Code 证明了:同一个 while(true) 循环,通过精细的配置和约束,就能呈现完全不同的行为模式。这对自研系统的启示是——先做配置化,再做新引擎

2. 占位符统一化是成本优化的基石

Fork 的占位符设计看似简单,但它解决了 LLM 系统最核心的成本问题。在自研系统中,任何"同一前缀、不同后缀"的并行请求场景,都可以用这个模式将边际成本降到接近零。

3. 黑名单/白名单的混合策略

低风险场景用黑名单(灵活),高风险场景用白名单(安全)。这种混合策略比"一刀切"更务实。关键是定义清楚什么场景是"低风险"、什么是"高风险"——判断标准是失败后果的可逆性

4. 利用现有基础设施做分层

三级记忆作用域没有引入任何数据库——利用目录层级天然映射共享范围。在自研系统中,先想想现有基础设施能否满足需求,再决定是否引入新组件。

5. 纵深防御:代码+提示双层保险

递归防护既在代码层检查,又在 prompt 层约束。在 LLM 系统中,永远不要只依赖一层防线——代码可能被绕过,prompt 可能被注入,但两者同时失效的概率极低。


下期预告

第10篇将深入第6章后半(6.6-6.12),解析Coordinator-Worker 结构化工作流编排——编排者为何只能拥有4个编排工具?Worker之间如何通过 Scratchpad 间接通信?五阶段异步生命周期如何保证资源可靠清理?以及 Fork vs Coordinator 的选型决策指南。


本系列持续更新中,欢迎关注。上一篇:权限状态机与渐进式授权

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询