☰
翻完HelloAgents源码,我搞懂了它的通信协议架构是怎么设计的
2026/10/8 2:27:24 网站建设 项目流程

翻完HelloAgents源码,我搞懂了它的通信协议架构是怎么设计的

先说为什么要研究这个

前两篇文章聊了"为什么需要通信协议"和"三种协议的设计理念对比",有读者私信问我:“道理我都懂,但具体到一个框架里,协议是怎么落地的?”

这个问题问得好。光看规范文档是一回事,看别人怎么把规范变成可运行的代码是另一回事。所以我花了两天时间,把HelloAgents这个框架的通信模块源码翻了一遍,今天把我的理解整理出来。

先说结论:HelloAgents的通信架构设计得相当巧妙,它没有简单地"选一个协议用到底",而是做了一层抽象,把MCP、A2A这些不同协议的差异都屏蔽在了底层。这种设计思路值得我们做架构的时候参考。

一、整体架构:三层结构

我第一次看HelloAgents的通信模块目录结构的时候,有点懵,文件太多了。后来画了张图才理清楚,它本质上是三层结构:

┌─────────────────────────────────┐ │ 应用层(Agent业务逻辑) │ │ 你的Agent代码、工具函数、Prompt │ ├─────────────────────────────────┤ │ 协议抽象层(Protocol Layer) │ │ 统一的消息格式、传输接口、路由器 │ ├─────────────────────────────────┤ │ 协议实现层(Transport Layer) │ │ MCP实现 │ A2A实现 │ ANP实现 │ └─────────────────────────────────┘

这三层各司其职,我一层一层说。

最底层:协议实现层

这一层最直接,就是各种协议的具体实现。MCP怎么握手、怎么发消息,A2A怎么建立对话、怎么传递任务卡,都在这一层。

我看源码的时候发现,HelloAgents并没有自己从零实现这些协议,而是尽量复用官方SDK。比如MCP用的是官方的mcpPython包,A2A也是基于Google的参考实现做的封装。这样做的好处是协议升级的时候,只需要升级依赖包就行,不用自己维护一套协议栈。

但这里有个问题:不同协议的SDK接口完全不一样。MCP的客户端调用方式是client.call_tool(),A2A的是agent.send_message(),参数格式也不同。如果让上层业务代码直接调用这些SDK,那换一个协议就得改一堆代码。

所以就有了中间那层——协议抽象层。

中间层:协议抽象层

这是整个架构的核心,也是我觉得设计得最精彩的地方。

HelloAgents定义了一套自己的"内部消息格式",不管底层用的是MCP还是A2A,消息进来之后都会被转换成统一的内部格式。反过来,内部消息发出去的时候,也会被转换成对应协议的格式。

我在源码里找到了这个内部消息的定义,大概长这样:

@dataclassclassAgentMessage:"""智能体之间传递的统一消息格式"""message_id:str# 消息唯一IDsender_id:str# 发送方Agent IDreceiver_id:str# 接收方Agent IDmessage_type:str# 消息类型:text / tool_call / task / result / errorcontent:Any# 消息内容context:dict# 上下文信息(对话历史、用户信息等)metadata:dict# 元数据(时间戳、协议类型、追踪ID等)

你看,这个结构跟具体协议无关,它只关心"谁给谁发了一条什么类型的消息,内容是什么"。

然后是传输接口的抽象。HelloAgents定义了一个Transport基类,所有协议实现都要继承它:

classTransport(ABC):"""传输层抽象基类"""@abstractmethodasyncdefconnect(self):"""建立连接"""pass@abstractmethodasyncdefsend(self,message:AgentMessage)->AgentMessage:"""发送消息并等待响应"""pass@abstractmethodasyncdeflisten(self,handler):"""监听 incoming 消息"""pass@abstractmethodasyncdefdisconnect(self):"""断开连接"""pass

有了这个抽象,上层代码只需要跟Transport接口打交道,完全不用管底层是MCP还是A2A。想换协议?换一个Transport实现就行,业务代码一行都不用改。

我自己写代码的时候也经常做接口抽象,但HelloAgents这个抽象的粒度让我挺受启发的——它没有试图把所有协议的特性都塞进接口里,而是只保留了"连接、发送、监听、断开"这四个最核心的动作。协议特有的能力(比如MCP的工具列表、A2A的任务卡)通过metadata字段透传,需要的时候上层自己去取。

最上层:应用层

这一层就是我们写业务代码的地方了。你定义自己的Agent,给它配工具,写Prompt,然后通过统一的消息接口跟其他Agent通信。

举个例子,在HelloAgents里创建一个能调用MCP工具的Agent,代码大概是这样:

fromhelloagentsimportAgent,MCPTransport# 创建一个MCP传输层,连接到天气工具服务器weather_transport=MCPTransport(server_url="https://mcp.weather.example.com")# 创建Agent,绑定传输层agent=Agent(name="weather_agent",transport=weather_transport,system_prompt="你是一个天气助手,帮用户查询天气信息")# 运行Agentawaitagent.run()

你看,应用层的代码非常干净。创建Transport、传给Agent、运行,就这三步。Agent内部会自动通过MCP协议去发现工具、调用工具,这些细节都被封装好了。

二、几个关键设计决策

光看架构图还不够,我在看源码的时候注意到几个有意思的设计决策,值得单独拎出来说说。

决策1:协议适配器模式,而不是协议无关模式

HelloAgents没有试图做一个"万能协议"来替代MCP和A2A,而是做了一堆"适配器",把不同协议适配到统一的内部接口上。

