总线、渠道与 Provider
这一章讲 nanobot 的「边缘」:消息怎么进出(总线 + 渠道),以及大模型后端怎么被抽象成可插拔的 Provider。想加一个新聊天平台或新模型,看这里。
3.1 消息总线:两个队列,仅此而已
MessageBus(bus/queue.py:8-44)朴素到一句话能说完:两个 asyncio.Queue,一个入站、一个出站。渠道 publish_inbound 投递、agent consume_inbound 消费;agent publish_outbound、渠道 consume_outbound。
这种极简解耦带来三个好处:渠道与核心互不依赖、天然支持多渠道并发、整个核心可以脱离任何真实平台单测。
两种消息(bus/events.py):
| 类型 | 关键字段 | 备注 |
|---|---|---|
InboundMessage | channel、sender_id、chat_id、content、media、metadata | session_key 默认是 f"{channel}:{chat_id}",可被 session_key_override 改(线程级会话) |
OutboundMessage | channel、chat_id、content、reply_to、media、buttons | metadata 可带 _agent_ui 富 UI blob,非 WebUI 渠道忽略未知键 |
session_key(events.py:32-35)是会话身份的根:同一个 chat 的消息归到同一个会话历史,这就是「跨多轮记住上下文」在键层面的落地。
3.2 渠道:实现一个抽象基类即可
所有平台集成都继承 BaseChannel(channels/base.py:21-102),要实现的核心是三个抽象方法:
start():长跑任务——连上平台、监听消息、把收到的转成InboundMessage投到总线。stop():清理资源。send(msg):把OutboundMessage发回平台;失败要抛异常,好让上层 channel manager 统一做重试。
可选能力以「有就重写、没有就默认」的方式提供:login()(如扫码登录)、send_delta()(流式分块)、send_reasoning_delta()(流式思考)、transcribe_audio()(语音转写,基类已给了 Whisper 默认实现,base.py:48-60)。基类还内建了配对/审批(pairing):陌生 DM 发送者需先配对才放行。
渠道的发现方式和工具一样:内置模块扫描 + entry-point 插件,由 channels/manager.py 协调(docs/architecture.md:76-88)。自定义渠道照着 docs/channel-plugin-guide.md 写即可。
3.3 Provider:注册表即真相源
它要解决的小问题
要支持几十家大模型,而它们的差异散落在很多维度:用哪个实现、API key 环境变量叫什么、模型名怎么匹配、是不是网关、思考模式怎么开、reasoning_effort 词表怎么映射……如果每家都散写,加一个就要改十处。nanobot 把全部差异收进一个 dataclass ProviderSpec(providers/registry.py:21-114),PROVIDERS 元组就是唯一真相源。
两步加一个 provider
文件顶部的注释直接写明了流程(registry.py:1-11):
- 往
PROVIDERS加一条ProviderSpec。 - 往
config/schema.py的ProvidersConfig加一个字段。
完事——「环境变量、配置匹配、status 显示」全都从这条 spec 派生。
ProviderSpec 携带的差异(挑要点)
| 维度 | 字段 | 作用 |
|---|---|---|
| 用哪个实现 | backend | openai_compat/anthropic/azure_openai/bedrock/openai_codex/github_copilot |
| 怎么匹配 | keywords、detect_by_key_prefix、detect_by_base_keyword | 按模型名关键词、key 前缀(如 sk-or-)、base URL 关键词 识别 |
| 网关/本地 | is_gateway、is_local、strip_model_prefix | 网关能路由任意模型族;本地部署走 fallback |
| 思考模式 | thinking_style、gateway_reasoning_style、reasoning_effort_remap | 各家开关思考/推理力度的方言差异 |
| 缓存 | supports_prompt_caching | 如 Anthropic 的 prompt 缓存 |
大多数托管 provider 复用 openai_compat 实现;只有 Anthropic、Azure、Bedrock、Codex、Copilot 有专用路径(docs/architecture.md:56-68)。实例化由 providers/factory.py 负责。
fallback 与重试
命名模型预设(modelPresets)支持 fallbackModels:主模型挂了自动切备用(README.md:347)。重试与流式 idle 超时在 LLMProvider 基类统一实现(providers/base.py:198-200 的 _CHAT_RETRY_DELAYS 等);AgentRunner 调的是 chat_with_retry/chat_stream_with_retry(runner.py:808-846),重试策略对上层透明。
3.4 网关与 WebUI 的关系
nanobot gateway 启动:所有启用的渠道 + (配置了的)WebSocket 渠道 + 工作区 cron 服务 + Dream/心跳等系统任务 + 健康端点(docs/architecture.md:90-107)。
一个容易踩的点:打 包的 WebUI 由 WebSocket 渠道在 8765 端口提供,不是健康端点;健康端点在 18790。WebUI 源码在 webui/,生产构建产物被打进 wheel 的 nanobot/web/dist/。
4. 巧妙之处
- 总线就是两个队列。 不上消息中间件,核心可纯单测,渠道可热插拔(
bus/queue.py)。 - Provider 差异全收进一个 dataclass。 加 provider 改两处,其余派生(
ProviderSpec)。 - session_key 默认 =
channel:chat_id。 会话身份零配置就成立,线程会话用 override(events.py:32-35)。 - send 失败必须抛。 重试集中在 manager,渠道实现保持简单(
base.py:92-102)。
5. 边界与局限
- 总线无持久化。 队列在内存里;进程重启时在途的入站消息会丢——耐久性靠会话存盘与运行时检查点(01 章),不是靠总线。
- provider 抽象偏向 OpenAI 形态。 非 OpenAI-兼容的家(Anthropic/Bedrock 等)需要专用 backend,
ProviderSpec里能看到大量「方言补丁」字段说明这层抹平并不免费。
6. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 消息总线 | nanobot/bus/queue.py | MessageBus |
| 消息事件 | nanobot/bus/events.py | InboundMessage、OutboundMessage、session_key |
| 渠道契约 | nanobot/channels/base.py | BaseChannel.start/stop/send、send_delta |
| 渠道发现 | nanobot/channels/manager.py | (扫描 + entry points) |
| Provider 注册表 | nanobot/providers/registry.py | ProviderSpec、PROVIDERS |
| Provider 基类/重试 | nanobot/providers/base.py | LLMProvider、chat_with_retry |
| 实例化 | nanobot/providers/factory.py | (按 spec 造实现) |