转载--Hermes Agent 13 | Gateway 架构:二十余渠道如何复用同一套 Agent Runtime

原文:Hermes Agent 13 | Gateway 架构:二十余渠道如何复用同一套 Agent Runtime

入口可以有一百个,但引擎只需要一个


先从一个实际场景出发

假设你同时在 Telegram 和 Slack 上部署了 Hermes。一个用户在 Telegram 私聊里说"帮我搜一下 LLM inference 的最新论文",另一个用户在 Slack 频道的线程里说"翻译一下这段 Go 代码"。这两条消息同时到达。

从 Agent runtime 的角度看,它不关心消息来自 Telegram 还是 Slack。它需要的只有三样东西:

  1. 一段文本(或者附带图片、语音、文件)

  2. 一个 session key,用来定位"这条消息属于哪段对话"

  3. 一个回调通道,用来把结果发回去

Gateway 就是做这件事的中间层:把 20 种平台 API 的差异,收敛成 Agent runtime 能消费的统一输入;再把 Agent 的输出,展开成各平台能渲染的原生格式。


整体架构:一图看完

复制代码
Telegram  Discord  Slack  Signal  Email  WeChat  ...  Webhook/API
   │         │       │       │       │       │            │
   └────┬────┘       │       │       │       │            │
        │            │       │       │       │            │
   ┌────▼────────────▼───────▼───────▼───────▼────────────▼──┐
   │                  BasePlatformAdapter                     │
   │  connect() / disconnect() / send() / get_chat_info()    │
   │  + 可选覆写: send_image / send_voice / edit_message ... │
   └────────────────────────┬────────────────────────────────┘
                            │  MessageEvent (统一消息格式)
                            │
   ┌────────────────────────▼────────────────────────────────┐
   │               GatewayRunner (gateway/run.py)            │
   │  - 平台适配器注册与生命周期管理                           │
   │  - 消息路由: 授权 → 命令拦截 → session 解析 → Agent 执行 │
   │  - 响应投递: 文本/图片/语音/文件的跨平台归一             │
   │  - Cron ticker: 后台定时任务触发与投递                    │
   │  - Hook 系统: 生命周期事件 → 可扩展处理器                 │
   └────────────────────────┬────────────────────────────────┘
                            │
   ┌────────────────────────▼────────────────────────────────┐
   │          AIAgent (run_agent.py) --- Agent Runtime          │
   │  同一套模型调用、工具系统、记忆/技能、上下文压缩          │
   └─────────────────────────────────────────────────────────┘

gateway/run.py 一个文件 10,485 行,gateway/platforms/base.py 2,201 行。数字看着吓人,但核心设计思路很克制。接下来逐层拆。


统一消息协议:MessageEvent

所有平台适配器的工作,最终都要归结到一件事:把平台原生事件转换成一个 **MessageEvent** 实例

这个 dataclass 定义在 gateway/platforms/base.py(第 656 行):

复制代码
@dataclass
class MessageEvent:
    # 消息内容
    text: str
    message_type: MessageType = MessageType.TEXT

    # 来源信息
    source: SessionSource = None

    # 原始平台数据(调试用)
    raw_message: Any = None
    message_id: Optional[str] = None

    # 媒体附件
    media_urls: List[str] = field(default_factory=list)
    media_types: List[str] = field(default_factory=list)

    # 回复上下文
    reply_to_message_id: Optional[str] = None
    reply_to_text: Optional[str] = None

    # 频道/话题绑定的自动技能
    auto_skill: Optional[str | list[str]] = None

    # 频道级别的临时系统提示(不持久化到对话历史)
    channel_prompt: Optional[str] = None

    # 内部合成事件标记(跳过授权检查)
    internal: bool = False

    timestamp: datetime = field(default_factory=datetime.now)

MessageType 枚举覆盖了 9 种消息类型:TEXTPHOTOVIDEOAUDIOVOICEDOCUMENTLOCATIONSTICKERCOMMAND

