引言
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-sonnetenv_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 生态。