跳到主要内容

Provider 与 Platform:两套「抹平差异」的适配器

这章讲什么: AstrBot 要同时接住 20+ 个大模型和 18 个聊天平台,每家接口都不一样。它靠两套对称的适配器抽象做到「核心逻辑只面对统一接口」:Provider 抹平模型差异,Platform 抹平 IM 差异。本章讲这两套抽象长什么样、怎么注册、怎么处理「不同模型/平台能力不一致」的问题。

1. 它要解决的小问题

OpenAI 的 SDK、Anthropic 的 SDK、Gemini 的 SDK——参数名、消息格式、工具调用协议全不同。QQ 的消息结构、Telegram 的、飞书的——也全不同。

如果核心代码直接调这些 SDK,就会到处是 if provider == "openai": ... elif ...,无法维护。AstrBot 的解法是经典的适配器模式:定义一个统一接口,每家厂商写一个适配器去实现它。核心代码只认接口。

核心 Agent 逻辑
(只认统一接口)
┌────────┴────────┐
▼ ▼
Provider 抽象 Platform 抽象
(text_chat 等) (AstrMessageEvent 进/出)
┌───┼───┐ ┌───┼───┐
▼ ▼ ▼ ▼ ▼ ▼
OpenAI Claude... QQ 飞书 TG...
各家适配器 各家适配器

2. Provider:模型适配器

2.1 统一接口

所有聊天模型适配器都继承 Provider(astrbot/core/provider/provider.py:66),核心方法是 text_chat:

# 简化自 provider.py:96 的抽象方法签名
async def text_chat(
self,
prompt: str | None = None,
image_urls: list[str] | None = None, # 多模态:图片
audio_urls: list[str] | None = None, # 多模态:音频
func_tool: ToolSet | None = None, # function calling 工具集
contexts: list[Message] | list[dict] | None = None, # 多轮上下文
system_prompt: str | None = None,
tool_calls_result=None, # 回传给 LLM 的工具结果
tool_choice="auto", # auto / required
) -> LLMResponse: ...

核心 Agent 循环里调的就是这个统一方法(经由 _iter_llm_responses_with_fallback),它完全不知道背后是 OpenAI 还是 Claude。流式版是 text_chat_stream(provider.py:135)。

2.2 不止聊天:五种 Provider

AbstractProvider(provider.py:27)是更顶层的基类,派生出五类:

干什么关键方法
Provider聊天大模型text_chat / text_chat_stream
STTProvider语音转文字get_text(provider.py:220)
TTSProvider文字转语音get_audio / get_audio_stream(provider.py:251)
EmbeddingProvider文本向量化(知识库用)get_embedding / get_embeddings_batch(provider.py:326)
RerankProvider检索结果重排rerank(provider.py:418)

每类都有 test() 自检方法(如 Provider.testREPLY PONG ONLY,provider.py:207),用于 WebUI 里「测试连接」。具体厂商实现都在 astrbot/core/provider/sources/(如 openai_source.pyanthropic_source.pygemini_source.py)。

2.3 能力差异怎么处理:modalities

不是每个模型都支持图片/音频输入。Provider 的配置里有 modalities(支持的模态列表)。Agent 装配时会查这个:

  • 选模型时降级:当前模型不支持图片但请求带图片,自动切到支持图片的 fallback 模型(_select_image_chat_provider,astr_main_agent.py:1301)。
  • 组装上下文时裁剪:模型不支持的模态,把对应内容替换成 [Image]/[Audio] 占位文本(_assemble_request_context_for_provider,tool_loop_agent_runner.py:330)。

这是「优雅降级」——能力不够就退而求其次,而不是直接报错。

2.4 注册与管理

厂商适配器用装饰器登记进 provider_cls_map(astrbot/core/provider/register.py)。ProviderManager(astrbot/core/provider/manager.py:31)读配置实例化所有 provider,维护 inst_map(id→实例,manager.py:62),并支持运行时切换(_notify_provider_changed,manager.py:99)。get_using_provider(umo) 按会话拿当前模型——这让「不同会话用不同模型」成为可能。

3. Platform:IM 平台适配器

3.1 统一的进与出

Platform 适配器干两件对称的事:

  1. :把某 IM 的原始消息转成统一的 AstrMessageEvent,投进事件队列。
  2. :把处理结果(MessageChain)转成该 IM 的格式发出去。

核心数据结构是 AstrMessageEvent(astrbot/core/platform/astr_message_event.py:35)。它是整条流水线传递的「事件对象」,承载了一条消息的全部信息和状态:

字段含义
message_str纯文本消息
message_obj完整消息结构(AstrBotMessage,含图片/At/引用等消息段)
platform_meta来自哪个平台
unified_msg_origin统一会话标识,格式 平台:消息类型:会话ID(astr_message_event.py:103)
is_wake / is_at_or_wake_command唤醒状态(上一章用到)
rolememberadmin
_result处理结果

3.2 unified_msg_origin:统一会话身份

这是个关键设计。无论消息来自 QQ 群还是 Telegram 私聊,都被编码成一个统一字符串 平台名:消息类型:会话ID(astr_message_event.py:104)。整个系统——配置路由、对话历史、会话锁、人格选择——都用这一个字符串当 key。这样核心逻辑彻底不关心「这是哪个平台的哪种会话」。

3.3 MessageChain:统一的消息内容

消息内容(进和出)都用 MessageChain 表示——一串消息段(BaseMessageComponent)的列表,如 Plain(文本)、ImageAtReplyRecord(语音)。各平台适配器负责在自己的原始格式和这串通用消息段之间互转。发送时 event.send(chain) / event.send_streaming(...) 由适配器实现。

3.4 注册与管理

平台适配器实现都在 astrbot/core/platform/sources/(18 个目录:aiocqhttp 即 QQ、lark 即飞书、telegramslackdiscordwecom 等)。PlatformManager(astrbot/core/platform/manager.py)按配置实例化,load_platform 给每个平台起一个长跑的 run() 任务监听消息(core_lifecycle.py:412)。

4. 两套适配器怎么对接核心

把上一章和本章串起来,一条消息的完整旅程:

QQ 原始消息
│ aiocqhttp 适配器(Platform 进)

AstrMessageEvent ──► event_queue ──► EventBus ──► Pipeline
│ ProcessStage

build_main_agent
│ 工具循环

provider.text_chat() ◄── Provider 抽象
│ (OpenAI/Claude/...)

MessageEventResult
│ RespondStage

event.send(MessageChain)
┌─────────────────────────────────────────────────┘
▼ aiocqhttp 适配器(Platform 出)
QQ 消息发回群里

5. 巧妙之处(可借鉴)

  • unified_msg_origin 单字符串当全局会话 key:把「平台 + 会话类型 + 会话 ID」三元组压成一个字符串,让所有按会话隔离的子系统(配置、历史、锁、人格)共用同一把钥匙,极大简化了核心逻辑(astr_message_event.py:103)。
  • 能力降级而非报错:模型不支持图片就切 fallback 或裁成占位文本,平台不支持流式就把流式结果攒成整条发(unsupported_streaming_strategy,internal.py:260)。系统尽量「能用就用」。
  • Provider 五分类共享 AbstractProvider:聊天、语音、向量、重排统一在一个基类下,test() 自检方法让 WebUI 配置体验一致。

6. 边界与局限

  • 新增平台/模型需要写一个完整适配器并实现统一接口,工作量不小;但一旦写好,核心逻辑零改动。
  • 不同平台能力参差(有的不支持流式、有的不支持引用),适配器要各自处理这些 quirk,AstrMessageEvent 里能看到不少平台特判(如各平台的 UNIQUE_SESSION_ID_BUILDERS,waking_check/stage.py:17)。

7. 横向对比

「多 Provider 抽象」是 chat-agents 和编码 Agent 兄弟项目的共性(大家都要支持多家模型)。AstrBot 的独特点在 Platform 这一侧:编码 Agent 通常只面对「终端 / 编辑器」一种前端,而 AstrBot 要面对 18 个差异巨大的 IM。它把大量复杂度吃在 Platform 适配器层(消息段互转、唤醒判定、会话身份统一),换来核心 Agent 逻辑的纯净。这是它作为「IM Agent 平台」最重的工程投入。

8. 代码地图(导航索引)

主题文件路径符号名
Provider 抽象基类(5 类)astrbot/core/provider/provider.pyAbstractProviderProviderSTTProviderTTSProviderEmbeddingProviderRerankProvider
聊天统一接口astrbot/core/provider/provider.pyProvider.text_chatProvider.text_chat_stream
Provider 管理astrbot/core/provider/manager.pyProviderManagerProviderManager.inst_map
Provider 注册表astrbot/core/provider/register.pyprovider_cls_mapllm_tools
厂商实现astrbot/core/provider/sources/openai_source.pyanthropic_source.pygemini_source.py
事件对象astrbot/core/platform/astr_message_event.pyAstrMessageEventAstrMessageEvent.unified_msg_origin
平台管理astrbot/core/platform/manager.pyPlatformManager
平台实现astrbot/core/platform/sources/aiocqhttp(QQ)、lark(飞书)、telegramslack
模态降级astrbot/core/astr_main_agent.py_select_image_chat_provider_provider_supports_modality