1. 从一次深夜告警说起:为什么架构选型不能只看Demo
凌晨两点,监控面板上一条红色的告警线突然拉起来——某个多智能体协作任务在跑了四十多分钟后卡死,日志里只有一句含糊的“等待下游响应超时”。这不是第一次了。过去两周,团队用某通用编排框架搭的原型在演示环境里跑得挺漂亮,三个智能体分工明确、消息流转顺畅,可一旦把任务复杂度提上来、把并发数从个位数拉到几十,各种诡异问题就开始冒头:状态丢失、消息重复消费、某个Agent明明已经完成了子任务却迟迟不触发下一步。
那次告警之后,我们花了整整三天做复盘,最终得出一个当时听起来有点“反直觉”的结论:通用框架和自研编排,我们都不该全押,真正适合我们业务的是混合架构。这篇文章就把这段选型过程完整拆开讲——不是给你一个标准答案,而是把我们在“自研编排”和“通用框架”之间反复横跳、最后走向混合架构的思考链路、踩过的坑、以及最终落地的分层设计,原原本本摊开。
如果你正在做多智能体系统(Multi-Agent System)的架构设计,或者团队里有人拍着桌子说“直接用现成框架不就行了”,那这篇内容应该能帮你少走至少两个月的弯路。我会尽量说人话,把编排、状态管理、通信协议这些听起来很唬人的东西,用实际项目里的场景讲清楚。
先说清楚一个前提:多智能体系统的“架构抉择”,本质上不是选一个框架那么简单,而是要回答三个问题——谁来定义任务流转的拓扑结构、谁来管理跨Agent的状态一致性、谁来兜底异常和重试。这三个问题,通用框架和自研编排给出的答案完全不同,而混合架构的价值,恰恰在于让它们各管一段。
2. 通用框架的甜蜜期与它的三堵墙
2.1 为什么一开始大家都倾向用通用框架
刚接触多智能体编排的时候,几乎所有人的第一反应都是找现成框架。原因很朴素:多智能体系统里有一堆“脏活累活”——消息队列、任务调度、Agent生命周期管理、工具调用封装、上下文传递——如果每个都自己写,光是把第一个能跑通的Demo搭起来就得花掉两三周。通用框架把这些东西打包好了,你只需要定义几个Agent、写几段提示词、连几条边,一个能跑的多智能体流程就出来了。
我们当时也是这么想的。选型阶段对比了几个主流方案,评估维度主要是这几项:
| 评估维度 | 通用框架表现 | 自研编排表现 |
|---|---|---|
| 上手速度 | 快,半天出Demo | 慢,至少一周搭骨架 |
| 拓扑灵活性 | 受框架抽象限制 | 完全自由 |
| 状态管理 | 框架内置,但黑盒 | 自己掌控,透明 |
| 异常处理 | 通用重试策略 | 可按业务定制 |
| 调试可观测性 | 依赖框架日志 | 全链路自己埋点 |
| 长期维护成本 | 受框架版本影响 | 自己背维护责任 |
表格里能看出来,通用框架在“上手速度”这一项上是碾压性的。我们当时用了一个下午就把三个Agent的协作流程跑通了:一个负责拆解任务,一个负责调用工具执行,一个负责汇总结果。演示给业务方看的时候,对方眼睛都亮了。那段时间团队士气很高,觉得这事儿稳了。
但甜蜜期大概只持续了两周。
2.2 第一堵墙:拓扑结构被框架的抽象“框死”了
通用框架通常会给你几种预设的协作模式,比如顺序执行、路由分发、群组讨论、层级委派。这些模式覆盖了大部分常见场景,但我们的业务里有一种很特殊的流转逻辑:某个Agent的输出需要根据内容动态决定是走“并行分支”还是“串行回退”,而且这个判断逻辑本身还需要调用一个外部服务来确认。
在框架里,这种动态拓扑很难优雅地表达。你当然可以用条件边(conditional edge)来模拟,但当分支嵌套超过三层之后,整个流程图就变成了一团意大利面,连当初写它的人都很难说清楚某个请求到底会走哪条路径。更麻烦的是,框架的抽象层把底层的执行顺序隐藏了,你想在某个特定节点插入一个“检查点”做状态快照,发现框架根本没暴露这个钩子。
这里有个经验:通用框架的抽象层次越高,你能插入自定义逻辑的地方就越少。而多智能体系统恰恰是一个“自定义逻辑密度极高”的场景。
2.3 第二堵墙:状态一致性在并发下变成玄学
单机、低并发的时候,框架的状态管理看起来没问题。但当我们把并发任务数拉到30以上,问题就来了。框架内部用的是一个共享的状态存储,多个Agent实例同时读写同一个会话状态时,出现了经典的“读-改-写”竞争。具体表现是:Agent A读取了状态S,准备写入S',但在它写入之前,Agent B已经把状态改成了S'',结果A的写入直接覆盖了B的修改,导致B的某个子任务结果凭空消失。
我们翻框架源码才发现,它默认的状态更新策略是“最后写入获胜”(last-write-wins),而且没有提供乐观锁或版本号的机制。这意味着在高并发下,状态丢失是必然的,只是概率问题。要解决这个问题,要么自己在框架外面再包一层状态管理,要么换框架——但换框架意味着前面两周的工作全部推倒重来。
2.4 第三堵墙:异常处理的“通用策略”救不了业务特有的失败模式
框架提供的重试策略通常是“失败后重试N次,指数退避”。这在调用外部API时很好用,但多智能体系统里的失败模式远不止“调用超时”这一种。比如:
- Agent输出了格式正确但语义错误的结果,框架认为“成功”,但业务上需要重新规划;
- 某个子任务依赖的上游数据还没准备好,需要“等待并轮询”而不是“立即重试”;
- 多个Agent同时失败,需要判断是“独立故障”还是“级联故障”,前者可以各自重试,后者必须整体回滚。
这些逻辑,通用框架要么不支持,要么需要用很别扭的方式绕过去。我们当时为了处理“级联故障回滚”,在框架外面写了一个协调器,结果这个协调器本身又变成了一个单点,复杂度不降反升。
3. 转向自研编排:自由是自由,但代价是什么
3.1 自研编排的核心设计:把控制权拿回来
被通用框架的三堵墙撞得头破血流之后,我们决定试试自研编排。核心思路很简单:把任务流转的拓扑定义权、状态管理的控制权、异常处理的决策权,全部拿回到自己手里。
具体设计上,我们定义了一个轻量的编排引擎,核心概念只有三个:
- TaskGraph:用有向无环图(DAG)描述任务流转,节点是Agent调用,边是数据依赖和条件分支。和框架不同的是,这个图是运行时动态构建的,每个请求进来时根据输入内容生成专属的图。
- StateStore:自己实现的状态存储,每个任务有独立的版本号,所有写入操作必须携带版本号做乐观锁校验,冲突时抛出异常由上层决定重试还是合并。
- FailurePolicy:每个节点可以挂载自己的失败策略,支持重试、跳过、回退到指定节点、触发补偿任务等。
这套东西搭起来大概花了一周半,比用框架慢,但搭完之后,之前遇到的三个问题全部有了清晰的解法。动态拓扑?TaskGraph本来就是运行时构建的,想怎么连就怎么连。状态一致性?版本号乐观锁直接解决。异常处理?每个节点自己定义策略,级联故障通过图的拓扑分析自动识别。
3.2 自研带来的新问题:基础设施的维护成本被低估了
但自研的代价很快就显现出来了。最直接的是基础设施的维护成本。通用框架帮你处理了消息序列化、网络通信、Agent进程管理、日志收集这些“非功能性需求”,自研之后这些全部得自己扛。
我们当时踩的几个坑:
- 消息序列化:一开始用JSON,简单但体积大,高并发下网络带宽成了瓶颈。后来换成二进制协议,又得自己处理版本兼容。
- Agent进程管理:Agent崩溃后如何自动拉起?如何做优雅停机?如何限制单个Agent的资源使用?这些在框架里是内置的,自研之后每个都得单独实现。
- 可观测性:框架自带了一套追踪和指标采集,自研之后得自己埋点、自己搭看板。有段时间我们排查一个问题,因为缺少链路追踪,硬是靠打日志加时间戳对齐,花了整整一天。
更隐蔽的问题是团队认知负担。自研编排引擎的代码只有最初设计它的两个人最清楚,其他人接手时,光是理解TaskGraph的动态构建逻辑和StateStore的版本号机制就花了好几天。而通用框架至少有公开文档和社区讨论,遇到问题还能搜一搜。
3.3 一个具体的性能对比:自研真的更快吗
我们做了一组对比测试,场景是“一个任务拆解Agent + 五个并行执行Agent + 一个汇总Agent”,并发数50,每个Agent模拟耗时200ms。结果如下:
| 指标 | 通用框架 | 自研编排 |
|---|---|---|
| 平均端到端延迟 | 1.8s | 1.2s |
| P99延迟 | 4.5s | 2.1s |
| 状态丢失率 | 0.3% | 0% |
| CPU占用 | 较高(框架开销) | 较低 |
| 开发耗时 | 2周 | 3周 |
| 维护人力 | 0.5人 | 1.5人 |
自研在延迟和可靠性上确实更好,但开发耗时多了50%,维护人力多了两倍。这个账怎么算,取决于你的业务对延迟和可靠性的敏感程度,以及团队有没有足够的人力去背这个维护责任。
4. 混合架构的诞生:让框架管“脏活”,自研管“决策”
4.1 混合架构的分层原则:什么该交给框架,什么必须自己攥着
经过通用框架和纯自研两轮折腾,我们逐渐摸清了一条分界线:通用框架擅长的是“通信和生命周期”这类标准化程度高的脏活,自研擅长的是“拓扑和决策”这类业务耦合度高的逻辑。混合架构的核心,就是沿着这条线把系统切成两层。
具体分层是这样的:
- 底层(框架负责):Agent的注册与发现、消息传输、进程健康检查、基础重试、日志与指标采集。这一层用通用框架的能力,不重复造轮子。
- 中层(自研负责):TaskGraph的动态构建与执行、状态版本管理、跨Agent的事务性协调、业务级异常策略。这一层完全自研,框架不介入。
- 上层(业务负责):每个Agent的具体提示词、工具调用逻辑、结果校验规则。这一层由业务团队自己维护。
这样切的好处是,框架的版本升级不会影响中层的核心逻辑,而中层的业务变化也不需要动底层的通信机制。两层之间通过一个薄薄的适配层通信,适配层的接口是我们自己定义的,框架只是它的一个实现。
4.2 适配层设计:让框架“可替换”的关键
混合架构能不能成立,关键看适配层设计得好不好。我们的适配层定义了三个核心接口:
class AgentRuntime: def spawn(self, agent_spec): ... def send(self, agent_id, message): ... def on_message(self, callback): ... class StateBackend: def read(self, key, version): ... def write(self, key, value, expected_version): ... def watch(self, key, callback): ... class TelemetrySink: def emit_trace(self, span): ... def emit_metric(self, name, value): ...底层框架只需要实现这三个接口,中层完全不关心底层用的是哪个框架。我们甚至做过实验:把底层从框架A换成框架B,中层的TaskGraph和StateStore代码一行没改,只换了适配层的实现,两天就完成了迁移。这个可替换性,是纯自研或纯框架都做不到的。
4.3 状态管理的混合方案:框架管传输,自研管一致性
状态管理是混合架构里最微妙的部分。我们的做法是:框架负责状态的传输和持久化,自研负责状态的一致性语义。
具体来说,框架提供的是一个“至少一次”的消息传递保证,消息可能会重复。自研的StateStore在写入时做幂等校验:每个状态更新都带一个唯一的事务ID,如果同一个事务ID已经写入过,直接返回成功而不重复应用。这样即使框架重复投递消息,状态也不会被重复修改。
版本号机制则用来处理并发写入。StateStore的每次写入都要求携带期望的版本号,版本号不匹配时抛出ConcurrentModificationError,由上层的FailurePolicy决定是重试、合并还是放弃。这套机制在纯框架方案里是没有的,在纯自研方案里是我们自己实现的,现在在混合架构里,它成了连接框架和业务的“一致性锚点”。
5. 落地过程中那些文档不会写的坑
5.1 框架的“默认配置”在高并发下全是雷
通用框架的默认配置通常是为“低并发、快速上手”优化的。比如默认的消息队列容量、默认的线程池大小、默认的超时时间,在Demo阶段完全够用,但一到生产环境的高并发场景,这些默认值就成了定时炸弹。
我们踩过的一个典型坑:框架默认的消息确认超时是30秒,意味着一条消息发出去后,如果30秒内没收到确认,框架会认为投递失败并重试。但在我们的场景里,某些Agent处理复杂任务需要超过30秒,结果框架在Agent还在处理的时候就把消息重新投递了一遍,导致同一个任务被处理了两次。这个问题的隐蔽性在于,它不会报错,只会让某些任务的结果出现重复,排查起来非常费劲。
经验:任何框架的默认配置,在上生产之前都必须逐项审查。重点看超时时间、重试次数、队列容量、线程池大小这四项。
5.2 自研的TaskGraph在动态分支下的死锁风险
自研TaskGraph的灵活性是一把双刃剑。我们设计了一种“动态分支”机制:某个节点的输出可以决定后续走哪条分支,分支的拓扑在运行时才确定。这个机制很强大,但也引入了一个新的风险——死锁。
具体场景是这样的:节点A的输出决定走分支1还是分支2,分支1依赖节点B的结果,分支2依赖节点C的结果,而节点B和节点C又都依赖节点A的某个中间状态。如果节点A的输出恰好让分支1和分支2互相等待对方的结果,整个图就死锁了。
我们后来加了一个静态分析器,在TaskGraph构建完成后做一次拓扑检查,检测是否存在“循环依赖”或“不可达节点”。但这个分析器只能检测静态的图结构,对于运行时才确定的分支,只能在执行时加超时兜底。这个坑让我们明白:动态拓扑的自由度越高,越需要配套的静态检查和运行时兜底。
5.3 混合架构的调试复杂度:跨层问题最难查
混合架构最大的代价是调试复杂度。一个问题可能出在框架层、适配层、自研层或业务层,而每一层的日志格式和追踪ID体系都不一样。我们遇到过一个案例:某个任务偶尔会卡住,日志显示框架层认为消息已投递,自研层认为状态已更新,但业务层就是没收到回调。最后查了两天才发现,是适配层在转换消息格式时,对某个特殊字符的处理有bug,导致消息在框架层和自研层之间“消失”了。
为了解决这个问题,我们后来强制要求所有层使用统一的追踪ID,并且在适配层加了详细的入参出参日志。虽然增加了日志量,但跨层问题的排查时间从平均两天缩短到了两小时。
6. 混合架构的适用边界与选型决策清单
6.1 什么情况下混合架构是过度设计
混合架构不是银弹。如果你的多智能体系统满足以下所有条件,那直接用通用框架就够了,混合架构反而是过度设计:
- 任务拓扑是静态的,不需要运行时动态构建;
- 并发量在个位数,状态一致性压力不大;
- 异常处理只需要通用的重试策略,没有复杂的业务级回滚需求;
- 团队规模小,没有足够人力维护自研层。
我们团队之所以最终选择混合架构,是因为上面四条我们一条都不满足。任务拓扑高度动态、并发量在几十到上百、异常处理有复杂的级联回滚需求、团队有专门的基础设施人力。这四个条件同时成立时,混合架构的收益才能覆盖它的成本。
6.2 一张选型决策表:从需求反推架构
下面这张表是我们复盘时整理的,把关键需求维度和对应的架构选择对应起来,供参考:
| 需求维度 | 倾向通用框架 | 倾向自研编排 | 倾向混合架构 |
|---|---|---|---|
| 拓扑动态性 | 低 | 高 | 高 |
| 并发规模 | 低(<10) | 高 | 高 |
| 状态一致性要求 | 最终一致即可 | 强一致 | 强一致 |
| 异常处理复杂度 | 通用重试 | 业务级策略 | 业务级策略 |
| 团队基础设施人力 | 无 | 充足 | 中等 |
| 上线时间压力 | 大 | 小 | 中 |
| 长期维护意愿 | 低 | 高 | 中高 |
这张表的使用方法是:先看“倾向混合架构”那一列,如果大部分维度都落在这一列,那混合架构值得考虑;如果大部分落在“倾向通用框架”,那就别折腾,直接用框架。
6.3 混合架构的演进路径:从框架起步,逐步自研
如果你决定走混合架构,我建议的演进路径是:先用通用框架快速搭出可运行的版本,验证业务逻辑;当遇到框架解决不了的问题时,把那一部分逻辑抽出来自研,通过适配层接回去。不要一上来就全自研,那样风险太高。
我们实际走的就是这条路:第一版全框架,第二版把状态管理抽出来自研,第三版把TaskGraph抽出来自研,第四版把异常策略抽出来自研。每一步都是在遇到具体问题后才动手,而不是提前设计一个“完美架构”。这样每一步的改动都有明确的收益,也容易回滚。
7. 一些关于多智能体架构的个人体会
做多智能体系统的架构设计,最容易犯的错误是“用框架的抽象来思考业务”。框架给你的是顺序、路由、群组这些模式,但业务需要的是“根据内容动态决定下一步”“多个Agent的结果需要事务性提交”“失败时要区分独立故障和级联故障”。这些需求,框架的抽象往往表达不了,硬套只会让代码越来越扭曲。
混合架构的本质,是承认“没有一种方案能同时满足标准化和定制化”,然后把系统切成两层,让每层做自己最擅长的事。框架管通信和生命周期,自研管拓扑和决策,适配层做翻译。这个思路不仅适用于多智能体系统,任何“标准化基础设施 + 高度定制业务逻辑”的场景都可以参考。
最后分享一个我们内部用的判断标准:如果你发现自己在框架外面写了很多“协调代码”,那这些协调代码就应该被抽出来,成为自研层的一部分。框架应该是被自研层调用的工具,而不是反过来让自研逻辑去适配框架的抽象。这个主次关系一旦理顺,架构的复杂度就会大幅下降。