hermes-agent:以消息总线重构多Agent协作架构

发布时间:2026/9/9 2:04:47
hermes-agent:以消息总线重构多Agent协作架构 做多Agent系统的人大概率都经历过这样的阶段单Agent的Demo跑得飞起一上多Agent协作就开始失控——消息乱飞、调用成环、上下文相互污染最后整个系统变成一团无法维护的线团。我踩过这个坑之后花了很长时间梳理了一套以“消息中枢”为核心的多Agent编排方案也就是这个hermes-agent项目。它本质上是一个轻量级的事件驱动Agent调度框架核心思路是把Agent之间的直接调用改成通过统一消息总线通信让每个Agent变成纯粹的“消息处理器”从而彻底解决协作混乱的问题。这篇文章就把这套架构的来龙去脉、核心设计、实操搭建过程以及我在生产环境里踩过的坑完整分享出来适合正在做多Agent应用、或者准备从单体Agent转向复杂协作架构的开发者参考。1. 多Agent系统失控的典型症状先从翻车现场说起1.1 从“单兵作战”到“团队协作”的那道坎单个Agent的架构其实非常清爽用户输入进来Agent调用工具工具返回结果Agent组织语言输出。整个过程是线性的、可预期的出问题也好排查。但一旦涉及到多个Agent事情就变得复杂了。最简单的方式是让Agent之间互相调用比如Agent A发现这个任务需要Agent B的能力就直接调Agent B的函数。这种“点对点”的调用方式在只有两三个Agent的时候还能凑合但只要Agent数量一多立刻就会演变成一场灾难——每个Agent都要知道该调谁、传什么参数、对方返回什么格式这相当于把系统的所有依赖关系都写死在代码逻辑里。我见过不少团队把多Agent系统做成了一团乱麻A调B、B调C、C又回过头来调A数据在多个Agent之间来回传递谁改了状态根本追踪不到。更麻烦的是只要其中一个Agent的接口稍微调整所有调用方都要跟着改。这种耦合程度系统根本不可能持续演进。1.2 消息风暴、死循环和上下文污染三个真实翻车场景那段时间我做技术复盘整理了三个高频翻车场景基本覆盖了多Agent系统的所有典型问题。第一个是消息风暴。Agent之间的通信没有约束A处理完一个事件之后习惯性地把结果广播给所有其他Agent。然后B收到消息后又处理了一遍再次广播结果。整个系统的消息量呈指数级增长最后LLM的API调用费用直接失控而且每个Agent都在重复处理无关消息大量算力被白白浪费。第二个是死循环。A认为任务应该由B处理就把消息转发给BB看了一眼觉得还是应该由A处理又把消息原路转回去。如果没有外部的循环检测机制这两个Agent会把这个消息踢皮球踢到天荒地老直到触发超时或者API额度耗尽。第三个是上下文污染这个最隐蔽也最致命。在做多Agent协作的时候很多人图省事让多个Agent共享同一个系统提示词和上下文窗口。结果A的中间推理过程被B看到了B受到了干扰甚至把A在某次任务里的一段数据当成自己该处理的内容。更麻烦的是不同任务的上下文混在一起Agent经常张冠李戴回答得牛头不对马嘴。根源在于Agent之间没有做上下文隔离消息边界完全模糊化。1.3 这些问题的根源架构上缺了一个“调度中枢”把这三个翻车场景放到一起看你会发现它们指向的是同一个架构缺陷——Agent之间缺少一个统一的调度中枢。点对点的调用方式让Agent之间强耦合消息流向不可控没有人知道一条消息在某一时刻应该由谁来处理、处理到什么程度算是结束。这就像一个大公司员工之间可以互相直接沟通但没有人做任务分配和管理结果自然是各干各的甚至重复干。我当时的核心诉求其实很朴素Agent之间不要直接调用所有消息都必须经过一个“中央调度层”由这个调度层负责路由、管理生命周期、检测循环、并且确保消息不丢失。这个思路借鉴的其实是后端开发里非常成熟的“消息中间件”思想只是把消息的“消费者”从普通的Worker换成了LLM Agent。hermes-agent这个项目就是在这个思路下从零搭建起来的。2. hermes-agent的设计内核用“信使模式”重构Agent协作2.1 名字背后的架构隐喻为什么叫HermesHermes是神话里的信使之神负责在众神之间传递消息、引导方向、连接不同的领域。我特意给这个项目取这个名字因为它要扮演的角色和神话里的形象高度吻合——不做任务的实际执行者只做消息的传递者和调度者。这个定位很关键。很多人在设计Agent协作框架时总想给中枢加一堆能力最后把中枢做得又胖又重。但hermes-agent的中枢从设计第一天起就只有一个职责确保一条消息能够以正确的顺序、被正确的Agent处理、在正确的时间结束。至于消息怎么被处理、处理结果如何中枢一概不管。这个边界划定对后续的可维护性和稳定性特别重要。2.2 三个核心概念Message、Route、Handler整个框架可以浓缩成三个核心概念理解了这三个概念基本上就理解了整套架构。Message是Agent之间传递的唯一数据单元。它包含消息ID、消息类型、路由键、携带的业务数据、元信息比如超时时间、优先级、以及消息的生命周期状态。所有Agent之间的信息传递都必须封装成Message不存在Agent之间直接传参数的通道。Handler是Agent本身。在hermes-agent里Agent被抽象成一种特殊的Handler——接收一条Message处理后输出一条或多条新的Message。Agent不关心这条消息是从哪来的也不关心处理完的结果会被谁消费它只需要做两件事读消息、写消息。这就把Agent从“通信”的复杂性中解放出来让它把全部精力放在“业务逻辑”上。Route是中枢里的路由规则它定义了某种类型的消息应该被哪个Handler处理。路由键支持通配符模式匹配比如ticket.*会匹配ticket.created和ticket.assigned但不会匹配order.created。这里的语义借鉴了RabbitMQ的Topic Exchange理解RabbitMQ的读者应该很眼熟。这三个概念的关系遵循一个非常简单的流程生产者的消息被发给Total消息总线Total根据消息的路由键找到匹配的Route把消息投递给对应的HandlerHandler处理完后产出新的消息再次进入消息总线。整个过程形成一个闭合的消息流转环。2.3 和LangGraph、AutoGen等方案的差异化思考讨论多Agent编排方案绕不开LangGraph和AutoGen。网上对比它们的文章很多我这里重点说架构哲学的差异。LangGraph本质上是给Agent画了一张有向图节点是Agent边是Agent之间的固定流转关系。这种方式的优点是流程清晰、可预测但缺点是图结构偏向静态——如果运行中遇到了没规划过的分支要么报错要么需要人为补图。AutoGen的对话模式则是让Agent之间自由对话自由度高但正因为太高失控的风险也更大必须有非常强的干预机制兜底。hermes-agent走的是第三条路基于消息路由的事件驱动。执行路径不是预先写死的而是由运行时的消息类型和路由规则共同决定的。这意味着你可以通过调整路由规则来改变系统的行为而不用修改任何Agent代码。测试的时候也可以对单条消息做重放排查问题比图编排和自由对话都更顺手。用生活化的比喻来说LangGraph是高铁轨道铺到哪车就跑到哪AutoGen是聊天室人人都能说话热闹但容易乱hermes-agent更像是物流分拣中心——每件包裹消息到了之后传送带会根据标签把包裹送到对应的工作台Agent标签变化了包裹自然就会被送到不同的地方但工作台本身不需要知道传送带怎么变。3. 实战搭建用hermes-agent做一个客服工单自动分诊系统3.1 项目初始化和环境准备说了这么多理论接下来进入实操环节。我会用一个“客服工单自动分诊”的场景来演示这个场景覆盖了消息路由、多Agent协作、超时处理和状态追踪等核心功能适合作为学习案例。先初始化项目目录创建一个虚拟环境mkdir hermes-demo cd hermes-demo python3 -m venv venv source venv/bin/activate pip install hermes-agent需要注意的版本兼容问题hermes-agent的3.x版本要求Python 3.10以上因为它内部用了PEP 604的联合类型语法。如果还停留在Python 3.8或3.9建议要么升级Python要么固定安装2.8版本。然后建一个配置文件config.yaml用来设置消息总线的参数和Agent的通用属性hermes: bus: type: memory # 开发阶段使用内存总线生产环境可以换Redis max_queue_size: 1000 retry_delay: 2 agent: default_timeout: 30 # Agent默认超时时间单位秒 max_hops: 10 # 一条消息最多经过10个Agent超过则判定为死循环这里先说两个关键参数的设置理由。max_hops是整个框架最重要的防死循环参数如果一条消息经过的Agent数量超过这个值总线会直接判死并记录日志。default_timeout必须结合LLM API的响应时间设置设置太短会导致正常的慢请求被误杀设置太长又会让错误消息长时间占用总线资源30秒是一个相对稳妥的起步值。3.2 定义分诊系统中的三种Agent系统需要三个Agent入口Agent负责接收用户消息并提取意图订单处理Agent负责查询订单退款处理Agent负责处理退款请求。先定义入口Agent它做的事情是从用户原始消息中提取意图然后根据意图把消息路由给下游:# agents/entry_agent.py from hermes_agent import Agent, Message class EntryAgent(Agent): 入口Agent负责意图识别和工单分发 async def handle(self, msg: Message) - list[Message]: content msg.data.get(content, ) # 这里实际业务中会调用LLM做意图识别 # 示例中直接根据关键词做简化处理 if 退款 in content: return [Message( typeticket.refund, data{content: content, user_id: msg.data.get(user_id)} )] elif 订单 in content: return [Message( typeticket.order, data{content: content, user_id: msg.data.get(user_id)} )] else: return [Message( typeticket.unknown, data{content: content, user_id: msg.data.get(user_id)} )]细心的读者可能已经发现了这个Agent输出的Message类型和输入完全不同——它做了一次“消息转换”。这正是事件驱动架构的核心特点Agent的输出不需要和输入保持相同的类型。入口Agent消费的是ticket.created产出的是新的消息类型每个类型都有对应的下游处理者。然后是订单处理Agent和退款处理Agent它们的结构非常相似因为在这个架构中Agent只需要关心自己负责的那类消息# agents/order_agent.py from hermes_agent import Agent, Message class OrderAgent(Agent): 订单处理Agent查询订单状态 async def handle(self, msg: Message) - list[Message]: user_id msg.data.get(user_id) order_info query_order(user_id) # 查询订单的具体实现 return [Message( typeorder.result, data{order_info: order_info} )]退款处理Agent的结构是同一个模式只是业务逻辑不同。用这种方式组织代码业务之间的边界被物理隔离任何Agent的实现细节变化都不会影响其他Agent。3.3 注册路由让消息找到正确的归处有了Agent之后下一步是注册路由规则。这一步是整个系统的“接线”过程也是运维层面最常调整的位置# main.py from hermes_agent import HermesApp from agents.entry_agent import EntryAgent from agents.order_agent import OrderAgent from agents.refund_agent import RefundAgent app HermesApp(configconfig.yaml) # 一条用户消息进来触发入口Agent app.route(ticket.created, EntryAgent()) # 入口Agent产出的不同类型消息分别路由给对应的业务Agent app.route(ticket.refund, RefundAgent()) app.route(ticket.order, OrderAgent()) # 对于无法识别的意图默认走人工处理逻辑 app.route(ticket.unknown, HumanHandoffAgent())执行这段代码之后消息流转链路是用户触发ticket.created进入总线总线把消息投递给EntryAgentEntryAgent产出新消息根据消息类型决定进入到ticket.refund、ticket.order还是ticket.unknown业务Agent处理完毕后产出最终结果这里面有一个看起来不起眼、实际很重要的细节Agent之间不认识、不引用彼此。EntryAgent根本不知道OrderAgent和RefundAgent的存在它只负责发出消息至于谁会接完全由路由规则决定。这种彻底解耦带来的直接好处是可以在不碰任何Agent代码的情况下通过调整路由规则来改系统的行为——比如想增加一个新的“售后”处理Agent只需要加一条app.route(ticket.after_sales, AfterSalesAgent())不需要改任何现有Agent。3.4 组装业务链路并启动服务现在把整个应用运行起来写一个简单的入口脚本# main.py 继续 import asyncio from hermes_agent import Message async def demo(): await app.start() # 模拟一条用户消息进入系统 await app.publish(Message( typeticket.created, data{content: 我的订单一直没发货想查询一下, user_id: 10086} )) await asyncio.sleep(5) # 等待消息被处理 await app.stop() if __name__ __main__: asyncio.run(demo())如果用--debug参数启动总线会把每条消息的流转路径打印出来这可以帮助理解消息生命周期也能快速定位问题。[2025-06-01 10:00:01] MSG-001 ticket.created - EntryAgent - ticket.order [2025-06-01 10:00:03] MSG-001-1 ticket.order - OrderAgent - order.result [2025-06-01 10:00:03] MSG-001-1-1 order.result - (terminal)注意消息ID的继承关系从MSG-001派生了MSG-001-1再派生了MSG-001-1-1。整套系统的追踪链路就是靠这个ID树串联起来的后面排查问题的时候这种ID设计能让你快速还原一条消息的完整生命周期。4. 踩坑实录并发风暴、循环调用与上下文爆炸的完整排查链路4.1 现象一Agent互相踢皮球任务永远不结束上线第一周就遇到了麻烦。测试人员反馈某些工单的状态会一直卡在“处理中”不结束也不报错日志里只有消息来回流转的记录。看调用链路日志发现一条消息在A和B之间反复跳转A发给BB处理完又发回给AA又发给B……所有Agent都正常执行了但合在一起就是死循环。排查的过程分了三步。第一步先圈定问题范围。在总线层面加了一个审计钩子统计每条消息当前经过的Agent数量和完整路径。这一步的目的是确认到底是所有消息都循环还是只有特定类型的消息在循环。第二步观察路径特征。发现所有循环的消息都带有同一个标签——用户在描述里同时提到了“订单”和“退款”。入口Agent的判断逻辑是关键词优先先匹配到“退款”就发给退款Agent退款Agent看到内容里又有“订单”字样以为应该走订单流程又转给订单Agent订单Agent再一看还是觉得自己不该管……第三步修复。问题根源很清晰关键词分类不是互斥的同一个文本可能同时命中多个分类规则而Agent之间没有“此消息已经被处理过”的标记机制。修复方案是给Message加一个processed_by字段记录这条消息已经经过的Agent。Agent在收到消息时先检查这个字段如果已经处理过同一类型的消息就暂时不再接单而是标记为“需要人工介入”。同时把关键词分类改成带优先级的规则匹配从业务上保证消息只会被路由到唯一的处理者。这类问题在事件驱动系统里几乎必现因为每个Agent都只看到消息的局部信息无法感知全局状态。所以在引入消息路由时务必预留processed_by这类审计字段这既是为了追踪问题也自带一重保障。4.2 现象二并发一高就丢消息系统处理量上来之后又一个问题浮出水面。原本用内存队列做消息总线并发量在几十左右还算正常但压测到每秒100条以上时开始出现消息静默丢失——状态日志里看不到终态消息也不报错消息就像凭空蒸发了一样。刚开始怀疑是网络问题但查了一圈发现根本不是。直到把排查焦点收到内部发现内存队列在正式处理之前会经过一个“预取出”机制——预先从队列里取出消息等前一条消息处理完成后再提交处理结果。问题就出在如果预取出的消息在处理过程中抛了异常并且异常没有被捕获这条消息就永远没有机会被重新放回队列但对上层来说它已经被取走了。准确说这不是丢消息而是异常消息被静默吞掉后队列Mark收走但Task哑火导致状态不一致。修复方案分两层。在框架层面所有Handler的执行必须包裹在统一异常捕获中任何异常都会触发重试入队并且重试次数计入元信息超过阈值就会进入死信队列。在业务层面生产环境必须加持久化层把消息写入Redis Stream或者Kafka这样即使进程崩溃消息也能从持久化日志中恢复。内存队列只能用在开发环境这是架构红线。4.3 现象三上下文塞满之后回复开始胡言乱语第三个坑是上线运行一段时间之后才逐渐暴露的。长期运行的系统开始出现奇怪的回复——订单Agent返回的内容里混入了退款Agent的判断逻辑甚至出现了其他用户的订单信息。第一反应是LLM发生的幻觉但仔细定位之后发现不是模型的问题而是上下文管理的问题。在早期版本里为了让Agent“记住”对话历史我把所有Agent的输出都累积到一个共享上下文对象里结果这个上下文被多个Agent读写互相污染得一塌糊涂。Agent B看到了Agent A的中间推理过程然后把这个信息当成了自己要处理的任务的一部分。修复方案是引入消息投影机制。每个Agent在接收消息时总线只发送给该Agent的“订阅键”相匹配的那部分数据每个Agent看到的是一份经过投影之后的数据视图而不是把所有Agent的所有消息都塞给其他Agent。投影规则写在路由配置里projection: ticket.order: include: [request_id, user_id, content] ticket.refund: include: [request_id, user_id, content, order_info]这样OrderAgent能收到的数据只有request_id、user_id和content退款Agent的上下文里也永远不会出现无关字段。这个改动上线之后上下文串话的问题从根上断了根。4.4 根因总结消息机制设计的三条铁律这三起事故虽然表现形式不同但根源其实指向同一个方向——一个面向生产环境的消息机制在设计阶段就必须考虑好边界。我把亲身踩过的这些坑归纳成三个原则第一消息必须有明确的终止状态。消息生命周期里除了“处理中”之外必须有“已完成”“已死信”“已超时”等多种终态。没有终态的消息体系排查问题无异于大海捞针。第二条消息的传递路径必须全程可审计。每一条消息在哪个时间点到了哪个Agent手里产出哪些后续消息都应该能追溯。结合消息ID的继承关系这是排查问题最重要的抓手。第三条Agent的输入输出必须做数据隔离。任何Agent都不应该直接拿到总线上的全量数据只能拿与自己相关的数据视图。这条原则从根本上杜绝了上下文污染和越权访问。5. 进阶配置让hermes-agent扛住生产环境5.1 超时、重试和熔断的合理设置开发环境里怎么跑都行生产环境就没这么宽容了。LLM API的延迟波动特别大同一套提示词有时候1秒返回有时候20秒才返回。超时设置如果拍脑袋很容易误伤正常请求。我的实践是给超时时间设计一个阶梯式配置基线。针对Agent级超时起步可以设成LLM节点响应上限的两倍留一点缓冲避免超调。在重试策略上单次重试间隔用2秒起步遇到慢调用时要做指数退避上限给到30秒这样既能容忍瞬时抖动也不会在崩溃时“死磕”。熔断参数上连续3次失败就熔断熔断后等待60秒再释放试探流量避免故障Agent被反复打爆。具体配置hermes: agents: default_timeout: 30 retry_policy: max_retries: 3 backoff_start: 2 backoff_max: 30 circuit_breaker: failure_threshold: 3 reset_timeout: 605.2 可观测性追踪一条消息的完整生命周期生产环境最重要的能力既不是性能也不是并发而是可观测性。发生事故时最崩溃的一句话永远是“这条消息现在在哪”。hermes-agent从框架层面内置了OpenTelemetry支持。每个消息都带一个trace_id总线和所有Agent在执行前后都会把上下文信息发送给追踪后端。这意味着你可以在Jaeger或者SkyWalking里看到一条消息的完整执行链路包括每个Agent的耗时、输入输出大小、以及是否触发了重试或熔断。实际体验是排查“消息卡住了”这类问题的时候直接在链路追踪页面搜索trace_id就能看到它最终阻塞在哪个Agent上。即使上线了一个表现极不稳定的模型也可以通过链路数据快速定位到具体是哪个环节的问题而不是靠猜。因为Agent之间完全解耦从Metrics里还能直接看到每个Agent的消息吞吐和错误率。配合告警规则比如某一类消息的失败率连续5分钟超过5%就触发告警整个生产监控就可以自动化不用人肉盯日志。5.3 性能基准与资源预估最后说性能。很多人担心引入消息总线会增加延迟实际上这层架构的调度开销相当低。要分清楚Agent执行本身调LLM需要秒级时间消息总线之间的传递只需要以毫秒为单位的内存拷贝在整链路延迟里几乎可以忽略。在一台普通开发机上实测内存总线模式下的消息分发延迟中位数约为0.3毫秒P99在0.8毫秒左右Redis总线模式下大约是2毫秒到5毫秒。这个延迟比任何一次LLM调用都低好几个数量级完全不应该成为性能瓶颈。真正影响系统能力上限的是Agent本身的执行并发度。假设平均每个Agent处理一条消息需要3秒Python的asyncio在同一进程内可以支撑约200个并发协程吞吐量大概在每秒60条左右。如果想要更高的吞吐方案是横向扩展——在总线后面多挂几个同样的Worker进程靠消息总线的持久化做负载均衡。这就不需要框架做任何特殊处理了因为Agent本来就是无状态的消息也都走总线天然支持水平扩容。从单Agent套壳到多Agent协作从点对点调用到消息驱动这个转变我走了不少弯路。架构这件事薛定谔的很好玩你以为多Agent的核心难点在“如何让模型更聪明”实际上生产环境中最容易翻车的往往是通信、状态和隔离这些基础设施问题。hermes-agent教给我的核心一课不是怎么把多个LLM拼在一起而是怎么让它们各司其职、互不干扰地协作。如果读者当前正在设计自己的多Agent系统建议先把消息边界和路由规则这层骨架搭扎实它会决定你的系统能走多远。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询