03:多 LLM 提供商统一接入:Provider 模式与 LiteLLM 实践

引言

AI 应用面临的一个核心挑战是如何优雅地支持多个 LLM 提供商。CountBot 通过 Provider 抽象模式 + LiteLLM 适配层,实现了对 9+ 种 LLM 提供商的统一接入。本文将深入分析这一设计。

架构设计

三层抽象

markdown 复制代码
AgentLoop
    ↓ 调用统一接口
LLMProvider (抽象基类)
    ↓ 具体实现
LiteLLMProvider
    ↓ 委托
LiteLLM 库 → OpenAI / Anthropic / DeepSeek / Gemini / ...

Provider 抽象基类

python 复制代码
class LLMProvider(ABC):
    def __init__(self, api_key, api_base, default_model, timeout=120.0, max_retries=3):
        self.api_key = api_key
        self.api_base = api_base
        self.default_model = default_model
        self.timeout = timeout
        self.max_retries = max_retries

    @abstractmethod
    async def chat_stream(
        self, messages, tools=None, model=None,
        max_tokens=4096, temperature=0.7, **kwargs
    ) -> AsyncIterator[StreamChunk]:
        pass

关键设计决策:

  • 流式优先chat_stream 返回 AsyncIterator[StreamChunk],而非一次性返回完整响应
  • 统一数据模型StreamChunk 封装了文本内容、工具调用、推理内容、错误信息等所有可能的响应类型

StreamChunk 统一响应模型

python 复制代码
@dataclass
class StreamChunk:
    content: str | None = None              # 文本内容
    tool_call: ToolCall | None = None       # 工具调用
    finish_reason: str | None = None        # 完成原因
    usage: dict[str, int] | None = None     # Token 用量
    error: str | None = None                # 错误信息
    reasoning_content: str | None = None    # 推理内容(思考模型)

    @property
    def is_content(self) -> bool: return self.content is not None
    @property
    def is_tool_call(self) -> bool: return self.tool_call is not None
    @property
    def is_reasoning(self) -> bool: return self.reasoning_content is not None

reasoning_content 字段是为 DeepSeek-R1、Kimi 等"思考模型"设计的,这些模型会在生成最终回答前输出推理过程。

Provider 注册表

python 复制代码
PROVIDER_REGISTRY = {
    "openai": ProviderMetadata(
        id="openai",
        name="OpenAI",
        default_api_base="https://api.openai.com/v1",
        default_model="gpt-4o",
        litellm_prefix="",
        env_key="OPENAI_API_KEY",
    ),
    "anthropic": ProviderMetadata(
        id="anthropic",
        name="Anthropic",
        default_api_base="https://api.anthropic.com",
        default_model="claude-3-5-sonnet-20241022",
        litellm_prefix="anthropic/",
        env_key="ANTHROPIC_API_KEY",
    ),
    "deepseek": ProviderMetadata(
        id="deepseek",
        default_api_base="https://api.deepseek.com",
        default_model="deepseek-chat",
        litellm_prefix="deepseek/",
        env_key="DEEPSEEK_API_KEY",
    ),
    # ... 更多提供商
}

注册表的核心字段:

  • litellm_prefix:LiteLLM 识别模型的前缀,如 anthropic/claude-3-5-sonnet
  • env_key:API Key 对应的环境变量名
  • env_extras:额外的环境变量配置(如 Azure 需要 endpoint 等)
  • model_overrides:特定模型的参数覆盖

LiteLLMProvider 实现细节

环境变量自动配置

python 复制代码
def _configure_litellm(self, api_key, api_base):
    provider_metadata = get_provider_metadata(self.provider_id)
    if provider_metadata:
        if provider_metadata.env_key and api_key:
            os.environ[provider_metadata.env_key] = api_key
        for env_name, env_val in provider_metadata.env_extras:
            resolved = env_val.replace("{api_key}", api_key or "")
            resolved = resolved.replace("{api_base}", effective_base)
            os.environ[env_name] = resolved

这种设计让用户只需在 Web UI 中填写 API Key,系统自动处理环境变量配置。

日志抑制

python 复制代码
def _suppress_litellm_logging(self):
    os.environ["LITELLM_LOG"] = "ERROR"
    litellm.suppress_debug_info = True
    litellm.set_verbose = False
    litellm.drop_params = True
    litellm.telemetry = False

LiteLLM 默认日志非常冗长,CountBot 通过全面抑制确保日志清洁。

模型参数智能适配

不同提供商对参数的支持不同,CountBot 通过 model_overrides 机制处理:

python 复制代码
# 例如 Kimi 模型要求 temperature 必须为 1.0
"kimi": ProviderMetadata(
    model_overrides={"moonshot-v1-auto": {"temperature": 1.0}},
)

工具调用兼容性

不同 LLM 对 Function Calling 的支持程度不同。CountBot 通过 ToolCallParser 提供了降级方案:

python 复制代码
class ToolCallParser:
    # 支持三种格式的工具调用解析
    @classmethod
    def parse(cls, text):
        result = cls._parse_json(text)      # 标准 JSON 格式
        if result: return result
        result = cls._parse_simple(text)     # 简单文本格式
        if result: return result
        result = cls._parse_pure_json(text)  # 纯 JSON 对象
        if result: return result
        return None

这确保了即使 LLM 不支持原生 Function Calling,也能通过文本解析实现工具调用。

切换提供商的零成本

由于统一的抽象层,切换 LLM 提供商只需修改配置:

python 复制代码
# 从 OpenAI 切换到 DeepSeek,只需改配置
config.model.provider = "deepseek"
config.model.model = "deepseek-chat"

AgentLoop、工具系统、渠道系统等所有上层代码完全不需要修改。

总结

CountBot 的多 Provider 架构展示了一个优秀的适配器模式实践:通过抽象基类定义统一接口,通过注册表管理元数据,通过 LiteLLM 处理底层差异。这种设计使得系统可以轻松适应快速变化的 LLM 生态。

相关推荐
kjmkq2 小时前
香港领先GEO服务商 XOOER 专注GEO/AEO赋能品牌全球扩张
人工智能
陈天伟教授2 小时前
人工智能应用- 材料微观:01. 微观结构的重要性
人工智能·神经网络·算法·机器学习·推荐算法
聊聊科技2 小时前
用清唱歌词音频来创作,原创音乐人通过AI编曲软件快速制作歌曲的编曲伴奏
人工智能
盲盒Q2 小时前
《内存之茧》
数据结构·人工智能·ruby
狮子座明仔2 小时前
REDSearcher:如何用30B参数的小模型,在深度搜索上击败GPT-o3和Gemini?
人工智能·gpt·深度学习·microsoft·语言模型·自然语言处理
沪漂阿龙2 小时前
从Chatbot到Agent:核心能力、工作原理与实战解析
人工智能
石去皿2 小时前
Token及模型参数准备篇——预训练数据去重、SFT数据量估算与正则化策略全解析
人工智能
聊聊科技2 小时前
清唱一遍歌词即可制作完整歌曲的编曲伴奏,原创音乐人借助AI编曲软件轻松出歌
人工智能
大模型任我行2 小时前
华为:CLI任务自动生成新范式
人工智能·语言模型·自然语言处理·论文笔记