Layer 6 + 横切系统:多 Agent 协作 & 支撑体系
当一个 Agent 不够用时,如何让多个 Agent 安全地协作?它们如何被创建、隔离、通信、协调?以及整个 Agent 系统的「基础设施」(系统提示词、指令文件、Skill)是如何构建的?
Part A: 多 Agent 协作系统(Swarm)
Section titled “Part A: 多 Agent 协作系统(Swarm)”1. 架构总览
Section titled “1. 架构总览” ┌──────────────────────┐ │ Leader (主 Agent) │ │ REPL / SDK │ └──────┬───────────────┘ │ ┌────────────┼────────────────────┐ ▼ ▼ ▼ ┌────────────┐ ┌──────────┐ ┌──────────────┐ │ Fork Child │ │ In-Proc │ │ Remote │ │ (同进程) │ │ Teammate │ │ Agent │ │ │ │ (同进程) │ │ (远程/子进程) │ └────────────┘ └──────────┘ └──────────────┘ 继承上下文 独立 query loop HTTP/Socket Cache 共享 Mailbox 通信 权限同步 Worktree 隔离 Task 系统协调 跨机器2. 三种 Agent 执行模式
Section titled “2. 三种 Agent 执行模式”| 模式 | 实现 | 场景 | 通信方式 |
|---|---|---|---|
| Fork | forkSubagent.ts | 省略 subagent_type 时隐式触发 | 继承父上下文,无需通信 |
| In-Process Teammate | inProcessRunner.ts | 显式创建 Team | Mailbox + Leader UI Bridge |
| Remote Agent | RemoteAgentTask | 远程会话 | HTTP/WebSocket |
3. Fork 模式 —— Prompt Cache 共享的极致
Section titled “3. Fork 模式 —— Prompt Cache 共享的极致”Fork 是最精妙的子 Agent 模式。它的核心目标是:让子 Agent 与父 Agent 共享 Prompt Cache。
父 Agent 的 API 请求:[system_prompt] + [msg1, msg2, ..., msgN, assistant(tool_use_A, tool_use_B)]
Fork 子 Agent 的 API 请求(与父完全相同的前缀):[system_prompt] + [msg1, msg2, ..., msgN, assistant(tool_use_A, tool_use_B)] + [user(placeholder_A, placeholder_B, "你的任务是 X")]因为前缀完全相同(包括 system_prompt 和所有历史消息),API 的 Prompt Cache 可以直接命中。
字节级相同前缀的实现
Section titled “字节级相同前缀的实现”// forkSubagent.ts — 所有 fork children 使用 IDENTICAL 占位符const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
function buildForkedMessages(directive, assistantMessage) { // 1. 完整克隆父的 assistant message(包含所有 tool_use blocks) const fullAssistantMessage = { ...assistantMessage, uuid: randomUUID() }
// 2. 为每个 tool_use 生成相同的占位结果 const toolResultBlocks = toolUseBlocks.map(block => ({ type: 'tool_result', tool_use_id: block.id, content: [{ type: 'text', text: FORK_PLACEHOLDER_RESULT }], // 所有子 Agent 都一样! }))
// 3. 只有最后的 directive 不同 return [fullAssistantMessage, createUserMessage({ content: [...toolResultBlocks, { type: 'text', text: buildChildMessage(directive) }], })]}★ 设计洞察:为什么所有 fork children 用相同的占位符?因为 Prompt Cache 是按前缀匹配的。如果 child A 的占位符是 “Processing A…” 而 child B 是 “Processing B…”,它们的前缀就不同了,cache 命中率会骤降。统一占位符 + 只在最后的 directive 处分岔,让 N 个 fork children 共享同一份 cache。
// forkSubagent.ts — Boilerplate tag 防止无限递归const FORK_BOILERPLATE_TAG = 'fork_boilerplate'
function isInForkChild(messages): boolean { return messages.some(m => m.content?.some(block => block.type === 'text' && block.text.includes(`<${FORK_BOILERPLATE_TAG}>`)))}
// buildChildMessage 注入的指令:// "STOP. READ THIS FIRST. You are a forked worker process. You are NOT the main agent.// RULES: 1. Do NOT spawn sub-agents; execute directly. ..."Fork Agent 定义
Section titled “Fork Agent 定义”const FORK_AGENT = { agentType: 'fork', tools: ['*'], // 继承父的完整工具池 maxTurns: 200, model: 'inherit', // 使用相同模型(保证相同的上下文窗口) permissionMode: 'bubble', // 权限请求冒泡到父终端 getSystemPrompt: () => '', // 不需要额外提示词(继承父的)}4. In-Process Teammate —— 同进程多 Agent 协作
Section titled “4. In-Process Teammate —— 同进程多 Agent 协作”TeamCreateTool 创建团队 │ ├── 为每个 teammate 分配:agentId, name, color, worktreePath │ ├── 启动 teammate query loop(与 leader 同进程) │ ├── AsyncLocalStorage 上下文隔离 │ ├── 独立的 AbortController │ ├── 独立的 readFileState(clone) │ └── 独立的 denialTracking │ ├── Teammate 开始工作 │ ├── tryClaimNextTask() → 从 Task 列表领取任务 │ ├── 执行 query loop │ ├── 遇到权限问题 → 两级权限处理 │ └── 完成任务 → 标记 completed,领取下一个 │ └── TeammateIdle Hook 触发 ├── exit 0 → teammate 进入空闲 └── exit 2 → teammate 继续工作两级权限处理
Section titled “两级权限处理”// inProcessRunner.ts — createInProcessCanUseTool()
function createInProcessCanUseTool(identity, abortController) { return async (tool, input, context, ...) => { const result = await hasPermissionsToUseTool(tool, input, context)
if (result.behavior !== 'ask') return result
// Level 1: Leader UI Bridge(优先) const setToolUseConfirmQueue = getLeaderToolUseConfirmQueue() if (setToolUseConfirmQueue) { return new Promise(resolve => { setToolUseConfirmQueue(queue => [...queue, { tool, input, toolUseID, workerBadge: { name: identity.agentName, color: identity.color }, onAllow(...) { resolve({ behavior: 'allow' }) }, onReject(...) { resolve({ behavior: 'deny' }) }, }]) }) }
// Level 2: Mailbox Fallback(UI 不可用时) return new Promise(resolve => { void sendPermissionRequestViaMailbox(request) const poll = setInterval(async () => { const messages = await readMailbox(identity.agentName, identity.teamName) // 找到 permission_response → resolve }, 500) // 每 500ms 轮询 }) }}★ 设计洞察:为什么需要两级?Level 1(UI Bridge)让 teammate 的权限请求直接出现在 leader 的终端 UI 中,体验与单 Agent 一致,只是多了一个彩色的 worker badge。但如果 leader 的 UI 不可用(如 SDK 模式),就回退到 Level 2 的文件系统 mailbox 轮询。
系统提示词组装
Section titled “系统提示词组装”// inProcessRunner.ts:900-969let teammateSystemPrompt: string
if (systemPromptMode === 'replace') { teammateSystemPrompt = systemPrompt // 完全替换} else { const parts = [ ...await getSystemPrompt(tools, model, undefined, mcpClients), // 完整默认提示词 TEAMMATE_SYSTEM_PROMPT_ADDENDUM, // "你是 team 中的 Agent,用 SendMessage 通信" ] if (agentDefinition) { parts.push(`\n# Custom Agent Instructions\n${agentDefinition.getSystemPrompt()}`) } if (systemPromptMode === 'append') { parts.push(systemPrompt) // 追加自定义内容 } teammateSystemPrompt = parts.join('\n')}5. Worktree 隔离 —— 并行修改不冲突
Section titled “5. Worktree 隔离 —— 并行修改不冲突”当多个 Agent 需要同时编辑代码时,Git Worktree 提供了文件系统级别的隔离:
主仓库:/Users/az/project/ └── .claude/worktrees/ ├── agent-a/ ← Agent A 的独立工作目录 │ ├── src/ (独立拷贝) │ └── node_modules → /Users/az/project/node_modules (符号链接) └── agent-b/ ← Agent B 的独立工作目录 ├── src/ (独立拷贝) └── node_modules → /Users/az/project/node_modules (符号链接)关键工程细节
Section titled “关键工程细节”Slug 扁平化防止 Git D/F 冲突:
// worktree.ts — `/` 替换为 `+`function flattenSlug(slug: string): string { return slug.replaceAll('/', '+') // 'user/feature' → 'user+feature'}// 因为 git 不允许 refs/heads/worktree-user (file) 和 refs/heads/worktree-user/feature (dir) 共存快速恢复路径:
// worktree.ts — 直接读 .git 指针,避免 subprocess 开销const existingHead = await readWorktreeHeadSha(worktreePath)if (existingHead) { return { worktreePath, worktreeBranch, headCommit: existingHead, existed: true }}// 绕过 `git rev-parse HEAD` 的 ~15ms 进程启动开销符号链接避免磁盘膨胀:
// node_modules、.next 等大目录用 symlink 而非拷贝await symlink(sourcePath, destPath, 'dir')// ENOENT/EEXIST 静默跳过(幂等安全)6. 跨进程权限同步
Section titled “6. 跨进程权限同步”Worker Agent (子进程) Leader Agent (主进程) │ │ ├── 遇到权限需求 │ ├── 写入 pending/ 目录 │ │ ~/.claude/teams/{team}/ │ │ permissions/pending/{id}.json │ │ │ │ ├── 轮询 pending/ 目录 │ ├── 发现请求 → 弹出 UI 确认 │ ├── 用户决策 │ └── 写入 resolved/ + worker mailbox │ │ ├── 轮询 mailbox (500ms) │ ├── 收到 permission_response │ └── 继续执行 │// permissionSync.ts — Schemaconst SwarmPermissionRequestSchema = z.object({ id: z.string(), workerId: z.string(), workerName: z.string(), workerColor: z.string().optional(), teamName: z.string(), toolName: z.string(), input: z.record(z.string(), z.unknown()), status: z.enum(['pending', 'approved', 'rejected']), permissionUpdates: z.array(z.unknown()).optional(), // "Always allow" 规则传播 createdAt: z.number(),})Part B: 系统提示词构建
Section titled “Part B: 系统提示词构建”7. 分段式组装 + Cache 边界
Section titled “7. 分段式组装 + Cache 边界”// prompts.ts:444-577 — getSystemPrompt()async function getSystemPrompt(tools, model, ...): Promise<string[]> { return [ // ══════ 静态区域(可全局缓存)══════ getSimpleIntroSection(), // "You are Claude Code..." getSimpleSystemSection(), // 系统行为指南 getSimpleDoingTasksSection(), // 任务执行指南 getActionsSection(), // 安全行动指南 getUsingYourToolsSection(tools), // 工具使用指南 getSimpleToneAndStyleSection(), // 语气与风格 getOutputEfficiencySection(), // 输出效率
// ══════ 缓存边界 ══════ SYSTEM_PROMPT_DYNAMIC_BOUNDARY, // '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
// ══════ 动态区域(按 Section Registry 管理)══════ ...await resolveSystemPromptSections([ systemPromptSection('session_guidance', () => ...), // 会话特定指导 systemPromptSection('memory', () => loadMemoryPrompt()), // CLAUDE.md 内容 systemPromptSection('env_info', () => computeEnvInfo()), // 环境信息 systemPromptSection('language', () => getLanguageSection()), DANGEROUS_uncachedSystemPromptSection('mcp_instructions', // MCP 指令(每轮重算) () => getMcpInstructionsSection(mcpClients), 'MCP servers connect/disconnect between turns'), systemPromptSection('scratchpad', () => ...), ]) ]}Section Registry 模式
Section titled “Section Registry 模式”// 两种 section 类型:
// 1. 缓存安全:首次计算后 memoize,直到 /clear 或 /compactsystemPromptSection('memory', () => loadMemoryPrompt())
// 2. 每轮重算:标记为 DANGEROUS,每次 API 调用都重新计算(会破坏 cache)DANGEROUS_uncachedSystemPromptSection('mcp_instructions', () => getMcpInstructionsSection(mcpClients), 'MCP servers connect/disconnect between turns')★ 设计洞察:为什么 MCP 指令是
DANGEROUS_uncachedSystemPromptSection?因为 MCP 服务器可能在两轮对话之间连接或断开,导致可用工具集变化。如果缓存了旧的 MCP 指令,模型可能尝试调用已断开的工具。DANGEROUS_前缀是一个代码审查信号——使用这个 API 的人必须清楚它会破坏 prompt cache。
CacheSafeParams —— Fork 的 Cache 共享基础
Section titled “CacheSafeParams —— Fork 的 Cache 共享基础”// forkedAgent.ts — 后置保存 cache 关键参数type CacheSafeParams = { systemPrompt: SystemPrompt // 系统提示词 userContext: { [k: string]: string } systemContext: { [k: string]: string } toolUseContext: ToolUseContext forkContextMessages: Message[] // 父的消息历史}
// Stop hooks 执行后保存,fork 时读取let lastCacheSafeParams: CacheSafeParams | null = nullPart C: CLAUDE.md 指令加载
Section titled “Part C: CLAUDE.md 指令加载”8. 层级发现机制
Section titled “8. 层级发现机制”优先级(低 → 高):1. /etc/claude-code/CLAUDE.md ← 全局管理员指令2. ~/.claude/CLAUDE.md ← 用户私有全局指令3. (项目根 →...→ CWD 逐层) CLAUDE.md ← 项目指令(越近 CWD 优先级越高) + .claude/CLAUDE.md + .claude/rules/*.md4. CLAUDE.local.md ← 本地私有指令(不入版本库)目录上行遍历:从 CWD 一路向上走到仓库根目录(或文件系统根),每个目录都检查上述文件。
@include 指令
Section titled “@include 指令”<!-- 在 CLAUDE.md 中可以引用其他文件 -->@./docs/coding-standards.md@~/shared-rules/security.md@/etc/company/global-rules.md规则:
- 只在叶文本节点中生效(不在代码块内)
- 循环引用被追踪和阻止
- 不存在的文件静默忽略
- 只允许文本扩展名(
.md,.ts,.json,.yaml等,禁止二进制文件) - 推荐总大小 ≤ 40,000 字符
Part D: Skill 系统
Section titled “Part D: Skill 系统”9. Skill 注册与调用
Section titled “9. Skill 注册与调用”Skill 来源: ├── Bundled Skills(编译进二进制) │ └── registerBundledSkill() ├── User Skills(~/.claude/skills/) │ └── Markdown frontmatter 定义 ├── Project Skills(.claude/skills/) │ └── 同上 └── MCP Skills(从 MCP 服务器发现) └── prompts/list RPC
Skill → Command 转换: ├── name → 命令名(/commit, /review 等) ├── getPromptForCommand(args, context) → 提示词生成 ├── allowedTools → 白名单工具 └── context: 'inline' | 'fork' → 执行模式两种执行模式:
inline:在主 query loop 中执行,Skill 的输出作为上下文注入fork:启动子 Agent 执行,不阻塞主对话
Part E: SubagentContext —— 隔离的工程实现
Section titled “Part E: SubagentContext —— 隔离的工程实现”10. 默认隔离 + 显式共享
Section titled “10. 默认隔离 + 显式共享”// forkedAgent.ts:345-462 — createSubagentContext()function createSubagentContext(parent, overrides?) { return { // ── 隔离(clone/new)── readFileState: cloneFileStateCache(parent.readFileState), nestedMemoryAttachmentTriggers: new Set(), loadedNestedMemoryPaths: new Set(), contentReplacementState: clone(parent.contentReplacementState), // ← 关键! localDenialTracking: createDenialTrackingState(),
// ── 默认 no-op ── setAppState: () => {}, // 隔离:不影响父状态 setInProgressToolUseIDs: () => {}, addNotification: undefined, // 无 UI setToolJSX: undefined,
// ── 始终共享 ── setAppStateForTasks: parent.setAppStateForTasks, // 任务管理必须到达根 store updateAttributionState: parent.updateAttributionState, // 归因追踪共享
// ── 显式 opt-in 共享 ── // shareSetAppState: true → 使用父的 setAppState // shareAbortController: true → 使用父的 AbortController // shareSetResponseLength: true → 使用父的指标追踪 }}★ 设计洞察:为什么
contentReplacementState要 clone 而不是用新的?因为 fork children 会处理父消息中的tool_use_id。如果用一个空白的 state,子 Agent 对同一个 tool result 可能做出不同的 keep/replace 决策——导致消息内容与父不同——Prompt Cache 就失效了。Clone 保证决策一致性。
Agent 工程实践 Takeaway
Section titled “Agent 工程实践 Takeaway”Fork = Prompt Cache 共享的终极形态
Section titled “Fork = Prompt Cache 共享的终极形态”通过字节级相同的前缀 + 统一占位符,N 个子 Agent 共享同一份 cache。这是一个 成本优化 > 性能优化 的设计——减少的不是延迟,而是重复的 input token 计费。
隔离 by Default, 共享 by Opt-in
Section titled “隔离 by Default, 共享 by Opt-in”createSubagentContext 的设计原则:mutable state 默认 clone,callbacks 默认 no-op。只有通过显式的 share* flag 才能打破隔离。这避免了并发 Agent 之间的意外状态污染。
文件系统是最可靠的跨进程通信
Section titled “文件系统是最可靠的跨进程通信”Permission sync 用文件系统 + 轮询而非 IPC 或 socket。看起来原始,但好处是:
- 跨 tmux/iTerm2/进程内三种后端统一
- 无需维护连接状态
- 自然持久化(请求/响应都有磁盘记录)
- 500ms 轮询延迟对权限审批场景完全可接受
系统提示词的 Cache 边界是架构决策
Section titled “系统提示词的 Cache 边界是架构决策”SYSTEM_PROMPT_DYNAMIC_BOUNDARY 不是一个随意的字符串——它是系统提示词的架构分界线。边界之前的内容跨用户全局缓存(节省大量成本),边界之后的内容按会话变化。移动这个边界需要同时更新 api.ts 和 claude.ts 两处代码。
与其他层的交互
Section titled “与其他层的交互”- → Agent Loop(L1):每个 teammate 运行独立的 query loop;fork child 继承父的消息历史
- → Tool 系统(L2):Agent 的工具集可以通过
allowedTools限制;useExactTools跳过过滤 - → 权限系统(L3):两级权限处理(UI Bridge + Mailbox);
bubble模式冒泡到父;shouldAvoidPermissionPrompts控制无头行为 - → Hook 系统(L4):SubagentStart/Stop/TeammateIdle Hook 控制 Agent 生命周期;WorktreeCreate/Remove Hook 自定义隔离策略
- → 上下文管理(L5):Fork 共享
contentReplacementState保证 cache 一致性;teammate 独立管理自己的 compact