news 2026/10/15 5:02:02

多Agent系统选型实录:为何Hermes成为综合Agent核心

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多Agent系统选型实录:为何Hermes成为综合Agent核心

如果你正在搭一个多 Agent 系统,或者正在纠结要不要把某个中间件提成团队的“综合 Agent 核心”,那我这篇内容应该能帮到你。HagiCode 是我们这边内部跑了大半年的一个多 Agent 协同项目,早期一直用的是一套自研的工具调用调度模块,后来逐步被各种现实问题逼着做选型,最后定下来用 Hermes 作为整个系统的 Agent 核心。这篇文章我不打算讲抽象概念,就用 HagiCode 的真实选型过程来说说,为什么最终是 Hermes,而不是 Actor 框架、任务队列或者我们自己继续硬写调度逻辑。

1. HagiCode 到底缺一个什么样的"核心"

1.1 多 Agent 协作的真正难点

HagiCode 这个项目最早的目标很简单:让几个不同职责的 Agent 互相配合,完成从需求理解、任务拆解、检索资料到执行工具调用的完整链路。听起来就是一个典型的编排问题,但真把这几个 Agent 拆开部署后,问题就接踵而至。

最大的痛点不是单个 Agent 的能力不够,而是 Agent 之间的通信方式太原始。早期我们让 Agent 之间直接通过 HTTP 回调互相调用,A 处理完任务后调用 B 的接口,B 处理完再调用 C。这在只有三个 Agent、链路固定的情况下还能勉强跑,但一旦引入并行协作、条件分支、人工确认环节,调用关系就变成了蜘蛛网。

举个例子,规划 Agent 拆解出三个子任务需要并行执行,这就要同时给三个执行 Agent 发消息,并且等它们全部返回后再进入下一步。如果用 HTTP 直连来做,就得自己维护一套任务完成状态表,还要处理超时、重试、并发控制,这些逻辑最终会把业务代码淹没。

我们还遇到一个更头疼的问题:Agent 不是只存在“请求-响应”这一种交互模式。有些任务发起后需要长时间等待外部人工确认,有些任务需要 Agent 主动通知事件给其他模块,有些任务会被更高优先级的任务打断重排。这套东西如果都靠在业务代码里手写状态流转,迟早要失控。

1.2 我们对核心组件的硬性要求

踩过一段时间的坑之后,我们把诉求总结成了几条。后来的选型基本就是拿这几条去套各种方案,能全部套上的不多。

第一条是必须支持多对多的异步通信。Agent A 的产出可能要被 Agent B 和 Agent C 同时消费,Agent D 也可能同时接收来自多个来源的消息,点对点的同步调用解决不了这种拓扑。

第二条是状态流转要通过核心组件来做,而不是散落在各个 Agent 的业务代码里。每个 Agent 在执行任务时都要具备“待执行、执行中、等待输入、已完成、失败”这些基本生命周期,核心组件要能统一管理并驱动这些状态迁移。

第三条是必须具备可控的背压能力。不同 Agent 的处理能力不一样,规划 Agent 可以快速拆解任务,但执行 Agent 可能因为工具调用耗时很长。如果核心组件没有缓冲和限流,快生产者会把慢消费者直接冲垮。

第四条是链路可观测。一个任务经过多个 Agent 后,出了任何问题都要能快速定位是在哪个环节发生了什么。这就意味着每条消息都要带追踪上下文,并且在核心组件层面就能看到全链路的流转记录。

这几条要求看着不复杂,但把它们组合在一起,市面上的通用组件就很少能一股脑满足了。

2. 备选方案逐个淘汰:从 Actor 框架到消息队列

2.1 Actor 模型入选和落选的原因

最开始组里有人提议用 Actor 框架。Actor 模型从理念上确实非常契合 Agent 场景,每个 Actor 就是一个独立的消息处理单元,Actor 之间通过消息传递通信,天然支持异步、隔离和并行。

我们找了一个成熟的 Actor 框架做了原型验证,把 Agent 封装成 Actor,消息投递和状态管理交给 Actor 系统。原型跑起来很顺畅,但往生产方向深入就发现了几个不可忽视的问题。

首先,Actor 系统的管理对团队来说是额外的认知负担。同一个 Agent 的逻辑里,哪些状态应该放在 Actor 内部、哪些应该外部管理,这套边界需要团队非常熟悉 Actor 语义才能把握得好。我们团队成员背景比较杂,既有从后端转过来的,也有搞算法和 RPA 出身的,统一 Actor 思维模型的成本比预想高得多。

