第六章 统一网关——一个大脑,万千渠道
核心问题:一个 Agent 大脑,如何同时"住在" WhatsApp、Telegram、Discord 等多个渠道里,并且在每个渠道都认识你?
你在 WhatsApp 和 Agent 聊了半小时:项目进度、代码风格偏好、下周的计划。你们配合得很顺畅,它已经摸清了你的工作习惯。
第二天你打开 Telegram,想接着聊昨天的话题。Agent 的第一句话:"你好!有什么我可以帮你的?"
它不认识你了。
这不是记忆出了问题——WhatsApp 里的那些对话确实存在。问题是,Telegram 上的它根本不知道你在 WhatsApp 上做过什么。在它眼里,你是个陌生人,你们是第一次见面。
如果你再接入 Discord,还会再来一次。N 个平台,N 个"第一次见面"。这是 Agent 面向真实世界时必须解决的第一个难题——平台孤岛。
统一网关,就是拆掉这些孤岛的答案。
一、平台孤岛的困境
每个平台都有自己的"语言"。你在 WhatsApp 的用户 ID 是手机号,在 Telegram 是一串数字,在 Discord 是一个 Snowflake ID。同一个人,在三个平台上是三个彼此不认识的陌生人。
更麻烦的是,这不只是 ID 格式不同:
| 平台 | 用户 ID 格式 | 消息字段 | 认证方式 |
|---|---|---|---|
| Telegram | 纯数字(如 123456789) | message.text | Bot Token |
| Discord | Snowflake ID(17-20位) | message.content | OAuth2 |
| 飞书 | open_id(ou_ 开头) | 嵌套 JSON | tenant_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 和飞书为例,两者的能力开关截然不同:
// 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 专注于真正该做的事。
→ 第七章 安全沙箱