我一直觉得,单个 AI Agent 的智商远没有多个 Agent 的组织方式重要。这个观点在我完整做完 OpenRig 这套多智能体编排内核之后,彻底变成了工作信条——我们手上那批离散的 AI Agent,单独跑每一个都很正常,可一旦要让它们互相传递结果、共享一份状态、在同一个业务流程里接力,系统的复杂度和脆弱程度立刻指数级上升。OpenRig 这个项目解决的就是这件事:不做新的 Agent,而是把已有的、各干各的 Agent 编织成一个可持久化、可恢复、可观测的协作系统。这篇文章我会把为什么需要这种编排、编排内核怎么设计、持久化怎么做、并发容错怎么扛,以及两个真实业务场景里的落地复盘一次性讲透。适合正在搭 AI Agent 中台、或者已经发现单个 Agent 扛不动真实业务复杂度的朋友参考。
1. 单体 Agent 的失控现场:OpenRig 想要解决的那个真实痛点
我最早也是从"做一个全能 Agent"入手的,后来发现这个路线在生产环境里走不通。这不是模型能力的问题,而是组织形态的问题。
1.1 三个 Agent 各自为战,我差点被在线事故压垮
当时团队里已经有几个跑得不错的 Agent:一个知识库问答 Agent、一个数据检索 Agent、一个工单处理 Agent。各自独立跑的时候,效果都挺好,用户反馈也不错。直到产品经理提了个需求:用户问一个问题之后,需要自动查数据、自动分析、自动开一个工单,然后由另一个 Agent 去执行,最后再回报结果。
听起来只是"串起来",但真正连的时候问题全冒出来了。第一个 Agent 分析完要传给第二个 Agent,传的不是结构化数据,而是一段对话文本;第二个 Agent 解析不了,就开始胡编字段。更头疼的是状态:第一个 Agent 已经完成了一件审批动作,第二个 Agent 不知道,又重复做了一遍,线上用户收到了两遍通知。最崩溃的一次是进程重启之后,工单 Agent 把自己之前已经处理过的会话上下文全丢了,从中间节点又开始重跑,差点把一笔已经审批通过的单子再提交一次。
这次事故之后我意识到:单个 Agent 的推理能力再强,它也只是系统里的一个节点。真正难的从来不是节点,而是节点之间的关系。
1.2 为什么生产环境最终都会走到多体系统
后来我复盘,发现业务一旦复杂一点,单体 Agent 必然是撑不住的,原因很朴素:
- 上下文窗口是硬约束。一个 Agent 把所有历史对话、工具返回、中间推导全塞进自己的上下文里,很快会被截断。更合理的方式是每个人只保留和自己职责相关的上下文。
- 角色冲突真实存在。让同一个 Agent 既写方案又审方案,它会倾向于自说自话,很难做出真正的批判。把审查拆成独立 Agent,用不同的模型、不同的 prompt 约束,效果会好很多。
- 数据权限不允许打通。有些 Agent 要访问财务数据,有些只能碰运维日志。单体 Agent 一旦都要访问,权限边界就很难控制。
- 业务天然有并行。一次告警排查,可能要同时查网络、查日志、查监控指标。单体 Agent 只能串行做,时间拖到不可接受。
这些约束其实和软件工程里微服务替代单体应用的理由一模一样。AI Agent 也是一样的道理:单体到一定程度,拆开是唯一出路。但拆开之后,随之而来的就是编排问题——谁来调度、怎么通信、状态放哪、挂了怎么恢复。这就是 OpenRig 切入的地方。
1.3 现成编排框架很好,但缺的是"持久化底座"
我并不是一开始就想自己写框架的。LangGraph、CrewAI、AutoGen 我都认真调研过,说实话每个都有值得抄的设计:
| 框架 | 编排思路 | 强项 | 生产短板 |
|---|---|---|---|
| LangGraph | 状态图 + 条件边 | 循环控制流、状态管理思路清晰 | 偏会话级状态,长时间任务的持久化恢复要靠自己补 |
| CrewAI | 角色协作 + 任务队列 | 写 Demo 很快,角色抽象自然 | 抽象偏上层,复杂分支和人工介入场景比较别扭 |
| AutoGen | 对话式多 Agent | 调试直观,Agent 间通过对话交换信息 | 对话流松散,结构化状态和审计链不够强 |
| MetaGPT | SOP 流水线 | 内容生产场景很顺手 | 偏单链路流水,不适合动态分支业务 |
这些框架共同缺一个东西:持久化的协作底座。所谓持久化,不只是把聊天记录存下来,而是指整个协作过程的状态——每个 Agent 跑到哪一步了、中间结果是什么、等待哪个外部输入、进程重启之后能不能从断点接上。Demo 里不怕进程挂,生产环境里这是最基本的生存需求。我评估一圈之后,决定基于现有框架的启发,自己写一个偏底层的编排内核,核心诉求就三个:可编排、可持久化、可观测。OpenRig 就这么立项了。
2. 编排内核怎么设计:图执行器、状态机和 Agent 之间的接口契约
OpenRig 的第一版,我花了两周时间只做了一件事:定清楚"一个流程怎么被描述、被调度"。这个阶段想不清楚,后面所有功能都会歪。
2.1 用图表达流程,用状态机控制流转
我把一个业务流程建模成一张有向图,节点是执行单元,边是流转关系。节点类型有五种:
- Agent 节点:调用大模型完成一个认知任务,比如"读取工单内容并判断紧急程度"。
- 工具节点:调用外部系统,比如"查询库存 ""调用支付 API",一般没有 LLM 参与。
- 人工节点:流程停下来等人确认或填信息,人回来之前一直挂着。
- 决策节点:根据当前共享状态走哪条分支。
- 网关节点:做扇出(fan-out)和汇聚(fan-in),比如并行让三个 Agent 各自查一类数据,然后等全部返回。
在 OpenRig 里,图的表达很直接,但执行并不是"跑完一个节点就走下一条边"这么简单。我在执行器上层叠了一个基于事件的状态机:每个节点跑完之后输出一个事件,调度器根据当前状态和事件内容决定下一步动作。比如"重试"事件会回到当前节点,"已确认"事件会从人工节点走向下一个节点。图上负责描述"有哪些可能路径",状态机负责决定"当前这次运行具体走哪条路径"。这两层分开,代码不会糊成一团。
为什么不用纯 DAG?真实业务里大量存在"分析完发现信息不够,回去再查一次"这种循环。DAG 表达不了回边,硬建模会很别扭。所以我允许图里有环,但给每个回边加了退出条件约束。比如最多重试三遍,达到上限直接走 fallback 分支,防止 Agent 陷入死循环。
2.2 别让 Agent 直接对话:消息总线与共享状态的分工
最开始我把 Agent 之间的交互设计成"直接传消息",就是 AgentA 把一段话推给 AgentB。实践了两周就放弃了——文本消息太随意,字段对不上、语义理解有偏差,调试像在解谜。
后来 OpenRig 采用了双通道通信模型:
| 通信方式 | 适用场景 | 我的用法 |
|---|---|---|
| 事件总线 | 通知时机、触发下一步动作 | AgentA 完成后发一个data_ready事件,订阅者感知并开始工作 |
| 共享状态区 | 传递结构化数据、中间结果 | AgentA 把结果写入状态槽位(slot),AgentB 从对应槽位读取 |
事件总线管"什么时候",共享状态管"数据是什么"。Agent 之间不直接知道彼此存在,只管自己对哪些事件敏感、需要读写哪些槽位。
槽位的定义是带 Schema 的。Agent 节点声明input_slots和output_slots,调度器在执行前校验上游数据是否满足下游的 Schema,不满足就报错。这样做的好处是:多个 Agent 协作的本质变成了"读写共享结构化状态",而不是互相猜自然语言。数据从 A 到 B 的过程里,谁写入的、什么时间写入的、版本号是多少,全部有记录,出了错可以直接查。
2.3 能力注册与动态发现:拓扑不再写死
OpenRig 里的每个 Agent 上架时必须提交一份 Manifest,描述三件事:能干什么、输入输出是什么、需要哪些权限。Manifest 长这样(简化的 YAML):
id: risk-scoring-agent name: 风控评分 Agent capabilities: - risk_scoring - fraud_check input_slots: - name: application_profile type: schema://credit/application output_slots: - name: risk_score type: schema://credit/risk_result permissions: - credit_db_read - risk_model_invoke执行器看到一个新 Agent 接入,不关心它内部是用 LangChain 还是直接调 API,只关心它的 Manifest 和能力。流程定义里写"需要一个能做风险评分的节点",执行器自己找符合条件且输入槽位匹配的 Agent 来绑定。新增一个 Agent 的时候,旧节点一行不用改。这个机制帮我躲过很多次"加一个 Agent 就要动所有上下游"的连锁修改。
2.4 一个编排定义的最小例子
空谈概念不如看实例。下面是一段 OpenRig 的流程定义 DSL,对应一个"客服工单自动分级并转派"的场景:
workflow: ticket-triager nodes: - id: parse-ticket type: agent agent: ticket-parser output_slots: [ticket_profile] - id: decide-severity type: decision input_slots: [ticket_profile] conditions: - if: ticket_profile.severity == "high" then: notify-manager - else: assign-agent - id: notify-manager type: agent agent: manager-notifier requires_confirmation: true - id: assign-agent type: tool tool: assign_ticket input_slots: [ticket_profile] edges: - from: parse-ticket to: decide-severity - from: decide-severity to: notify-manager - from: decide-severity to: assign-agent recovery: snapshot_interval_events: 50 resume_strategy: from_checkpoint这段 DSL 编译之后会被执行器加载成一张可运行图。decide-severity是一个纯逻辑节点,走哪条边由共享状态里的severity字段决定。notify-manager带requires_confirmation: true,意思是它做完之后流程会挂起,等人工在页面上点"确认已通知",然后才继续。这就是持久化协作系统里最典型的"人机接力"节点。
3. 持久化是分水岭:状态、事件与断点续跑
如果说编排模型解决的是"Agent 怎么协作",那持久化解决的就是"协作到一半,天塌了怎么办"。OpenRig 从第一版开始就把持久化当一等公民,不然后面全是在给线上事故填坑。
3.1 为什么我放弃了纯快照,选了事件溯源
第一版我偷懒,用的纯快照方案:每隔一段时间把流程实例的完整状态存一份到数据库,恢复时候直接读最近快照。跑了一周就发现问题:快照只能恢复"当时的结果",恢复不了"当时为什么这么做"。比如一个风控 Agent 拒绝了一笔贷款,我们要解释给用户听,结果快照里只有最终拒绝状态,中间的推理过程、引用数据、调用过哪些模型,全丢了。
后来我换成事件溯源(Event Sourcing)思路:一个流程实例的生命周期里,所有有意义的变化都记录成一条不可变事件,追加写入事件存储。节点完成、消息传递、工具返回、人工确认、Agent 决策,全部是事件。恢复的时候,从最近的一次快照开始,把增量事件一条条重放,就能精确重建出当时的全部上下文。
这个切换带来的额外收益是审计能力。多 Agent 协作里,出了业务事故你要能回答"哪个 Agent 当时看到了什么、做了什么决定、手抖改了哪个字段"。没有事件流,这个问题永远回答不了。
3.2 恢复流程:快照+重放增量,Agent 失忆也能接话
OpenRig 里每个流程实例都有一个生命周期状态,恢复流程的伪代码大概是这样的:
def resume(workflow_instance_id): snapshot = load_snapshot(workflow_instance_id) # 加载最近快照 events = load_events_after(snapshot.version) # 查增量事件 state = snapshot.state for event in events: # 重放时只更新内存状态,不触发真实副作用 if event.type == "slot_updated": state.slots[event.slot_name] = event.value elif event.type == "node_completed": state.node_status[event.node_id] = "completed" elif event.type == "awaiting_human_input": state.pending_human_nodes.append(event.node_id) # 其他事件类型同理 # 根据恢复后的状态,把未完成节点重新入队 pending_nodes = compute_ready_nodes(state) scheduler.enqueue(pending_nodes)关键点是重放过程不能触发真实副作用。也就是说重放时不能真去调支付接口、不能真发消息。副作用操作必须单独走一层"幂等执行器",Run 的时候记录一个幂等键,恢复时检查这个键是否已经执行过,执行过就直接跳过。这样就不会出现"进程重启,Agent 把已发出的转账又发了一遍"这种事故。
3.3 人工介入节点怎么持久化
生产环境里几乎每个正经流程都有人工节点。审批要人点一下,工单要人确认一下。人工介入给持久化带来的麻烦是:等待状态不能放在 Agent 进程里。如果流程正等着人点击确认,这时候进程重启了,难道人点的那一下就没了吗?
OpenRig 的解法是把"等待"也状态化。每当流程执行到人工节点,执行器向事件存储写入一个awaiting_human_input事件,然后流程实例进入waiting状态,执行器把它从调度队列里撤下来,不再占用任何线程。人工在系统里确认之后,回调 API 往事件流里追加一条human_confirmed事件,调度器被唤醒,重新加载该实例的状态,从等待节点继续往下走。整个流程里,人点的那个动作也是一条持久化事件。进程重启与否,对流程来说完全无感。
持久化做到这个程度,我才敢说这套系统是"协作系统"而不是"在一堆 Agent 外面套了层流程控制"。
4. 并发与容错:Agent 真正一起干活之后才遇到的坑
单 Agent 跑的时候,并发和容错都很好处理,一个请求进来,处理完返回就行。多 Agent 同时跑一个流程、多个流程实例共享一组 Agent 资源的时候,才真正理解了什么叫"AI Agent 怎么扛并发"。
4.1 共享状态写冲突:版本号、CAS 与幂等
两个 Agent 同时更新同一个槽位,是最常见的并发事故。我们线上踩过一次:告警恢复 Agent 和值班提醒 Agent 同时改同一个告警单,一个要把级别调低,一个要调高,最后最后写入的覆盖了前一个,级别被改错了。
OpenRig 的共享状态槽位从一开始就带了版本号,更新用 CAS(Compare-And-Swap)语义:只有当前版本号等于我读到的版本号,这次写入才成功;不相等就说明有人抢先改了,需要读取最新值重新计算再提交。这个机制成本很低,但能挡住大多数并发写冲突。
比写冲突更隐蔽的是副作用幂等。Agent 调用外部工具是天然非幂等的——发消息发两次就重复通知,调扣款接口调两次就重复扣款。OpenRig 里所有工具调用在进执行器之前都要生成一个幂等键,事件存储里记录这个键的执行状态。重试的时候先查幂等键,查到了直接返回已记录的结果,不二次执行。这一条是所有 AI Agent 上生产环境的底线,没有它,重试机制就不是兜底而是事故放大器。
4.2 Agent 跑飞、超时与重试:熔断要设计在哪个层级
LLM 调用超时不是异常,是常态。OpenRig 对每种节点都设了不同的执行 deadline:Agent 节点默认 90 秒,工具节点默认 10 秒,人工节点没有 deadline 但受挂起时间上限约束。超时之后进入退避重试,间隔 1 秒、4 秒、16 秒,最多三次。三次还失败就走 fallback:要么降级到更简单的模型 Agent,要么转人工处理,要么直接结束本次流程并输出需要人工介入的说明。
循环节点要防死循环。我在 2.1 提过图里允许有环,但必须配终止条件。OpenRig 的做法是给每个环附带最大迭代数配置,例如"分析-再分析"的环最多转 3 圈,超过之后强制跳出并标记needs_human_review。别指望 LLM 自己知道什么时候该停,它很容易在循环里越滚越深。
熔断要放在两个层级。第一层是外部依赖:某个模型供应商的错误率超过阈值,执行器短暂熔断,不再调度新的调用。第二层是单个 Agent 节点:连续 N 次执行失败的 Agent 会被标记为 unhealthy,后续流程动态改绑到备用 Agent 或直接走人工兜底。这些边界在写流程定义时就要想清楚,等线上出事再补,已经晚了。
4.3 可观测性:把"思考-行动-观察"变成可回放的轨迹
多 Agent 系统的调试难度,比单 Agent 高一个数量级。问题可能发生在第三个 Agent 的中间推理里,而它读到的数据是被第一个 Agent 写错的。没有完整的执行轨迹,只能靠猜。
OpenRig 给每个节点每次执行都产生一条执行追踪记录,包含:流程实例 ID、节点 ID、Agent ID、输入槽位摘要、输出槽位摘要、执行的起止时间、重试次数、调用的模型名与 token 数。我常用的一张关键指标表长这样:
| 指标 | 作用 |
|---|---|
| 节点成功率 | 区分是模型问题还是链路问题 |
| 节点平均耗时 / P99 | 定位瓶颈,最常见的是超长 Agent 节点 |
| Token 消耗分布 | 发现某个 Agent 反复读超大上下文 |
| 重试分布 | 重试集中在哪个节点,往往就是不稳定源头 |
| 等待队列深度 | 判断并发调度是否积压 |
但指标只是辅助,真正让我在事故里脱身的是事件回放:打开一条流程实例的时间线,能像看录像一样看到每个 Agent 在哪个时刻看到了什么、做了什么决定、改了什么槽位、触发了哪个工具。这套能力其实是在事件溯源基础上白捡的。所以我说可观测性和持久化不是两件事,是同一件事的两面。
5. 两个真实落地场景:审批流与告警排查的编排复盘
OpenRig 在内部接手了不少业务流,我挑两个最有代表性的复盘一下,能看得更清楚这套编排到底怎么工作。
5.1 信贷审批:串行风控链路与人工复核的接续
信贷审批是一个典型的串行链路:申请解析 Agent → 反欺诈 Agent → 风控评分 Agent → 人工复核节点 → 放款执行 Agent。链路上最怕两件事:第一,风控 Agent 和评分 Agent 对同一份申请数据各自处理,写槽位时互相覆盖;第二,人工复核之后如果进程重启,Agent 忘了"已复核通过"这回事,又走一遍放款。
第一件靠槽位版本号解决,第二件靠事件溯源解决。人工复核节点的确认动作本身就是一条事件,进程重启后重放事件流,流程实例会精确恢复到"人工复核通过、放款执行之前的等待态",放款 Agent 从该节点重新被唤醒。而且放款执行带了幂等键,就算因为网络抖动重复调度,也不会重复放款。
这套链路在 OpenRig 上线之前,我们是靠人工在数据库里改状态来续接的,每周都有那么一两次状态改错。上线之后最直观的变化是:出错之后可以回放完整链路,几分钟就能定位是哪个 Agent 的判断问题,而不是翻日志翻到怀疑人生。
5.2 告警响应:多路并行排查与决策后的执行
告警响应则是另一个极端,并行多、时效强。流程大概是:一条告警进来 → 并行触发网络排查 Agent、日志排查 Agent、指标分析 Agent → 汇总 Agent 整合三路结果 → 决策 Agent 给出处理方案 → 人工确认 → 执行 Agent 落地操作。
并行排查的难点在汇聚节点。OpenRig 的网关节点支持两种汇聚语义:AND(等待所有上游完成)和 OR(任一上游完成即继续)。告警排查必须用 AND,三路结果缺一路就下结论,容易误判。三个排查 Agent 各写各的槽位,汇总 Agent 是第一个读它们的节点,所以读之前要做完整性检查——三路槽位都 ready 才允许执行。
另一个优化是槽位冲突的优先级策略。告警场景里,规则 Agent(基于预设规则判断)的结论比统计 Agent(基于指标异常判断)的结论更硬,所以同一个槽位两边同时写时,规则 Agent 的版本优先被保留。这个是在线上踩过坑之后加的,代价是不过是想让两个结论并存,结果互相覆盖的那份恰恰是关键证据。
5.3 复盘时发现的两个性能和存储问题
事件溯源好用,但存储是真贵。第一个问题是事件量过大:Agent 节点的内部推理过程如果逐步入库,一天能攒几千万条事件。我们的解法是分两级:热事件先写 Redis,异步批量落库到 PostgreSQL;同时定期做事件压缩,把旧事件聚合成快照,超过审计保存期的直接清理。
第二个问题是恢复时间变长。事件多了之后,从快照位置重放到最新需要几分钟,这对于告警响应场景不可接受。后来我们按租户和流程类型做了分区存储,并且调了快照生成频率——高频运行的流程类型每 50 个事件就出一份快照,低频的流程类型可以放宽。恢复时还做了并行加载,不同槽位状态分线程重建,整体恢复时间从分钟级降到了秒级。
6. 写在最后的工程心得
OpenRig 做到现在,我最大的体会是:多智能体编排的核心不在"调度"本身,而在"契约"。Agent 与 Agent 之间读什么槽位、写什么事件、遵循什么 Schema,这些契约定清楚了,后面的一切都顺。反过来,如果只盯着流程引擎怎么调度、节点怎么并发,Agent 之间还是会互相踩脚。
另外,持久化这件事,一定要从第一天就做。我第一版偷懒用快照,第二周就被审计需求打脸;后来切事件溯源,才真正体会到"失忆的 Agent 能接上话"对协作系统有多关键。很多团队做多 Agent 系统,先把流程跑通再补持久化,结果跑通那天就是事故开始那天。
最后分享三个我自己用下来最值的小技巧:第一,设计任何多 Agent 流程之前,先拿人脑把流程走一遍,画一张人肉流程图,再转成机器流程图,人脑都理不顺的流程机器更别想跑顺;第二,所有 Agent 可能触发的副作用操作,一律加幂等键,没有例外;第三,每个 Agent 节点上线前,必须写清楚输入输出槽位的 Schema,哪怕只写一句话的注释,也不能省。
OpenRig 这套编排内核我自己还打算继续演进,方向是把执行器拆成独立库,Agent 节点的插件化做得更彻底,再往下一层做一些支持不同模型路由的抽象。如果你们团队也在被多 Agent 的协作和持久化问题折磨,我的建议是从一个小小的、状态清晰、有人工节点的流程开始,让事件溯源先跑起来,你很快就会感受到"能接上话、能说清楚为什么"到底值多少钱。