主题
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——网页聊天
稍微复杂一点,提供了两种输入方式和一个健康检查:
| 端点 | 方法 | 用途 |
|---|---|---|
/chat | POST | 接收 JSON 消息(适合脚本和测试) |
/ws | WebSocket | 实时聊天(适合前端页面) |
/health | GET | 健康检查(系统是否正常运行) |
内部工作原理:不管消息从 /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 -> 自动用微信解析器处理五、三种渠道对比
| 维度 | CLIChannel | WebChatChannel | WebhookChannel |
|---|---|---|---|
| 适合谁 | 开发者调试 | 普通用户、前端联调 | 企业应用对接 |
| 输入方式 | 终端键盘输入 | 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: 会话管理 将解决这个问题。