恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

多Agent系统通信架构设计与实践:从消息总线到任务编排

  • 首页
  • 资讯中心
  • /
  • 多Agent系统通信架构设计与实践:从消息总线到任务编排

相关资讯

Floyd算法详解:全源最短路径与动态规划实现 2026/9/9 12:48:56
如何用 Sherlock 的 --json 参数试用新的站点清单,包括直接用上游 PR 编号指定 2026/9/9 12:43:56
用原生JavaScript与Canvas开发找茬小游戏:坐标换算与交互反馈实战 2026/9/9 12:43:56

最新资讯

斯纳克图书馆管理系统PHP版v6.0:部署、安全加固与二次开发全解析
图书馆管理系统PHP版v6.0实战:部署、安全与二次开发全解
金属凝固传热仿真的核心难点与潜热处理算法解析
如何 10 分钟跑通 DeepEval 本地 LLM 评测:一份完整的新手指南
10个毕业论文AI工具实测:从文献阅读到降重查重
KernelSU 全程 4 步:从解 BL 到开不了机怎么查,一条时间线讲完

今日推荐

基于MongoDB的图书管理系统:数据建模与Spring Boot+Vue实战
Claude Code安装配置全攻略:从零开始用上终端AI编程助手
tmux 会话管理与终端复用:AI 编程工作流的调度中枢实战

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

多Agent系统通信架构设计与实践:从消息总线到任务编排