这个选择很务实。现在Agent协议还在快速演进,今天是MCP和A2A,明天可能又冒出来一个新协议。如果自己造一个万能协议,等于跟整个社区对抗,而且用户不一定买账。做适配器就灵活多了——新协议出来了,写个适配器就行,不影响现有代码。

我在项目里也遇到过类似的选择。当时我们要接多个大模型API,有人提议做一个"统一的大模型协议",后来发现不现实,最后也是用了适配器模式,每个模型写一个adapter。事实证明这个选择是对的,后来新出的模型接起来很快。

决策2:消息路由用"注册中心",不用硬编码

在多Agent系统里,一个很常见的问题是:Agent A想给Agent B发消息,怎么知道B的地址?

HelloAgents的做法是搞了一个AgentRegistry(Agent注册中心)。每个Agent启动的时候,会把自己的ID、能力、传输方式注册到Registry里。其他Agent要发消息的时候,先去Registry查一下目标Agent的信息,然后建立连接。

classAgentRegistry:"""Agent注册中心"""defregister(self,agent_id:str,capabilities:list,transport_config:dict):"""注册Agent"""self._agents[agent_id]={"capabilities":capabilities,"transport":transport_config}defdiscover(self,capability:str)->list:"""按能力发现Agent"""return[agent_idforagent_id,infoinself._agents.items()ifcapabilityininfo["capabilities"]]

这个设计跟ANP协议的服务发现理念是一致的,但HelloAgents把它做成了框架内置的能力,不需要额外部署ANP服务器。在中小规模的Agent系统里,这个方案足够用了。

我自己之前写多Agent的时候,是把每个Agent的地址写在配置文件里的。加一个Agent就得改配置、重启服务,挺麻烦的。看了HelloAgents的设计之后,我也准备在自己的项目里加一个轻量的注册中心。

决策3:上下文传递用"信封模式"

前面提到过,多Agent协作中一个老大难问题是上下文传递。用户跟Agent A聊了半天,A把任务转给B,B不知道之前聊了什么。

HelloAgents的解决方案是"信封模式"——每条消息都带一个context字段,里面装着对话历史、用户信息、会话状态等。消息从A传到B,再从B传到C,context一路跟着走,每个Agent都能看到完整的上下文。

# 消息转发时,context自动传递asyncdefforward_message(self,message:AgentMessage,target_id:str):# 添加上下文信息message.context["forwarded_by"]=self.agent_id message.context["forward_time"]=datetime.now().isoformat()# 转发给目标Agentreturnawaitself.transport.send(message)

这个设计看起来简单,但其实解决了大问题。以前我做上下文传递的时候,每个Agent都要自己写一套"接收上下文-处理-传递上下文"的逻辑,很容易出错。现在框架层统一处理了,业务代码不用关心这些。

不过我也注意到一个潜在问题:如果Agent链路很长,context会越来越大,最后可能超出token限制。HelloAgents目前好像没有做上下文压缩,这可能是后续需要优化的点。

三、我踩的一个坑

看源码归看源码,真到动手跑demo的时候还是踩了坑。

我按照文档写了一个最简单的双Agent通信demo,用的是A2A协议。结果两个Agent就是连不上,报的错是connection refused。

我查了半天,先是以为端口被占了,换了个端口还是不行。然后以为是防火墙的问题,关了防火墙也没用。最后翻源码才发现——A2A传输层默认用的是WebSocket,但我启动Agent的时候没有启动WebSocket服务,只启动了HTTP服务。

文档里没写这个细节,源码里也只有一行注释提到。后来我在Agent配置里加了enable_websocket=True,问题就解决了。

agent=Agent(name="test_agent",transport=A2ATransport(host="0.0.0.0",port=8000),config={"enable_websocket":True}# 这个配置很重要!)

这个坑提醒我:看框架源码真的很有必要。文档永远不会覆盖所有细节,遇到问题的时候,源码是最可靠的参考。

四、对这个架构的评价

最后说说我对HelloAgents通信架构的整体评价。

做得好的地方:

  • 三层分离很清晰,抽象层的粒度恰到好处
  • 适配器模式务实,不重复造轮子
  • 注册中心和上下文传递都是开箱即用,省了很多事
  • 代码可读性不错,关键模块都有注释

可以改进的地方:

  • 文档不够详细,有些配置项只能翻源码才知道
  • 上下文没有自动压缩机制,长链路可能有问题
  • 协议实现层目前只支持MCP和A2A,ANP还在开发中
  • 错误处理可以更细致一些,现在有些异常直接抛出来,用户体验不太好

总体来说,这个架构的设计思路是值得学习的。尤其是"协议抽象层"这个思路,不管你用不用HelloAgents这个框架,在做自己的多Agent系统的时候都可以借鉴。

写在最后

研究HelloAgents的通信架构,最大的收获不是学会了怎么用这个框架,而是理解了"框架设计者是怎么思考问题的"。

面对多个并存的协议,他们没有选边站,而是做了一层抽象;面对上下文传递的难题,他们用了最简单直接的信封模式;面对服务发现的需求,他们做了一个轻量的内置注册中心。这些决策不一定是最优的,但都是在当前阶段最合理的选择。

做架构就是这样,没有完美的方案,只有最合适的取舍。

你们在做多Agent系统的时候,通信层是怎么设计的?是直接用某个协议,还是自己做了抽象?欢迎在评论区聊聊。


下一篇预告:10.1.4 本章学习目标与快速体验。我会带大家动手跑通HelloAgents的第一个通信demo,从安装到运行,把环境搭建过程中可能遇到的坑都踩一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询