Hermes Agent 源码探秘 (6):多平台网关 — 一个 Agent 服务所有平台

系列:Hermes Agent 源码探秘 作者:元思未来 字数:约3200字


前五篇我们一直在讨论 Hermes 在终端(CLI) 下的工作方式。但你可能不知道:Hermes 真正的"杀手级功能"是------它可以同时接入 15+ 聊天平台,同一个 Agent 服务微信、Telegram、Discord、Slack 等所有渠道。

这篇就来拆 Gateway(网关) 的架构。


一、问题:一个 Agent 怎么服务多个平台?

先想一下,如果让你设计"一个 AI 跑在多个聊天平台上",你会怎么做?

最简单的方案:

复制代码
每个平台部署一个独立的 Agent 实例
  WeChat → Agent 实例1
  Telegram → Agent 实例2
  Discord → Agent 实例3

问题很明显:

  • 每个实例都有自己的上下文和状态------用户在微信上聊了半天的内容,切到 Telegram 就"失忆"了
  • 维护成本高------每个实例单独管理

Hermes 的做法是:

复制代码
WeChat ──┐
Telegram ─┼── Gateway ──→ 同一个 AIAgent 实例
Discord ──┘

所有平台的消息都汇聚到 Gateway ,Gateway 统一路由给 同一个 Agent。Agent 处理完后,Gateway 把回复发回对应的平台。

这就是网关模式


二、Gateway 的整体架构

Gateway 的代码在 gateway/ 目录下:

csharp 复制代码
gateway/
├── run.py                    # 入口,启动 Gateway 服务
├── session.py                # 会话管理
├── base.py                   # 平台适配器基类
├── platforms/                # 各平台适配器
│   ├── telegram.py           # Telegram
│   ├── discord.py            # Discord
│   ├── slack.py              # Slack
│   ├── wecom.py              # 企业微信
│   ├── weixin.py             # 个人微信
│   ├── feishu.py             # 飞书
│   ├── dingtalk.py           # 钉钉
│   ├── whatsapp.py           # WhatsApp
│   ├── signal.py             # Signal
│   ├── email.py              # 邮件
│   ├── sms.py                # 短信
│   ├── matrix.py             # Matrix
│   ├── homeassistant.py      # 智能家居
│   └── ...                   # 还有更多

架构图

scss 复制代码
┌──────────┐  ┌──────────┐  ┌──────────┐
│ Telegram │  │  WeChat  │  │ Discord  │  ... 用户平台
└─────┬────┘  └─────┬────┘  └─────┬────┘
      │             │             │
      └─────────────┼─────────────┘
                    │
                    ▼
           ┌────────────────┐
           │  Gateway       │
           │  (gateway/run) │
           └───────┬────────┘
                   │
                   ▼
          ┌──────────────────┐
          │  Session Manager │
          │  (会话路由)       │
          └───────┬──────────┘
                  │
                  ▼
          ┌──────────────────┐
          │  AIAgent         │
          │  (核心循环)       │
          └──────────────────┘

三、核心机制拆解

3.1 Platform Adapter(平台适配器)

每个平台对应一个适配器文件。适配器继承自基类 BasePlatform

python 复制代码
# gateway/base.py

class BasePlatform:
    """所有平台适配器的基类"""
    
    @property
    def platform_name(self) -> str:
        return "base"
    
    async def send_message(self, message: str, chat_id: str, **kwargs):
        """发送消息到平台"""
        raise NotImplementedError
    
    async def send_file(self, file_path: str, chat_id: str, **kwargs):
        """发送文件到平台"""
        raise NotImplementedError
    
    async def start_polling(self, message_handler):
        """开始轮询/监听消息"""
        raise NotImplementedError

具体平台的适配器实现这个接口。以 Telegram 为例:

python 复制代码
# gateway/platforms/telegram.py

