高级 📋 6 个步骤 第 570 / 572 篇

用 OMA 在自己机器上跑多智能体团队:runTeam 编排、审批门禁与 verifyRun 离线核验实操

接多智能体编排常常要顺手接受托管控制面与厂商遥测。OMA 是一个 MIT 协议的 TypeScript 自托管运行时:三种执行模式、危险调用可挂起等人批准并能从检查点恢复,事后还能用 verifyRun 逐字节核对上下文来源。本教程六步落地。

2026-10-27· 13 分钟上手· 约 4140 字· 🧩 OMA / 🤝 多智能体编排

把多智能体编排接进自己的后端,通常要顺手接受三件事:一个托管控制面、一份发往厂商云的遥测、以及「跑完到底发生了什么」只能靠日志猜。OMA(open-multi-agent)把这三条反过来做:它是 MIT 协议的 TypeScript 运行时,以 @open-multi-agent/core 这个库的形式装进你自己的进程,没有 OMA 账号、没有托管控制面,README 里也写明了它不做分析、许可、更新或回连请求。真正让它和别的编排器区分开的是另外两件事——计划、任务分派与危险的工具调用都可以挂起等人批准,并且能从检查点恢复;每次运行可以写一份追加式日志,事后用 verifyRun 冷读,逐字节核对每个模型可见块能否从具名源事件复现。本教程按脚手架起跑、runAgent、runTasks、runTeam、审批门禁、日志与治理六步把它用起来。

🧩 本教程适合:想把智能体编排嵌进自己 Node 后端的 TypeScript 团队,尤其是必须先拿到人类授权才能写文件或跑命令的场景。你需要 Node.js 20 及以上;生产建议用当前维护中的 LTS 版本——Node 20 只作为迁移窗口保留,官方说明它会在下一个大版本移除,时间不早于 2026-10-31。文中命令以官方脚手架与核心库的说明为准。

Step 1:一条命令起跑,先看不需要密钥的演示

1 脚手架会自己跑一个确定性演示

脚手架在交互式终端里让你选一个 starter 与运行时,装依赖,然后跑一个确定性本地演示。这个演示不需要 API 密钥,也不发出任何模型请求,所以可以先在离线或受限环境里把结构看清楚,再决定要不要接真模型。想加进已有后端则只装核心库。要接真实模型时,官方的 OpenAI 例子用环境变量给凭据;其它托管厂商、本地服务器、OpenAI 兼容端点与 AI SDK provider 各自需要对应的凭据或 baseURL 配置。

# 脚手架:选 starter 与运行时、装依赖、跑确定性本地演示(无需密钥)
npm create oma-app@latest my-oma

# 已有后端:只装核心库
npm install @open-multi-agent/core

# 官方 OpenAI 例子需要的凭据
export OPENAI_API_KEY="你的密钥"

# 内网或本机模型:provider 用 openai,baseURL 指向本地服务,
# apiKey 给一个非空占位值(SDK 会校验该字段已设置,即使服务端忽略它)

顺手把依赖面记下来:核心运行时只带三个依赖——Anthropic SDK、OpenAI SDK 与 Zod。这个刻意压到最小的依赖面,目的是减小安全敏感部署的攻击面。可选的可观测性适配器与离线 Run Viewer 是分开装的,不进主链路。适配器覆盖托管厂商、本地模型服务器、OpenAI 兼容端点、Vercel AI SDK provider,以及通过 process 与 ACP 后端接进来的外部编码 Agent(Claude Code、Gemini CLI、Codex)。

Node 版本是有期限的:20 只是迁移窗口,官方写明会在下一个大版本移除,且不早于 2026-10-31。现在能跑不等于明年还能跑;新项目直接上当前维护中的 LTS,别把编排器钉在一个即将被摘掉的运行环境上。

Step 2:runAgent,一个智能体的最小闭环

2 系统提示 + 工具 + 模型

三种执行模式里最朴素的是 runAgent:一个 Agent、一份系统提示、若干工具、一个模型,跑完就结束,不涉及任何协调。它是后面两种模式的积木,也适合那些本来就不需要拆分的任务——比如把一段报错日志整理成一张待办表。工具用普通函数声明,配 Zod 校验入参,返回值直接进上下文。

