Skip to content

Layer 6 + 横切系统:多 Agent 协作 & 支撑体系

当一个 Agent 不够用时,如何让多个 Agent 安全地协作?它们如何被创建、隔离、通信、协调?以及整个 Agent 系统的「基础设施」(系统提示词、指令文件、Skill)是如何构建的?


┌──────────────────────┐
│ Leader (主 Agent) │
│ REPL / SDK │
└──────┬───────────────┘
┌────────────┼────────────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────┐ ┌──────────────┐
│ Fork Child │ │ In-Proc │ │ Remote │
│ (同进程) │ │ Teammate │ │ Agent │
│ │ │ (同进程) │ │ (远程/子进程) │
└────────────┘ └──────────┘ └──────────────┘
继承上下文 独立 query loop HTTP/Socket
Cache 共享 Mailbox 通信 权限同步
Worktree 隔离 Task 系统协调 跨机器
模式实现场景通信方式
ForkforkSubagent.ts省略 subagent_type 时隐式触发继承父上下文,无需通信
In-Process TeammateinProcessRunner.ts显式创建 TeamMailbox + Leader UI Bridge
Remote AgentRemoteAgentTask远程会话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 可以直接命中。

// 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. ..."
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 继续工作
// 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 轮询。

// inProcessRunner.ts:900-969
let 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 (符号链接)

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 静默跳过(幂等安全)

Worker Agent (子进程) Leader Agent (主进程)
│ │
├── 遇到权限需求 │
├── 写入 pending/ 目录 │
│ ~/.claude/teams/{team}/ │
│ permissions/pending/{id}.json │
│ │
│ ├── 轮询 pending/ 目录
│ ├── 发现请求 → 弹出 UI 确认
│ ├── 用户决策
│ └── 写入 resolved/ + worker mailbox
│ │
├── 轮询 mailbox (500ms) │
├── 收到 permission_response │
└── 继续执行 │
// permissionSync.ts — Schema
const 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(),
})

// 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 类型:
// 1. 缓存安全:首次计算后 memoize,直到 /clear 或 /compact
systemPromptSection('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 = null

优先级(低 → 高):
1. /etc/claude-code/CLAUDE.md ← 全局管理员指令
2. ~/.claude/CLAUDE.md ← 用户私有全局指令
3. (项目根 →...→ CWD 逐层) CLAUDE.md ← 项目指令(越近 CWD 优先级越高)
+ .claude/CLAUDE.md
+ .claude/rules/*.md
4. CLAUDE.local.md ← 本地私有指令(不入版本库)

目录上行遍历:从 CWD 一路向上走到仓库根目录(或文件系统根),每个目录都检查上述文件。

<!-- 在 CLAUDE.md 中可以引用其他文件 -->
@./docs/coding-standards.md
@~/shared-rules/security.md
@/etc/company/global-rules.md

规则:

  • 只在叶文本节点中生效(不在代码块内)
  • 循环引用被追踪和阻止
  • 不存在的文件静默忽略
  • 只允许文本扩展名(.md, .ts, .json, .yaml 等,禁止二进制文件)
  • 推荐总大小 ≤ 40,000 字符

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 —— 隔离的工程实现”
// 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 保证决策一致性。


通过字节级相同的前缀 + 统一占位符,N 个子 Agent 共享同一份 cache。这是一个 成本优化 > 性能优化 的设计——减少的不是延迟,而是重复的 input token 计费。

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.tsclaude.ts 两处代码。


  • → 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