Skip to content

Day 2 — Channel 适配器

读完这章你能获得什么:理解"渠道"层的设计思想——如何让用户通过命令行、网页、微信等不同方式与 Agent 交互,而 Agent 不需要关心用户是从哪来的。


一、前情提要

在 Day 1 中,我们搭建了 Gateway——一个基于 WebSocket 的消息分拣中心。但有一个问题:

用户必须自己写 WebSocket 客户端才能和系统交互。这对普通用户来说太难了。

我们需要提供更友好的接入方式:有人喜欢用命令行,有人习惯网页聊天,有人用飞书/微信。这些不同的"入口"就是 Channel(渠道)


二、生活类比:快递的不同寄件方式

还是回到快递公司的例子。Day 1 我们造好了分拣中心,现在要解决的是:用户怎么把包裹交给我们?

寄件方式 1:上门取件
  快递员到你家门口 -> 你把东西给他 -> 他贴上标准面单 -> 送到分拣中心
  对应 -> CLIChannel(命令行,快递员就是终端程序)

寄件方式 2:驿站寄件
  你走到楼下驿站 -> 把东西交给前台 -> 前台帮你填面单 -> 送到分拣中心
  对应 -> WebChatChannel(网页聊天,驿站就是 HTTP 服务)

寄件方式 3:合作网点
  你在淘宝下单退货 -> 淘宝通知合作网点 -> 网点安排取件 -> 送到分拣中心
  对应 -> WebhookChannel(第三方回调,淘宝就是飞书/微信)

关键洞察:不管从哪寄的,到了分拣中心都是同一种面单(GatewayMessage)。分拣中心不关心你是上门取的还是驿站收的——它只看面单。


三、本章要解决的问题

问题解决方案
不同渠道的输入格式不一样每个 Channel 内部做转换,输出统一的 GatewayMessage
新增渠道时不想改已有代码定义 Channel 抽象基类(ABC),新渠道只需继承并实现接口
Agent 回复时要知道从哪来的Channel 负责把回复"送回去"(CLI 打印到屏幕、WebChat 推送到浏览器)

四、核心概念详解

4.1 Channel 抽象基类——"寄件方式的规范"

Channel 是一个"模具"——它规定了"不管什么渠道,都必须有这四个能力":

方法类比说明
start()营业:打开门,准备接客启动渠道(打开终端、启动 HTTP 服务等)
stop()关门:收拾东西,下班停止渠道,释放资源
receive()收件:源源不断地收包裹异步迭代器,持续接收用户输入
send(message)派件:把回复送到用户手上向渠道发送 Agent 的回复

为什么要定义抽象基类?因为这样 Agent 运行时可以用同一套代码驱动所有渠道:

python
# Agent 不需要知道 channel 到底是 CLI 还是 WebChat
async for msg in channel.receive():   # 不管从哪来
    reply = await agent.process(msg)   # 统一处理
    await channel.send(reply)          # 不管送到哪

4.2 CLIChannel——命令行交互

最简单的渠道,适合本地开发和调试:

  • 用户在终端里打字,程序读取输入
  • 如果输入以 / 开头(如 /help),识别为 COMMAND 类型
  • 普通文本识别为 TEXT 类型
  • 输入 /quit 退出
用户                        CLIChannel
 |                              |
 |-- 键入 "你好" ------------->  |
 |                              |-- 转为 GatewayMessage(type=TEXT)
 |                              |
 |-- 键入 "/help" ----------->  |
 |                              |-- 转为 GatewayMessage(type=COMMAND)
 |                              |
 |<- 打印 "Agent: 你好!" ----|

4.3 WebChatChannel——网页聊天

稍微复杂一点,提供了两种输入方式和一个健康检查:

端点方法用途
/chatPOST接收 JSON 消息(适合脚本和测试)
/wsWebSocket实时聊天(适合前端页面)
/healthGET健康检查(系统是否正常运行)

内部工作原理:不管消息从 /chat 还是 /ws 进来,都放入同一个 asyncio.Queue(队列),由 receive() 统一消费:

为什么用队列? 类比驿站前台:不管是顾客自己送来的还是快递员送来的包裹,前台都先放到一个架子上,后台的人按顺序处理。这样前台不会被堵住。

4.4 WebhookChannel——第三方平台回调

当你的 Agent 要接入飞书、微信等平台时,这些平台会在有新消息时主动向你发送 HTTP 请求(这叫 Webhook,即"网络钩子")。

问题:每个平台发来的 JSON 格式都不一样。

解决方案:可插拔的解析器——给不同平台注册不同的"翻译器":

python
# 注册飞书解析器
webhook.register_parser("feishu", parse_feishu_message)

# 注册微信解析器
webhook.register_parser("wechat", parse_wechat_message)

# 飞书的 Webhook 发到 /webhook/feishu -> 自动用飞书解析器处理
# 微信的 Webhook 发到 /webhook/wechat -> 自动用微信解析器处理

五、三种渠道对比

维度CLIChannelWebChatChannelWebhookChannel
适合谁开发者调试普通用户、前端联调企业应用对接
输入方式终端键盘输入HTTP POST 或 WebSocket第三方平台 HTTP 回调
输出方式终端打印WebSocket 推送可选的回调 URL
并发能力单用户多用户多用户
部署要求需要开放端口需要公网可达

六、动手实验指南

6.1 运行示例

bash
python day2-channel/example/main.py

这会启动一个 CLI 渠道的 echo 机器人。你输入什么它就回什么(加上 echo 前缀)。输入 /quit 退出。

6.2 改一改,看看会怎样

实验 1:在 CLIChannel 的示例中,添加一个命令 /time,输入后返回当前时间。

实验 2:把示例改成使用 WebChatChannel,然后用 curl 发送消息测试:

bash
curl -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "hello", "user_id": "test_user"}'

6.3 运行测试

bash
pytest day2-channel/channel/ -v

七、常见问题 FAQ

Q1:为什么 receive() 是异步迭代器(async generator),而不是返回一个消息列表?

A:因为消息是源源不断来的,我们不知道总共有多少——就像快递驿站的包裹,不可能等"今天所有包裹都到了"再开始处理。异步迭代器可以"来一个处理一个",不浪费等待时间。

Q2:Channel 和 Gateway 有什么区别?为什么不合并成一个?

A:职责不同。Channel 负责"从不同来源收集消息,转成统一格式";Gateway 负责"接收统一格式的消息,路由分发处理"。分开可以随意增减渠道,而不影响核心的路由逻辑。类比:快递公司的"收件网点"和"分拣中心"是两个部门。

Q3:如果我想加一个 Telegram Bot 渠道怎么办?

A:创建一个 TelegramChannel 类,继承 Channel,实现 start/stop/receive/send 四个方法就行。核心是在 receive() 里把 Telegram 的消息格式转成 GatewayMessage。其他代码一行不用改。


下一章

消息能进来了,但还有一个问题:如果同一个用户发了多条消息,系统怎么知道"这些消息是同一场对话"?

下一章 Day 3: 会话管理 将解决这个问题。