跳到主要内容

总线、渠道与 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):

类型关键字段备注
InboundMessagechannelsender_idchat_idcontentmediametadatasession_key 默认是 f"{channel}:{chat_id}",可被 session_key_override 改(线程级会话)
OutboundMessagechannelchat_idcontentreply_tomediabuttonsmetadata 可带 _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):

  1. PROVIDERS 加一条 ProviderSpec
  2. config/schema.pyProvidersConfig 加一个字段。

完事——「环境变量、配置匹配、status 显示」全都从这条 spec 派生。

ProviderSpec 携带的差异(挑要点)

维度字段作用
用哪个实现backendopenai_compat/anthropic/azure_openai/bedrock/openai_codex/github_copilot
怎么匹配keywordsdetect_by_key_prefixdetect_by_base_keyword按模型名关键词、key 前缀(如 sk-or-)、base URL 关键词识别
网关/本地is_gatewayis_localstrip_model_prefix网关能路由任意模型族;本地部署走 fallback
思考模式thinking_stylegateway_reasoning_stylereasoning_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.pyMessageBus
消息事件nanobot/bus/events.pyInboundMessageOutboundMessagesession_key
渠道契约nanobot/channels/base.pyBaseChannel.start/stop/sendsend_delta
渠道发现nanobot/channels/manager.py(扫描 + entry points)
Provider 注册表nanobot/providers/registry.pyProviderSpecPROVIDERS
Provider 基类/重试nanobot/providers/base.pyLLMProviderchat_with_retry
实例化nanobot/providers/factory.py(按 spec 造实现)