其次,Actor 框架擅长处理并发模型,但它并不会主动帮你解决“任务编排”层面的问题。什么任务在什么条件下触发、并行分支什么时候汇聚、任务失败后策略是重试还是人工介入,这些还是需要业务层自己做。本质上我们需要的不仅仅是消息传递,更是一个编排状态机,Actor 框架在这一层只是提供了底层的零件。

另外还要考虑技术栈的一致性。我们大量的工具调用链、外部系统对接代码都是 Python 写的,把核心换成某一特定语言的 Actor 框架,意味着要么写桥接层,要么把存量代码迁移一遍,这两种选择的风险都不小。

2.2 任务队列方案为什么不够用

第二个被否掉的思路是直接用任务队列,比如用现成的 Celery、RQ 这样的东西。这类组件的设计目标是“把任务从生产者搬运到消费者”,但它默认的交互模式是单向的。任务扔进去,Worker 拿到执行,执行完把结果写到后端存储,生产者过一段时间去取。

这个模式和 Agent 协作有个结构性的差异:Agent 协作过程中消息是双向、多频次的。Agent A 发起任务给 B,B 在处理过程中可能要向 A 追问参数,或者向工具调度模块申请执行某个外部操作,然后再以事件的形式广播结果。任务队列的“投递即结束”语义没办法承载这种你来我往的长对话式交互。

同步请求加异步结果回调的混合模式在任务队列里也做得很别扭。你要么把所有交互都变成异步任务加上结果存储,要么引入额外的消息通道。把结果存储作为 Agent 之间的缓冲,使得业务逻辑里充满了“等结果轮询”的 glue code,整个系统的心智模型反而变得更复杂。

2.3 自研调度模块的切肤之痛

这里要坦诚地讲一下我们初期的做法。在考虑外部方案之前,HagiCode 其实是有过一版自研调度模块的。当时觉得场景足够明确,三个 Agent 嘛,写个状态机加一个 HTTP 路由就够了。

自研方案在早期确实灵活,想加什么逻辑直接改代码。但随着 Agent 数量增加、交互模式多样化,调度模块迅速膨胀。状态机的状态越来越多,消息回调函数引用的全局状态到处都是,排查一个任务卡住的问题可能要同时翻代码和日志。

更难受的是我们花了大量时间在实现“基础设施功能”:消息重试、超时管理、并发控制、请求 ID 追踪,这些不是业务价值,但每个都不能少。有一段时间团队的核心时间不是花在提升 Agent 的智能水平上,而是在修调度模块的内存泄漏和并发 bug。

这段经历给我们的教训是:Agent 编排是一个足够通用的基础问题,不应该每个项目都在业务代码里重新发明一遍。选择是否自研的标准也很简单——如果你发现自己正在写“消息重发”“超时判断”“状态持久化”这类通用代码,那就说明你可能需要一个现成的核心组件了。

3. Hermes 的核心设计为什么贴合 Agent 编排

3.1 它本质上是"消息路由加状态机"的组合

Hermes 进入我们视野是在一次技术分享上。当时分享的人讲的是它的消息路由机制,但我听完后发现它的设计比单纯的消息中间件多了一层东西:它本身内置了任务生命周期的状态管理能力。

简单说,Hermes 把一次 Agent 协作抽象成了一条消息的完整旅程:消息进入 Hermes 后,会根据消息头里的路由标记被分发到对应的 Agent 处理器;处理器可以同步响应,也可以把消息标记为等待中,挂起这条链路;之后某个外部事件可以重新激活挂起的消息,驱动状态继续流转。

这个能力对 Agent 场景简直是对症的。规划 Agent 给执行 Agent 派发任务,执行 Agent 说“我需要调用某个外部 API”,处理到一半发现该 API 需要人工审批,于是消息在 Hermes 里被挂起。审批通过后外部系统回调 Hermes,挂起的消息被唤醒,执行 Agent 继续往下处理。整个过程里同一个任务的上下文没有被拆散,而是一直内聚在同一条消息的生命周期内。

如果用传统消息队列表达这个过程,就要拆成至少两到三条消息,并且业务层要维护这些消息之间的关联关系。Hermes 用挂起和唤醒机制把复杂度收进了核心组件,业务层只关心自己的处理逻辑。