注意两个不那么显然的字段:

  • auto_skill:Telegram 的论坛话题(forum topic)或 Discord 频道可以绑定技能。消息到达时,适配器把绑定的技能名写入这个字段,Agent runtime 会自动加载对应的 SKILL.md

  • channel_prompt:Discord 的频道描述(channel topic)或 Slack 的频道说明可以充当临时系统提示。它只在本次 API 调用时注入,不写入对话历史,也不影响 prompt cache。

这是一个很聪明的设计:平台特有的上下文信息,通过 MessageEvent 的扩展字段传递,而不是让 Agent runtime 去感知平台差异。


平台适配器:4 个抽象方法 + N 个可选覆写

BasePlatformAdapterbase.py 第 853 行)定义了适配器的完整协议。必须实现的抽象方法只有 4 个:

方法 职责
connect() 连接平台,开始接收消息
disconnect() 断开连接
send(chat_id, content, reply_to, metadata) 发送文本消息
get_chat_info(chat_id) 获取聊天/频道信息

实现了这 4 个方法,一个平台适配器就能工作了。但只靠 send() 发文本,体验会很粗糙------图片变成 URL 链接、语音变成文件路径、长消息被截断。所以 base class 提供了一组带默认实现的可选方法

复制代码
# 媒体相关
async def send_image(chat_id, image_url, caption, ...) -> SendResult
async def send_animation(chat_id, animation_url, ...) -> SendResult
async def send_voice(chat_id, audio_path, ...) -> SendResult
async def send_video(chat_id, video_path, ...) -> SendResult
async def send_document(chat_id, file_path, ...) -> SendResult
async def send_image_file(chat_id, image_path, ...) -> SendResult

# 消息编辑(流式输出场景)
async def edit_message(chat_id, message_id, content, *, finalize=False) -> SendResult

# 输入状态指示
async def send_typing(chat_id, metadata=None) -> None
async def stop_typing(chat_id) -> None

# 生命周期钩子
async def on_processing_start(event: MessageEvent) -> None
async def on_processing_complete(event: MessageEvent, outcome: ProcessingOutcome) -> None

默认实现都是"降级方案"。比如 send_image() 的默认行为是把 URL 作为文本发出去;edit_message() 默认返回 success=False,调用方会 fallback 到发一条新消息。各平台按自身能力覆写。

这种设计意味着:写一个最简适配器可能只需要 200 行代码,但做到体验完善可以展开到上千行。 实际上,Discord 适配器 3,648 行(因为有 Rich Embed、Reaction、Thread 等丰富交互),而 Signal 适配器只有 897 行。

20 个平台适配器

Platform 枚举定义在 gateway/config.py(第 48 行),当前包含 20 个值:

复制代码
class Platform(Enum):
    LOCAL = "local"
    TELEGRAM = "telegram"
    DISCORD = "discord"
    WHATSAPP = "whatsapp"
    SLACK = "slack"
    SIGNAL = "signal"
    MATTERMOST = "mattermost"
    MATRIX = "matrix"
    HOMEASSISTANT = "homeassistant"
    EMAIL = "email"
    SMS = "sms"
    DINGTALK = "dingtalk"
    API_SERVER = "api_server"
    WEBHOOK = "webhook"
    FEISHU = "feishu"
    WECOM = "wecom"
    WECOM_CALLBACK = "wecom_callback"
    WEIXIN = "weixin"
    BLUEBUBBLES = "bluebubbles"
    QQBOT = "qqbot"

从即时通讯(Telegram、Discord、Signal)到企业协作(Slack、飞书、钉钉、企业微信),从邮件/短信到智能家居(Home Assistant),再到通用接入(Webhook、API Server)------覆盖面已经相当完整。加上微信(weixin)和 QQ(qqbot),中国生态也没落下。

适配器注册流程

GatewayRunner 在启动时遍历配置,按需创建适配器:

复制代码
# gateway/run.py --- 适配器注册(简化)
for platform, platform_config in self.config.platforms.items():
    if not platform_config.enabled:
        continue
    adapter = self._create_adapter(platform, platform_config)
    adapter.set_message_handler(self._handle_message)      # 消息回调
    adapter.set_fatal_error_handler(self._handle_adapter_fatal_error)
    adapter.set_session_store(self.session_store)
    adapter.set_busy_session_handler(self._handle_active_session_busy_message)
    success = await adapter.connect()
    if success:
        self.adapters[platform] = adapter