import { OpenMultiAgent } from '@open-multi-agent/core';
import { z } from 'zod';

const oma = new OpenMultiAgent({
  model: { provider: 'openai', name: 'gpt-x' },
});

const triage = await oma.runAgent({
  name: 'triage',
  instructions: '把日志里的报错按服务归类,输出待办清单,不要改任何文件。',
  tools: [{
    name: 'read_log',
    description: '读取一段服务日志',
    inputSchema: z.object({ service: z.string(), lines: z.number().int() }),
    execute: async (input) => readLog(input.service, input.lines),
  }],
  input: 'services/api 今天 09:00 之后有失败请求,看看是什么',
});

这一步最容易被忽略的是 instructions 的写法:它决定了后面两种模式下这个 Agent 会被怎么分派。写「把日志归类并输出待办」这种可判定的描述,协调者才好在 Step 4 里把它排进合适的任务;写「帮忙看看问题」这种描述,调度器只能靠猜。同理,工具的 description 是写给模型看的,不是写给人看的。

💡 先只用 runAgent 把单个 Agent 调稳,再上编排。多智能体不会修好一个本身就含糊的智能体,只会把含糊复制三份,并且让排查变难——因为你得先搞清楚是哪一环含糊。

Step 3:runTasks,把你已经知道的流程写死

3 依赖顺序执行,独立任务并行

当流程本身是确定的——先取数、再清洗、再汇总——就没必要让模型去拆。runTasks 接一份开发者自己定义的任务图,按依赖顺序执行,彼此独立的任务并行跑,互不依赖的分支同时推进。这份图是可读的数据,能进代码评审,也能在事后回答「当时为什么先做这一步」。

const result = await oma.runTasks({
  tasks: [
    { id: 'fetch',   agent: 'collector', input: '拉取昨日订单' },
    { id: 'clean',   agent: 'cleaner',   dependsOn: ['fetch'] },
    { id: 'stats',   agent: 'analyst',   dependsOn: ['clean'] },
    { id: 'notify',  agent: 'reporter',  dependsOn: ['clean'] },
    { id: 'digest',  agent: 'writer',    dependsOn: ['stats', 'notify'] },
  ],
  maxConcurrency: 5,   // 并发上限,默认值就是 5
});

失败处理的设计值得单独说一句:某个任务失败,它自己被标记为失败,直接依赖它的任务保持阻塞,但失败不会顺着依赖链往外扩散。也就是说,上例里 stats 挂掉不会把 notify 一起拖死,只有 digest 会停在等它的状态上。这个「把爆炸半径收在直接依赖链内」的取舍,比「一有失败就整体回滚」更贴近真实流水线——已经跑出来的中间结果还能留着复用。

💡 并发数不是越大越好。五个并发意味着可能同时有五个 Agent 在读同一个仓库或同一张表;先把副作用隔离好(各自 worktree、各自事务),再谈把 maxConcurrency 往上调。

Step 4:runTeam,让协调者拆任务,调度器只管排

4 两次模型调用,中间不给协调者发言权

runTeam 是三种模式里最像「团队」的一个:你给一句自然语言目标,它把目标变成一个有向无环的任务图再执行。关键在于它怎么拆——协调者在开头那次被调用时,只拿到目标与团队里各 Agent 的名字与能力,产出一份 JSON 任务数组,每条任务含标题、真正交给执行方的描述、执行者,以及依赖哪些任务的标题;随后由确定性调度器接手,执行期间协调者完全不被咨询;全部任务跑完之后,协调者被调用第二次,读完所有任务输出,合成针对原始目标的最终答复。

const team = await oma.createTeam({
  name: 'release-check',
  agents: [triageAgent, testAgent, docAgent, reviewAgent],
});

const run = await oma.runTeam(team, '检查这次发版有没有回归风险,并给出结论', {
  scheduler: 'dependency-first',   // 默认:优先跑阻塞下游最多的任务
});