3.2 路由机制的变化:从"人肉分发"到"按能力注册"

Hermes 另一个让团队惊艳的点是它的路由机制。传统的消息分发要么是基于主题订阅,要么是基于生产者的显式指定。Hermes 支持一种按能力注册的路由方式:某个 Agent 启动时向 Hermes 注册自己“能处理哪些类型的请求”,消息进入后 Hermes 根据内容和目标标签自动寻找可处理的 Agent。

这个机制给 HagiCode 带来了一个张性很大的好处:Agent 可以动态上下线而不影响整体路由。

比如我们有一个“代码执行 Agent”,早期只有一个实例,后来为了测试隔离又加了一个实例,同一个能力注册了两份处理器,Hermes 会自动做负载分发。某个时刻其中一个实例要重启,只需要取消注册,Hermes 会把消息全部路由给存活的实例,整个系统不用停机。

对我们这种经常要调 Agent、改 Agent 的场景来说,这个能力太实用。随便把一个 Agent 拉起来加入集群就能干活,不需要改任何路由配置。

3.3 状态内聚与事件驱动是一体两面

之前我们做自研调度模块时,最分裂的一个问题就是:任务状态和事件通知被拆成了两套东西。状态存在一个中心表里,事件通过回调广播,两边的数据经常对不上——状态已经变迁了,但事件还没发出去,或者事件发出去了但状态还没来得及落库。

Hermes 把状态和事件统一在消息生命周期里。状态迁移本身就是事件产生的过程,状态机每完成一次迁移就会触发对应的事件,这个事件可以被其他 Agent 或者外部系统接收。

这带来的直接效果是 HagiCode 的业务代码显著简化了。以前状态机代码里到处都是“先改状态再发通知”,现在只需要描述“这个状态下收到什么消息该迁移到哪个状态”,通知由 Hermes 的机制自动完成。

也因为如此,我们把很多原来写在业务代码里的条件判断下沉到了 Hermes 的消息过滤器里。比如“只在任务处于等待确认状态时接受外部审批结果消息”,这类规则用 Hermes 的过滤规则就能表达。核心层保证消息只在正确的时间到达正确的地方,业务层不用每处理一条消息都先检查一遍当前状态是否合法。

4. 从 Demo 到生产:Hermes 落地时解决过的六个实际问题

4.1 死信风暴与重试策略的重新设计

刚到生产环境时我们遇到的第一波冲击是死信队列堆积。原因是执行 Agent 调用一个三方查询接口时,对方返回了标准的限流错误。默认重试策略是每三秒重试一次,连续重试二十次后消息进入死信。

我们把重试策略改成了按错误类型区分对待。可重试错误(限流、超时、网络抖动)用指数退避加抖动,从五秒起步,每次乘二,最多重试六次;不可重试错误(参数校验失败、业务规则不满足、权限不足)直接进入人工确认队列,不再浪费重试次数。

调整之后死信量降了大概九成。这个经验说起来简单,但很多团队落地时就是舍不得在策略上做区分,最后全都一套重试逻辑打天下。

4.2 traceId 贯穿:没有全链路追踪就没法排查问题

Agent 协作里最痛苦的问题就是任务在多跳传递之后出了问题无从定位。A 给 B 发消息,B 又调用了工具 C,C 失败后把错误往回抛,最后在总控端看到的是一个泛泛的失败信号,但中间哪个环节出的问题要靠猜。

Hermes 的消息信封设计里本身就带 traceId 字段,消息在进入 Hermes 到被 Agent 消费、再到产生新消息继续传递的整个过程中,traceId 会一直保留。我们在接入落地时做的主要工作是确保所有业务日志都会打印这个 traceId,以及所有对三方系统的调用都会把它透传过去。

有了 traceId 之后,排查链路问题的方式就变成了一条命令拉出所有相关日志。我们甚至在 Hermes 的外围加了一层简单的事件归档,每次消息状态迁移都按 traceId 记录一条明细,这样任意时刻都能回放一个任务完整走过了哪些 Agent、每个环节耗时多少、在哪一步挂了。

4.3 消息体膨胀:不可变载荷的价值

Agent 协作天然会有上下文累积现象。规划 Agent 生成的任务定义会给到执行 Agent,执行 Agent 在工具调用中产生的中间结果又需要回传给规划 Agent。如果每次更新都直接改消息体,消息会像滚雪球一样越来越大,很快整个链路都在传输大量重复数据。