关键点:每个适配器拿到的 _handle_message 是同一个函数引用 ------GatewayRunner._handle_message。这就是"多入口复用同一套处理逻辑"的接入点。


Session Key:一条公式决定"这是哪段对话"

session 管理是 Gateway 最精妙的设计之一。核心问题是:同一个 Agent runtime,面对来自不同平台、不同聊天、不同用户、不同线程的消息,如何正确地隔离会话上下文?

答案是一个确定性的 key 构造函数,定义在 gateway/session.py(第 440 行):

复制代码
def build_session_key(
    source: SessionSource,
    group_sessions_per_user: bool = True,
    thread_sessions_per_user: bool = False,
) -> str:

规则分两大类。

DM(私聊)规则

私聊场景最简单------一个用户、一个对话:

复制代码
if source.chat_type == "dm":
    if source.chat_id:
        if source.thread_id:
            return f"agent:main:{platform}:dm:{source.chat_id}:{source.thread_id}"
        return f"agent:main:{platform}:dm:{source.chat_id}"
    if source.thread_id:
        return f"agent:main:{platform}:dm:{source.thread_id}"
    return f"agent:main:{platform}:dm"

Telegram 的"论坛话题"(forum topic)在技术上是 DM 里的 thread,所以会走 chat_id:thread_id 的路径------同一个 DM 聊天窗口下的不同话题各有独立 session。

群组/频道规则

群组场景复杂得多,因为要处理"同一个群里不同用户是否共享上下文"的问题:

复制代码
participant_id = source.user_id_alt or source.user_id
key_parts = ["agent:main", platform, source.chat_type]

if source.chat_id:
    key_parts.append(source.chat_id)
if source.thread_id:
    key_parts.append(source.thread_id)

# 线程内默认共享;非线程群组默认按用户隔离
isolate_user = group_sessions_per_user
if source.thread_id and not thread_sessions_per_user:
    isolate_user = False

if isolate_user and participant_id:
    key_parts.append(str(participant_id))

return ":".join(key_parts)

这里有一个非常关键的设计决策:

线程内默认共享(所有参与者看到同一段对话),非线程群组默认按用户隔离(每个人各有独立上下文)。

为什么?因为 Telegram 论坛话题、Discord 线程、Slack 线程的用户心智模型就是"大家在同一个线程里讨论同一件事"。如果按用户隔离,A 在线程里说的话 B 看得见,但 Agent 却当两段独立对话处理------这会让用户困惑。

而在不带线程的群组里,用户 A 说"帮我翻译这段代码"、用户 B 说"搜一下最新的 Rust 版本",如果共享 session,Agent 的上下文就乱了。所以默认按用户隔离。

几个实际 session key 的例子:

场景 Session Key
Telegram 私聊 agent:main:telegram:dm:123456789
Telegram 论坛话题 agent:main:telegram:dm:987654321:123
Discord 频道(用户隔离) agent:main:discord:channel:123456789:user_456
Discord 线程(共享) agent:main:discord:channel:123456789:987654321
Slack 线程(共享) agent:main:slack:channel:C123ABC:ts1234567890

Session key 到 session_id 的映射默认持久化在 ~/.hermes/sessions/sessions.json 里;如果启用了 profile,则会随对应的 HERMES_HOME/sessions/ 目录变化。SessionEntry 记录了创建时间、更新时间、token 用量、预估成本等元数据。


消息路由:从平台事件到 Agent 执行

当一个 MessageEvent 到达 GatewayRunner._handle_message 时,它会经过一条完整的处理管线。

第一步:授权检查

复制代码
event 到达
  │
  ├── internal=True?→ 跳过授权(合成事件)
  ├── user_id 为空?→ 拒绝
  └── _is_user_authorized()
        ├── 允许名单匹配?→ 通过
        └── DM 配对 fallback → 等待首次绑定

授权在 gateway/run.py_is_user_authorized() 方法中完成。这是第 10 讲安全防线中"第一层:用户授权"在 Gateway 中的落地。

第二步:命令拦截

