上一篇我们看到流水线的 MessageProcessor 把一条 Query 交给「模型」处理。但「模型」这个词背后藏着真正的复杂度:要同时接入 OpenAI、Anthropic、DeepSeek、月之暗面等上百家服务,要支持 LLM/Embedding/Rerank 三类任务,还要让大模型能调用来自插件、MCP、技能、原生四种来源的工具。这一篇就拆开 pkg/provider——LangBot 把「模型」和「工具」这两件最易发散的事,抽象成了什么样子。

一、这一层解决什么问题

把它的职责拆成两条独立的线:

  1. 模型抽象线——无论底层是哪家厂商、什么协议,上层只想说一句「用这个 model,把这串 messages 发出去,给我结果」。Provider 层要把厂商差异、API Key 轮换、调用监控全部吃掉。
  2. 工具抽象线(Function-Calling)——大模型要能调用工具,但工具来自四个完全不同的子系统(原生内置、插件、MCP 服务器、技能)。Provider 层要把它们聚合成一个统一的工具列表喂给模型,再在模型回调时精准路由回正确的来源。

这两条线在 pkg/provider 下分别由 modelmgr/ 和 tools/ 承载。下面分别拆解。

二、模型抽象:Manager / Provider / Model 三层

2.1 三层各管什么

classDiagram
    class ModelManager {
        +provider_dict: dict~uuid, RuntimeProvider~
        +llm_models: list~RuntimeLLMModel~
        +embedding_models: list~RuntimeEmbeddingModel~
        +rerank_models: list~RuntimeRerankModel~
        +load_models_from_db()
        +get_model_by_uuid(uuid)
    }
    class RuntimeProvider {
        +provider_entity: ModelProvider
        +token_mgr: TokenManager
        +requester: ProviderAPIRequester
        +invoke_llm()
        +invoke_llm_stream()
        +invoke_embedding()
        +invoke_rerank()
    }
    class RuntimeLLMModel {
        +model_entity: LLMModel
        +provider: RuntimeProvider
    }
    class ProviderAPIRequester {
        <<abstract>>
        +invoke_llm()
        +invoke_embedding()
        +invoke_rerank()
    }
    ModelManager o-- RuntimeProvider
    ModelManager o-- RuntimeLLMModel
    RuntimeLLMModel --> RuntimeProvider
    RuntimeProvider --> ProviderAPIRequester

三层职责泾渭分明:

注意一个关键的组合关系:模型不持有调用逻辑,调用逻辑全在它指向的 RuntimeProvider 上。所以「换 provider」时(reload_provider,modelmgr.py:388)只需把所有指向旧 provider 的模型的 .provider 引用换掉即可,模型本身不动。

2.2 启动时如何被装配

ModelManager.initialize()(modelmgr.py:63)做了三件事:

# src/langbot/pkg/provider/modelmgr/modelmgr.py:63
async def initialize(self):
    self.requester_components = self.ap.discover.get_components_by_kind('LLMAPIRequester')
    # ... 把非 litellm 的 requester 类登记进 requester_dict ...
    await self.load_models_from_db()
    # ... 若未禁用,再从 LangBot Space 同步最新模型清单 ...
  1. 发现请求器组件——通过第二篇讲过的组件发现引擎,按 kind='LLMAPIRequester' 找出所有请求器。这里有个值得注意的分流(modelmgr.py:67):凡是 manifest 里声明了 litellm_provider 的组件,都跳过加载它的 Python 类——因为它们会统一走 LiteLLMRequester(见第三节)。只有少数需要特殊处理的请求器才保留自己的 Python 类。
  2. 从数据库加载——load_models_from_db() 先建 provider,再分别加载三类模型,每个模型用 provider_dict 里对应的 RuntimeProvider 绑定。
  3. 从 Space 同步——可选地从 LangBot Space 拉取官方维护的模型清单做 upsert(sync_new_models_from_space,modelmgr.py:170)。注释里有个很有意思的细节:Space 会复用 uuid 跨重命名(比如曾经是 claude-opus-4-6 的那个 uuid 后来变成 claude-opus-4-7),所以是按 uuid upsert 而非按名字。