Hermes 的消息本身是不可变的,这个消息设计约束让我们不得不在外面建立一套引用式的上下文管理。工具执行结果不整段塞到消息体里,而是把结果写入对象存储,消息体里只带一个引用 ID。下游 Agent 需要时才按引用 ID 拉取。

这个改动对性能的正面影响非常显著。大文件类的结果不用再反复传输了,消息在核心组件里的流转速度快了很多,内存占用也下来了。核心启示是 Hermes 的不可变消息约束看似限制了灵活性,实际上逼着团队用更合理的方式管理大数据上下文。

4.4 动态注册冲突:Agent 版本管理怎么做

前面提到 Hermes 支持按能力注册,这个机制很灵活,但在实际运行中也暴露了一个问题:同一套能力如果由新旧两个不同版本的 Agent 同时注册,消息可能会被路由到旧版本产生不预期的行为。

我们最后定下了一套命名约定。Agent 上报给 Hermes 的能力标识必须带版本主元信息,例如“代码执行/含沙箱访问/v2”。系统层面上,Hermes 路由到某个能力时如果发现存在多个版本,则默认路由到最新稳定版。旧版本可以通过消息头里的显式版本字段被定向调用,用于灰度对比。

这套约束解决了两个问题:一个是避免新旧版本的无差别混跑;另一个是给灰度发布留了入口。我在网上看到不少团队接入 Hermes 时没注意这层,出现过明明部署了新 Agent 但消息仍然落在旧实例上,就是因为能力标识都是用同一个裸字符串。

4.5 本地调试模式:摆脱对中心节点的依赖

开发环境里不可能每个人都连上生产或者预发环境的 Hermes 集群。最开始我们本地开发是起一个精简版 Hermes 单节点,但后来有些同事开发的 Agent 模块不涉及消息交互,直接本地起单节点也是负担。

Hermes 提供了一种嵌入式运行模式,可以在应用进程内直接启动一个迷你版核心,不需要依赖外部服务。这个模式一开启,本地调试体验大幅提升:写 Agent 时不需要额外启动任何服务,Agent 启动时把依赖注入进来就行。

不过这个模式在生产环境不建议用,毕竟它缺少分布式的可靠性保证。我们的规矩是:本地开发可以嵌入,代码级联调和单元测试可以嵌入,一旦要联调或者上预发,全部切换成独立集群。

4.6 可观测性面板:核心组件自身不能成为黑盒

Hermes 把我们的大部分状态流转逻辑收进了核心层,这带来了一个副作用:以前出问题打开业务代码就能看到逻辑,现在业务逻辑很简单,反而核心内部的状态流转成了潜在黑盒。如果不给 Hermes 配上足够好的可观测能力,团队会陷入另一种盲区。

我们在 Hermes 集群外面搭了一套监控面板,重点盯几类指标:队列积压的分布情况、消息处理耗时、状态迁移失败的次数、以及挂起消息的存活时间。其中挂起消息存活时间是我们额外加的,因为 Agent 协作里经常有人工确认这类长期挂起,但如果挂起时间超过预期,往往意味着某个外部系统忘掉了这次回调。

这些监控指标上线后效果立竿见影,有一次我们通过“挂起超过三十分钟的消息列表”发现某个审批流服务没有回调,直接避免了业务上线事故。

5. 选型之外一点心得:核心组件与业务代码的边界切割

5.1 哪些东西必须留在核心层,哪些不能放进去

经历了大半年的使用,我最大的体会是 Hermes 解决了“通信和状态流转”这一层,但它不是一个全能的编排引擎。把哪些逻辑放进去、哪些留在业务层,这中间有一条很重要的边界线。

通信、路由、状态持久化、超时、重试、消息不可变、链路追踪,这些通用能力应该由核心组件承担。它们与具体业务语义无关,几乎任何 Agent 场景都会用到,而且放核心层会获得更统一的实现质量。

具体的 Agent 策略、调度规则、业务分支条件则应该留在业务代码里。比如“什么样的情况下规划 Agent 需要重新规划”“执行 Agent 返回的什么结果认为失败”,这些不能下沉到核心层,否则核心组件会越来越重,最终变成一个什么都耦合在一起的巨型框架。

一个判断标准我一直在用:如果一个逻辑离开了这个领域就失去意义,那就放进业务层;如果它换一个 Agent 场景还能复用,那就应该放进核心层或者是核心层之上的支撑库。

