Article / Agent 与编排
多 Agent 协作与隔离
SubAgent 的独立上下文与 Teams 的跨进程协作:多 Agent 的适用边界、隔离手段与失败模式。
多 Agent 经常被理解为「让几个 AI 一起干活」,但决定成败的是隔离:上下文会不会互相污染、消息能不能可靠送达、几个执行者会不会踩到同一份工作区。以 NovaCode 的 SubAgent 与 Teams 为案例,可以看到多 Agent 的两种形态、支撑它们的隔离机制,以及什么时候该放弃多 Agent。
SubAgent 的独立上下文
SubAgent(子 Agent)解决的第一个问题是上下文污染:搜索、探索、试错这类子任务会产生大量中间内容,如果全塞进主对话,主对话很快被噪声填满,模型注意力和上下文预算一起被消耗。子 Agent 有自己的对话窗口,只在结束时把结论交回主 Agent,宿主看到的是报告而不是过程。代价也要说清楚:子 Agent 必须自己重建上下文,报告是唯一的信息通道,拆分不当,主 Agent 拿到的是一个缺少细节的结论,还得回头补问。
它的定义是一份带 frontmatter 的 Markdown,可以放在项目 .novacode/agents/、用户 ~/.novacode/agents/ 或内置目录,字段包括工具白名单与黑名单、模型、maxTurns、权限模式、是否后台运行、是否 worktree 隔离。定义文件支持热重载,解析失败时回退缓存版本。
---
name: Explore
description: Fast read-only search agent for locating code
disallowedTools: [EditFile, WriteFile]
model: haiku
---
你是一个文件搜索专家……
内置的四个定义覆盖最常见的分工:Explore 只读检索、用便宜模型;general-purpose 拿全量工具;Plan 限制轮数;Verification 做对抗式验证、要求输出明确裁决,且需要显式开启。执行形态有四种:定义型前台同步返回结果;定义型后台交给 TaskManager,立即拿到任务 ID,完成后通知注入下一轮;fork 不指定类型、深拷贝父对话,并追加一段规则——不得再 fork、不得向用户提问、报告不超过 500 字符,强制后台执行;worktree 隔离则先建独立工作树再运行。fork 最省事也最值得警惕:它继承的是上下文,工具池是父级的浅拷贝;而 Skill 的 fork 路径连权限检查器与 Hook 引擎都没传,技能体内的写文件和跑命令完全绕过检查(S6)。工具过滤另有四层,从全局禁用一路收到定义级,MCP 工具始终放行。
一个容易被忽略的事实是:所有子 Agent 共用父级的规则引擎,但各自新建权限检查器与路径沙箱,新检查器的沙箱兜底默认关闭——「有沙箱所以自动放行」这条近路不会自动继承。每新增一种拉起子 Agent 的方式,就多出一个必须单独定义的信任剖面,这是多 Agent 系统最容易欠下的安全债。trace 树记录每个子 Agent 的类型、父子关系、token 用量与状态,这是多 Agent 能不能被调试的前提:只能看到最终报告的系统,出了问题无从下手。
从子 Agent 到 Teams
SubAgent 是一次性的,派出去、干完、回来。Teams 解决的是另一类问题:长期协作。Lead 负责分解与调度,teammate 是独立进程里长驻的 worker,双方通过磁盘上的团队目录交换消息与任务,而不是 RPC。开启 Coordinator 模式后,Lead 的工具集被收窄到 Agent、SendMessage、TaskStop 这几个;收窄的判据不是「读/写」,而是「会不会把大段内容灌进 Lead 的上下文」——Lead 需要留出空间装任务分解与队员状态。调度指引约 8 KB,首轮全文、之后每 5 轮复述一次、其余轮次只发精简提醒,省下的上下文不能被指引本身又填回去。这套「首轮详细、之后降噪」的节奏是个可参考的做法:调度指引也是上下文预算的一部分,它的占位不应超过任务本身。
协作的载体是两个文件系统结构。邮箱是每个成员一个 {agent_id}.json 收件箱:同进程用线程锁串行化,跨进程用 .lock 文件锁——指数退避加抖动抢锁,总时限 5 秒,超过 10 秒的锁视为持有者崩溃、可强制接管。投递语义是 at-most-once:consume 即标记已读,崩溃会丢消息,这是一个明确的取舍。结构化消息(shutdown 请求、计划审批)带 request_id,让 Lead 能同时向多个队友发起请求后对上答复。共享任务板是团队目录下的 tasks.json,任务有 pending、in_progress、completed、blocked 四种状态和 blocks / blocked_by 依赖,由四个工具读写;当前实现是内存字典整体覆写、没有文件锁,多进程并发写会互相覆盖(T5)。
用文件做跨进程协调是单机单人场景的最小实现:进程之间不需要共享内存,谁都能读,崩溃后状态还在。代价是文件系统只提供读写两个原语,原子性、锁、投递语义都得自己补。改进方向也很清楚:临时文件加原子替换、消息确认与幂等键、共享状态加锁——否则并发一上来,消息丢失和任务板竞争会同时暴露。
三种拉起方式与 Worktree 隔离
Teams 的 worker 有三种拉法:进程内、tmux 窗格、iTerm2 标签页。选择是自动的,但只有进程已经身处 tmux 或 iTerm2 会话中才用窗格后端;显式指定进程内、非交互模式、Windows 一律回退。tmux 用 new-window -d 开独立窗口、不抢焦点,iTerm2 用 AppleScript 新建标签页;拉起命令会做 shell 转义,初始任务在进程启动前写入队友邮箱,新进程第一次空闲轮询就能取到。
tmux new-window -d -n <team>-<member> \
"cd <worktree> && <python> -m novacode --teammate \
--team-name <team> --agent-name <member>"
窗格队友是长驻进程;进程内队友跑完一轮后发一条空闲通知,然后每 0.5 秒轮询邮箱,但只在 60 秒窗口内有效,窗口结束就退出。同一套接口在两种后端上的寿命语义并不一致(T4),调用方按一种假设写逻辑,另一种就会出错。
多 Agent 改同一份代码,麻烦集中在文件系统。worktree 隔离把子 Agent 放进独立工作树:路径在仓库的 .novacode/worktrees/ 下,分支名统一加 worktree- 前缀。两个工程细节值得单独说。其一是快速恢复:创建前先从 worktree 的 .git 文件、commondir、HEAD、packed-refs 直接读 HEAD 提交号,不启动 git 子进程就能识别「这个 worktree 已存在」,进程重启后可复用。其二是创建后 setup:复制本地配置与 .env、设置 git hooks、软链依赖目录(默认 node_modules、.venv、vendor)、按 .worktreeinclude 复制被 .gitignore 忽略但确实需要的文件。软链依赖是并行 Agent 能落地的关键——每个 worktree 单独装一遍依赖,会把并行省下的时间全部还回去。
退出与清理有变更保护:先数未提交改动与新提交,任一大于零就默认拒绝删除;子 Agent 的自动清理也只在没有任何变更时执行,有变更就保留并回报路径与分支。后台每小时扫描一次陈旧工作树,只清理超过 24 小时未动、名字命中临时模式、且无改动无未推送提交的那些——有未推送提交就保留,避免误删未合并的工作。worktree 隔离的边界也要写清楚:它隔离的是文件,不是资源。端口、数据库、缓存仍然共享;依赖目录软链在多数情况下正确,但两个 worktree 同时改同一个被软链的锁文件仍会互相影响。还有一个资源容易被忽略:模型的并发限制与额度。多个 worktree 同时跑检索与生成,速率上限与开销是全局共享的,调度层需要限流,而不是假设资源无限。
什么时候需要多 Agent
把多 Agent 当成能力升级是常见的误判。它的收益只有三类。上下文隔离:子任务的中间过程不该进入主对话,这是最扎实、收益最直接的一类,SubAgent 就是为它而生。并行:任务之间确实独立,且墙钟时间重要——「独立」比想象中难得,两个改同一模块的任务就不独立,只是把冲突推迟到合并时爆发。角色分工:不同角色需要不同工具集、模型、权限甚至不同的验证立场,对抗式验证是典型——验证者必须能推翻执行者,才不会退化成自我背书。
判断标准可以排成三个问题:这个子任务会不会产生大量不该进主对话的中间内容;它是否与其他任务完全独立;它是否需要不同的工具、模型或权限。三个都答否,就用单 Agent 加更好的上下文管理。如果任务是一条连贯的推理链,拆开只会让每一段都缺少上下文;如果成本敏感,每个子 Agent 都是一份完整的上下文开销,并行不省成本,只是把等待换成了吞吐。fork 让「开一个子 Agent」只要一次工具调用,所以更需要上面的判断标准。还有一个常被忽略的成本:协调本身要消耗上下文与轮次,两个 Agent 来回对一次就能解决的事,拆成三个角色反而更慢。
对话式协作与流程式编排
行业里的多 Agent 大致有两种形态。对话式协作让多个 Agent 在一个群里轮流发言,由协调者决定谁说话,代表是 AutoGen 的群聊与 CrewAI 的角色小组:灵活,适合开放式探索,但收敛不可预测、成本难以预估,而且「谁该说话」本身就要花模型调用。流程式编排把协作写成图或状态机,节点是角色、边是路由规则,由代码决定下一步,LangGraph 的图编排与 CitedRAG 的 supervisor 路由都属于这一类:路径可预期、可审计、方便加预算与人工审批,代价是未预定义的协作方式表达不了。
NovaCode 的 Teams 更像第三种:主管加 worker,但通信不走对话,而走文件邮箱与共享任务板。它不预先定义完整图,也不让 Agent 自由交谈,而是把「谁做什么」放在共享任务板上,把「怎么沟通」降级为消息投递。好处是进程解耦,worker 可以是独立进程;代价是协调状态散落在磁盘上,一致性问题要自己兜。三种形态没有优劣:对话式适合作研究和开放式任务,流程式适合阶段明确的生产任务,邮箱加任务板适合跨进程、需要持久化协作状态的场景——关键是通信媒介与权限模型要匹配。对话式框架通常还要自己解决收敛问题——轮数上限、发言顺序、达成一致的判据,这些在流程式编排里本来就是图的一部分。
多 Agent 的失败模式
Demo 能跑不等于系统可用,多 Agent 的失败集中在几处。消息丢失最难查:at-most-once 加上非原子写,崩溃或并发读会整箱丢消息;窗格路径还存在邮箱键不一致,初始任务按名字写、后续消息按 trace uuid 写,而窗格进程只读名字键,表现为「队友能收到第一个任务,之后再也收不到消息」(T1)——静默丢消息比报错更难定位。任务板竞争则是两个 worker 的更新互相覆盖,或者同时认领同一个任务;共享状态的原子性不能靠「应该不会同时写」来保证。
成本失控很常见:每个子 Agent 都是完整上下文,重试、轮询、计划复述都在叠加 token,长驻 worker 空闲也在消耗。约束必须是显式的——轮数上限、任务预算、运行级调用上限,以及按子 Agent 的 token 统计;没有观测就无法控制成本。生命周期不一致同样常见:进程内队友 60 秒后结束而窗格队友长驻,spawn 失败不回滚会留下僵尸成员与工作树(T3),同一套接口在不同后端语义不同。最后是信任剖面漂移:主 Agent、子 Agent、teammate、Skill fork、非交互模式各自的权限与隔离组合都不同(D3),梳理不清就会出现「同样的命令在主进程被拦、在 fork 里放行」。
判断一个多 Agent 系统是否可用,看它能不能回答四个问题:投递语义是什么,共享状态怎么保证原子,预算在哪里强制,每个入口的信任剖面是什么。回答不了这四个问题,协作规模越大越危险。对应到工程实践,就是给消息加确认与幂等键、给共享文件加锁与原子替换、给预算加上限与告警、给每种入口写下信任剖面并让启动时校验。NovaCode 为这些问题列了修复计划,也把每条边界记进了已知问题清单。