Multi-agent / Claude Code source notes
Claude Code 里的 Multi Agent:把协作做成协议
这次我重新顺着 Claude Code 的源码看了一遍 multi agent。越看越觉得,重点并不在“可以同时开几个模型”,而在它对协作这件事的处理方式:它先把协作拆成一组运行时能记录和传递的状态。谁属于哪个队伍,任务写在哪里,消息怎么送达,后台 agent 怎么结束,权限向谁申请,失败后还能不能接着跑。模型当然重要,但多个 agent 能不能一起工作,最后还是要看这些运行时协议。
先有队伍,才谈协作
在 Claude Code 里,team 首先是一个名字空间。创建 team 时,运行时会写入一份 team config;后面的成员发现、任务归属和消息路由,都围着这个名字空间展开。
Prompt 决定要不要协作,运行时只分流
要不要建 team、要不要起 teammate,是 prompt 层教模型做的选择。AgentTool 运行时不重新判断“该不该协作”,它只看 name、team_name 等参数走分支。
普通回复不会被队友听见
teammate 的系统提示说得很直白:想沟通必须调用 SendMessage。运行时把消息写进 pending queue 或 mailbox,再在对方下一轮上下文里注入。
结束也要能交接、能恢复
后台 agent 完成后会发回 task-notification。停止过的 agent 也不一定报废,SendMessage 可以尝试从 transcript 把它恢复成新的后台任务。
01 / Runtime shape
几条不同的执行车道
Claude Code 里至少有两类并行。普通 subagent 更像“我临时派出去的一个后台工作包”:它有 agentId、输出文件、进度、abort controller,适合做研究、实现、验证这类边界清楚的任务。它跑完以后,结果通过 task-notification 回到主线程。
team teammate 则更像“加入项目组的长期成员”:它有 name@team 的身份,有 team config,有自己的 mailbox,有 idle loop,还会在空闲时继续等下一条指令或自动认领任务。它跑完一轮不会立刻销毁,下一次 prompt 还能接上已有上下文。
这里还有一个很关键的分层:Prompt 层负责告诉模型什么时候应该创建 team、什么时候应该 spawn teammate、什么时候只需要普通 subagent。运行时不替模型做这个产品判断。到了 AgentTool.call 里,事情已经变成很机械的参数分支:有 team_name 和 name,就走 teammate spawn;没有 name,就按普通 subagent / background / sync / worktree 等路径处理。
这两条车道虽然都从 AgentTool 进入,表面看起来都是“开一个 agent”,但源码里它们的生命周期、权限处理、消息回流和 UI 表示都不一样。这个区分很重要,因为一次性后台作业和长期队友解决的是两种问题。
两条 agent 车道
Background agent
像一个可监控、可停止、可恢复的后台作业。有 agentId、进度、输出文件和 task-notification。适合“去查这一块”“改这个文件”“跑这组验证”这种独立工作包。
Team teammate
像一个常驻队友。有 name@team 身份、team file、mailbox、任务认领逻辑和 idle loop。适合持续协作、任务池分工、来回沟通。
02 / Protocol
从创建队伍到收到结果
Prompt guidance
TeamCreate / Agent / SendMessage 的 prompt 先把协作策略教给模型:复杂任务可以建 team,spawn teammate 时要传 team_name 和 name,普通文本不会被队友看到。
TeamCreate
写入 ~/.claude/teams/{team}/config.json,登记 team-lead,并重置对应 task list。队伍名随后会成为任务和 inbox 的共同坐标。
TaskCreate / TaskUpdate
把工作拆成共享任务。status、owner、blockedBy 这些字段承担协议职责,用来减少 agent 之间的重复劳动。
AgentTool
运行时只看参数分支:带 team_name 和 name 时触发 teammate spawn;不带 name 时通常走普通 subagent。这里也解析 worktree、model、tool pool、permission mode。
SendMessage
对运行中的本地 agent 放进 pending queue;对 teammate 写 mailbox;对已经 stopped 的 agent 尝试从 transcript resume。
Notification
后台 agent 以 <task-notification> 回来,带状态、摘要、结果、用量和 output path;teammate 每轮结束后向 team-lead 发 idle notification。
03 / Return flow
难点在于让结果回到正确的人手里
概念化伪代码,来自 TeamCreate / AgentTool / SendMessage 的结构抽象
const team = TeamCreate()
TaskCreate('research parser')
TaskCreate('verify tests')
Agent({
team_name: team.name,
name: 'researcher',
prompt: 'claim a task and report findings'
})
// later, every visible handoff is explicit
await SendMessage({ to: 'researcher', message: 'continue with this spec' })
await waitFor('<task-notification> or idle notification')04 / Lifecycle
生命周期里有很多“看起来啰嗦但很必要”的细节
普通后台 agent 的生命周期很像一个任务 runner:注册任务,创建 abort controller,启动 runAgent,边跑边更新 progress,结束时把结果封装成 AgentToolResult。比较细的一个点是,它会先把 task 标成 completed,再去做 handoff classifier、worktree cleanup 这些额外工作。这样 TaskOutput 之类等待结果的地方不会因为一个慢 API 或慢 git 操作被卡住。
in-process teammate 的生命周期更长。它在同一个 Node.js 进程里跑,但用 AsyncLocalStorage 带上 teammate identity。每一轮完成后不直接退出,会进入 idle:先发 idle notification,再轮询自己的 mailbox、pendingUserMessages 和 task list。收到 shutdown_request 时,运行时也不会直接杀掉它,会把请求交给模型,让它走协议响应。
这些细节让 multi agent 看起来“不那么魔法”。模型不会只靠感觉协作;运行时会不断把状态推进到一个可观察、可恢复的位置。
Progress
后台 agent 会统计 tool use、token 和最近活动,UI 和 SDK 都能看到它在干什么。
Abort
后台作业、foreground agent、in-process teammate 当前工作,都有不同层级的 abort controller。
Ordering
task 完成状态先落地,通知增强逻辑后执行,避免慢分类或清理阻塞等待方。
Idle
长期 teammate 的“空闲”表示等待下一次消息或任务认领,并不等于结束。
05 / Shared state
共享状态在哪里
这套设计刻意避免让 agent 彼此读取终端。源码里的提示甚至明确说:不要用 terminal tools 去看团队活动,要用 SendMessage 和 Task tools。原因很简单:终端输出主要给人看,稳定性和结构都不适合作为协作协议。能被多个 agent 共同使用的状态,必须落在可读、可锁、可恢复的数据结构里。
所以 Claude Code 把共享事实拆成几张表:team config 记录成员,task list 记录工作,mailbox 记录消息,AppState.tasks 记录本进程内任务状态,transcript/output file 记录后台 agent 的执行痕迹。每一张表都很朴素,但组合起来就有了协作系统需要的最小秩序。
成员名、agentId、agentType、model、颜色、pane 信息和工作目录。它回答“这个队伍里有哪些人”。
任务状态、owner、blocks/blockedBy。创建、更新、认领都用 lockfile,避免多个 agent 同时抢同一件事。
按 team/name 分桶的 JSON inbox。消息有 from、text、timestamp、read、summary,写入时加锁。
本进程内 agent 的状态、abort controller、进度、pending messages、是否 retained。它回答“现在谁还活着”。
后台 agent 停止后 resume 的依据,也是 output file 的来源。它回答“这个作业刚才做到了哪里”。
06 / Conflict control
它怎么避免 subagent 互相撞车
这里要先说清楚一个边界:Claude Code 没有给所有源码文件加全局写锁,AgentTool 运行时也不会自动判断两个 subagent 会不会改同一个文件。它的做法更工程化一点:把冲突拆成几类,再放到不同层处理。调度归 prompt,任务归 task list,沟通归 mailbox,纠偏归 TaskStop / SendMessage,高风险写入再交给 worktree 隔离。
第一层仍然是 prompt。coordinator 的提示词会告诉模型:研究类任务可以并行,写同一批文件时要收敛到更少 worker,验证可以并行但最好覆盖不同区域。换句话说,让不让两个 worker 同时写,属于 coordinator 的规划责任;AgentTool.call 只执行已经表达成参数的分支。
进入 team 之后,task list 会把这种规划写进共享任务状态。任务有 owner、status、blockedBy,claimTask 会在 lockfile 保护下改状态;已经被别人认领、已经完成、或者被未完成任务阻塞的任务,会被拒绝。启用 busy check 时,同一个 agent 还有未完成任务,也不能再抢新任务。这些机制的价值在于把“谁在做什么”落到共享事实里,让所有 teammate 都能读到同一份状态。
因此,它处理冲突的目标更接近“尽早可见”,不承诺“永远不会撞车”:谁领了任务,谁被阻塞,谁跑偏了,谁需要被 stop,哪一类写操作需要 worktree 隔离。这个边界很重要,因为源码里没有一个万能机制能自动阻止两个 agent 同时编辑同一个文件;能依赖的是 prompt 调度、任务协议、消息顺序和隔离策略一起发挥作用。
Prompt scheduling
research 可以并行,write-heavy 按文件区域收敛;这是避免编辑冲突的第一层。
Task ownership
owner / status / blockedBy 把工作归属落盘,claimTask 用 lockfile 防止重复认领。
Flat team topology
teammate 不能再 spawn teammate,队伍不会长成难追踪的多层树。
Message ordering
mailbox 写入加锁;in-process runner 会优先处理 shutdown 和 team-lead 消息。
Stop / resume
方向错了可以 TaskStop;需要继续时,用 SendMessage 把上下文和下一步重新送回去。
Worktree isolation
高风险写任务可以用 isolation: worktree,把改动放到独立工作区里做。
07 / Coordination
Coordinator 要把并行结果收束成下一步
冲突控制解决的是“别互相踩脚”,但这只是 multi agent 能工作的底线。多个 subagent 查到的东西不会自动变成方案;它们只是把信息带回来,下一步边界仍然需要 coordinator 重新整理。
源码里的 coordinator prompt 很强调这种收束动作:研究可以并行,写代码要谨慎收敛;上下文重叠高的时候,可以继续同一个 worker;需要独立验证时,可以换一个新 worker。coordinator 每次 handoff 前都要判断:这件事应该继续交给原 worker,还是整理成一条新任务交给别人。
如果继续同一个 worker,那个 worker 通常还保留着刚才研究时的上下文。它可能刚读过相关文件、刚跑过测试、刚定位到某个函数的问题。继续它是合理的,但 follow-up 仍然要把关键事实说清楚:哪个文件、哪一行、为什么出错、希望怎么改、完成标准是什么。这样既利用了 worker 的上下文,又不会把理解责任重新推回 worker。
如果新开一个 worker,要求更高。新 worker 没有前一个 worker 的发现上下文,“根据你的发现去修”对它基本没有意义。coordinator 必须把前一个 worker 的结论整理成完整任务说明,不能假设不同 worker 之间会共享脑内状态。
这里的原则不是“永远不能提到 worker 的发现”,而是不能用一句含糊的“根据你的发现”代替综合。并发可以加速信息获取,质量取决于 coordinator 能不能把发现、约束和下一步动作重新组织到一起。
研究可以并行,写同一批文件时要收敛到更少 worker。
继续同一个 worker 时,可以利用它已有上下文,但 follow-up 仍要复述关键事实。
新开 worker 时,提示必须自包含,不能依赖另一个 worker 的历史上下文。
验证最好换一个新 worker,避免实现者自己的假设污染检查。
失败时优先继续同一个 worker,因为它保留了错误上下文。
一个更具体的例子
“根据你的发现去修。” 这句话只在最理想的 continuation 场景里勉强可用,而且仍然不够清楚。
“继续修你刚才定位到的 src/auth/validate.ts:42。session.user 在 session 过期但 token 仍缓存时可能为空;在访问 user.id 前加空值判断,返回 401 / Session expired,并跑 validate.test.ts。”
“修复 src/auth/validate.ts:42 的空指针。研究结论:session.user 在 session 过期但 token 仍缓存时可能是 undefined。添加空值判断,返回 401 / Session expired,更新并运行相关测试。”
权限是协作系统的边界
权限层是这套系统里最容易被低估的一层。in-process teammate 运行在同一个 Node.js 进程里,但身份仍然独立。它用 AsyncLocalStorage 带上 teammate identity,工具调用时可以把权限请求挂到 leader 的 ToolUseConfirm UI 上,让用户看到是哪个 worker 想做什么。
如果 leader UI 不可用,还有 mailbox 版本的 permission_request / permission_response。普通后台 agent 不能弹 UI,所以 runAgent 会把 shouldAvoidPermissionPrompts 打开,并且工具池会过滤到适合异步执行的集合。换句话说,执行形态决定权限形态。
原因也很直接:多 agent 一旦能改文件、跑命令、开 MCP,就已经是多个执行体共享一个工作区。没有权限协议,它们只是在并发地制造风险。
我从源码里带走的判断
第一,Multi Agent 的瓶颈在综合。并行结果能不能可靠回到正确上下文,决定了这套系统是不是真的可用;task-notification、idle notification 和 mailbox 都在解决这个问题。
第二,名字比 UUID 更重要。源码反复强调给 teammate 发消息要用 name,不要用 agentId,因为协作协议服务的是人可理解的角色和任务归属。
第三,长期 teammate 需要 idle loop,一次性 subagent 需要 resume path。前者像队友,后者像可继续的后台作业;把它们混成一种抽象会损失很多工程细节。
第四,好的 multi agent 系统看起来会有点“保守”:文件锁、任务状态、显式消息、权限回路、停止/恢复。这些东西没有模型能力酷,但它们决定系统能不能在真实项目里用。
第五,不要把“避免冲突”理解成运行时有一个神奇的全局仲裁器。Claude Code 更依赖调度提示、任务归属、消息协议和可选隔离,把冲突从隐形并发变成可观察、可处理的工程状态。