多 Agent 编排实战:Orkas 如何调度一个主 Agent 和它的子 Agent

2 阅读13分钟

拆解 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 这个字段。默认一步只等它前面那一步(一条简单的链),但一步可以声明它依赖前面好几步——「写总结」可能同时要等「调研市场」和「摸排竞品」。这是一个菱形,不是一条直线,编排器把它当图来处理。

谁说了算:编排器,不是模型

再看一眼那个结构体里的切分。模型在第一次写计划时填的是 意图 ——标题、执行者、输入、依赖关系。但所有关于 执行状态 的东西——statusoutput_summaryfailure_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 使用指南

把发布项目拆为研究、文案与视觉任务,为每项指定输出位置和验收条件,再列出最终评审所依赖的结果。

下一步

Orkas 官方指南 · 最后核验:2026-09-14