← [ BACK_TO_NOTES ]

OpenAI-Compatible LLM Provider

OpenAI-Compatible LLM Provider 是 LLM Provider Abstraction 的一种具体适配器模式:复用 OpenAI 风格的协议表面,通过 base_url、模型能力映射与规范化层接入多个兼容服务。

#concept #growing #ai

[!info] related notes

OpenAI-Compatible LLM Provider

核心问题

LLM Provider Abstraction 定义“上层只依赖统一 Provider 接口”的架构边界;本笔记只讨论其中一种具体实现:

当多个服务暴露 OpenAI 风格 API 时,怎样用一个兼容适配器承接协议差异,而不是为每个服务重新实现完整 Provider?

因此本笔记不是“LLM Provider 抽象”的同义入口。即使系统已经有抽象层,也仍需要判断某个后端到底是真正兼容、部分兼容,还是必须使用独立 adapter。

设计目标

  1. 一处配置,全局生效 — 环境变量切换模型,代码零修改
  2. 上层不感知模型细节 — 统一的 chat 和 chat_stream 接口
  3. 流式累积封装在内部 — 上层不用关心 tool_call 的 chunk 拼接
  4. 可测试 — 通过 Protocol 接口注入 mock

接口设计

class LLMProvider(Protocol):
    async def chat(
        self,
        messages: list[ChatMessage],
        tools: list[ToolDefinition] | None = None,
        temperature: float = 0.7,
        max_tokens: int = 2048,
    ) -> ChatMessage: ...

    async def chat_stream(
        self,
        messages: list[ChatMessage],
        tools: list[ToolDefinition] | None = None,
        temperature: float = 0.7,
        max_tokens: int = 2048,
    ) -> AsyncIterator[StreamChunk]: ...

用 Protocol 而不是 ABC:不需要显式 implements,只要方法签名匹配就行。

统一数据结构

@dataclass
class ChatMessage:
    role: str           # "system" | "user" | "assistant" | "tool"
    content: str | None
    tool_calls: list[ToolCall] | None
    tool_call_id: str | None  # tool 结果的关联 ID

@dataclass
class StreamChunk:
    delta: str = ""                          # 文本增量
    tool_calls: list[ToolCall] | None = None  # 流结束时才填充完整 tool_calls
    finished: bool = False

关键设计:StreamChunk 把 tool_calls 的累积逻辑封装在 Provider 内部。上层只需要在 finished=True 时拿到完整的 tool_calls,不需要自己拼 chunk。

为什么选 OpenAI 兼容 API

几乎所有主流模型都兼容 OpenAI API 格式:

服务商base_url说明
OpenAI默认原生
DeepSeekhttps://api.deepseek.com/v1完全兼容
阿里通义https://dashscope.aliyuncs.com/compatible-mode/v1兼容
OpenRouterhttps://openrouter.ai/api/v1聚合多模型

只需要一个 OpenAICompatibleProvider,通过 base_url 切换。

环境变量配置

LLM_MODEL=gpt-4o-mini
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://api.deepseek.com/v1  # 切换模型只改这一行

单例 vs 依赖注入

# 全局单例 — 生产环境
_default_provider: OpenAICompatibleProvider | None = None

def get_llm_provider() -> OpenAICompatibleProvider:
    global _default_provider
    if _default_provider is None:
        _default_provider = OpenAICompatibleProvider()
    return _default_provider

单例的好处是避免重复创建 HTTP 连接。测试时可以通过依赖注入传 mock。

与 LangChain 的区别

维度自建 ProviderLangChain
复杂度~200 行框架级
依赖只有 openai SDK大量传递依赖
灵活性完全可控受框架约束
适用接口简单、模型少需要 chain/agent/memory

如果只需要”发消息、收回复、调工具”,自建就够了。如果需要复杂的 chain 编排、memory 管理、多 agent 协作,考虑 LangChain 或 LangGraph。

常见错误

硬编码模型名

# ❌ 每个调用点写死模型
response = await client.chat.completions.create(model="gpt-4o", ...)

# ✅ 通过 Provider 统一管理
provider = get_llm_provider()
response = await provider.chat(messages, tools=tools)

API Key 泄露

永远不要把 API Key 硬编码在代码里。用环境变量或 secret manager。

不处理模型不支持 function calling

不是所有模型都支持 function calling。Provider 应该在 tools 不为 None 时检查模型能力,不支持时降级为纯文本提示。

边界与兼容性风险

“OpenAI-compatible”通常只意味着请求/响应形状大体相似,不等于所有语义都一致。需要显式验证:

  • tool calling schema 与增量流式 chunk 是否完全兼容;
  • usage、finish reason、reasoning 字段是否需要归一化;
  • JSON / structured output 是否支持同一套参数;
  • 模型名、能力发现、错误码和重试语义是否一致;
  • provider-specific header、rate limit 和鉴权方式是否仍需单独配置。

一旦这些差异开始影响上层契约,应把差异收敛在 adapter 内,而不是把 provider 分支泄漏到业务调用点。

学习检查

  1. LLM Provider Abstraction 与 OpenAI-Compatible Provider 为什么是“抽象边界”和“具体适配器”的关系?
  2. 一个接口能成功返回文本,为什么仍不能证明它对 tool calling 或 streaming 真正兼容?
  3. 当两个兼容服务的错误码、usage 字段不同,差异应该放在哪一层归一化?
  4. 什么情况下应该放弃单一 OpenAI-compatible adapter,改为独立 Provider 实现?
创建于 2026/6/25 更新于 2026/10/1
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本