授权通过后,系统检查是否为特殊命令。这些命令在 Agent 之前被拦截处理:

  • /stop:中断正在运行的 Agent

  • /new:重置当前 session

  • /approve / /deny:危险命令审批响应

  • /steer:向运行中的 Agent 注入引导提示

  • /queue:查看排队消息

拦截发生在 _handle_message 的第 2865-3172 行之间。注意这些命令不经过 Agent runtime ,而是由 Gateway 直接处理。这保证了即使 Agent 卡死,用户仍然可以用 /stop 中断它。

第三步:Session 解析与上下文构建

复制代码
session = session_store.get_or_create_session(session_key)
context = build_session_context(session, source, connected_platforms, home_channels)

SessionContext 包含 SessionSource(来源信息)、已连接平台列表、home channel 字典。这些信息会被 build_session_context_prompt() 函数序列化为系统提示的一部分注入到 Agent:

复制代码
## Current Session Context

**Source:** Telegram (DM with alice)
**User:** alice
**Connected Platforms:** local (files on this machine), telegram: Connected ✓, slack: Connected ✓
**Home Channels (default destinations):**
  - telegram: My Notes (ID: 123456789)

**Delivery options for scheduled tasks:**
- `"origin"` → Back to this chat (alice)
- `"local"` → Save to local files only (<HERMES_HOME>/cron/output/)
- `"telegram"` → Home channel (My Notes)

这段系统提示让 Agent 知道自己"身处何方"------知道消息来自 Telegram 私聊、知道有哪些平台可用、知道 cron 任务可以投递到哪里。

一个细节值得注意:对于共享线程(多用户参与的线程),系统提示不会写死某个用户名,而是标注"Multi-user thread --- messages are prefixed with sender name"。原因在代码注释里写得很清楚:

复制代码
# In shared thread sessions (non-DM with thread_id), multiple users
# contribute to the same conversation.  Don't pin a single user name
# in the system prompt --- it changes per-turn and would bust the prompt
# cache.  Instead, note that this is a multi-user thread; individual
# sender names are prefixed on each user message by the gateway.

如果每次换个人说话就改系统提示里的用户名,LLM 的 prompt cache 就会被打碎。所以改为在每条用户消息前加 [sender name] 前缀,系统提示保持稳定。这是一个为了 prompt cache 效率而做的有意识的设计妥协。

第四步:后台异步执行

消息处理不在主 event loop 中阻塞。BasePlatformAdapter.handle_message() 会立即 spawn 一个后台 task:

复制代码
async def handle_message(self, event: MessageEvent) -> None:
    # 返回很快------spawn 后台任务
    task = asyncio.create_task(
        self._process_message_background(event, session_key)
    )
    self._background_tasks.add(task)
    task.add_done_callback(self._background_tasks.discard)

_process_message_background() 是真正的执行主体,完整生命周期如下:

复制代码
1. 启动 typing 指示器循环 (_keep_typing)
2. 触发 on_processing_start 钩子
3. 调用 _message_handler → Agent runtime 执行
4. 响应投递(文本/图片/语音/文件)
5. 触发 on_processing_complete 钩子
6. 检查 pending message 队列
7. 停止 typing 指示器

这个后台模型的好处是:新消息可以在 Agent 运行期间继续到达 。如果用户在 Agent 运行时追加了新消息,它会被放入 pending queue;如果用户发了 /stop,它会通过中断机制取消当前运行。


跨平台消息格式归一

Agent runtime 的输出是 Markdown 文本,但各平台对 Markdown 的支持千差万别。Gateway 在响应投递时做了一系列归一化处理。

图片提取与原生投递

Agent 经常在输出中嵌入 ![alt](url) 格式的图片。extract_images() 方法(base.py 第 1154 行)会把这些标记提取出来,通过 send_image() 以平台原生方式投递------在 Telegram 里显示为内联图片,在 Discord 里显示为 Embed。

如果 URL 以 .gif 结尾,还会自动切换到 send_animation(),让 GIF 在 Telegram 上自动播放而不是作为静态图显示。

长消息分片

truncate_message() 方法(base.py 第 2072 行)处理超长消息的分片:

复制代码
@staticmethod
def truncate_message(
    content: str,
    max_length: int = 4096,
    len_fn: Optional[Callable[[str], int]] = None,
) -> List[str]:

两个设计细节:

  1. 代码块感知:如果分片点落在三反引号(`````)代码块内部,会在当前分片末尾关闭代码块,在下一个分片开头重新打开(带上原始的语言标签)。这避免了跨分片的渲染错误。

  2. 自定义长度函数 :Telegram 按 UTF-16 code unit 计算消息长度(一个 emoji 占 2 个 unit),所以 Telegram 适配器传入 utf16_len 而不是 Python 的 len

分片后会加上 (1/3)(2/3) 这样的指示器。

Pending Message 合并

用户经常连发多条消息,尤其是在 Telegram 上。比如先发一张截图,紧接着发一句"帮我看看这个报错"。如果不做合并,第一条消息触发 Agent 开始运行,第二条消息就变成了 pending message,下一轮才处理------用户体验很割裂。

merge_pending_message_event() 函数(base.py 第 742 行)解决这个问题:

复制代码
def merge_pending_message_event(
    pending_messages, session_key, event, *, merge_text=False,
) -> None:
  • 照片连发 (Photo burst):多张图片合并到同一个 MessageEventmedia_urls 列表里

  • 图文混合:图片和文字合并,caption 拼接

  • 文字追加merge_text=True,Telegram 启用):连续的文字消息用 \n 拼接,避免只保留最后一条


流式编辑:让消息"动起来"

对于支持消息编辑的平台(Telegram、Slack、Discord、Mattermost),Gateway 可以实现"流式输出"效果:先发一条消息,然后持续 edit_message() 更新内容,用户看到文字逐步生成。

edit_message() 的签名有一个 finalize 参数:

复制代码
async def edit_message(
    self, chat_id, message_id, content, *, finalize=False,
) -> SendResult:

大多数平台(Telegram、Slack、Discord)不区分"中间编辑"和"最终编辑"------edit 就是 edit。但钉钉的 AI Card 有"进行中"和"已完成"两种 UI 状态,需要在最后一次编辑时显式关闭。所以 base class 定义了 REQUIRES_EDIT_FINALIZE 类属性(默认 False),钉钉适配器覆写为 True,流式消费端据此决定是否发出 finalize 编辑。

这种"默认安全降级 + 按需覆写"的模式贯穿了整个适配器设计。


生命周期钩子与 Hook 系统

Gateway 有两层事件机制。

适配器级钩子

on_processing_starton_processing_complete 是适配器级别的钩子,在 _process_message_background() 的头尾调用:

复制代码
await self._run_processing_hook("on_processing_start", event)
# ... Agent 执行 ...
await self._run_processing_hook(
    "on_processing_complete", event,
    ProcessingOutcome.SUCCESS if processing_ok else ProcessingOutcome.FAILURE,
)

ProcessingOutcome 有三个值:SUCCESSFAILURECANCELLED

典型用法:Discord 适配器在 on_processing_start 时给消息加一个 👀 reaction(表示"正在处理"),在 on_processing_complete 时根据结果换成 ✅ 或 ❌。

Gateway 级 Hook 系统

更通用的 Hook 系统定义在 gateway/hooks.py,支持 8 种事件:

事件 触发时机
gateway:startup Gateway 进程启动
session:start 新 session 创建
session:end session 结束(/new/reset
session:reset 自动重置完成(idle 超时或每日重置)
agent:start Agent 开始处理消息
agent:step 工具调用循环的每一步
agent:end Agent 处理完成
command:* 任意斜杠命令(通配符匹配)

Hook 从 hooks 目录发现和加载。默认路径是 ~/.hermes/hooks/;启用 profile 后,对应的是当前 profile 的 HERMES_HOME/hooks/。每个 hook 是一个目录,包含 HOOK.yaml(声明名称和监听的事件列表)和 handler.py(提供 handle(event_type, context) 函数):

复制代码
# HookRegistry.emit() --- 简化

handlers = list(self._handlers.get(event_type, [ ]))

# 通配符匹配:command:* 匹配任意 command:xxx
if ":" in event_type:
    base = event_type.split(":")[0]
    wildcard_key = f"{base}:*"

    handlers.extend(self._handlers.get(wildcard_key, [ ]))

for fn in handlers:
    result = fn(event_type, context)
    if asyncio.iscoroutine(result):
        await result

一个内置 hook 的例子是 boot-md:它监听 gateway:startup 事件,在 Gateway 启动时自动执行 BOOT.md 里的引导任务。默认文件位于 ~/.hermes/BOOT.md;启用 profile 后则位于当前 HERMES_HOME/BOOT.md

Hook 的错误处理原则是:catch + log,绝不阻塞主管线。一个坏掉的 hook 不会导致消息处理失败。


Cron 投递与 Home Channel

Hermes 的 Gateway 进程内置了一个 Cron ticker------一个后台 daemon 线程,每隔 60 秒检查一次定时任务:

复制代码
# gateway/run.py --- Cron ticker 启动
cron_thread = threading.Thread(
    target=_start_cron_ticker,
    args=(cron_stop,),
    kwargs={"adapters": runner.adapters, "loop": asyncio.get_running_loop()},
    daemon=True,
    name="cron-ticker",
)

Cron 任务执行完毕后,输出需要投递到某个地方。这就涉及到投递路由机制。

DeliveryTarget

gateway/delivery.py 定义了 DeliveryTarget 类,支持 4 种目标格式:

格式 含义
"origin" 回到创建任务的原始聊天
"local" 保存到本地文件(默认 ~/.hermes/cron/output/;profile 下随 HERMES_HOME 变化)
"telegram" 发到 Telegram 的 home channel
"telegram:123456" 发到指定的 Telegram 聊天

Home Channel

每个平台可以配置一个 home channel------默认投递目的地:

复制代码
@dataclass
class HomeChannel:
    platform: Platform
    chat_id: str
    name: str  # 显示用的可读名称

配置示例(config.yaml):

复制代码
gateway:
  platforms:
    telegram:
      enabled: true
      token: "bot_token_here"
      home_channel:
        chat_id: "123456789"
        name: "My Notes"

当用户创建一个 cron 任务,指定 deliver="telegram" 但不给 chat_id 时,输出就发到这个 home channel。如果 origin 不可用(比如 cron 任务是在 CLI 创建的,但 CLI 已经退出),home channel 是 fallback;如果 home channel 也没配,local 作为最终兜底------输出永远不会丢。

这套投递机制还被注入到系统提示中(前面展示的 Session Context),让 Agent 知道"我可以把定时任务的结果投递到哪些地方"。这样当用户说"每天早上帮我搜一下新闻,发到我的 Telegram",Agent 可以正确构造 cron 任务的 deliver 参数。


故障恢复:断了还能接上

Gateway 长驻运行,网络断连是常态。适配器的故障处理分三层。

发送重试

_send_with_retry()base.py 第 1487 行)是所有消息发送的底层包装:

  • 遇到连接类错误(connectionresetconnectionrefused 等):指数退避重试,最多 2 次

  • 遇到格式化错误:降级为纯文本重试

  • 明确区分 timeout 和连接错误_is_timeout_error() 单独判断。read/write timeout 说明请求可能已经到达服务端,盲目重试会导致重复发送------所以 timeout 不重试

    _RETRYABLE_ERROR_PATTERNS = (
    "connecterror", "connectionerror", "connectionreset",
    "connectionrefused", "connecttimeout", "network",
    "broken pipe", "remotedisconnected", "eoferror",
    )

注意列表里有 connecttimeout(连接建立超时,安全重试)但没有 timed out(读写超时,不安全重试)。

适配器级断连恢复

如果一个平台适配器遇到不可恢复错误(比如 bot token 被撤销),它调用 _set_fatal_error() 标记自己。GatewayRunner._handle_adapter_fatal_error() 接管:

  • 如果 retryable=True:放入 _failed_platforms 队列,等待后台重连

  • 如果 retryable=False:记录错误,不再尝试

后台重连看门狗

_platform_reconnect_watcher() 是一个后台 asyncio task,专门负责重连失败的平台:

  • 初始延迟 10 秒

  • 指数退避:30 * (2 ** (attempt - 1)),最大封顶 300 秒

  • 最多重试 20 次

  • 重连成功后更新运行时状态、重建 channel directory,并恢复适配器服务

这保证了即使某个平台临时不可用(比如 Telegram API 维护),Gateway 整体继续运行,等 API 恢复后自动重连------用户无需手动干预。


PII 脱敏:别把电话号码送给 LLM

一个容易被忽略的细节:Gateway 在构建系统提示时支持 PII 脱敏。

复制代码
_PII_SAFE_PLATFORMS = frozenset({
    Platform.WHATSAPP, Platform.SIGNAL,
    Platform.TELEGRAM, Platform.BLUEBUBBLES,
})

对于这些平台,build_session_context_prompt() 可以安全地把用户 ID / chat ID 替换成确定性的 SHA-256 哈希。关键判断标准不是"ID 是否就是手机号",而是这些平台不依赖原始 ID 参与 mention 语法,因此脱敏后不会破坏 Agent 的交互能力:

复制代码
def _hash_sender_id(value: str) -> str:
    return f"user_{hashlib.sha256(value.encode('utf-8')).hexdigest()[:12]}"

而 Discord 被排除在外,因为 Discord 的消息提及(mention)语法是 <@user_id>,LLM 需要真实 ID 才能正确 at 用户。

哈希是确定性的:同一个用户 ID 永远映射到同一个哈希值。这意味着 Agent 虽然看不到真实标识符,仍然可以跨轮次识别"这是同一个用户"。对于 WhatsApp、Signal 这类平台,手机号会在脱敏过程中被一并剥离;而像 Telegram 这样的场景,即使 user ID 本身不是手机号,也仍然可以安全哈希。路由层继续使用原始 ID------脱敏只发生在送往 LLM 的系统提示中。


总结:Gateway 的设计原则

回顾整个 Gateway 架构,有几个一致的设计原则:

  1. 归一化在边缘做,runtime 保持平台无关。 适配器把平台差异消化在自己内部,MessageEvent 是 Agent runtime 看到的唯一接口。

  2. 默认安全降级,按需覆写增强。 4 个抽象方法保证最小可用,十余个可选方法允许丰富体验。新平台的接入成本被拉到最低。

  3. Session key 决定上下文边界。 一条确定性公式,根据 platform / chat / thread / user 的组合计算 key。线程内共享、群组内隔离的默认策略,映射了真实的用户心智模型。

  4. 后台执行 + 中断支持。 消息处理在后台 task 中异步运行,新消息随时可到达。/stop 可以中断运行中的 Agent,pending queue 保证消息不丢。

  5. 投递永远有兜底。 origin → home channel → local,三级 fallback 确保 cron 输出不会凭空消失。

如果你正在做类似的事情------让同一个 Agent 能力跑在多个消息平台上------Hermes 的这套方案提供了一个经过实战验证的参考。核心不是"写 20 个适配器",而是定义好那个居中的协议层:统一的消息格式、确定性的 session 隔离、渐进式的能力覆写。有了这一层,每多接一个平台,就只是再填一个适配器。

相关推荐
小杨在厦门1 小时前
从“凭感觉管”到“靠数据管”:AI验布数据如何重塑纺织企业决策模式
人工智能·服装·服装厂·服装机械·铺布机
小草cys1 小时前
NVIDIA 驱动(550版本)成功安装后安装支持 GPU 加速的 PyTorch
人工智能·pytorch·python
深小乐1 小时前
Obsidian首页实在忍不了了,花了两个小时,没想到能捣鼓到这么漂亮
人工智能
共享家95271 小时前
OpenClaw的通道配置
人工智能·学习·openclaw
Omics Pro1 小时前
「自兹以往」动物肠道微生物组
数据库·人工智能·机器学习·语言模型·自然语言处理
oddsand12 小时前
pgvector 三大相似度算法
人工智能·算法·机器学习
2601_955781982 小时前
私有化本地 AI,Windows 平台 OpenClaw 功能详解与配置
人工智能·开源·github·open claw
红色星际2 小时前
Momenta赴美招揽AI人才
人工智能
贺国亚2 小时前
Spring-AI与LangChain4j
java·人工智能·spring