Skip to content

第六章 统一网关——一个大脑,万千渠道

核心问题:一个 Agent 大脑,如何同时"住在" WhatsApp、Telegram、Discord 等多个渠道里,并且在每个渠道都认识你?


你在 WhatsApp 和 Agent 聊了半小时:项目进度、代码风格偏好、下周的计划。你们配合得很顺畅,它已经摸清了你的工作习惯。

第二天你打开 Telegram,想接着聊昨天的话题。Agent 的第一句话:"你好!有什么我可以帮你的?"

它不认识你了。

这不是记忆出了问题——WhatsApp 里的那些对话确实存在。问题是,Telegram 上的它根本不知道你在 WhatsApp 上做过什么。在它眼里,你是个陌生人,你们是第一次见面。

如果你再接入 Discord,还会再来一次。N 个平台,N 个"第一次见面"。这是 Agent 面向真实世界时必须解决的第一个难题——平台孤岛

统一网关,就是拆掉这些孤岛的答案。


一、平台孤岛的困境

每个平台都有自己的"语言"。你在 WhatsApp 的用户 ID 是手机号,在 Telegram 是一串数字,在 Discord 是一个 Snowflake ID。同一个人,在三个平台上是三个彼此不认识的陌生人。

更麻烦的是,这不只是 ID 格式不同:

平台用户 ID 格式消息字段认证方式
Telegram纯数字(如 123456789message.textBot Token
DiscordSnowflake ID(17-20位)message.contentOAuth2
飞书open_idou_ 开头)嵌套 JSONtenant_access_token

三套解析逻辑,三套认证流程。如果不加以处理,每接入一个新平台,就要在系统核心里凿出一块位置,塞进去一套平台专用代码。半年后,"支持 Telegram"和"推理用户意图"的代码深度交织,改哪里都战战兢兢。

最痛的不是格式不同,而是身份割裂:Agent 不知道"那三个陌生人其实是同一个你"。


二、统一网关:一个入口,万千渠道

Gateway 的核心思路很简单:在平台和 Agent 大脑之间,放一个翻译层。

┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────┐
│ WhatsApp │  │ Telegram │  │ Discord  │  │  飞书   │
└────┬─────┘  └────┬─────┘  └────┬─────┘  └───┬────┘
     │             │             │            │
     │         各平台适配器(方言 → 普通话)      │
     └──────────────────┬─────────────────────┘
                        │  统一消息格式
                 ┌──────┴──────┐
                 │   Gateway   │
                 └──────┬──────┘

                 ┌──────┴──────┐
                 │  Agent 大脑  │(永远只看到"用户"和"消息")
                 └─────────────┘

Agent 大脑永远只接收统一格式的消息:谁发的、在哪个会话里、说了什么。它对"这条消息来自 Telegram 还是 Discord"这件事一无所知,也永远不需要知道。

Gateway 承担三个核心职责。身份识别:把各平台的陌生 ID 翻译成"这是张三"。会话路由:把不同渠道的消息分发到正确的对话上下文,不串线。格式翻译:把各平台的原始消息结构,统一成 Agent 能处理的标准格式。

这条边界一旦划定,系统的演化就自由了。Agent 大脑的推理能力可以独立升级,不受渠道更迭的干扰;接入新平台也不需要动 Agent 的任何一行代码。


三、适配器模式:新平台接入的代价

接入新平台的代价应该是多少?

理想答案是:理解那个平台的 API,然后实现一个接口。Agent 核心代码零改动。

OpenClaw 通过 ChannelPlugin 接口实现了这一点。每个平台对应一个适配器,负责完成双向翻译:

入站(用户 → Agent)
  平台原始事件  →  适配器解析  →  统一 Message 格式  →  Agent

出站(Agent → 用户)
  Agent 回复  →  适配器渲染  →  平台原生格式  →  用户