class TelegramPlatform(BasePlatform):
    platform_name = "telegram"
    
    def __init__(self, config):
        self.token = config["telegram"]["bot_token"]
        self.api_base = f"https://api.telegram.org/bot{self.token}"
    
    async def send_message(self, message, chat_id, **kwargs):
        url = f"{self.api_base}/sendMessage"
        payload = {
            "chat_id": chat_id,
            "text": message,
            "parse_mode": "Markdown"
        }
        async with httpx.AsyncClient() as client:
            resp = await client.post(url, json=payload)
            return resp.json()
    
    async def start_polling(self, message_handler):
        """轮询 Telegram API 获取新消息"""
        offset = 0
        while True:
            url = f"{self.api_base}/getUpdates"
            resp = await httpx.post(url, json={
                "offset": offset,
                "timeout": 30
            })
            updates = resp.json().get("result", [])
            for update in updates:
                if "message" in update:
                    msg = update["message"]
                    # 统一消息格式后传给 handler
                    await message_handler({
                        "platform": "telegram",
                        "chat_id": str(msg["chat"]["id"]),
                        "user_id": str(msg["from"]["id"]),
                        "text": msg.get("text", ""),
                        "raw": update
                    })
                offset = update["update_id"] + 1
            await asyncio.sleep(0.5)

每个适配器的核心职责只有两件事:

  1. 接收消息 → 转成统一格式 → 交给 Gateway
  2. 发送消息 → 从 Gateway 拿到回复 → 发回平台

3.2 Gateway 主循环

Gateway 的入口在 gateway/run.py,核心逻辑很简单------一条消息处理流水线

python 复制代码
async def handle_message(platform_msg):
    """处理来自任意平台的消息"""
    
    # 1. 统一消息格式
    unified = {
        "platform": platform_msg["platform"],
        "chat_id": platform_msg["chat_id"],
        "user_id": platform_msg["user_id"],
        "text": platform_msg["text"],
    }
    
    # 2. 会话路由 ------ 找到或创建对应的 Agent 会话
    session = session_manager.get_or_create(
        platform=unified["platform"],
        chat_id=unified["chat_id"]
    )
    
    # 3. 交给 Agent 处理
    response = await session.agent.run_conversation(unified["text"])
    
    # 4. 回复发回原平台
    platform_adapter = get_adapter(unified["platform"])
    await platform_adapter.send_message(
        response["final_response"],
        unified["chat_id"]
    )

这个流水线清晰地分层:

复制代码
平台消息 → 统一格式化 → 会话路由 → Agent处理 → 回复发送

3.3 会话管理(Session Manager)

一个关键设计问题是:不同平台的用户消息,怎么路由到正确的 Agent 会话?

python 复制代码
class SessionManager:
    def __init__(self):
        self._sessions: Dict[str, AgentSession] = {}
    
    def _make_key(self, platform, chat_id):
        return f"{platform}:{chat_id}"
    
    def get_or_create(self, platform, chat_id):
        key = self._make_key(platform, chat_id)
        
        if key in self._sessions:
            return self._sessions[key]
        
        # 创建新 Agent 会话
        agent = AIAgent(
            platform=platform,
            session_id=str(uuid.uuid4()),
            ...
        )
        session = AgentSession(agent=agent, key=key)
        self._sessions[key] = session
        return session

每个 "platform:chat_id" 对应一个独立的 Agent 实例。 这样:

  • 微信用户 A 有自己的 Agent 实例,保持对话上下文
  • Telegram 用户 B 有自己的 Agent 实例,互不干扰
  • 同一用户在不同平台上的会话是独立的

3.4 启动 Gateway

bash 复制代码
hermes gateway run

这个命令会:

python 复制代码
# gateway/run.py 中简化后的启动逻辑

async def run_gateway():
    # 1. 读取配置, 加载已启用的平台
    config = load_config()
    enabled_platforms = config.get("gateway", {}).get("platforms", [])
    
    # 2. 为每个平台创建适配器实例
    adapters = {}
    for name in enabled_platforms:
        adapter = create_platform_adapter(name, config)
        adapters[name] = adapter
    
    # 3. 启动所有平台的监听
    tasks = []
    for name, adapter in adapters.items():
        task = asyncio.create_task(
            adapter.start_polling(handle_message)
        )
        tasks.append(task)
    
    # 4. 持续运行
    await asyncio.gather(*tasks)

所有平台的监听是并发的 ------使用 asyncio 让多个平台同时运行。


四、从 WeCom(企业微信)适配器看实现细节

我们看看 gateway/platforms/wecom.py 是怎么工作的。

企业微信的接入方式比较特殊。它使用 Webhook 回调机制,而不是轮询:

markdown 复制代码
用户发消息 → 企业微信服务器 → HTTP POST → Hermes Gateway
                                                  ↓
Hermes Gateway → HTTP POST → 企业微信服务器 → 用户收到回复

核心交互:

python 复制代码
# gateway/platforms/wecom.py (简化)

