多 Agent 编排实战:Orkas 如何调度一个主 Agent 和它的子 Agent 拆解 Orkas 的多 Agent 编排主 Agent 把一句请求变成一份计划按依赖派遣子 Agent在步骤之间传递上下文并在出错时自愈。上一篇讲的是怎么让一个agent 可靠地跑起来运行循环、工具路由、上下文压缩、可自愈的会话。那一层回答的是「单个 agent 怎么不出岔子地走完一个任务」。这一篇讲它上面的一层——当一个 agent 不够用、一件事得拆给一个团队来做时会发生什么。这就是人们常说的多 Agent 编排multi-agent orchestration一个掌管对话的主 agent 把请求拆成若干块每块交给一个专门的子 agent等该等的那几个跑完再开下一个把结果往后传并在某一步失败时把整件事稳在轨道上。Orkas 把这一整套完全跑在用户自己的机器上。下面就拆一下这层编排是怎么搭起来的——代码做了脱敏和泛化但结构是真实的。一句话版本一个主 Agent 带着子 Agent在同一个工作区里这就是 Orkas 现在派发工作的方式Commander 召集专职 Agent并行或串行地跑而你能看着它发生。主 Agent 与子 Agent心智模型是一支指挥链清晰的小团队。一个主 agent我们叫它 commander掌管对话和整体上下文。它不亲自干所有活它的职责是决定什么该做、按什么顺序、由谁来做。子 agent则是专才——各自配着自己的系统提示词、自己被允许用的工具、自己的一套技能。一个子 agent 只擅长其中一小块工作轮到那一块时才被调起来。有两点让它不只是个口号。第一子 agent 和技能是一等单元不是提示词技巧子 agent 是一个真实、单独配置的 agent派遣给它是一次带着独立上下文的真正交接。第二协调不靠模型的良好意愿——它由一份显式的产物来驱动而这份产物的真伪由系统、而非模型来保证。这份产物就是计划plan。计划是一张图不是一段脚本当主 agent 判断一个请求需要不止一步时它会写一份计划。这份计划既不是自由的散文也不是一条线性清单——它是一张小小的依赖图DAG。每个节点是一步每一步带着编排器派遣它所需要的一切interface PlanStep { index: number; // 从 1 开始稳定永不重新编号 title: string; // 给人看的标题会显示在 UI 上 assignee: string; // 由谁执行user | commander | 某个子 agent input?: string; // 派遣负载——一个模板见下文 wait_for?: number[]; // 前置步骤的 index默认是 [index - 1] on_failure?: abort_plan | continue | ask_commander; // --- 运行时状态归编排器所有模型不写 --- status: pending | in_progress | done | failed | skipped | blocked; output_summary?: string; // 这一步产出的简短摘要 output_files?: string[]; // 这一步产出的文件 failure_reason?: string; }把列表变成图的正是wait_for这个字段。默认一步只等它前面那一步一条简单的链但一步可以声明它依赖前面好几步——「写总结」可能同时要等「调研市场」和「摸排竞品」。这是一个菱形不是一条直线编排器把它当图来处理。谁说了算编排器不是模型再看一眼那个结构体里的切分。模型在第一次写计划时填的是意图——标题、执行者、输入、依赖关系。但所有关于执行状态的东西——status、output_summary、failure_reason——只归编排器所有。模型只提一次计划它永远没机会把自己的步骤标成「done」。这个分离是刻意的也是整个设计里最重要的一个决定。语言模型完全可能在第 3 步明明报错时乐呵呵地宣布「第 3 步完成」也可能在一段长对话里跑到一半就忘了还有哪些步骤没做。如果状态活在模型脑子里计划就会和现实越漂越远。把状态做成一份只有执行器才写、而且只在某一步真正结束时才写的结构化产物计划就始终是「真实发生了什么」的准确镜像。模型决定工作的形状运行时决定关于进度的事实。派遣那些已经就绪的步骤计划一旦存在一个小引擎——执行器executor——就推着它往前走。核心动作是「找出就绪的步骤并派遣它们」。一步在它仍是pending、且它所等待的每一步都到达了某个终态、且足够成功时才算就绪function findReadySteps(plan): PlanStep[] { return plan.steps.filter((s) { if (s.status ! pending) return false; const deps s.wait_for ?? (s.index 1 ? [s.index - 1] : []); return deps.every((d) isTerminal(plan.step(d).status)); // done 或 skipped }); }它不是靠定时器跑的而是由事件驱动。每当一步结束——一个子 agent 返回了、commander 收尾了一轮综合——执行器就做一次「对账reconcile」先记下刚结束那一步的结果再重新扫一遍看哪些因此变就绪了然后派遣出去。编排就是这个对账循环一轮接一轮地翻动直到没有步骤剩下。派遣走的是同一个对话不是旁路通道有个选择让系统始终诚实派遣一步用的不是一条隐藏的 RPC 通道。它是往用户正盯着看的那个群组对话里发一条消息以主 agent 的名义、 那个子 agent。在子 agent 看来「被派遣」和「在聊天里被点名」没有区别——它就是照常跑一轮。不存在第二条要和第一条保持同步的执行路径。执行者assignee可以是三种之一每种派遣方式略有不同子 agent——最常见的情况。执行器把 agent 名字解析成 id以 commander 的名义发出agent 渲染后的输入。子 agent 接住跑一轮完整的 agent turn。用户——当某一步确实需要人来提供信息时这一步会变成一张表单并暂停计划下文细说。在用户回答之前下游什么都不会动。commander 自己——用于综合或决策步骤「把上面的都读一遍写出总结」。这是一次私密唤醒不会再发一条多余的、用户可见的消息主 agent 直接带着收集到的上下文跑一轮。把上下文从一步传到下一步一个团队只有当工作能在成员之间流动时才有用。这个机制就是input模板。主 agent 写一步时输入并不是一个冻死的字符串——它可以引用前面的结果执行器在派遣那一刻把这些引用渲染出来// 第 3 步的 input由主 agent 写下 基于下面的调研结论起草发布说明。\n\n{{step_1.output_summary}} // 派遣时子 agent 实际收到的 基于下面的调研结论起草发布说明。\n\n- 市场同比增长约 20%两家在位者……注意往后传的是什么output_summary每一步完成后的一份简短摘要——不是它的完整对话记录。这是一个预算上的决定背后是和 harness 上下文压缩同一个直觉。如果每个下游步骤都继承前面所有内容逐字逐句的完整历史上下文会迅速膨胀几跳之后成本就爆了。摘要让每一次交接都很便宜也让每个子 agent 专注在它真正需要从上游拿到的东西上而不是去趟上游「是怎么走到这一步」的浑水。最初的用户消息和任何附件也会一路带着走所以链条下游第三跳的步骤依然知道最初的诉求是什么。默认串行——以及为什么你可能以为当好几步同时变就绪时——比如菱形的两条分支——编排器会把它们一起并行发出去。它本可以但今天它的做法是一次只派遣一个就绪步骤按 index 取最早的那个其余的等下一次对账。让一整支团队严格地一次只跑一个是一个刻意的、保守的选择值得老实讲清楚为什么。原因是并发下的正确性。设想两个子 agent 几乎在同一瞬间结束。两个结束都会触发对账两次对账都会读计划两次都看到同一个下游步骤还停在pending——于是两次都把它派了出去。现在同一步跑了两遍。为了让这件事根本不可能发生对某个对话的每一次「读取—修改—派遣」循环都被串行化在一把以对话为粒度的锁后面// 一个对话里所有会改状态的路径都在同一把 mutex 下跑 planLock(uid, cid).runExclusive(async () { const plan await readPlan(uid, cid); applyOutcomeOfFinishedStep(plan); // 标记 done / failed / skipped await dispatchReady(plan); // 派遣下一个就绪步骤 });这把锁保证「记录什么结束了」和「决定接下来做什么」作为一个不可分割的整体发生于是下游步骤永远不会被派两遍。有了这把锁一次派一个就是那种「一眼就看得出是对的」的最简做法。真正的并行扇出fan-out是在这个地基之上一个完全可行的扩展——但地基是一个串行化、无竞态的执行器而这个优先级排序先对再快正是关键所在。当某一步出错时跑在真实机器上、对接外部模型 API失败是家常便饭编排器把它分成几类来处理而不是把所有错误一视同仁。它先问这次失败是不是只是瞬时的——连接断了、被限流、抖了一下。如果是且这一步还没烧光那一点点重试预算就悄悄把它回滚到pending让下一次对账重新派遣。这一层在 harness 自己的单轮重试之上只有当 agent 自己的尝试都用尽后计划才会重新派遣而且有硬上限免得一个真坏掉的步骤无限打转。如果是真失败这一步声明的on_failure策略决定团队接下来怎么走abort_plan——这一步重要到没了它下游全都没意义。把它标记为失败然后级联每一个还 pending 的步骤都标成skipped。计划干净地停下而不是在一个缺失的地基上继续盖。continue——这一步是可选的。把它标成skipped让下游步骤当它什么也没产出照常往下走。ask_commander默认——既不盲目中止也不盲目继续。把它标记为失败唤醒主 agent 来看看发生了什么、再决定——换个方式重试、绕过它、还是停下来问用户。还有第六种状态值得单独点出来blocked。一个子 agent 跑到一半可能意识到它需要只有用户才能给的东西于是抛出一张表单或一个问题。这一步不算失败——它进入blocked整份计划暂停。用户一回答执行器就对账团队从刚才停下的地方原样接着干。一份 blocked 的计划是暂停的计划不是坏掉的计划。每一步都是一次完整的 agent 运行值得把环路接回上一篇。当编排器把一步派给一个子 agent 时这个子 agent 跑的不是某种精简版例程——它跑的是完整的 harness 循环 自己的流式运行循环、自己的工具调用、自己带压缩的上下文窗口、自己可自愈的会话。编排干净地坐在单 agent 运行时之上从不伸手进它内部。主 agent 决定工作的形状和交接的顺序每个子 agent 一旦接过自己那一块本身就是一个完整的 agent。正是这种分层让两篇文章能拼起来。harness 让一个 agent 在一个任务上值得信任。编排器把若干个值得信任的 agent 拼成一支团队去啃一个对任何单个 agent 都太大、或太杂的任务。几个关键的决定执行状态归编排器意图归模型。模型提计划只有运行时才把步骤标成 done、failed 或 skipped而且只在真有事情发生时才标。就这一条边界让计划是现实的诚实镜像而不是模型乐观的猜测。派遣走同一个对话不走旁路。被派遣的一步只是主 agent 发给子 agent 的一条消息。一条执行路径没有藏起来会跑偏的东西用户还能在自己正读的那个会话里看着团队干活。步骤之间传摘要不传全文。每次交接带的是上游产出的一份简短摘要。它让长链条上的上下文预算保持理智也让每个子 agent 专注于它需要的东西而不是前一个是怎么得到的。先对再并行。一把以对话为粒度的锁把每一次「读取—修改—派遣」循环串行化步骤一次派一个。严格串行是那个「一眼看得出没有重复派遣竞态」的版本并行扇出是叠在一个已经正确的地基上的优化。小结Orkas 的编排层核心没有什么奇异算法。它的价值在于守住了几条边界计划是依赖图而不是脚本执行状态归运行时而不归模型派遣流经用户正看着的同一个对话上下文以摘要的形式在步骤间流动执行器把正确性摆在并发之前。每一条单看都简单。合起来它们把一个可靠的单 agent变成一支会分工、会把工作往后传、在某一块出错时还能恢复的团队。想看这一层下面那一层去读一个 agent 是怎么被工程化得可靠运行的 。想看让每个 agent 越用越好用的那一层去读 Orkas agent 是怎么从自己的工作里学习的 。如果你更想直接指挥这一层而不是自己造一套Orkas 把它做成了跑在你自己机器上的开源 AI agent 编排 。实践补充版本与使用说明这是一篇记录发表时实现方式的工程文章其中的内部接口、阈值和调度细节不是跨版本不变的产品约定。实际使用请以当前使用指南为准。当前 Orkas 使用指南把发布项目拆为研究、文案与视觉任务为每项指定输出位置和验收条件再列出最终评审所依赖的结果。下一步阅读官网原文下载 OrkasOrkas 官方指南 · 最后核验2026-09-14