适配器是翻译官,出现在链路的两端。入站时把"平台方言"翻译成"系统普通话",出站时再把普通话翻译回方言。Agent 大脑始终生活在普通话的世界里,对方言的存在毫无察觉。

接口本身分两层,设计上刻意降低了入门门槛:

ChannelPlugin 接口
├── [必选] 身份层     ── 渠道的唯一标识
├── [必选] 消息收发层 ── 接收与发送消息
└── [可选] 扩展能力层
        ├── 流式回复(打字机效果)
        ├── 交互按钮(飞书卡片、Discord 组件)
        ├── OAuth 认证
        └── 线程回复

一个新平台的原型适配器,只需实现必选两层就能跑通基本流程——能收消息,能发消息。其余能力按需渐进添加。低门槛入口,高上限天花板,这是好的可扩展接口设计共同具备的特质。

这里有一个重要的设计哲学:各显神通。Gateway 不应该把所有平台强制统一成最低公分母——Discord 支持流式输出就让它流,飞书支持"正在输入"指示器就让它显示。平台的差异化能力是通过配置项来声明的,而不是靠代码里的 if-else 判断。

以 Discord 和飞书为例,两者的能力开关截然不同:

json5
// Discord:开启流式输出,Agent 生成时用户实时看到进度
{
  channels: {
    discord: {
      enabled: true,
      streaming: true,
      actions: { messages: true, reactions: true, threads: true }
    }
  }
}

// 飞书:开启"正在输入"指示器,Agent 思考时给用户反馈
{
  channels: {
    feishu: {
      enabled: true,
      typingIndicator: true
    }
  }
}

两个平台,两套能力开关,各自发挥所长。


四、优雅降级与跨平台身份

优雅降级:统一而不单调

Agent 的回复始终以 Markdown 格式输出。这是一个有意的设计选择:Markdown 是内容意图,不是平台表达。具体怎么呈现,由各平台的适配器决定。

            Agent 输出 Markdown 回复

          ┌─────────────┼──────────────┐
          │             │              │
    Discord 适配器  飞书适配器      纯文本平台
          │             │              │
    嵌入式消息卡片  交互式按钮卡片   去除格式标记
    代码块高亮      "确认/取消"按钮  内容完整呈现
          │             │              │
          └─────────────┼──────────────┘

              用户看到最适合其平台的形式

Discord 把 Markdown 渲染成带颜色边框的结构化卡片;飞书把"请确认是否部署"变成两个可点击的按钮;没有富文本能力的平台,就去掉格式标记,把内容清晰地以纯文字呈现。

信息被完整传递,差异只在"外衣",而非"内容"。统一而不单调,多样而不混乱——这是 Gateway 出站设计的核心价值观。

除了格式渲染,平台能力的差异化还体现在交互反馈上。举个典型例子:你在 Discord 问 Agent "帮我分析这个项目"——这需要几秒钟。Discord 适配器开启流式输出后,系统会先发一条预览消息,然后随着 Agent 逐步生成内容,不断编辑这条消息——用户看到消息在实时更新,就像看到对方正在打字一样。等 Agent 生成完毕,预览消息就变成了完整答案。飞书没有这个能力,但飞书有"正在输入"指示器,同样能让用户知道 Agent 没有沉默,而是在思考。

有能力的平台提供更好的体验,没有这个能力的平台至少保证功能可用——这就是优雅降级的实际含义。

跨平台身份关联:一份记忆,无处不在

身份割裂的问题,通过 identityLinks 配置来解决。做法是显式声明:不同平台上的 ID,属于同一个人。

第一步是 ID 规范化。所有平台的用户 ID 统一转换成 平台:原始ID 格式:

telegram:123456789
discord:987654321012345678
slack:U12345ABC

第二步是显式绑定。在配置中声明这三个 ID 都是"张三":

identityLinks:
  "zhangsan": ["telegram:123456789", "discord:987654321012345678"]

绑定建立后,无论张三从哪个渠道发消息,Agent 都能认出他:

张三在 Telegram 发消息
  → Gateway 规范化 ID:telegram:123456789
  → 查询 identityLinks → 匹配到"张三"
  → 加载张三的偏好与记忆 → 按他的习惯生成回复

第二天张三改用 Discord
  → Gateway 规范化 ID:discord:987654321012345678
  → 查询 identityLinks → 同样匹配到"张三"
  → Agent 仍然认识他,偏好完整保留

不过并非所有信息都应该跨渠道共享:

信息类型范围理由
用户偏好(代码风格、语言习惯等)全局共享这是"认识你这个人",与渠道无关
当前对话上下文会话隔离不同渠道的对话有各自的节奏和目的

可以把这想成 Agent 手里有两个笔记本

  • 笔记本一(共享记忆):记录跨渠道的长期信息——你的代码风格偏好、常用语言、项目约定。所有渠道共用这本笔记本,无论你从哪个平台来,Agent 都翻同一本。
  • 笔记本二(独立上下文):每个会话自己的对话历史。你在 WhatsApp 聊的,Telegram 那边的笔记本上没有;你在 Telegram 问的,WhatsApp 那边也不知道。

这就是为什么同一个 Agent 在 WhatsApp 和 Telegram 上都认识你(翻的是同一本偏好笔记本),但两边的对话互不干扰(各自的上下文笔记本是隔离的)。渠道是临时的选择,身份是持久的存在。


五、容错与消息分割

错误分类:什么能重试,什么不能

网络抖动、平台 API 限流、Token 过期——多渠道架构下,发送失败是家常便饭。Gateway 不是简单地报错了事,而是根据错误类型决定对策:

错误类型说明处理策略
渠道未启用渠道配置为 disabled 或未启动不重试,直接报错(重试也没用)
速率限制(Rate Limit)平台返回 429 错误等待后重试(暂时的,稍后会好)
临时错误网络抖动、连接超时指数退避自动重试
发送彻底失败内容违规、权限不足不重试,记录日志,通知用户

这套分类的核心逻辑:判断失败是临时的还是永久的。临时的,等一等再试;永久的,尽早失败、尽早通知。

当遇到可重试的错误时,系统采用指数退避策略——每次重试前等待时间指数增长,既能自动恢复临时故障,又不会把平台 API 打挂。

长消息自动分割:代码块边界感知

不同平台对单条消息的长度有限制(Discord 默认 2000 字符,Telegram 默认约 4096 字符)。当 Agent 生成超长回复时,Gateway 会自动切分发送。

但这里有一个工程细节:切分时要识别代码块边界,不在代码块中间截断。想象一段 Python 函数被切成两半发送——第一条消息只有前半截,第二条消息突然从函数中间开始,可读性会很差。聪明的切分策略会找合适的分割点(段落边界、空行),确保每一段都是语义完整的。


小结

核心能力实现方式
多平台统一入口Gateway 作为所有渠道消息的唯一处理层
低成本接入新平台ChannelPlugin 适配器接口,核心代码零改动
跨平台一致体验适配器双向翻译,Agent 只看统一格式
优雅降级出站渲染按平台能力决定,内容不变,外衣各异
跨平台身份统一identityLinks 显式绑定,偏好全局共享,上下文会话隔离
容错与重试按错误类型分类处理:临时错误指数退避重试,永久错误快速失败
长消息分割按 textChunkLimit 自动切分,识别代码块边界,不在代码块中间截断

Gateway 是 Agent 的感官系统。它把外部世界的复杂性——各平台不同的 ID、格式、认证——全部消化在自己这一层,让 Agent 大脑活在一个只有"用户"和"消息"的纯粹世界里。感官的工作是规范化,大脑的工作是推理;二者之间有清晰而稳定的边界,是系统长期健康的根本保证。

一个设计良好的 Gateway 是无感的——它把自己的工作做得悄无声息,让 Agent 专注于真正该做的事。


第七章 安全沙箱