发布时间:2026/9/9 12:48:56
多Agent系统通信架构设计与实践:从消息总线到任务编排 她叫Hermes是因为我把agent之间的通信当成了一门需要认真对待的学问。刚开始做多agent协作的时候我被各种agent互相调用的混乱状态搞得头疼A调BB又回调A中间消息丢了没人知道出问题还得打开终端一个个查。后来我彻底想明白一件事多agent系统里最难的从来不是单个agent的推理能力而是它们之间怎么可靠地传递信息、怎么分工、怎么让整个系统烂了还能找到烂在哪。hermes-agent就是在这个认知下开始的个人项目。这篇文章写给两类人一类是想自己做多agent工具的开发者另一类是已经在用各种agent框架、但总觉得哪里不太对劲的构建者。我尽量把设计的思路、代码的取舍、踩过的坑都讲清楚没有完全复刻每一步的代码但核心逻辑和可复现的细节都有。1. 为什么我会自己写一个agent框架1.1 现有框架的瓶颈我实际遇到的问题先说背景。去年我在搞一个内部智能分析工具需要几个AI代理分工一个负责信息检索一个负责结构化数据提取一个负责生成结论还有一个负责把结果转成汇报格式。一开始我觉得这很简单直接用当时的社区现成框架来编排就行。结果跑了两周问题全暴露出来了。第一编排逻辑写在代码里因为分支条件太多经常要改。团队里另一个人接我的代码光理解哪些agent在什么条件下被调用就花了一整天。第二agent之间是点对点直接调用比如检索agent要到结果后直接调用提取agent的接口。一旦链路中间加一个agent或者调整顺序要改一堆地方。第三也是最致命的没有任何可观测性。对话到第8轮检索agent和生成agent之间双双死锁谁都在等对方可日志里连个trace都看不到只能一遍遍地加日志重新跑。当时的结论很直白我需要一个让agent互相不认识、但又能协作的框架。agent之间不直接持有彼此的引用或API地址而是通过一个中心来通信。所有消息有标准格式所有协作过程可追踪。这就是hermes-agent的出发点。1.2 为什么叫Hermes信使这个隐喻的价值Hermes在神话里是神的信使负责传递消息、引导灵魂、掌管道路。取名hermes-agent是有意的整个项目的内核不是让agent更聪明而是让消息更可靠地到达该去的地方。这个定位帮我避免了一个常见的误区——很多人一说做agent框架就想着怎么封装LLM调用、怎么催prompt、怎么搞ReAct循环。这些当然重要但一个只有LLM调用的框架跟调用一个function的脚本没什么本质区别。多agent系统真正的复杂性在于拓扑结构、消息路由、状态同步、失败恢复。所以我一开始就把hermes-agent的架构设计重点放在消息通路上所有agent只做一些事声明自己能做什么、接收指令、返回结果、把需要转交的消息发给中心。现在回想起来这个隐喻还带来了一个实际好处你不需要为每个agent单独设计一套通信协议了。整个世界只有一种语言——消息。一个agent不管内部是调OpenAI还是调Claude它对外只是一个信使端点统一通过消息对象说话。2. 架构设计的三个核心消息、注册、编排2.1 消息中心所有通信都必须经过中转hermes-agent最核心的组件是一个异步事件总线。任何两个agent不允许直接通信必须把消息发布到总线上由总线按照路由规则派发。为什么做这种中心化设计因为它把谁在跟谁说这个问题从业务代码里抽离出来了。举个例子检索agent完成后要把结果发给多个下游agent在点对点架构里你得给检索agent加一个下游列表配置在总线架构里检索agent只需要发一条检索完成事件下游是否订阅这个事件是下游自己的事。消息对象我用了一个带schema的dict实际上就是JSON但强制包含以下字段message_id全局唯一用于追踪task_id所属任务编号sender_id发送方agent名字recipient_id/topic目标agent或事件主题二选一payload数据主体created_at时间戳context_id上下文隔离键一开始我把recipient_id设计成必填后来发现行不通当一个agent产出的结果究竟给谁要由当时哪个agent正在等待它来决定时做不到提前指定。于是改成支持topic广播模式也让编排器可以动态订阅。这个调整让系统的灵活性高了很多。2.2 能力注册表agent之间不需要互相认识每一个agent在启动时会向注册中心上报自己的能力声明。能力声明用一组JSON描述包括nameagent标识description自然语言描述说明这个agent能做什么input_schema期望接收的输入参数结构output_schema输出结果的结构tags领域标签注册中心维护一张全局能力表。当消息总线收到一个“请求”类消息时路由管理器会拿着请求的意图描述去注册表里匹配最合适的一个或多个agent。这里匹配方式分三层规则匹配如果请求消息里明确写了target_capability直接按这个字段查注册表语义匹配对于没有指定目标的消息用一个小型embedding模型做意图向量匹配找到能力描述最接近的agent兜底路由如果上面两层都失败消息进入“人工处理队列”或者回退给默认agent这三层套下来实际系统的准确率到了很高水平。从运维视角看注册表还有另一个价值它天然是一份活的系统拓扑文档。我甚至后来做了一个可视化面板直接读注册表信息展示当前有哪些agent在线、各自处理了什么类型的消息。2.3 任务编排一次请求的完整旅程把消息路由想清楚之后编排就变成了状态机的自然展开。在hermes-agent里一个任务的生命周期是这样的收到来自用户或上游系统的TaskStart请求编排器解析任务拆解为多个子目标每个子目标作为独立消息发布路由到对应agent执行执行结果发布TaskPartCompleted事件编排器汇集所有结果判断依赖是否满足若有依赖未完成则继续派发全部完成后发布TaskCompleted输出最终结果我刻意让编排器不做任何智能判断它只是一个受信状态机。它拥有每个任务的依赖图和进度表但不负责决定具体怎么完成某个子任务——那是agent的事。这样好处很明显不管单个agent内部推理多复杂编排器看到的始终是一组确定性的状态转换出了问题可以精确地指出是在哪一步、谁没有完成。3. 核心实现细节从原型到第一版3.1 事件总线的实现与选择我用了asyncio队列加一张路由表先说事件总线的实现。因为项目主要跑在Python上我用asyncio实现了一个本地事件总线。核心结构是一个全局asyncio.Queue作为入口队列一组订阅表topic - List[handler]一个后台消费任务从队列取出消息根据recipient_id或topic调用对应handler代码大概长这样简化版class EventBus: def __init__(self): self._queue asyncio.Queue(maxsize10000) self._subscribers {} # topic - [handler] async def publish(self, message: dict): await self._queue.put(message) async def subscribe(self, topic: str, handler): self._subscribers.setdefault(topic, []).append(handler) async def _consumer_loop(self): while True: msg await self._queue.get() handlers self._subscribers.get(msg.get(topic), []) # 支持直接指定recipient if msg.get(recipient_id): handlers self._subscribers.get(msg[recipient_id], []) for handler in handlers: # 每个handler并发执行互不阻塞 asyncio.create_task(handler(msg))一开始所有handler都是串行执行的后来性能压测发现串行是整个系统的瓶颈——一个agent执行LLM调用要几秒其它消息全都堵在队列后面。改成asyncio.create_task并发消费后吞吐立刻翻了几倍。但并发也带来了乱序问题这个我后面在踩坑部分会详细讲。你如果还在犹豫要不要用现成的消息队列Redis Stream、RabbitMQ之类我的建议是前期不要。单机场景下asyncio队列完全够用少一个中间件就少一个故障源。等真正需要多机部署时把EventBus的底层实现替换成Redis Stream版本对外接口保持不变就行。我在后续版本里就是这么做的成本并不高。3.2 路由匹配语义路由是个必要之恶路由管理器负责把请求消息分发到合适的agent。规则路由和精确字段路由都很简单不值得展开我想重点讲语义匹配部分。我最初用了一个相对笨的办法把每个agent的description字段存成一个列表然后对每个请求消息用LLM来选。这个方案的准确率确实高但代价太大——一次路由就要调用一次大模型延迟增加1-3秒。在多个子任务连续编排时这种开销是不可接受的。后来我改成双级方案第一级把agent的description和输入输出schema用embedding模型编码成向量存内存向量表。请求消息先embedding然后算余弦相似度取top5。第二级如果top5里的最高相似度大于阈值我用的是0.72这个值是压测标定的直接用top1如果低于阈值才调用LLM做一次意图判断。这个方案把LLM路由的频率降到了大约5%而因为LLM在低相似度场景下的兜底判断整体路由准确率反而保持在99%以上。这里有几个容易忽略的坑agent的描述文本写得好不好直接决定embedding路由的生死。写得越像这个agent会对什么样的用户请求负责效果越好。空泛的描述比如本agent负责处理所有任务会让相似度计算几乎失效。向量表要支持动态增加因为agent是随时可能上线的。用numpy矩阵追加增量行就行不用每次都重建。如果embedding模型比较大路由这块可以单独起一个进程避免阻塞主事件循环。我踩过这个坑共享库加载会把主线程卡几百毫秒在多任务并发时已经能感知到明显卡顿。3.3 上下文隔离与记忆管理context_id是系统的护城河多agent系统里最容易出问题的不是路由而是上下文互相污染。两个任务同时发出去都调用了同一个摘要agent如果摘要agent把任务A的中间结果错误地当成任务B的输入排查起来极其痛苦。hermes-agent的解决方案是强制所有消息携带context_id并由一个ContextManager统一管理每个context的状态ContextManager保存每个context_id的运行快照当前任务进度、结果集、当前活跃agent集合。每次agent进入执行前从ContextManager读入自己需要的数据执行结束后把结果写回对应context。任何context之间不允许跨读只有编排器可以查看全局所有context。在实现上我用的是一个defaultdict存储context数据每个context内含一个字典。关键点是agent执行时传入的是context数据的深拷贝防止一个agent私自改动另一个agent的结果。深拷贝会带来一定的内存开销但一个context的数据量通常只有几十KB完全可接受。记忆分层方面我把记忆分成了三类短期记忆只存在于当前context内任务结束即释放工作记忆agent执行期间保存的中间推理结果存在context中由编排器汇总长期记忆由用户主动设置或由特定agent写入向量数据库跨任务复用长期记忆是后期加的。最初我以为多agent协作不需要长期记忆只要每个任务独立运行就好。后来发现很多业务问题的背景知识比如公司内部产品术语、团队偏好的报告格式如果每个任务都从prompt里传一遍token成本太高且不稳定。有了长期记忆层可以让一个知识管理agent专门负责读写记忆其他agent通过消息向它请求。这算是我在设计上的一次重要迭代。4. 真机运行后踩过的坑4.1 并发消费导致的消息乱序第一个让我在凌晨抓头的问题就是乱序。场景是这样一个任务拆成了三个子任务分别发给三个agent。三个agent执行速度不一样A快C慢。结果编排器先收到C的结果后收到A的结果但C的逻辑依赖A的输出。于是C的结果不能被正确汇总。我的第一个反应是给消息加序号再排序。但实测发现在复杂任务里消息序号本身不好定义——因为任务是动态拆解的一个agent可以在一轮中产生多个消息序号会产生歧义。最后采用的方案是编排器维护每个context的依赖图而不是依赖到达顺序。具体实现每个子任务消息声明depends_on字段表示该任务依赖哪些前置子任务的消息ID。编排器收到结果时不是直接写入结果集而是先检查依赖是否满足。不满足则暂存在pending列表。当某个context的所有子任务结果都已就绪才进入汇总阶段。这样即使消息乱序到达只要依赖元数据正确就不会出错。代价是每个子任务都必须明确声明依赖这对拆解器提出了更高要求。我在拆解器里加了一步依赖推断用LLM直接生成子任务之间的DAG正确率在95%以上剩余5%通过人工修正。4.2 超时、重试与死循环不能相信任何agent真实运行最大的教训就是不要把任何agent当成可靠的组件。一个agent从LLM拿到的响应可能是空白的、是重复的、甚至压根没返回。我就遇到过两个agent在互相调用中无限循环的情况。流程是这样的agent A被分配生成摘要它发现摘要缺少资料就发消息给检索agent索取资料检索agent检索后发现资料相关度低又发消息给规则检查agent规则检查agent认为摘要格式不对又回头给A发请重新生成摘要……三个agent形成了一个环形调用消息总线里刷了几百条消息API费用肉眼可见地猛涨。针对这个现象我做了三件事给每个context设定最大消息数上限默认200条达到上限自动终止并上报。给每条消息设定TTL生存时间超过时间未处理就丢弃并记录告警。给每个agent的执行函数封装了看门狗计时器超时后强制返回超时错误消息而不是无限等下去。这三件套上了之后系统再没出现过刷消息刷到断片的情况。关键认知是在多agent架构里agent本身也是不可靠的第三方组件你必须用分布式系统里面对下游服务的心态去对待它们——有超时、有重试、有限流、有熔断。4.3 内存泄漏context越攒越多第三个坑是内存。跑了两周后发现内存持续上涨gc也救不回来。排查后发现ContextManager里的context数据在任务结束后没有清理而且每个context里存的不是我预期的结构化小数据而是各个agent根本不清理的调试日志、临时变量快照。解决方法很直接context结束进入TaskCompleted状态后立即将其状态标记为可回收并在TaskCompleted事件分发完成5分钟后彻底删除。为每个context设置最大size限制默认2MB超过后自动把最旧的工作记忆移到磁盘上的临时存储。引入一个后台清理协程每分钟扫描一次所有已完成且已超过保留期的context执行删除。加了这三个规则后内存在跑了72小时后基本稳定。后来我用tracemalloc检验过泄漏率从每小时几十MB降到几乎为零。4.4 工具注册的命名冲突最后一个值得说的坑是工具命名空间。我在设计能力注册表时一开始允许agent随意给自己起名。结果两个agent插件包都起了mailer这个名字注册时后注册的覆盖了先注册的导致部分路由被错误转发用户看到邮件发了两份另一份发错了对象。后来我改成强制命名空间agent的注册名必须是插件名.功能名的形式比如notification.mailer、inbox.mailer。注册时如果发现同名直接拒绝新注册并抛出冲突告警由人工决定是卸载旧插件还是放弃新注册。这个设计的改动很小但避免了线上最蠢的一类问题。记住任何注册表系统命名冲突都是必须前置解决的根问题别等出事故了再去查覆盖链。5. 实测性能与优化5.1 测试场景设计为了搞清楚hermes-agent到底能扛多大压力我搭了一个基准测试环境在一台8核16G的Linux服务器上部署了10个agent包括检索摘要、代码生成、格式化、检查等用编排器随机创建依赖关系模拟同时运行100个任务。测试结果如下单位秒指标第一版优化后平均完成时间14.29.7P95完成时间31.818.6最大并发任务数1260单任务平均消息数4238优化前最大的瓶颈有两个一个是所有handler串行执行这我前面已经提过另一个是agent内的LLM调用并发度太低默认是每agent一个会话空闲等待时间太久。5.2 瓶颈定位与两个关键优化第一个优化把EventBus的消费循环从串行改为并发同时给每个topic维护独立的消费队列。每个agent运行在自己的协程组里不会因为某个agent阻塞而导致全局堵车。代价是不同topic之间的消息不再保证严格顺序所以依赖图必须更完备。这个我在踩坑部分也讲过了。第二个优化是LLM调用的连接池。很多agent的LLM调用都是同一家服务商原来的实现里每个agent都开一个HTTP会话重复建立连接很浪费。我抽了一个LLMClient统一管理连接池所有agent共享同一池子并发能力和连接复用一起提升API延迟大概下降了20%。优化后发现还有一块可以再探embedding向量匹配。之前每次路由都现场计算embedding即使缓存了agent侧的向量请求侧的消息embedding也没法避免。后来加了一个简单缓存把高频请求的原始文本哈希存储命中后直接返回embedding向量大概又省了百毫秒级别。6. 重构了两次才得到现在的模块划分刚开始写hermes-agent时我采用的是一刀切分层消息总线在最底层所有业务agent继承一个BaseAgent里面封装了LLM调用、prompt模板、日志。这个设计在demo阶段很舒服三个agent、一个总线跑起来毫无压力。但一旦规模上来这种智能基类设计的坏处开始暴露每个agent都对BaseAgent产生了强依赖改一行BaseAgent就要重新测试所有子类而真正混在一起的两个部分——消息处理逻辑和业务工具逻辑——却无法独立演进导致想给消息总线加一个重试机制都会担心影响具体业务。第二次重构我把agent分成了外壳和内核两个东西外壳HermesAgentRunner统一处理订阅、发布、上下文读写、超时、重试、协议解析。每个agent只要向Runner注册三个回调on_message、on_error、on_timeout。内核业务逻辑只管以一个函数处理消息假装自己是单机脚本。这样agent开发者完全不需要关心消息总线怎么工作只需提供一个纯函数。这极大降低了插件开发者的心智负担也是hermes-agent能吸引别人给我提PR的重要原因。到了这一步我才觉得hermes-agent的设计基本成熟了剩下的都是增量优化。7. 留给后来项目的三条经验第一多agent系统里通信协议是最大的架构杠杆。agent本身谁来写不重要重要的是它们怎么说话。先把消息格式、路由规则、失败语义定清楚后面可以少改几十处。第二不要过早引入外部消息中间件。asyncio的本地事件总线够用很久等真正需要跨进程、跨机器再替换底层。我的经验是替换代价远小于一开始就用重型中间件带来的运维负担。第三所有agent都不可靠。你写的下游agent哪怕只是一个调大模型的函数也可能因为模型输出格式变化而崩掉。超时、重试、熔断、监控、日志追踪这些基础设施要在一开始就埋进去不能等出事故再补。这些天有很多朋友问我什么时候出一个能写文档的agent我一般都会提示先把怎么观察agent之间发生了什么搞定再谈agent的智能。hermes-agent到现在也没有成为什么知名框架但它是第一个让我觉得多agent系统是可以被工程化驾驭的项目。如果你也在搭类似的系统希望上面的设计思路和踩坑记录能帮你少走几段弯路。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号