多 Agent 协同编排
一个大任务拆成几块,开几个会话分头跑,这件事本身不难 —— 难的是谁来盯。
你得守在屏幕前,等 A 跑完去看一眼,不满意就让它返工,满意再把结果喂给 B。人一走开,整条流水线就停了。
如果让一个 AI 会话来当这个「工头」呢?它拆任务、派活、验收、驳回,全程不用你在场。
AICoder 的协同编排就是这个:一个总控会话 + 若干工作会话,总控通过 MCP 工具指挥其它会话,你只在开头确认方案、结尾看结果。
为什么不是轮询
让总控「每隔一会儿去问问好了没」是最直觉的做法,但它在真实任务里跑不通:
| 做法 | 代价 |
|---|---|
| 轮询「好了吗」 | 一小时几百轮完整推理,全烧在看进度上;几小时后总控的编排计划会被上下文压缩(compact)挤掉 |
| 事件唤醒 | 总控派完活就真的停下(会话空闲、零 token 消耗),工作会话跑完时由 AICoder 主动唤醒它 |
事件唤醒的消耗只跟真实事件数有关,跟等待时长无关 —— 派 3 个活就醒 3 次,等 10 分钟和等 3 小时的花费完全一样。
总控会话 工作会话 A / B / C
│
│ ① start_agent_session 派活 ──────► 开始跑
│
│ ② 登记等待(派活时默认自动登记)
│
▼
┌─────┐ (跑了很久……)
│ 空闲 │ 零 token 消耗
│ │
└─────┘ │
▲ │ 跑完
│ ③ [AGENT-EVENT] 注入输入框并回车 ◄────┘
│
│ ④ 读产出 → 裁决 → 驳回或派下一步
▼四个编排工具
总控通过这四个 MCP 工具指挥全场(需先在设置里接入 AICoder 的 MCP 服务,见让外部 AI 接入 AICoder):
| 工具 | 作用 | 关键点 |
|---|---|---|
start_agent_session | 新开一个会话并派活 | 返回 sessionId,后续所有指令都认这个 id |
send_to_session | 往已在跑的会话追加指令 | 驳回返工走这条,对方上下文不丢 |
watch_session | 登记「被盯的会话跑完就叫醒我」 | 一次性;派活时系统默认已替总控登记 |
get_session_status | 查一批会话此刻在干什么 | 派活前确认目标还活着 |
另有直接控制子会话、交接子会话的两个工具,见下文子会话为什么停下与子会话归谁管。
派活:start_agent_session
新建会话、开 Tab、启动真实 CLI、把提示词喂进去并回车,一步到位。可以指定工作目录、provider(Claude Code / Codex / Gemini / OpenCode 等)、模型。要并行跑多个模块就多次调用。
它返回的 sessionId 是后续一切操作的凭据。早期版本只返回标题,编排方只能按标题去模糊匹配 —— 并行开多个会话时标题可能重名,必然认错。
追加指令:send_to_session
驳回返工必须走这条,不能新开会话:新开等于让对方从头再来,之前积累的上下文全丢。
语义是排队而非立即送达:消息排进目标会话的任务队列,等它当前这轮答完、终端回到空闲才自动发出。目标在长作业时可能要等几分钟,不是没生效。
| 情况 | 怎么做 |
|---|---|
| 中途发现它跑错了方向 | 加 interrupt_first=true:先打断它当前这一轮再送达,不用等它把错的方向整个跑完 |
| 目标会话的 Tab 已经关了 | 照常发,会自动重新打开它(要多等一段 CLI 启动时间) |
| 同一段较长的指令刚派过 | 对方还没收下、或 15 分钟内已送达时会拒绝重复下发,免得总控以为丢了又发一遍 |
登记等待:watch_session
现在 start_agent_session 派活时默认就替总控登记了等待(返回里 watching 为 true),send_to_session 发给自己派出去的子会话也一样,总控派完直接停下即可。只有返回 watching 为 false(例如系统认不出总控是谁)时,才需要自己调它登记,然后停下别动。
| 参数 | 说明 |
|---|---|
watch_session_ids | 要盯的会话 id 数组,最多 20 个 |
notify_session_id | 唤醒谁(总控自己的会话 id) |
timeout_min | 超时分钟数,默认 60;到点没完成会推 timeout 事件 |
label | 这批等待的名字,唤醒时回显,便于认出是哪一步 |
每次登记只管一轮:触发即失效。派新活时系统会替你重新登记;自己手动登记的,醒来派完新活要记得再登记一次,否则下一轮永远等不到。超时、被盯会话被关掉也会推事件,不会让总控无声干等。
唤醒事件会附上子会话这一轮最后一条回复的摘录和这一轮花了多久。摘录取自 CLI 自己记下的会话记录,与读完整正文读到的是同一份,多数时候看它就够裁决;不够再用 read_session_transcript 定点读原文。少数拿不到会话记录的情况下不附摘录 —— 终端画面上残留的文字并不可靠,拿它凑一份不准的摘要会让总控基于错误信息做判断,比不给更糟,这时总控要自己读原文。
查看状态:get_session_status
| 状态 | 含义 |
|---|---|
running | 正在回答(此刻发指令会排队等它答完) |
idle | 空闲,可接指令 |
dead | 没有打开的 Tab(追加指令时会自动重新打开) |
unknown | 主窗口没运行,拿不到运行态 |
「不在回答」还会细分成等待按键、额度受限、等待继续、CLI 已退出等几种,处置各不相同,见下文子会话为什么停下。
返回里还有 queued(排队未发出的消息数)和 paused(队列是否被暂停,如余额不足)。另外会标出哪条是总控自己(isYou)、它派出去的全部子会话(childSessionIds)以及每个会话的上下文占用(contextPercent)—— 这份「派过谁」的记录由系统保存,总控的上下文被压缩后也能找回,不必因为记不得 id 就重开一批会话。
这是只读查询,用来「派活前确认目标还活着」或「被唤醒后确认还有几个没回来」,不能拿它写轮询循环 —— 那正是事件唤醒要消灭的做法。
怎么用起来
有几个现成入口,不用自己从零写提示词:
| 入口 | 位置 | 适合 |
|---|---|---|
| 状态栏「编排」 | 右键状态栏的 「编排」按钮,或在编排看板底部点「发起协同编排」 | 最快:直接弹出填写框,填完点「发起编排」就发给当前会话 |
| 新建会话模板 | 新建会话弹窗 → 模板选「协同编排」 | 固定入口,开箱即用 |
| 编排配方片段 | ⚡ 指令面板 → 「协同编排」 | 带 ,填任务目标 / 验收标准 / 模型分工,可复制改成自己的配方 |
| 编排(含真验收) | 新建会话模板或 ⚡ 指令面板 → 「编排(含真验收)」 | 验收方必须把改动真跑起来(桌面功能用 AgileShot 点一遍、网页功能用内置浏览器跑一遍),只读代码就判通过一律算没验完;确实跑不了的标「需人工确认」,收尾时汇总给你 |
从状态栏或看板发起时,正文会发给当前打开的会话,它就成了总控,并自动锁定、不会被自动关闭;没有打开的 AI 会话时会提示先打开一个。点「发起编排」直接发出,不用再回终端按回车;没发出去(终端还没起来或已关闭)时填写框保持打开,填好的内容不丢。
片段版会弹出变量填写框,逐项填完再注入终端:
| 变量 | 填什么 |
|---|---|
| 任务目标 | 这次要做成什么。留空 = 让它承接本会话上文,先归纳成一句话目标复述给你确认,再派活 |
| 工作目录 | 项目根目录绝对路径(按当前会话自动填入) |
| 验收标准 | 怎样算通过。留空 = 让它按项目形态自己定一份可执行清单、先给你看;也可以点旁边的「按项目建议」生成一份再改 |
| 实现模型 / 验收模型 | 分别用哪个 provider 和模型,可以让不同模型互相把关;默认「跟我一样」 |
| 验收方式 | 要不要另开会话验收,见下文;会记住这次的选择 |
| 提交与推送 | 每步怎么提交、要不要推送,见下文;会记住这次的选择 |
| 最大驳回轮次 | 同一步驳回几次仍不过就停下汇报,避免无限重试;默认 2,可填 1 到 5 |
| 总控会话id | 已按当前会话自动填入,别改 —— 总控登记等待时要用它 |
模板选中后可以就地编辑再发出 —— 改过之后旁边会出现「还原模板」。
验收方式
| 选项 | 做法 |
|---|---|
| 按风险分级(默认,推荐) | 高风险的步骤每步另开会话验收;其余(页面、样式、文案、文档)攒 3~5 步合并验收一次;全部做完再整体验收一次 |
| 逐条验收 | 每一步实现完都另开会话独立验收 |
| 只在最后验收一次 | 中间步骤不派验收,全部做完后另开一个会话整体验收 |
| 不派验收 | 实现方自测,回复里必须附上实际跑过的测试 / 构建结果;收尾汇报注明「本次未经独立验收」并列出建议你重点看的地方。选它时「验收模型」用不上 |
「高风险」指满足任一条:涉及外部数据或 API、动了数据库结构或接口约定、安全 / 权限 / 凭据、同时改前后端、数据迁移或同步、实现超过 20 分钟或改了 15 个及以上文件。
为什么默认按风险分级
本机 411 次派活的统计:派了验收的编排里实现被退回返工的占 19%,没派的只有 5% —— 验收查出的是真问题;但验收约占每个任务四成时间,而且页面类几乎不被驳回、同步 / 迁移类过半被驳回。所以把验收花在高风险的步骤上。
提交与推送
| 选项 | 做法 |
|---|---|
| 只提交不推送(默认,推荐) | 每步通过后在本地提交(只提交这一步改的文件),全程不推送,收尾列出没推送的提交 |
| 全部通过后推送一次 | 每步通过后本地提交,全部做完且整体验收通过后按项目约定的方式推送一次并核对远端 |
| 全部通过后再提交 | 中间不提交,改动留在工作区;全部通过后停下,把改动按主题分组列给你,由你决定怎么分批提交、要不要推送 |
发起时定好,总控就不会每做完一批停下来问你「要不要推送」—— 人不在的时候,这一问往往要白等几十分钟到几小时。
实现方与验收方
| 选项 | 说明 |
|---|---|
| 跟我一样(默认) | 沿用发起编排的这个会话所用的 CLI。编排默认跟着当前这家 CLI 走,不会把活悄悄派回 Claude —— 你往往正是因为那家额度用完才换过来的 |
| Claude Code · 默认模型 / Codex · 默认模型 | 不写死模型,跟随设置里的默认模型或当前档案的模型,下拉里写明这次实际会用哪个 |
| 写死机型的选项 | 当前档案跑不了这个模型时,下拉直接写明「当前档案用不了,会换成某某」;本机没安装的 CLI 标「未安装」 |
| Claude 的 1M 变体 | 接中转站时可选,每项写明按多大窗口跑:不带 1M 的按 200K 计、会提前压缩;带 1M 的需要中转站支持,不支持时超过 200K 会报错 |
验收方建议换一家模型做独立复核(下拉里标了「异构验收首选」的那项)。下拉里没有的模型可以手填,写成「Codex · gpt-6-luna(provider_id=codex, model=gpt-6-luna)」这样带上 provider_id,漏了总控可能会派错 CLI。
Codex 会话(终端或聊天模式)同样能当总控。
验收标准自动建议
「验收标准」旁的「按项目建议」按钮,会按项目形态(Tauri 桌面应用、Web 前端、Rust、Java、Python 等)和任务描述生成一份可执行的验收清单:每条都是「一个能真正跑起来的动作 + 一个明确的期望结果」,而不是「确保功能正常」这类没法执行的话。
它只建议你本机实际可用的手段 —— 比如没接入 AgileShot,就不会建议「点一遍桌面界面」那条,免得验收方照着做、做不成,再把「环境不行」当成「实现有问题」报回来。生成后可以再改。
编排看板
编排进行中,谁在等谁、哪个子会话卡住了、哪条唤醒没送达,都能在编排看板上看到,不用一个个点开标签去猜。
| 打开方式 | 说明 |
|---|---|
| 状态栏 「编排」 | 单击打开看板;有总控在等子会话时,按钮角标显示等待数 |
| 命令面板 | Ctrl+Shift+P 搜「编排看板」 |
看板分三块:
| 区块 | 看什么 |
|---|---|
| 等待关系 | 哪个总控在等哪些会话跑完、这批等待的名字、已等多久、离超时还剩多久。编排卡住时先看这里:是不是压根没人在等 |
| 会话运行态 | 每个 AI 会话此刻的状态(见下节)、排队消息数、距上次输出多久(显示「回答中」却长时间没输出,多半卡住了)、是否有总控在等它、归属(编排中 / 已交给你)。需要人管的排在最前 |
| 最近唤醒事件 | 每次唤醒是怎么来的:跑完、疑似卡住、没跑起来、超时、已关闭、用户接手、用户交还等。标着「未送达」的 = 事件没能送进总控(多半是总控的标签已关),这条编排已经断了 |
点任意会话可直接切过去处理;看板底部有「发起协同编排」按钮。
总控标题前的小图标
标签栏和侧边栏里,总控会话的标题前有一个 小图标(只标总控自己,子会话不标):
| 样子 | 含义 |
|---|---|
| 常亮 | 正在等子会话跑完 |
| 变淡 | 派过活,但当前没在等 |
悬停可看到在等几个、派出过几个。
子会话为什么停下
子会话「不在输出」可能是几种处置完全相反的情况。看板和 get_session_status 会把它们分开,总控不必再靠读对话内容去猜:
| 看板显示 | 状态值 | 什么情况 | 该怎么办 |
|---|---|---|---|
| 空闲 | idle | 正常答完一轮 | 读产出、派下一步 |
| 等待按键 | awaiting-input | 停在确认菜单上等按键 | 按菜单键;别发文字,文字会被当成菜单输入把选项顶飞 |
| 额度受限 | rate-limited | 撞了额度或限流 | 此时发什么都白发,等额度恢复或换档案 |
| CLI 已退出 | crashed | CLI 退了,只剩一个命令行 | 发消息会被命令行当命令执行,要重启会话 |
| 等待继续 | context-full | 上下文吃紧,CLI 在等一句「继续」 | 发「继续」 |
| 状态未知 | unknown | 读不出它停在哪 | 别当成已经跑完,切过去看一眼 |
看板上还会出现这几种:
| 看板显示 | 什么情况 |
|---|---|
| 回答中 / 还在跑 / 重试中 | 系统还在动(正在输出、前台工具没返回、请求失败后 CLI 自己在倒计时重试),等着就行,不会唤醒总控 |
| 未启动 | CLI 还在启动、派的提示词还没被收下;此刻读产出是空的,也别重发 |
| 提示词未被受理 | 提示词一直没被 CLI 收下,这一轮没开始;确认正文为空后重发一次 |
| 这一轮失败 | API 报错或连接中断,没有产出,要重发这一步 |
| 登录过期 | 只有人能修:到那个终端重新登录或换档案;队列已暂停,重新登录后手动回车一次即恢复 |
| 轮换中 | 撞了额度,额度自动轮换正在切档案并重启会话,稍等即可;切不成会转为「额度受限」 |
| 空闲(挂着后台) | 这一轮答完了,但还挂着没收的后台命令;总控已被唤醒,由它读产出判断 |
能直接对子会话做的动作
停在确认菜单上的会话恰恰是「空闲」的,这时要的是按一个键,而不是一段话。总控可以绕过排队,直接操作停下来的子会话:
| 动作 | 用在什么时候 | 工具 |
|---|---|---|
| 按菜单键 | 等待按键 | control_session(key) |
| 发「继续」 | 等待继续 | control_session(continue) |
| 打断 | 正在跑但需要叫停 | control_session(interrupt) |
| 重启 | CLI 已退出或卡死;重启后接着原会话 | control_session(restart) |
| 中途纠偏 | 发现跑错方向,先打断再送达新意见 | send_to_session 加 interrupt_first=true |
你自己处理的话,在看板上点那个会话切过去手动操作即可。
子会话归谁管
编排派出去的会话默认归总控管:跑完就回报给总控。你想自己接着推进某个子会话时,可以把它接过来 —— 接手后它跑完不再回报给总控,总控也不能再盯它。
| 操作 | 在哪里 | 效果 |
|---|---|---|
| 我来接手 | 标签右键「我来接手(不再回报总控)」,或编排看板「会话运行态」里的「接手」 | 撤掉总控对它的等待,之后由你推进 |
| 交还给总控 | 标签右键「交还给总控」,或看板里的「交还」 | 总控收到一条通知接着管。派它出来的总控会话没开着时交还不了,先把那个会话打开 |
总控也可以用 handoff_session 主动把子会话交给你(例如你在对话里说「这个我自己看」)。
直接在子会话里打字会怎样
你在子会话里亲手发了一条消息,智码会自动判断这是「协助」还是「接手」:
| 判定 | 什么时候 | 结果 |
|---|---|---|
| 协助 | 它停在确认菜单上;或它上一句是在问你,而你的回复不长(80 字以内) | 你是在帮它把这一步走完,这一轮跑完仍回报给总控 |
| 接手 | 其余情况,比如给它布置了新的事 | 从编排中接手,总控不再收到它的回执 |
只按回车、在菜单里敲个数字或 y 不算发消息。判定后会弹一条提示,判错了点一下就能翻转(「我来接手」/「这只是协助,继续回报」)。
另外,如果你正盯着某个子会话(它是当前标签、主窗口在前台、最近 90 秒内你在里面有过操作),它正常答完时会先提示「这一轮跑完了,8 秒后回报给总控」:点「我自己看」就由你接手,不理会则照常回报。等按键、撞额度这类本来就要人处理的情况不扣,照常立刻回报。
子会话起的服务:临时任务
子会话要把项目跑起来测时,按配方会走智码的任务系统:先用已登记的启动任务,没有就扫描候选并登记;改了后端代码先重启任务、等服务真正起好再测。它不会在命令行里另起一份服务,也不会为了避开端口占用去改端口 —— 那样会和任务里的进程抢端口,测到的还可能是旧代码。AI 侧怎么启动和重启任务,见让外部 AI 接入 AICoder。
子会话为验收或预览单独起的服务算临时任务:
| 特点 | 说明 |
|---|---|
| 放在哪 | 侧边栏该项目任务下的「临时」子组,默认收起(你正看着的那条会自动展开) |
| 什么时候消失 | 停止它(右键菜单里显示为「停止并移除」)、或重启智码后自动从列表移除 |
| 想留下来 | 右键「保留为常驻任务」,之后就不会被自动移除 |
几个自动处理的场景
| 场景 | 智码怎么做 |
|---|---|
| 你正在打字时,编排要建新会话 | 新会话会切到前台、新终端会抢焦点,你打的后半截字会落进新终端。所以检测到你最近 2 秒在打字就推迟,等你停手再建;最多等 10 秒,到点照常创建,不会无限拖住编排 |
| 你停在远程 SSH 工作区 | 编排派出的子会话固定建在本机;必要时先切回本机工作区并提示「编排派出的新会话在本机运行,已切回本机工作区」,切换同样等你停手 |
| 子会话撞了额度 | 唤醒先扣住不发(总控收到也办不成事,多半它自己额度也用完了);等额度恢复、它真正跑完这一轮才叫醒总控。额度恢复后 CLI 自己接着跑的那一轮也认得出来 |
| 子会话停下来等自己起的后台任务(如长编译) | 等任务回来、那一轮跑完才叫醒总控;如果先发了回执,会注明「系统已替你续挂了一个跟进等待」,真结果在下一条回执里 |
| 派出去的提示词收没收到 | 以 CLI 自己的会话记录为准:启动期的屏幕重绘不会被当成回答,不会派出去几秒就报「跑完」,总控也不会把以为丢了的任务再发一遍 |
| 智码主程序重启 | 跑到一半的子会话不会被判成「状态不明」;重启期间 AI 的调用会等主程序起来(最多约 40 秒) |
| 内存吃紧 | 自动关会话时优先收已交差的编排子会话;总控还在等的子会话、正在等子会话的总控不会被关,详见多标签会话管理 |
配方里的关键约定
内置配方不只是提示词,其中几条是可靠性的关键:
- 派完活必须登记等待然后停下,不许轮询 —— 否则烧 token,且编排计划会被上下文压缩挤掉
- 每次醒来先读
state.json确认自己在哪一步 —— 总控的上下文可能已被压缩,编排计划只有落盘(<工作目录>/.agent-pipeline/state.json)才不会丢 - 驳回走追加指令,不新开会话 —— 保住对方的上下文
- 超过最大驳回轮次就停下汇报,不无限自动重试
- 只有阻断项才驳回返工 —— 验收方把发现分成「阻断 / 非阻断」两级,非阻断的建议记为遗留项,全部做完后合并加固一次或列给你定,不再每条单开一轮加固(那样比实现本身还耗时);涉及数据损坏、安全 / 凭据、真实计费、核心流程走不通的一律按阻断处理
- 默认串行,改代码的活一律串行 —— 子会话共用同一个仓库和编译产物,同时改会互相卷进对方的改动;只有只读的调研、出方案、审计可以并行派出去
- 实现到验收必须另开会话 —— 验收方看过实现方的全部思路就会被带偏,独立复核才有意义
派给子会话的任务书也有固定要求:只做指定那一步、答完停下回报、范围写死、结论区分「实测」与「未验证」、允许否决(查下来不该做就给建议否决,不为了有产出硬做)、长任务分批跑、要把项目跑起来测时走任务系统。
注意事项
| 事项 | 说明 |
|---|---|
| 总控自己也是一个会话 | 它需要知道自己的会话 id 才能登记唤醒(从状态栏 / 看板发起时已自动填入) |
| 子会话的 Tab 别随手关 | 追加指令时会自动重新打开,但正在等它的总控会立刻收到「已关闭」事件 |
| 默认全自动 | 派出去的会话按 provider 注入跳过权限确认的标志,自己跑到底。仅用于可信任务 |
| 主窗口要运行 | AICoder 主窗口关闭时无法新建会话,也拿不到运行态 |
| 远程主机上的会话当不了总控 | 出于安全,远程会话调不了在本机起会话、起任务的工具;子会话也总是建在本机 |
自己写的 hook 要认得出回执
总控收到的输入里有两类不是人发的:
| 输入 | 开头 | 谁发的 |
|---|---|---|
| 子会话回执 | [AGENT-EVENT] | AICoder,在被盯的会话跑完、超时或被关掉时 |
| 后台任务完成通知 | <task-notification> | Claude Code 自己,后台命令、子代理等后台任务结束时 |
Claude Code 的 UserPromptSubmit hook 对这两类输入照常运行。如果你的项目用这类 hook 给每条提问附一段固定内容(常见的是「技能清单」),总控一次编排要收几十上百条回执,每条都会被塞一份。我们实测 25 个总控会话,这类注入占写进上下文内容的 8%,其中近一半跟在回执和通知后面,对总控毫无用处。
建议在 hook 里先跳过它们(Node.js 写法,prompt 是 hook 从标准输入读到的 input.prompt):
// 长回执会被 Claude Code 包进 <pasted_content id="…">,先剥掉外壳再认;
// 只认开头,正文里引用了一段通知原文的普通提问照常处理
const unwrapped = prompt.replace(/^(?:<pasted_content[^>]*>\s*)+/, '');
if (unwrapped.startsWith('[AGENT-EVENT]') || unwrapped.startsWith('<task-notification>')) {
process.exit(0);
}[AGENT-EVENT] 这个开头是固定约定,后续版本不会改;回执正文里的字段会随版本增补,hook 不要依赖它们。<task-notification> 的格式由 Claude Code 决定。
为什么不能从 AICoder 这边关掉
Claude Code 没有「对某一条输入不跑 hook」的开关,唯一的全局开关 disableAllHooks 会把拦危险命令的 PreToolUse 等安全 hook 一起关掉。所以只能由 hook 自己认出来跳过。
