先说结论:我们最终把多智能体系统的编排层拆成了两层——外层流程编排自研,内层Agent执行交给通用框架。这个决定不是谁拍脑袋定的,也不是出于“技术洁癖”,而是被生产环境里一连串事故和卡点逼出来的。这篇文章就把这次架构抉择的前因后果、对比过程、落地细节和踩坑记录完整写出来,给正在做类似选型的朋友一个参考。
从我的视角看,多智能体系统的架构选型,本质上不是“哪个框架更火”的问题,而是“谁更适合管住生产环境里的不确定性”的问题。通用框架让起步很爽,但到了线上,状态一致性、可观测性、故障恢复这些事,框架帮不了你多少;而全自研又意味着你先把轮子造完才能跑业务。我们想要的是一个既能快速上线、又能在关键路径上完全可控的方案,于是才有了这套混合架构。
1. 先交代清楚:我们要做的多智能体系统是什么
1.1 系统形态与四个核心痛点
我们做的系统本质上是一个面向客服场景的多智能体协作平台,用来处理用户在售前、订单、物流、售后这几个环节的咨询。系统内部一共运行了五个职责不同的Agent:意图路由Agent、订单查询Agent、物流跟踪Agent、售后处理Agent,以及一个负责兜底的通用问答Agent。用户的一句话进来,先由路由Agent判断意图,再决定把任务分给哪个Agent执行,执行过程中还可能涉及调用外部接口、判断结果、追问用户、甚至转人工等动作。
这个系统从Demo走到生产环境之后,我们陆陆续续遇到了四个非常具体的痛点。第一个是长流程编排,像售后处理这种场景,一个会话可能要经历用户描述问题、Agent查询订单、判断是否符合退货条件、询问用户意见、生成退货单这么多个步骤,每个步骤还有可能分支,流程一不小心就走乱。第二个是状态一致性,多个Agent协作的时候,任何一个环节超时或报错,整个会话的状态就可能卡在一个不上不下的位置,用户重试也好、换个说法再问也好,都无法继续。第三个是可观测性,我们很难回答“这个会话现在到底走到哪一步了”这个问题,定位一次线上异常通常要翻半天日志。第四个是成本控制,模型调用和工具调用的开销在生产环境里非常敏感,流程编排不精准,浪费的token都是真金白银。
这四个痛点指向同一个结论:多智能体系统能不能稳定跑起来,瓶颈不在单个Agent的智商,而在Agent之间的“编排层”——谁来决定下一步做什么、谁在状态异常时兜底、谁记录整个过程。
1.2 为什么说“编排层”才是架构的分水岭
我在内部评审会上反复强调一个观点:如果把多智能体系统比作一家公司,单个Agent就是某个岗位的员工,而编排层就是这家公司的流程制度。员工再能干,如果流程制度混乱——任务没人跟进、交接没有记录、出了问题没人负责——公司照样转不起来。
技术选型的真正分水岭,就在于你选择把“流程制度”交给谁。交给通用框架,相当于直接买一套成熟的管理系统,上线快、功能全,但制度的细节你说了不算;全自研,相当于自己从零定制度,最贴合业务,但前期要投入大量精力。
我们当时面临的选择其实就在这两端之间:通用框架负责Agent的跑动逻辑和工具调用,但把流程状态和步骤控制交还给自己的代码。这个想法听起来并不复杂,难的是具体怎么切分职责、怎么封装接口、怎么保证切换之后系统的稳定性不会反而变差。后面几节就是这套方案从思路到落地的完整记录。
2. 决策拆解:自研编排和通用框架各自的底牌
2.1 通用框架好在哪里,又坑在哪里
先说通用框架的优点,这个不能否认。当时我们选型时重点看了两类开源方案,一类偏图谱式编排,把多智能体的流程画成一张图,节点之间用边连接;另一类偏角色式对话,让多个Agent像开讨论会一样互相聊天完成任务。这两类方案我们都实际搭过Demo,体感和宣传基本一致:上手快、抽象层次高、内置了常用工具调用逻辑。一个最简单的意图路由加订单查询流程,用通用框架大概一天就能跑通,这在早期验证业务价值时帮了我们大忙。
但通用框架的问题,也是我们真正上了生产之后才深刻体会到的。第一是编排逻辑的“黑盒感”,图编排框架虽然给了你可视化的流程图,但流程真正跑起来时,每一步状态切换、重试策略、异常分支的处理,很多逻辑藏在框架内部,你只能在有限的回调点里做文章,想精确控制某个分支非常吃力。第二是可观测性不够细,框架确实会输出一些日志和trace,但粒度是它定义好的,不是你的业务语言,出了问题你很难回答“这个用户的退货单为什么在这个节点停住了”。第三是升级绑架,框架版本迭代频繁,我们因为需要某个新特性升级过一次,结果一个不兼容的改动导致十几个流程要重新回归测试。
还有一个更微妙的问题:通用框架的“泛化能力强”反过来意味着“定制能力弱”。它为了适配所有用户,抽象层级一定偏高,而我们的业务其实有大量特殊情况,比如某些节点必须人工审批、某些Agent调用外部接口有特殊的鉴权协议、某些流程步骤数不能超过限制。这些东西做进通用框架里,要么等官方支持,要么自己改源码维护分支,两条路都不好走。
2.2 自研编排的优势与真实成本
正因为通用框架在生产环境的控制力不够,我们一度产生了“干脆全自研编排”的冲动。这个冲动的起点其实很朴素:我们的流程虽然多,但并没有多到几十上百种的地步;我们的状态虽然复杂,但仔细梳理之后,核心状态机是可以枚举清楚的。自研编排引擎的好处正是这种“完全可控”——每个节点做什么、状态怎么迁移、异常怎么兜底、日志怎么记录,全部由我们自己的代码说了算。
另外一个容易被忽视的好处是“无绑定”。用通用框架,你的核心业务代码会被框架的抽象方式影响,时间一长,你会发现自己不是在写业务,而是在适配框架的API。而自研编排引擎没有这个问题,业务的表达方式就是你最自然的方式。
但自研的真实成本,我们也算了一笔账。首先是开发成本,一个能支撑生产环境的编排引擎,至少包含状态机、任务队列、重试机制、超时控制、持久化、可观测性这些模块,从零写起,一个三到四人的小组,保守估计要六到八周才能达到可上线状态。其次是长期维护成本,编排引擎一旦成为基础设施,后续的每个改动都要非常谨慎,这对团队的能力要求比用框架高一个级别。最后是“轮子风险”,开源框架背后有整个社区在踩坑,自研引擎的坑只能自己踩,有一些场景——比如极端并发下的状态竞争——你没遇到过就永远不会知道问题在哪。
2.3 一张决策矩阵表看清边界
为了把两边的边界理清楚,我们整理了一张决策矩阵,核心就看几个维度:开发成本、可控性、生态能力、可观测性、升级风险和适用阶段。这张表后来在我们内部评审会上反复被拿出来讨论,也帮我彻底想明白了混合架构的方向。
| 评估维度 | 纯自研编排 | 通用框架编排 | 混合架构(自研外层+框架内层) |
|---|---|---|---|
| 初期开发成本 | 高(约6~8周) | 低(约1周跑通Demo) | 中(约3~4周) |
| 流程控制力 | 完全可控 | 受框架抽象限制 | 核心路径完全可控 |
| 工具生态能力 | 自己对接 | 开箱即用 | 通过内层框架复用 |
| 可观测性 | 按需定制 | 受框架粒度限制 | 按需定制 |
| 技术绑定风险 | 无 | 高 | 低 |
| 适合场景 | 流程复杂且团队能力强 | 快速验证/简单固定流程 | 生产级复杂业务 |
从这个表格能很明显地看出,纯自研和纯用通用框架都不是最优解。我们真正想要的,是把通用框架的生态能力和自研的流程控制力结合起来。这就是混合架构的出发点:外层用自研引擎控制“流程怎么走”,内层用通用框架解决“单个Agent怎么干活”,两者各管一段,职责分明。
3. 混合架构怎么落地:外层自研、内层通用的三层设计
3.1 架构总览与职责划分
混合架构落地之后,整体分成了三层:接入层、编排层、执行层。接入层负责接收用户请求、做简单的意图预处理、把请求标准化成任务对象;编排层是我们自研的流程引擎,负责状态管理和流程推进;执行层则是通用框架的运行时,负责承载具体的Agent,处理工具调用和模型交互。
三层之间遵循一个关键原则:粗粒度编排,细粒度执行。意思是说,编排层只在“流程节点”级别做控制和决策——比如判断当前该调用哪个Agent、前一步的结果是否符合预期、下一步是继续还是终止——它不去管Agent内部怎么跟模型对话、怎么选择工具、怎么拼接上下文,这些细粒度逻辑全部收口在执行层的通用框架里。
这个切分的好处是边界非常清晰。一旦某一个Agent出了问题,编排层能准确知道是哪个节点失败;一旦流程需要调整,我们只需要改编排层的配置,不需要动Agent的实现。对我们来说,这相当于把“公司的流程制度”和“员工的工作方式”彻底解耦了。
3.2 自研编排引擎的核心实现
自研编排引擎的核心是状态机。我们把一条业务流程抽象成一组节点,每个节点有明确的输入、输出和状态,状态迁移由事件驱动。最开始我们用的是现成的状态机库,但后来发现状态机的语义跟业务不完全匹配,干脆自己写了一个轻量版本,核心代码并不复杂,关键在状态转移规则的清晰定义。
# 自研编排引擎的状态机核心骨架 from enum import Enum class AgentStatus(Enum): PENDING = "pending" # 等待调度 RUNNING = "running" # 执行中 WAITING = "waiting" # 等待外部事件(如人工审批) SUCCESS = "success" # 执行成功 FAILED = "failed" # 执行失败 TIMEOUT = "timeout" # 超时终止 class WorkflowState: def __init__(self, session_id, current_node, status: AgentStatus): self.session_id = session_id self.current_node = current_node self.status = status self.payload = {} # 节点间传递的数据 self.attempts = 0 # 重试次数 self.created_at = now() self.updated_at = now() def transit(self, event: str, context: dict): # 状态转移规则表,每个(当前状态, 事件)对对应一个目标状态 rule = TRANSITION_RULES[(self.status, event)] if rule is None: raise IllegalTransition(self.status, event) self.status = rule.target self.payload.update(context) self.updated_at = now()状态转移规则表是我们花了不少心思梳理的,每种业务节点一张表。以售后处理为例:用户描述问题后,状态从PENDING切到RUNNING;售后Agent执行成功返回SUCCESS,此时判定是否需要人工审批,如果需要就切到WAITING;人工审批通过后事件是APPROVE,才允许进入生成退货单的后续流程。任何一个状态收到不合法事件,直接抛异常并触发告警,这比静默吞掉错误要安全得多。
调度器则是状态机的“发动机”。它维护一个优先级队列,不断取出“已就绪”的任务分发给执行层,同时处理重试和超时。为了避免一个慢Agent拖垮整个会话,我们给每个节点都配了超时阈值,超时后先触发一次重试,仍然失败就进入降级分支,由兜底Agent接替或者转人工,保证用户的会话永远不会因为某个Agent卡死而永久中断。
3.3 通用框架执行器的封装与协议统一
外层编排引擎并不直接跟具体Agent打交道,它面对的是一个统一的Agent执行器接口。每个Agent(无论内部用的是什么模型、什么工具)都要实现这个接口,这样编排层就只依赖协议,不依赖实现。
# Agent执行器统一接口 class AgentExecutor(Protocol): async def run(self, task: Task) -> AgentResult: ... async def cancel(self, task_id: str) -> None: ... def healthcheck(self) -> bool: ...接口统一之后,具体实现就交给通用框架来完成。我们把原来跑在通用框架上的Agent原样保留,只是在外面包了一层适配器:从编排层接收任务,转换成通用框架的输入格式,调用框架的Agent运行时,再把结果转换回标准结果格式返回给编排层。
这个封装解决了一个很实际的问题:模型供应商的SDK版本升级、内部某个Agent的prompt大改、工具调用的参数调整,这些变动全部被隔离在执行层内部。编排层完全不受影响,状态机也不需要跟着改。有一次我们为了适配新的模型上下文格式,几乎重写了Agent内部逻辑,但整个流程编排层一行代码没动,那次之后团队里再没有人质疑混合架构的价值了。
4. 从Demo到生产:实操过程与关键数据
4.1 基线验证与性能对比
混合架构落地之后,我们并没有直接全量切换,而是先跑了一轮完整的基线验证。验证分两条线:一条线是把原来的通用框架编排跑在测试环境,作为对照组;另一条线是新的混合架构跑同样的业务场景。测试数据来自脱敏后的真实会话样本,一共覆盖五种意图、两千多条消息,重点对比三个指标:P95端到端延迟、任务失败率、故障恢复时间。
对照组测出来的数据并不算好看,P95延迟在8秒左右徘徊,任务失败率接近5%,而且有大约三成失败会卡住会话状态,需要人工介入才能恢复——这个现场我们内部用“流程卡死”来形容,用户的会话停在某个节点上,你再怎么发消息都走不下去。混合架构跑同一批样本,P95延迟降到了3秒左右,任务失败率降到1.2%,更关键的是,会话状态不会再轻易卡死,超时之后自动重试或者降级到兜底Agent,绝大多数情况下用户无感知。
延迟降下来主要是因为编排层省掉了大量不必要的框架内部开销,流程明确之后,节点之间的跳转变得非常直接。失败率降低则得益于自研状态机的精确控制和重试策略。恢复能力提升是混合架构最明显的收益:故障恢复时间从人工介入的几十分钟缩短到自动重试的十几秒。
4.2 踩到的三个真实坑
这套架构并非一帆风顺,有三个坑我想重点记录,因为它们都不是靠看文档能避开的。
第一个坑是状态机“死锁”。最初我们设计状态机时,只考虑了正常事件流,没有充分考虑“用户中途取消会话”这类边界事件。结果上线后出现了一个诡异的现象:部分售后会话的状态变成了RUNNING,但流程永远停在那里,既不前进也不失败,因为这个状态没有定义CANCEL事件的转移规则。定位了半天才反应过来——状态机写得太理想化,把边界事件漏掉了。这次事故之后,我们专门补了一张“全事件×全状态”的转移矩阵,把所有非法组合全部排查一遍,边界事件一个都不放过。
第二个坑是上下文爆炸。外层编排层为了流程判断,会把每个节点的输出都存起来,导致一个长会话的payload越积越大。刚开始不觉得,直到有用户连续咨询了二十多轮,我们发现内存和token开销成倍增长,甚至影响到了响应速度。解决办法是给payload做“摘要式保留”:每个节点输出先做一次压缩,只保留流程判断需要的关键字段,完整内容存到外部存储,需要时再按ID取回。
第三个坑是回调风暴。多个Agent并发的场景下,执行层完成的回调如果同时到达编排层,我们最早的处理方式是每个回调都触发一次状态变更和持久化,结果在压测时数据库连接直接被打满。后来改成事件批处理:回调先写入内存队列,编排层按批次统一处理、统一落库,写压力直接降了一个数量级。这个改动让我深刻理解了“削峰填谷”在编排层的重要性。
4.3 可观测性与稳定性建设
经历过几次线上事故之后,我们把可观测性提到了最高优先级。编排层每个状态迁移都产生一条结构化事件记录,包含 session_id、节点名、旧状态、新状态、触发事件、耗时、payload摘要。这些事件写入独立的日志存储,支撑两类查询:一是按session_id拉出完整的流程轨迹,二是按节点名统计成功率、耗时、重试次数。
我们还为每个流程节点设计了“运行画像”,记录最近一周的P50/P95耗时、失败率、重试率、降级率这些指标。每周Review一次,哪个节点异常、哪个Agent不稳定,一眼就能看出来。可以说,混合架构最大的收益之一,就是它让系统变得“可解释”了——每次线上问题都能快速定位到具体节点和具体原因,而不是在黑盒里猜。
稳定性建设方面,除了前面提到的超时重试和降级分支,我们还加了两个保险机制:一个是“会话冻结恢复”,如果工作进程崩溃了,重启后能根据持久化的状态快照从最近一个稳定状态继续执行,而不是从头开始;另一个是“全链路超时兜底”,单个节点超时是3秒,整条编排流程还有一个总超时,总超时到了之后不管流程走到哪,都强制收敛到人工兜底节点,宁可转人工也绝不让用户无限等待。
5. 常见问题速查与排障经验
5.1 问题速查表
把实际操作中遇到的问题整理成了一张速查表,方便团队内部排查问题时快速定位。这里挑几个高频问题分享出来。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 会话状态卡在RUNNING不动 | 缺少边界事件的状态转移规则 | 拉取该会话的完整事件轨迹,查最后一条事件 | 补齐状态转移矩阵,增加超时强转 |
| 多个Agent并发时延迟飙高 | 执行层回调风暴打满数据库连接 | 看数据库连接数和事件落库写入TPS | 改事件批处理,削峰填谷 |
| 相同业务请求重复执行 | 重启后状态没有从快照恢复 | 查进程启动日志和状态快照时间 | 实现幂等键和状态快照恢复 |
| Agent返回结果格式不稳定 | 模型输出解析失败 | 查看执行层原始输出和解析失败率 | 增加输出校验和自动重解析逻辑 |
| 某节点重试依然秒败 | 依赖的下游接口已不可用 | 查节点指标里的下游API错误率 | 增加熔断开关,直接走降级分支 |
5.2 三个最有价值的经验
总结下来,我觉得有三个经验如果一开始就意识到,能少走很多弯路。
第一个经验是“可观测性必须在第一天就设计进去”,不要等出了线上事故再补。我们最开始用通用框架的时候就是因为可观测性不足,导致排查问题非常痛苦。后来自研编排层第一件事就是把结构化事件日志设计好,之后所有线上问题的定位时间都以分钟计。
第二个经验是“兜底方案往往比主流程更重要”。用户的咨询路径不会总是按你想的流程走,开发的重心很容易放在“正常流程跑通”上,但生产环境里真正决定体验的,是异常情况下的兜底:超时怎么办、解析失败怎么办、下游接口挂了怎么办。这些分支一开始就要设计好,而不是出事后再补。
第三个经验是“架构是演进出来的,不是选出来的”。刚开始用通用框架没有错,它帮我们快速验证了业务价值;后来纯自研的探索也有意义,它帮我们看清了核心痛点在哪里。最终混合架构不是某一天的灵光一现,而是逐步演变的结果。如果让我给一个建议,那就是别等到“完美方案”再动手,先把业务跑起来,然后再在真实的反馈里优化架构。
6. 混合架构的后续演进路径与个人体会
6.1 后续可以做哪些改进
混合架构当前版本已经稳定运行了一段日子,但方向上还有不少可以继续深挖的地方。我们在内部规划了几个后续演进方向,这里也列出来供参考。
第一个方向是引入决策引擎替换硬编码的路由分支。目前的流程编排虽然相比之前灵活了不少,但节点之间“走哪条分支”基本上还是靠代码写死的规则。后续我们打算让路由判断改成一个独立的决策模块,基于用户画像、会话上下文和实时指标做更智能的路径选择,让同一个节点在不同场景下走不同分支。
第二个方向是完善“人工审批”这类异步节点的体验。目前WAITING状态虽然支持,但人工审批的结果回填还是一个比较重的操作,涉及跨系统回调。我们计划把审批流统一抽象成一种特殊Agent,让它跟普通Agent走同一个执行器接口,这样编排层不需要为人工流程单独维护一套逻辑。
第三个方向是给编排层加一个“流程沙箱”。现在修改一条流程的编排配置,必须走完整回归测试才能上线,周期比较长。我们想做一个预生产环境,新流程先在沙箱里跑,用历史会话回放的方式验证正确性,通过之后一键发布。这个如果能做好,“编排治理”的体验会上一个台阶。
6.2 个人体会
最后聊一点私人的体会。这一年多下来,我对“框架选型”这件事的理解变了很多。早些年我倾向于追新框架,总觉得用了某个热门方案就站在了技术前沿;踩过这么多坑之后,我反而觉得:好的架构不是选出来的,是长出来的。通用框架给了你快速验证的翅膀,自研代码给了你掌控细节的双手,而混合架构的精髓,恰恰是在两者之间找到那个最适合自己业务现状的平衡点。
如果你也在做多智能体系统的架构决策,我的建议很简单:先用通用框架把业务跑通,把核心痛点列清楚;然后只对痛点最多的那一层做自研;最后让框架和自研代码各管自己最擅长的事。千万别一上来就追求“纯自研”的极致控制,也千万别为了省事把所有东西都压在通用框架里。生产环境的复杂性永远比我们预想的更曲折,而混合架构,至少给了我们一个既能快速应变、又能稳住底盘的务实选择。