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 生态。

相关推荐
镜象科技2 小时前
职业院校AI心理健康解决方案怎么选?三个特殊场景与三类路线
人工智能
Leo.yuan2 小时前
企业Data Agent架构选型:帆软Dora基于NL2BI的现有BI体系复用方案
大数据·人工智能
朱涛的自习室2 小时前
70 美金的 Beyond Compare,我让 Gemini 4 和 DeepSeek 造个开源免费版(Day 2)
android·前端·人工智能
米小虾2 小时前
上下文:Agent 的眼睛里到底装着什么
人工智能·agent
米小虾2 小时前
Agent = 大脑 + 眼睛 + 手脚:一篇看懂现代 AI Agent 的骨架
人工智能
小小测试开发2 小时前
安全评测得分 0.9556 掉到 0.3257,只改了 top_k 和 prompt_style 之外的题面比例
人工智能·安全·prompt
slacker-kian3 小时前
[实践]-用LLM对文本结构化:把多段文本描述变成规范的 JSON 知识库
人工智能·llm·prompt·结构化·pydantic·one-shot 提示
品牌常新3 小时前
景区巡逻机器人怎么选?派宝S5全地形守护解析
运维·人工智能·机器人
冬奇Lab4 小时前
LLM 驱动的自动化测试系列(13):Flaky Test 不是重跑几次就能解决的分类问题
人工智能·测试