调度器是确定性的,这也是「可复现」三个字落地的地方式。它有四种策略:dependency-first 是默认,优先跑那些下游被卡住的任务,目的是尽快解锁后续工作;round-robin 求均匀;least-busy 求负载均衡;capability-match 则拿 Agent 的名字与系统提示去和任务要求打分匹配,把活派给更合适的那个。任务之间靠共享内存与消息总线衔接,所以下游 Agent 能直接读到上游已经产出的结果,不需要协调者当中转。

# 命令行:一条命令跑一个团队
npx oma run --goal "Build a REST API with token auth" --team ./team.json

# 常用参数
# --orchestrator  合并进 new OpenMultiAgent(...) 的配置片段
# --coordinator   作为 runTeam(..., { coordinator }) 的协调者配置
# --dashboard     结束后把 Run Viewer 静态页写进 oma-dashboards/
# --pretty / --include-messages  输出控制

npx oma run --goal "审查这次改动" --team ./team.json --dashboard --pretty

--dashboard 会给一次运行产出两份东西:正常的运行结果,以及一个自包含的静态 Run Viewer 页面,里面有任务 DAG、带层级感知的 span 瀑布图、过滤器与安全化过的证据细节。要注意它是把一次运行的 trace 记录与精确的运行结果合并渲染出来的,所以页面里看到的就是那一次运行的快照;与 dashboard 有关的提示走 stderr,不会被混进结构化输出里污染解析。

协调者不看过程,意味着错误分解没人拦:它只在开头拆一次、结尾收一次,中间完全不介入。如果开头那次拆解就把任务分错了人,或者漏了一环,没有任何机制会在中途纠正。所以团队里每个 Agent 的能力描述必须写得可判定,并且对关键团队先用固定目标反复跑几次,确认拆解稳定之后再放开给真实目标。

Step 5:审批门禁:挂起、恢复与「审批的就是你看到的那份」

5 三处可挂起,重启之后还能接上

这是 OMA 最不像玩具的地方:计划、任务分派、以及危险的工具调用,这三处都可以返回挂起,等人批准后再从检查点恢复。一个后端团队要求的「写文件之前必须有人点头」,在这里不是提示词里的恳求,而是运行时的行为——服务重启之后待审批项还在,批准之后从断点继续。整个持久审批体系在 1.15.0 完成,支持中途恢复并把已提交的结果重放回来。

const run = await oma.runTeam(team, goal, {
  gates: {
    plan: 'required',        // 计划放行前需要批准
    dispatch: 'required',    // 派发任务前需要批准
    toolCall: 'consequential' // 有副作用的工具调用需要批准
  },
  checkpoint: true,
});

// 结果里会带 pendingApprovals;批准之后从检查点恢复
// 恢复时把已提交的结果重放,不重复执行已经跑过的部分

审批记录怎么存,决定了它能不能当证据用。每条持久审批请求都与它的检查点存在一起,并用 SHA-256 绑定到审批人当时看到的确切内容;决策是原子的、先到者胜出。有两条失败关闭的规则值得记住:被改过的请求会被拒,存储若不支持 compare-and-set 原子语义也会失败关闭——宁可不批,也不批一个可能被并发改写过的东西。

// 脱敏要小心:RedactingStore 在写入时脱敏,是有损的
// 它对 JSON 容器做结构感知脱敏(脱敏后仍是合法 JSON)
// 并且刻意不暴露 compareAndSet —— 持久化审批需要精确内容哈希
const store = new RedactingStore(new FileStore('./.oma/run.json'));

// 所以:需要可挂起审批时,请用受保护的、不脱敏的审批/检查点存储
// 日志侧的脱敏在写入时执行,形状与 RedactingStore 一致
new JsonlRunJournal('./.oma/run.jsonl', {
  redact: { patterns: [/\bcust-\d+\b/g] }
});

还有一条边界容易被漏掉:遥测脱敏不会倒过来处理已经持久化的运行态。共享内存写入与检查点保存默认是原样存储 Agent 输出的,除非 store 本身被脱敏层包住。也就是说,想让「写进磁盘的东西里不含敏感值」,必须显式包一层,而不是指望某个全局脱敏开关。