5.2 把 Agent 的"能力边界"变成显式声明

用 Hermes 之后我们还做了一个额外的抽象:每个 Agent 不只是简单地消费消息、生产结果,而是在启动时向 Hermes 注册自己完整的能力边界。能力边界包含它能处理的消息类型、它会产生的外部副作用、它允许被并发调用的上限。

这个做法对接下来的容量规划和故障隔离帮助很大。系统面板上可以看到每个 Agent 当前的负载、吞吐能力、以及是否已经触达并发上限。回到选型对比的背景下,只有 Hermes 的消息模型能够让我们这样完整地表达 Agent 能力,而不只是把消息投递到某个主题就算完事。

5.3 版本演进中的兼容性策略

最后想说一个和核心组件长期相处的心态问题。任何核心组件都不可能完全不变,Hermes 也一样,它后续也在迭代。我们的原则是:团队直接依赖的 API 面尽量保持稳定,而把变化隔离在外围适配层里。

比如 Hermes 如果改进了路由匹配规则,我们就直接在适配层做转换,业务 Agent 不需要跟着改。这也意味着选型时最好选一条 API 稳定且升级路径平滑的路线,避免那种每个版本都要重写一遍接入代码的方案。

HagiCode 目前的状态是已经稳定运行了大半年,期间没有因为核心组件本身的问题中断过业务。新的 Agent 接入从开始到联调上线基本控制在一天内,大部分时间花在 Agent 自己的逻辑上,而不是通信层。

对我来说,选 Hermes 这件事最大的收获并不是它带来的具体功能,而是它让我意识到:综合 Agent 系统的复杂度注定会超过单体业务系统,把基础能力交给成熟的核心组件,团队才能把时间花在真正该花的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 5:02:02

ModelSim/QuestaSim中Delta Cycle详解:解开同一时间片信号多次翻转之谜

做数字前端验证的朋友,一定遇到过这种场景:仿真波形上明明只有一个时钟沿,比如10 ns,但旁边的数据、Valid信号却在这个时间戳上连续跳了好几次。放大波形,时间标尺一动不动,信号却在“瞬间”变了好几个状态…

作者头像 李华
网站建设 2026/10/15 5:01:48

用Python控制ZigBee灯和风扇:ZCL集群与硬件实战拆解

作为智能家居里最基础也最容易被低估的控制对象,ZigBee灯和风扇的控制其实很适合用Python编程来实现。这篇文章写给两类人:一类是想跳过各种智能家居APP、自己掌控设备联动的人,另一类是已经在用网关方案、但想知道底层协议和Python接口到底怎…

作者头像 李华
网站建设 2026/10/15 5:01:42

DELETE与TRUNCATE区别:大表删除影响与生产实践

很多人第一次接触这两个命令的时候,第一反应都是“不就是清空一张表嘛”,等真正在线上环境跑了一把,才发现DELETE和TRUNCATE背后的机制差异大到你怀疑人生。这篇既是作业复盘,也是我这些年处理大表清理的实战笔记,希望…

作者头像 李华
网站建设 2026/10/15 5:01:21

运行时错误53:MathPage.WLL文件未找到?Word加载项修复全指南

先说结论:遇到“运行时错误 53:文件未找到:MathPage.WLL”时,绝大多数情况不需要重装系统,甚至连 MathType 也不一定需要完全重装。这行提示的意思是,Word 在启动时按某个登记好的地址去加载 MathType 的公…

作者头像 李华
网站建设 2026/10/15 5:01:21

交直流混合配电网潮流计算:VSC换流器建模与统一求解法实践

1. 交直流混合配电网潮流计算,到底在算什么东西先说一句大实话:搞配电网研究的,没人天生喜欢潮流计算,但做混合交直流配电网,潮流计算是第一道绕不过去的坎。我最初接触这个题目,是某导师给了一个模拟项目X…

作者头像 李华
网站建设 2026/10/15 5:00:50

PPT文件打不开?用文件头识别真实格式并批量修复双扩展名

简介:本资源是一份面向企业管理者、快消行业从业者及商科学生的学习型培训课件,聚焦康师傅在品牌运营、市场营销、供应链与人力资源等核心模块的实战经验总结。课件以PPT格式呈现,共1个文件,大小1.27MB,结构清晰、图文…

作者头像 李华