class WeComPlatform(BasePlatform):
    platform_name = "wecom"
    
    async def handle_callback(self, request):
        """处理企业微信的回调请求"""
        
        # 1. 解密消息(企业微信的消息是加密的)
        encrypted = request.xml["Encrypt"]
        decrypted = self.decrypt_message(encrypted)
        
        # 2. 解析消息内容
        msg_type = decrypted["MsgType"]
        content = decrypted["Content"]
        from_user = decrypted["FromUserName"]
        
        # 3. 统一格式后交给 Gateway 处理
        await handle_message({
            "platform": "wecom",
            "chat_id": from_user,
            "user_id": from_user,
            "text": content
        })
    
    async def send_message(self, message, chat_id, **kwargs):
        """发送消息到企业微信"""
        # 企业微信使用主动发送消息的 API
        access_token = await self._get_access_token()
        url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"
        
        payload = {
            "touser": chat_id,
            "msgtype": "markdown",
            "markdown": {"content": message}
        }
        await httpx.post(url, json=payload)

这里有个有趣的细节:企业微信的消息是加密的,需要解密后才能处理。每个平台的适配器都要处理自己平台的"方言"(加密方式、消息格式、API差异),然后转成统一的内部格式。


五、适配器模式的价值

看完 Gateway 的源码,最值得学习的设计模式就是适配器模式

传统写法(反例)

python 复制代码
def handle_platform_message(platform, message):
    if platform == "telegram":
        # Telegram 特殊的处理逻辑
        ...
    elif platform == "wecom":
        # 微信特殊的处理逻辑
        ...
    elif platform == "discord":
        # Discord 特殊的处理逻辑
        ...
    # 每加一个平台,就要改这个函数

问题:违反了"开闭原则"(对扩展开放,对修改封闭)。每加一个平台,就要改核心代码。

适配器模式(正解)

python 复制代码
# 核心代码只依赖抽象基类
class BasePlatform:
    async def send_message(self, ...): ...
    async def start_polling(self, ...): ...

# 新增平台 = 新建一个类实现接口
class NewPlatform(BasePlatform):
    async def send_message(self, ...): ...
    async def start_polling(self, ...): ...

# 核心代码完全不需要改

新增一个平台就是新建一个文件,不改已有代码。 这就是适配器模式的魅力。


六、总结:Gateway 的核心设计哲学

设计点 实现方式 好处
平台适配器 每个平台一个类,继承 BasePlatform 新增平台不改核心代码
统一消息格式 各平台消息转成统一 dict 消息处理逻辑与平台无关
会话隔离 platform:chat_id → Agent 实例 各用户上下文互不干扰
异步并发 asyncio 同时监听多平台 一个进程服务所有用户
即插即用 配置启用/禁用平台 按需加载,资源效率高

七、下一篇预告

Agent 会思考(核心循环),会干活(工具系统),有自我认知(System Prompt),还能在多平台服务(Gateway)。但还有一个关键能力:它能学习和记忆。

第七篇我们拆 记忆系统和技能系统

  • Agent 怎么记住你是谁、你喜欢什么?
  • "技能"到底是什么?怎么通过 Markdown 文件教 Agent 新技能?
  • 长期记忆和短期记忆怎么协同工作?

代码位置: ~/.hermes/hermes-agent/gateway/
平台数量: 15+(还在增长中)
核心模式: 适配器模式


元思未来 · 行稳致远,进而有为

相关推荐
Hyyy5 小时前
如何设计Agent的Harness
llm·agent·ai编程
nbtang20267 小时前
AI Agent 入门(三):Tool Use 入门 —— Function Calling 原理与实战
人工智能·ai·agent
新知图书7 小时前
RAG之生成技术
人工智能·agent·ai agent·智能体·langgraph
武子康7 小时前
调查研究-211 AgentBound 深度解析:AI Agent 不只要“有权限”,还要有可验证的行为治理
人工智能·llm·agent
aovenus8 小时前
Skill / Agent / Workflow 使用场景指南及对比
agent·workflow·skill
JouYY9 小时前
聊一下知识答疑Agent的“层次聚类”流程
架构·llm·agent
L3S10 小时前
Agent为什么会死循环?
人工智能·agent
云烟成雨TD10 小时前
LangFlow 1.x 系列【3】入门案例
人工智能·python·agent
墨流藏于库10 小时前
Electron 应用 macOS 自动更新的正确姿势 —— 没有 Apple Developer Program 也能用
agent
新知图书10 小时前
智能体基础架构
人工智能·agent·ai agent·智能体·langgraph