脱敏与可审批性是一对矛盾,别同时要:脱敏会改变被审批内容,从而让精确哈希失效。凡是走挂起审批的链路,审批与检查点存储要用不脱敏的受保护存储;脱敏只包在共享内存与日志这类不影响审批哈希的地方。把两者搞混,最坏的结果是审批门禁悄悄失效而不是报错。

Step 6:日志、verifyRun 与治理底线

6 事后能回答「这一步到底看到了什么」

挂上日志后端之后,OMA 会记录模型可见的块、工具调用与结果、以及上下文重写。这套可验证日志体系在 1.17.0 引入,1.18.0 又加了视频内容块支持和执行 span 上的工具输入输出记录。真正的收口动作是 verifyRun:它冷读这份记录,逐个核对每个块能否从它具名的源事件里逐字节复现。这不是在证明日志没被人改过——项目自己就说明了这一点——而是在证明「记录的这堆上下文确实是从这些事件里来的」,已经足以挡住大部分扯皮。

import { JsonlRunJournal, verifyRun } from '@open-multi-agent/core';

const journal = new JsonlRunJournal('./.oma/run.jsonl');
const run = await oma.runTeam(team, goal, { journal });

// 冷读:逐字节核对每个模型可见块能否从具名源事件复现
const report = await verifyRun('./.oma/run.jsonl');
console.log(report.blocksChecked, report.mismatches);

往上一层是治理底线:你可以声明一次运行必须满足哪些角色、以及什么样的执行顺序,然后拿执行回执去核对实际发生的角色顺序与依赖边。这让「我们要求独立评审」从一句制度变成一条可检查的断言。配套的还有离线 Run Viewer 与可选的 OpenTelemetry 适配器;出口策略体系在 1.16.0 加入,在建立适配器连接之前检查可配置的网络限制,并且层级化地强制——子策略只能收紧父策略,不能放宽。检查点 schema 目前是有日志运行时的第 5 版,允许持久化格式演进同时保持向后兼容。

💡 落地顺序建议:先把日志与 Run Viewer 打开跑两周,摸清这类任务真实产生多少上下文;再给写文件与跑命令加工具调用门禁;最后才写治理底线。反过来做的话,你会在不知道正常形态长什么样的情况下,先给自己上了一堆断言。

verifyRun 证明的是来源,不是完整性与正确性:它能告诉你记录里的每个块来自哪个事件、能否复现,但官方明确说了这不等于证明日志从未被编辑。要把它当审计证据用,日志文件的写入权限与存储位置必须独立收紧——例如只追加、单独落盘、并把哈希同步到另一处。

常见问题速查

现象原因与处理
脚手架跑不起来要求 Node.js 20 及以上;生产用当前维护中的 LTS 版本
本地演示要我给密钥官方演示不需要密钥也不发模型请求;要密钥说明当前选的不是演示 starter
接内网模型连不上provider 用 openai 并配 baseURL;apiKey 必须给非空占位值
runTasks 里某步失败,全链都停了只有直接依赖它的任务会阻塞,失败不沿依赖链外扩;检查依赖是否写宽了
并发跑出互相覆盖先把副作用隔离(各自 worktree、各自事务),再调 maxConcurrency
协调者把任务分错了人写可判定的能力描述;它在开头拆一次、结尾收一次,中途不纠正
想让活派给更合适的人调度策略改用 capability-match,按名字与系统提示打分匹配
审批门禁看起来不生效脱敏层改了被审批内容会让精确哈希失效;审批存储要用不脱敏的受保护存储
服务重启后审批丢了审批与检查点并存;确认用了持久存储而不是内存存储
--dashboard 没产出页面需要显式带 --dashboard,输出落在 oma-dashboards/,相关提示走 stderr
verifyRun 报不匹配它只证明块能否从具名源事件复现;查源事件与日志是否被外部改动过
子环境想放宽网络限制出口策略层级化强制,子策略只能收紧不能放宽
← 返回教程中心