多智能体系统编排实战:从0到1搭建可恢复的协作系统
说个实在话多智能体系统真正难的地方根本不在怎么让单个 Agent 更聪明而是怎么让十几个各怀绝技的 Agent 好好配合别互相踩脚、别忘事、别一崩全崩。我之前做单 Agent 的时候觉得上下文窗口大点、工具调得顺点就挺能打了直到把业务拆成多个 Agent 协作才发现所谓编排就是一场大型项目管理事故现场——任务没人认领、对话状态说丢就丢、某个 Agent 超时了后面全堵死。OpenRig 这个名字就是干这个的把离散的、各自为战的 AI Agent编织成一个能扛真实业务、状态不丢、可恢复的持久化协作系统。这篇文章会把我从 0 到 1 搭 OpenRig 的完整思路、架构取舍、并发处理和踩过的坑都摊开来讲适合那些已经跑通单 Agent Demo、正准备往多 Agent 生产环境迈进的人。1. 为什么离散 Agent非编不可单 Agent 的天花板与协作的真相先说清楚我理解的离散 Agent是什么。不是指 Agent 之间物理隔离或者部署在不同机器上这么简单而是指每个 Agent 天然是独立自治的个体它有自己的人格设定、自己的工具集、自己的上下文记忆对话时它是在场的但对话结束、请求处理完它就像没这回事一样。这种特性在单 Agent 场景下没问题一旦要多人协作问题就全冒出来了。1.1 单 Agent 的三堵墙上下文、串行、单点我做了几个真实项目之后对单 Agent 的边界体会特别深。第一堵墙是上下文窗口。哪怕现在主流模型把上下文做到百万 token 级别你真塞满那么多内容进去推理速度和成本都是灾难而且模型对超长上下文的注意力其实会衰减开头的信息经常被遗忘。第二堵墙是串行执行。单 Agent 处理任务时内部那套 ReAct 循环本质上是一个链走完一步才能走下一步复杂的、可以并行拆解的工作全被压成了串行。第三堵墙是单点故障。一个 Agent 挂了整个流程跟着挂没有任何兜底和恢复机制。这三堵墙放在个人助手场景还能忍一旦放到业务流程里比如一个 Agent 负责查资料、一个 Agent 负责算数据、一个 Agent 负责写报告、一个 Agent 负责质检单线程的天花板立刻变成致命伤。1.2 团队协作的本质分工、契约、协调我做 OpenRig 之前也试过硬编码 workflow——就是写死调用链A 做完调 BB 做完调 C。跑通 Demo 倒是快可业务一变就要改代码而且每个 Agent 之间没有真正的协商能力只有机械的接力。这跟多智能体协作的初衷完全是两回事。真正的多智能体协作我觉得得满足三件事第一是分工每个 Agent 清楚自己擅长什么、不擅长什么遇到不擅长的知道转交出去第二是契约Agent 之间要按约定好的格式交换信息而不是丢一段自然语言让人家猜第三是协调得有一个人或者一个模块负责拆任务、派活、收回结果、处理失败。OpenRig 里的编排器干的就是这个协调者的活Agent 之间不直接说话所有通信都经过编排器这样状态可控、过程可审计、失败可恢复。1.3 持久化不是顺便存个档是协作系统的地基标题里我特意用了持久化协作系统而不是多 Agent 聊天系统因为持久化不是锦上添花是地基。你想一个真实场景Agent A 花了半小时查完资料Agent B 正要接手结果服务重启了。没有持久化全部重来。再比如用户中途关掉页面、过了两小时又回来问刚才那个结论是什么没有持久化的会话状态Agent 一脸懵。这个领域里Redis 持久化几乎是绕不开的话题。Redis 的 RDB 和 AOF 两种持久化机制我一开始只当它是缓存加速工具后来才发现它在多 Agent 协作系统里承担的角色远不止缓存——任务队列、状态快照、会话存储、分布式锁全都可以落在 Redis 上。RDB 负责定期打快照恢复快但可能丢最后一次快照之后的数据AOF 追加写操作日志数据更安全但文件体积大。真正常用的是两者结合AOF 保证数据安全性RDB 保证重启时的加载速度。具体到 OpenRig 里怎么用后面我会专门拆一节说。2. OpenRig 的整体架构编舞者、演员、舞台三者各司其职OpenRig 的架构设计我自己总结成一句话编舞者定节奏演员干专业活舞台提供场地和基础设施。这里的编舞者就是编排器Orchestrator演员是各个 Agent舞台是消息总线、状态存储、注册中心这些基础设施。2.1 为什么我放弃了Agent 直连的微服务式架构最开始我参考微服务的思路让 Agent 之间通过 REST 接口互相调用。设计了半天发现不对最大的问题是耦合。Agent A 要调用 Agent B就得知道 B 的地址、接口格式、鉴权方式B 一改接口 A 就崩。而且调用关系一多整个系统变成一张蜘蛛网出问题的时候你根本不知道链路哪断的。更要命的是Agent 之间的调用是同步阻塞的一个 Agent 卡住调它的 Agent 全卡住。后来我想明白一个道理Agent 不是微服务它们处理的是开放性的、语义层面的任务而不是封闭的、协议明确的接口调用。所以 OpenRig 里我把 Agent 之间的点对点通信全部砍掉只保留Agent - 编排器 - Agent的星型结构。这样做的好处是任何 Agent 挂了影响的只是它自己的任务编排器可以把它未来得及处理的任务重新派给别的 Agent要加新的 Agent 能力只需要在注册中心登记一下老 Agent 完全不用改代码。2.2 核心模块拆解编排器、注册中心、消息总线、状态存储OpenRig 的核心模块一共四个我列个表说清楚各自职责模块职责选型思路编排器Orchestrator拆解任务、派发、监控、汇总、失败重试无状态服务可水平扩展核心逻辑是任务状态机注册中心Registry登记每个 Agent 的能力描述、健康状态、当前负载用 Redis Hash 存 Agent 元数据带 TTL 做心跳消息总线Bus传递任务事件、Agent 返回的中间结果、异常通知基于 Redis Streams天然支持消息持久化和消费者组状态存储State Store存会话状态、任务快照、Agent 长期记忆Redis 主存储 向量库存语义记忆分层设计编排器是整个系统的大脑但它自己不做任何智能的事情它只做确定性的调度决策。这其实是我刻意做的设计编排器越笨越好因为调度逻辑如果也依赖模型推理整个系统就变成黑盒了出了问题没法排查。Agent 才是智能的地方编排器负责把智能组织起来。2.3 Agent 的能力描述让编排器知道谁该干活每个 Agent 接入 OpenRig 之前要提交一份能力描述文件相当于它的简历。这个设计很关键我一开始觉得多此一举后来发现没有它编排器就只能靠猜。简历长这样name: research_agent description: 负责从互联网检索资料并整理成结构化摘要 capabilities: - web_search - web_scrape - summarization input_schema: topic: string depth: optional_int output_schema: sources: list summary: string max_runtime_seconds: 300编排器拿到任务后先做语义理解然后根据能力描述里的capabilities字段匹配候选 Agent再结合注册中心里的负载情况决定派给谁。input_schema和output_schema是契约层Agent 之间通过编排器传数据时编排器会做格式校验格式不对直接打回重做避免后面 Agent 拿着残缺数据瞎跑。这套机制跑起来之后我发现很多以前要靠提示词调教才能避免的互相误解直接在契约层就解决了。3. 持久化设计三种状态必须分开存混在一起必出事故这是我踩坑最狠的一块单独拿出来写一节。多 Agent 协作系统里的状态其实是三类完全不同的东西会话状态、任务运行态、Agent 记忆。我最早偷懒全塞一个 Redis key 里结果任务一多读写冲突、序列化开销、恢复逻辑混乱全都来了。后来我把三者彻底分开设计世界清净了。3.1 会话状态用户和系统的谈判桌会话状态指的是多轮对话的上下文——用户问过什么、系统答过什么、当前正在讨论哪个主题。这个状态的特点是生命周期跟用户会话绑定可能很长用户隔几天回来接着聊但单个状态的数据量不大。OpenRig 里会话状态我用 Redis Hash 存每个会话一个 key字段包括history对话历史压缩后的、active_task_id当前关联的任务、preferences用户偏好比如语气、格式要求。关键是给会话设 TTL。有些会话用户聊完就走了永远不回来不设 TTL 的话 Redis 内存迟早被垃圾会话塞满。我设的是 7 天无访问自动过期用户回来接着聊通过active_task_id能找到之前任务的状态快照无缝衔接。这里有个经验对话历史别整个存大字符串要按轮次拆开存或者做摘要压缩。上下文窗口有限全量历史塞回去既不现实也没必要Agent 只需要最近几轮的完整内容加上之前所有内容的语义摘要就能保持对话的连贯性。3.2 任务运行态状态机驱动的施工日志任务运行态是最容易出问题的。一个复杂任务经历过拆解成子任务 - 派给 Agent A - A 返回中间结果 - 再派给 Agent B - B 失败重试 - 最终汇总这么一串过程中间任何一步断了整个任务就悬在那。OpenRig 把所有任务都建模成状态机每个任务有明确的状态流转我贴一下核心的状态定义TASK_STATES { PENDING: 任务已创建等待调度, DISPATCHED: 已派发给某个 Agent等待执行, RUNNING: Agent 正在处理中, WAITING_INPUT: 需要依赖其他子任务的结果, COMPLETED: 成功完成, FAILED: 执行失败等待重试或人工介入, TIMEOUT: 超时未返回, CANCELLED: 被用户或编排器取消, }状态本身存在 Redis String 里值就是状态名每次流转都通过编排器写一条事件到 Redis Streams。这样做的好处是任务在任意时刻我都能准确回答它现在到哪一步了、为什么卡在这。更重要的是崩溃恢复——服务重启后编排器扫描所有非终态不是 COMPLETED/FAILED/CANCELLED的任务凡是 DISPATCHED 超过心跳间隔没更新的统一拉回 PENDING 重新调度。3.3 Redis 持久化选择AOF 为主 RDB 为辅任务状态这类数据丢失了影响极大所以 Redis 持久化配置我调过好几轮。默认配置下 Redis 只做 RDB 快照默认策略是60 秒内 1 万次写才触发高并发下还好低并发下可能几分钟才存一次一旦宕机最后几分钟的任务状态全没了。我把持久化策略调成 AOF 为主# 开启 AOF并采用 everysec 策略每秒刷盘一次 appendonly yes appendfsync everysec # 同时保留 RDB 作为快速加载的快照 save 60 1000everysec意味着最多丢失 1 秒的数据对任务状态来说完全可接受。代价是 AOF 文件会持续变大所以要配aof_rewrite策略等 AOF 文件膨胀到上次重写后的一倍时自动触发重写压缩成只保留最小操作序列。RDB 快照的作用是重启加载快毕竟 AOF 重放比加载 RDB 慢不少两者配合既保障数据安全又保障恢复速度。3.4 Agent 记忆分层短期工作记忆和长期语义记忆分开最后是 Agent 的记忆。这块我走过弯路曾经把所有 Agent 的执行历史都塞进 Redis没过多久内存告警。后来参考认知科学的分层记忆思路把记忆拆成两层短期工作记忆存当前任务上下文比如我正在写报告第三章已经收集了哪些素材还缺哪个数据存在 Redis跟任务状态绑定任务结束后自动清理。长期语义记忆存 Agent 沉淀下来的知识比如用户偏好简洁的技术文档风格、上次查资料发现某数据源质量不行这些转成向量存向量数据库Agent 启动时按需检索增强。这么拆分之后Redis 的压力小了很多Agent 的回答质量也提升了。因为它既能想起当前任务做到哪了又能回忆过往积累的经验而不是每次都从零开始。4. 并发与扩展多 Agent 协作场景下扛并发的真问题搜ai agent 怎么扛并发的人特别多说明大家都卡在同一关。我一开始天真地以为多 Agent 天然就能并发——毕竟 Agent 多了嘛各干各的不就行了实际一跑才发现并发带来的问题不是算力不够而是状态打架。4.1 并发冲突的三种典型事故现场第一种是共享状态竞争。两个子任务同时试图更新同一个父任务的进度一个写已完成 50%另一个写已完成 70%后写的把先写的覆盖了进度条就失真。最危险的是两个 Agent 同时往同一个会话历史里追加内容直接互相覆盖对话内容缺胳膊少腿。第二种是重复执行。任务派发出去之后编排器和 Agent 之间通过网络通信消息可能丢失、超时然后编排器重试结果 Agent 那边其实已经处理完了只是返回结果丢了。这样同一个任务被干了两次浪费资源是小事关键是有副作用的操作比如发邮件、扣款被执行两次就是事故。第三种是消息乱序。Agent A 处理完子任务 1 的结果还没发给编排器Agent B 已经把子任务 2 的结果发回来了编排器按照固定的聚合逻辑处理时可能先处理了后面的数据导致汇总错乱。4.2 解法一任务队列 消费者组让消息不丢不乱OpenRig 的消息总线用 Redis Streams不光是看中它持久化更看中它的消费者组机制。每个 Agent 对应一个消费者组任务消息进 Stream组里的消费者也就是 Agent 实例轮流取消息。这本质上是个公平的负载均衡——哪个 Agent 实例空闲消息就派给谁。消费者组还有一个关键特性是pending entries list也就是已投递但未确认的消息列表。Agent 处理完任务必须发 XACK 确认如果 Agent 崩溃没确认消息会一直留在 pending 列表里编排器可以定期扫描 pending 列表把超时未确认的消息重新投递给其他健康的 Agent 实例。这直接解决了任务派出去没人认领的问题。4.3 解法二幂等设计 分布式锁把重复执行堵死重复执行的解法是幂等。OpenRig 给每个子任务生成全局唯一的task_idAgent 在执行前先查 Redis 里有没有这个task_id的执行记录有就直接返回之前的结果没有才开始执行。这个查重操作必须在开始执行前和执行完成后各做一次执行前查是为了避免重复干活执行后写入是为了让后续重复请求能拿到旧结果。对于真正需要互斥的场景——比如两个 Agent 同时要改同一个文件、或者要抢同一个数据源——我用 Redis 分布式锁。锁的 key 设计成lock:{资源类型}:{资源ID}加锁时用SET NX EX原子操作保证同一时间只有一个 Agent 能持有锁。锁的过期时间要留够余量因为 Agent 处理任务可能很慢我一般设成任务预计耗时的 3 倍并且用一个守护线程在任务没结束时自动续期防止锁提前过期导致两个 Agent 同时进入临界区。4.4 压测数据单机 8 个 Worker 的实际表现说下我压测的结果。机器配置是 8 核 16G 内存Redis 单独部署在另一台 4 核 8G 的机器上。用 8 个 Worker 实例消费任务队列每个任务平均耗时 20 秒主要是等待大模型响应系统稳定跑出了每秒新增约 30 个任务、同时在线执行任务数 160 个左右的吞吐。这个数字看着不大但对多 Agent 协作场景来说已经够用了——因为每个任务背后都是真实的大模型调用真正的瓶颈在模型 API 的响应速度而不是编排层。再往上扩Redis Streams 的消息吞吐本身不是瓶颈瓶颈在 Worker 实例的并发数和模型 API 的 rate limit。水平扩展的时候我只需要多启动几个 Worker 实例加入消费者组Redis 会自动做消息的再平衡业务代码一行都不用改。这一点是我选 Redis Streams 而不是自己写消息队列的最大原因——扩展成本几乎为零。5. 实测中翻过车的三个场景完整排查链路复盘光讲设计不讲事故等于纸上谈兵。OpenRig 从开发到现在翻车的场景不少挑三个最有代表性的复盘。这三个问题的共同特征是表面现象很迷惑如果不把排查链路完整走一遍根本找不到根因。5.1 事故一任务全部卡在 DISPATCHED没有任何 Agent 认领现象压测跑到第 40 分钟突然大量任务堆积在DISPATCHED状态过一会儿全部超时变成FAILED。排查过程我第一反应是 Agent 崩了但看进程都活着。接着看 Redis Streams 的消费者组状态发现了一条关键线索pending 列表里的消息数级增长但consumers列表里消费者数量是 0。也就是说消息投递出去了但没有消费者在线。再查 Agent 的日志发现它们都没报错只是静默不消费了。根因Worker 里有一个隐藏的 bug——当消息里带的task_id格式不合法时Worker 代码会抛异常而我的消费循环没有 catch 住这个异常Worker 直接退出消费循环但进程没退出于是它变成了一个僵尸消费者注册中心里还挂着它的心跳队列里消息却没人接。修复消费循环套了一层 try/catch任何异常都记录日志并继续消费下一条同时加了监护人协程定期检查消费循环是否还在运行一旦发现卡死自动重启 Worker。这个坑的核心教训是消费循环里不能有任何未经捕获的异常一旦异常导致循环退出Redis 不会自动把该消费者的 pending 消息转给他人。5.2 事故二Redis 内存飙升AOF 重写把 CPU 打满现象系统跑了一周之后Redis 内存持续上涨监控发现每过一段时间 CPU 会突然冲到 100%整个系统响应变慢。排查过程看内存分布发现task:*前缀的 key 占了将近 70%。再看 key 的 TTL问了 Redis 才发现大量任务完成后我虽然把状态改成了COMPLETED但没有删除任务相关的中间产物——原始输入、Agent 返回的中间结果、重试记录这些全留着。任务一多垃圾数据堆积成山。AOF 文件也膨胀严重触发自动重写时 Redis 要 fork 子进程做全量重写CPU 自然被打满。根因清理机制缺失。我只设计了任务状态流转没有设计任务数据生命周期。完成的任务其实只需要保留最终结果和关键审计信息中间过程数据完全可以删掉。修复加了任务数据的 TTL 策略——任务进入终态后中间产物默认保留 24 小时后自动过期只有标记为重要任务的才长期保留。同时把 AOF 重写阈值调低让它在低峰期分散执行避免集中触发。修完之后 Redis 内存稳定在原来的 1/3CPU 尖峰也消失了。这个事故的教训是持久化设计不能只考虑写入必须同步考虑过期清理写进去容易攒了一堆垃圾再清理就困难了。5.3 事故三两个 Agent 陷入无限循环对话现象一个查资料的 Agent 和一个写摘要的 Agent 突然开始互相反复调用任务流里出现了几百个子任务而且内容基本一致像死循环一样。排查过程看编排器的任务拆分记录发现最初用户的问题是帮我查一下这家公司的信息Planner 把任务拆成了查公司基本信息和生成摘要两个子任务。查资料的 Agent 返回结果后带了一个链接引用写摘要的 Agent 认为这个链接里的信息还没查过于是又生成一个新的查资料子任务。查资料 Agent 再查回来发现又有新链接……就停不下来了。根因任务拆分逻辑里缺少去重和终止的判断。Agent 之间传递信息时带了新的待办事项Planner 把每个待办事项都当成新任务派发没有检查这个事项是不是已经做过了。修复给编排器加了一道流程任何子任务在执行前先做相似度检查对比历史子任务的输入和输出如果相似度超过阈值且历史任务已成功完成直接复用历史结果不重复派发。另外加了一个全局的任务深度上限默认 10 层超过上限自动终止并提示任务过于复杂需要人工介入。这个事故让我意识到多 Agent 系统的编排器必须内置防呆机制Agent 是生成式的它会输出各种你预想不到的情况编排器必须假设所有 Agent 都可能犯错用确定性的规则兜底。6. OpenRig 的落地形态与后续演进从技术玩具到业务工具最后说点实际的。OpenRig 这个系统跑到现在我从里面最大的感悟是编排层的设计决定了一个多 Agent 系统是能跑还是能扛事。当初设计时很多看似多余的约束——能力描述、契约校验、状态机、幂等检查——在业务真正跑起来之后每一个都派上了大用场。6.1 两个真实的落地场景参考我拿 OpenRig 接了两个典型业务场景可以作为参考。第一个是复杂报告生成。原来用单 Agent 写行业分析报告经常写到一半忘了前面的数据、格式不稳定、引用的数据源靠不靠谱没人核对。改成多 Agent 协作后分工是研究 Agent 负责收集数据和来源分析 Agent 负责计算指标和趋势写作 Agent 负责组织语言质检 Agent 负责查事实错误和格式问题。四步走下来报告质量明显稳定了最重要的是每步都有中间产物用户能实时看到进度不像以前黑盒等结果。第二个是售后工单自动跟进。用户提交工单后分类 Agent 判断工单类型匹配 Agent 检索历史相似工单方案 Agent 生成初步回复人工审核 Agent 负责判断是否需要转人工。整个流程里融入了一堆历史的语义记忆——比如这类问题用户通常还关心退款时效这些记忆是在一次次处理中沉淀进向量库的越用越准。6.2 技术上的三个演进方向代码层面我接下来打算做三件事。第一把编排器从单进程调度改成分布式调度目前编排器虽然无状态可以水平扩展但任务拆分逻辑还是集中在单点上大规模部署时这块会成为瓶颈。第二给状态存储加一层冷热分离超过 30 天未访问的会话和任务归档到磁盘存储Redis 只保留热数据进一步降低成本。第三补一个人工介入通道——当编排器检测到任务多次重试仍失败或者任务深度超限时自动生成一份人类可读的问题摘要并挂起任务等人工决策后再继续。6.3 我个人最想分享的一条经验如果只能留一条经验我会说多 Agent 编排系统里确定性逻辑要多智能逻辑要少。编排器的调度、状态流转、幂等检查、超时重试这些核心路径全部用确定性代码实现一个模型调用都不要有。模型只出现在 Agent 真正需要理解语义、生成内容的地方。这样系统出了问题你可以一步步回溯、写单测、仿真演练。反过来如果把调度逻辑也交给大模型相当于把别人公司的数据中心当刹车踏板看起来踩了刹车实际上刹车在哪、好不好用全不由你说了算。OpenRig 这个项目还在迭代现在最让我满意的不是它能跑多复杂的任务而是每次出问题我都能在半分钟内定位到具体是哪个环节、哪条消息、哪个状态。这种确定性和可控感是单 Agent 永远给不了的也是多 Agent 编排这个方向真正值得深挖的价值所在。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →