一个只接入 OpenAI 的应用,模型创建代码通常很短:
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="example-model",
api_key="<OPENAI_API_KEY>",
)加入第二家 Provider 后,看起来只要换掉 API Key:
model = ChatOpenAI(
model="provider-model",
api_key="<PROVIDER_API_KEY>",
)这段代码还缺少一个决定请求去向的值:base_url。继续加入 DeepSeek、Kimi、豆包、GLM 和本地 Ollama 后,差异也不再只有 Key 和 URL,还包括 SDK 类、模型名、参数名称、Tool Calling、结构化输出以及响应中的扩展字段。
因此,“多 Provider 接入”包含两件不同的事:
- 模型列表中能够展示和选择 Provider
- 每一条实际调用链都能根据选择创建正确的 LangChain ChatModel
只完成第一件事,页面上虽然出现了新模型,聊天、工作流或后台任务仍可能继续创建原来的 ChatOpenAI。统一模型工厂解决的是第二件事。
先分清五个容易混在一起的概念
Provider
Provider 是提供模型运行服务的平台,例如 OpenAI、DeepSeek 或本地 Ollama。它通常决定:
- 请求发送到哪个 API 地址
- 使用哪一种认证方式
- 哪些模型可以调用
- 支持哪些参数和高级能力
- 使用哪个 LangChain 集成类
Provider 不是模型。一个 Provider 可以托管多个模型,同一个开源模型也可能由多个 Provider 托管。
Model
Model 是一次调用中选中的具体模型标识。provider="deepseek" 只说明服务来自哪一方,仍要用 model="..." 指定调用哪个模型。
不同 Provider 的模型名处在各自的命名空间中。只替换 Provider,不同步检查模型名,请求可能在发送前就因模型不存在而失败。
ChatModel
LangChain 的 ChatModel 是应用代码实际调用的 Python 对象,例如:
ChatOpenAI
ChatDeepSeek
ChatOllama这些类把统一的 invoke()、stream() 等接口转换成各家 Provider 的请求。
model_config
model_config 是业务保存的一次模型选择:
{
"provider": "zhipu",
"model": "provider-model-name",
"parameters": {
"temperature": 0.7,
"max_tokens": 4096
}
}它回答“选择谁、选择哪个模型、这次生成怎样控制”,不保存 API Key、Base URL 或 Python 类。
模型目录与模型工厂
模型目录记录 Provider 和模型的静态信息;模型工厂读取业务配置与目录,创建真正参与调用的 ChatModel:
model_config Provider / Model Catalog
业务选择 连接信息、能力和参数规则
│ │
└──────────┬───────────────────┘
▼
LanguageModelManager
│
▼
ChatOpenAI / ChatDeepSeek / ChatOllama目录是数据,工厂是执行逻辑。把两者分开后,业务 Service 不需要知道每家 SDK 的构造参数。
OpenAI-compatible 解决了哪一层
许多 Provider 提供兼容 OpenAI Chat Completions 的端点。标准文本对话可以继续使用 ChatOpenAI,只改变模型名、Key 和 Base URL:
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="provider-model-name",
api_key="<PROVIDER_API_KEY>",
base_url="https://provider.example/v1",
)
message = model.invoke("解释一下什么是线程")
print(message.content)这段代码中的数据流是:
"解释一下什么是线程"
│
▼
ChatOpenAI 把消息转换成 OpenAI Chat Completions 请求
│
▼
base_url 决定请求发送到第三方兼容端点
│
▼
Provider 返回兼容格式
│
▼
LangChain 转换成 AIMessageLangChain 的 Chat model integrations 将这种方式限定为基础 Chat Completions 兼容。ChatOpenAI 以 OpenAI 官方 API 规范为目标,第三方增加的 reasoning_content、reasoning 等字段不一定被提取或保留。
因此,OpenAI-compatible 不等于“所有能力完全相同”:
| 能力 | 兼容端点通常能否统一 | 仍要单独核对什么 |
|---|---|---|
| 普通消息输入输出 | 通常可以 | 消息类型和返回字段 |
| 流式文本 | 通常可以 | usage 和结束事件 |
| Tool Calling | 取决于模型与端点 | Tool Schema、tool_choice |
| 结构化输出 | 取决于具体实现 | JSON Schema、JSON Mode、Function Calling |
| 推理内容 | 常有厂商扩展 | 专属响应字段 |
| 多模态 | 差异较大 | 图片格式、大小与模型能力 |
需要 Provider 扩展字段时,专属集成类更合适。例如 ChatDeepSeek 使用 langchain-deepseek 包,并单独标注不同模型的 Tool Calling 与结构化输出能力。
同一套工厂可以同时容纳两种接法:
| Provider 类型 | 工厂创建的类 | 适用范围 |
|---|---|---|
| OpenAI 官方 | ChatOpenAI | OpenAI API |
| OpenAI-compatible | ChatOpenAI + base_url | 标准兼容能力 |
| 有专属集成的 Provider | ChatDeepSeek 等 | 保留厂商能力和字段 |
| 本地原生协议 | ChatOllama | 使用本地 SDK 参数与响应 |
一次模型选择由哪些数据组成
模型创建时需要的数据可以分成四类:
| 数据 | 示例 | 谁负责 |
|---|---|---|
| 业务选择 | Provider、模型、温度、输出长度 | 应用或工作流配置 |
| Provider 连接 | API Key 环境变量名、Base URL | Provider 目录 |
| 模型能力 | 上下文、输出上限、参数范围、结构化策略 | 模型目录 |
| 运行实现 | ChatOpenAI、ChatDeepSeek、ChatOllama | Provider 适配层 |
业务配置只保存第一类:
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
class LanguageModelConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
provider: str
model: str
parameters: dict[str, Any] = Field(default_factory=dict)extra="forbid" 会拒绝意外字段。API Key 不进入这份对象,可以避免模型配置序列化进数据库或接口响应后泄露凭据。
Provider 目录保存连接与实现信息:
- name: deepseek
label: DeepSeek
api_key_env: DEEPSEEK_API_KEY
base_url: https://api.example.com
supported_model_types:
- chat
- name: zhipu
label: GLM
api_key_env: ZHIPU_API_KEY
base_url: https://provider.example/v1
supported_model_types:
- chat这里保存的是环境变量名称,不是 Key 的值。进程启动后,工厂才从服务端环境读取对应凭据。SDK 类可以由 Provider 名称和模型类型按约定加载:deepseek + chat 找到 DeepSeek 的 Chat 适配类,zhipu + chat 找到基于 ChatOpenAI 的兼容适配类。这样 YAML 不需要保存 Python 类对象,Provider 差异仍然留在适配目录中。
模型目录保存单个模型的规则:
model: provider-model-name
model_type: chat
context_window: 128000
max_output_tokens: 8192
parameters:
- name: temperature
type: float
default: 1
min: 0
max: 2目录中的 context_window 和 max_output_tokens 是应用侧能力声明,用于展示、校验和预算计算。它们不会自动修改远端 Provider 或本地推理引擎。
模型工厂怎样创建一次真实调用
下面的示例省略缓存和展示字段,但包含完整的创建路径:
import os
from dataclasses import dataclass
from typing import Any
FORBIDDEN_PARAMETERS = {
"api_key",
"base_url",
"model",
}
@dataclass
class ProviderSpec:
model_class: type
api_key_env: str | None
base_url: str | None
models: dict[str, "ModelSpec"]
@dataclass
class ModelSpec:
attributes: dict[str, Any]
parameter_rules: dict[str, dict[str, Any]]
class LanguageModelManager:
def __init__(self, providers: dict[str, ProviderSpec]) -> None:
self.providers = providers
def create_chat_model(self, raw_config: dict[str, Any]):
config = LanguageModelConfig.model_validate(raw_config)
provider = self.providers[config.provider]
model_spec = provider.models[config.model]
parameters = self._validate_parameters(
config.parameters,
model_spec.parameter_rules,
)
if FORBIDDEN_PARAMETERS & parameters.keys():
raise ValueError("业务参数不能覆盖连接字段")
init_kwargs = {
**model_spec.attributes,
**parameters,
}
if provider.api_key_env:
api_key = os.getenv(provider.api_key_env)
if not api_key:
raise ValueError(
f"missing environment variable: {provider.api_key_env}"
)
init_kwargs["api_key"] = api_key
if provider.base_url:
init_kwargs["base_url"] = provider.base_url
return provider.model_class(**init_kwargs)
def _validate_parameters(
self,
parameters: dict[str, Any],
rules: dict[str, dict[str, Any]],
) -> dict[str, Any]:
validated = {}
for name, value in parameters.items():
if name not in rules:
raise ValueError(f"unknown model parameter: {name}")
validated[name] = value
for name, rule in rules.items():
if name not in validated and "default" in rule:
validated[name] = rule["default"]
return validated假设输入是:
raw_config = {
"provider": "zhipu",
"model": "provider-model-name",
"parameters": {"temperature": 0.7},
}工厂内部会产生这些中间状态:
1. Pydantic 校验
provider = "zhipu"
model = "provider-model-name"
parameters = {"temperature": 0.7}
2. 查 Provider 目录
model_class = ChatOpenAI
api_key_env = "ZHIPU_API_KEY"
base_url = "https://provider.example/v1"
3. 查模型目录并校验参数
temperature 是已声明的 float 参数
4. 合并构造参数
{
"model": "provider-model-name",
"temperature": 0.7,
"api_key": "<从进程环境读取>",
"base_url": "https://provider.example/v1"
}
5. 创建 ChatOpenAI这条链保证 model、api_key 和 base_url 来自同一个 Provider。业务传入 parameters={"base_url": "..."} 时会在第三步前被拒绝,不能绕过服务端目录把请求改发到任意地址。
工厂的意义也不只是减少重复代码。未知 Provider、未知模型、参数类型错误、参数越界和缺少凭据都在“创建模型”这个边界暴露,不必等到业务链运行到一半才失败。
模型列表接入不等于运行链路接入
一个应用常有多条 LLM 调用入口:
应用聊天
工作流 LLM 节点
应用调试
已发布 API
会话摘要
会话标题
候选问题
提示词优化
健康检查或调试 Ping如果其中一处仍然写着:
model = ChatOpenAI(model="example-model")这条入口就绕过了 Provider 选择。页面列表可以显示 GLM,应用聊天也能使用 GLM,但后台标题生成仍然会读取 OPENAI_API_KEY。
判断完整接入不能只看 Provider 列表,而要看调用图:
每一个业务入口
│
▼
LanguageModelManager
│
▼
Provider 对应的 ChatModel项目测试还可以反向扫描业务目录,禁止在 Provider 适配层之外直接导入 langchain_openai、langchain_deepseek 或 langchain_ollama。这样新增 Service 时,直接实例化 SDK 会立刻让测试失败。
应用模型与系统辅助模型
同一个系统通常存在两类模型选择:
| 调用场景 | 配置来源 | 生命周期 |
|---|---|---|
| 应用聊天 | 应用保存的 model_config | 跟随应用 |
| 工作流节点 | 节点自己的 model_config | 跟随节点 |
| 会话标题、摘要 | 系统默认模型 | 跟随平台部署 |
| 候选问题、提示词优化 | 系统默认模型 | 跟随平台部署 |
系统模型可以用独立变量表达:
SYSTEM_LLM_PROVIDER=zhipu
SYSTEM_LLM_MODEL=provider-model-name工厂把它们重新组装成同一种 LanguageModelConfig:
def create_system_chat_model(
manager: LanguageModelManager,
parameters: dict[str, Any] | None = None,
):
return manager.create_chat_model({
"provider": os.getenv("SYSTEM_LLM_PROVIDER", "openai"),
"model": os.getenv("SYSTEM_LLM_MODEL", "default-model"),
"parameters": dict(parameters or {}),
})两类配置共用创建逻辑,但不共用选择来源。修改系统默认模型不会改写已发布应用的模型;应用切换 Provider 也不会让平台辅助任务跟着变化。
load_dotenv() 默认不会覆盖进程里已经存在的同名变量。修改系统模型环境变量后,如果 Flask、Celery 或其他 Worker 没有完整重启,旧进程仍然保留旧选择。此时出现 OPENAI_API_KEY 错误,不一定是 Key 填错,也可能是运行中的进程仍在创建默认 OpenAI 模型。
模型目录还要处理兼容性
模型目录既服务于新建配置,也要解析数据库里的历史配置。某个旧模型停止展示时,直接删除目录项会让已有应用无法加载。
model: legacy-model-name
visible: false
deprecated: truevisible: false 只影响新选择列表,解析能力仍然保留。Provider 的存储 ID 也应保持稳定:页面标签可以从 “Moonshot” 改成 “Kimi”,数据库中的 provider="moonshot" 不必跟着迁移。
目录中的稳定 ID、展示标签和远端模型名是三件事:
| 字段 | 用途 | 是否适合频繁修改 |
|---|---|---|
| Provider ID | 数据库存储和代码查找 | 否 |
| Label | 页面显示 | 可以 |
| Model ID | 业务配置选择 | 需要兼容历史 |
| SDK attribute | 真正传给 Provider | 随适配层转换 |
参数也有同样的层次。业务统一使用 max_tokens,OpenAI 新接口可能使用 max_completion_tokens,Ollama 原生类使用 num_predict。字段转换属于 Provider 适配层,完整路径见 Ollama 模型参数适配:从 max_tokens 到 num_predict。
lc_versions 是谁加进去的
调试模型对象时,有时会看到:
{
"lc_versions": {
"langchain": "<version>",
"langchain-core": "<version>",
"langchain-openai": "<version>"
}
}这是 LangChain 在模型初始化和序列化过程中加入的追踪元数据,不是 Provider API 返回的模型内容,也不是 LLM 生成的文本。它适合用于定位依赖版本,但业务接口没有必要把模型对象的全部 metadata 原样返回。
结构化输出的 method 也属于运行时适配信息。它不必出现在模型选择页面,却必须由模型工厂按 Provider 能力使用。具体链路见 LangChain 跨 Provider 结构化输出与降级。
怎样验证工厂真的完成了接入
第一层验证不需要发送真实请求。给环境变量放入测试值,只构造模型对象,就能检查大部分接线:
def test_factory_builds_provider_model(monkeypatch, manager):
monkeypatch.setenv("ZHIPU_API_KEY", "test-key")
model = manager.create_chat_model({
"provider": "zhipu",
"model": "provider-model-name",
"parameters": {"temperature": 0.5},
})
assert model.model_name == "provider-model-name"
assert model.temperature == 0.5
assert str(model.openai_api_base) == "https://provider.example/v1"离线测试可以覆盖:
- Provider 和模型是否能解析
- 工厂选择了哪一个 Python 类
- Base URL 是否正确注入
- 缺少哪一个环境变量时会失败
- 参数默认值、类型和范围是否生效
- 业务参数能否覆盖连接字段
- 旧模型是否隐藏但仍可解析
- SDK 导入是否只存在于适配层
它不能证明:
- API Key 真实可用
- 远端模型当前在线
- Provider 完整实现了兼容协议
- Tool Calling 或结构化输出在真实模型上表现正确
- 网络、限流和计费配置正常
后一层需要明确开启的真实 Provider smoke test。离线构造通过只能说明“应用把正确参数交给了正确的 LangChain 类”,不能写成端到端调用已经成功。
回到最初的切换问题,一次真正的 Provider 选择包含完整链路:
应用保存 provider + model + parameters
│
▼
Provider 目录补充 Key 来源、Base URL 和 SDK 类
│
▼
模型目录补充能力、默认值和校验规则
│
▼
统一工厂创建 ChatModel
│
▼
所有聊天、工作流和辅助任务都从工厂取得模型只换 Key、只增加模型 YAML 或只修改列表接口,都还没有走完整条链。