大多数人启动项目时并不会考虑提供商的可移植性。他们从最容易注册的供应商那里拿到一个密钥,编写集成代码,然后发布。几个月后,他们读到了一篇基准测试,或者打开了一张账单,又或者撞上了速率限制,于是他们想尝试点别的。这时痛苦就出现了,因为他们已经把对第一个提供商的假设深深嵌入了 HTTP 层。
解决办法不是什么抽象库。而是一种通信格式。OpenAI 的聊天补全 API 已经悄然成为与 LLM 通信的默认协议,一旦你基于它编写代码,切换提供商基本上就只是改两个字符串的事。
所有人都抄袭的 API
OpenAI 定义了一个简单的契约:你向 /v1/chat/completions POST JSON,然后拿回一个 choices[0].message.content 字符串。请求长这样:
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "用一段话解释 DNS"}]
}'现在几乎所有主流模型都说这种方言。DeepSeek、Qwen、ERNIE、Doubao 以及其他模型都暴露了一个端点,接受相同的请求体并返回相同的结构。这意味着你的客户端代码并不关心另一端是谁。你可以只替换 model 字符串和端点,其他什么都不用改。
下面是把同一个调用指向一个聚合器,它在同一个基础 URL 下承载了其中好几个模型:
curl https://api.token8341.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKENWORKS_API_KEY" \
-d '{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "用一段话解释 DNS"}]
}'一个客户端,多个模型
在 Python 中,你通常会实例化一次 OpenAI SDK,并在每次请求时传入模型名称。如果你的提供商支持兼容 API,你只需设置一次 base_url,其他一切保持不变:
from openai import OpenAI
client = OpenAI(
base_url="https://api.token8341.com/v1",
api_key="sk-your-tokenworks-key",
)
def ask(model: str, prompt: str) -> str:
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
# 同一个函数,三个不同的模型家族。
print(ask("gpt-4o", "帮我总结这段日志。"))
print(ask("deepseek-chat", "帮我总结这段日志。"))
print(ask("qwen-max", "帮我总结这段日志。"))函数不变。模型字符串变。这就是全部诀窍。流式传输也是同样的方式:
stream = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "写一首关于数据库的俳句"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content or ""
print(delta, end="")函数调用、JSON 模式和嵌入全都运行在同一个兼容接口之上,所以切换时你并不局限于纯聊天。
切换并不能带给你什么
我想把那些不会自动改变的部分说清楚。传输层是可移植的;模型不是。
不同的模型对提示词的响应方式不同。为 GPT-4o 调优的系统提示词在一个需要不同结构的推理模型上可能表现不佳。上下文窗口各不相同,有时差异很大:一个模型可能接受 128k token,而另一个只接受 32k。分词器也不同,所以同一份文档在每个模型上消耗的 token 数不一样。最大输出长度是另一个按模型而非按协议决定的参数。
所以“不修改代码就能切换”实际上是“不用重写 HTTP 客户端就能切换”。在把生产流量指向一个新模型之前,你仍然需要为自己做一次评估运行。好消息是,兼容 API 让这种评估跑起来很便宜,因为测试框架不过是对模型名称的一个循环。
一个实用的模式
我在业余项目中实际的做法是维护一张小的配置表,而不是改代码:
MODELS = {
"fast": "deepseek-chat",
"smart": "qwen-max",
"frontier": "gpt-4o",
}
def run(task, prompt):
return ask(MODELS[task], prompt)想为 fast 通道尝试一个更便宜的模型?改一行。想在第一个提供商被限流时回退到第二个?捕获异常并用下一个模型字符串重试。这些都不触及请求的构造。
这种回退模式正是多模型端点体现价值的地方。用一个基础 URL 和一个密钥,你就可以跨提供商进行路由、重试和 A/B 测试,全部通过你第一天写的那两行客户端设置完成。
需要注意的响应怪癖
传输层是兼容的,但一些响应层面的差异仍然会漏出来。推理模型有时会在正常消息之外返回一个额外字段,比如 reasoning_content。读取 choices[0].message.content 的代码无论如何都能继续工作,但你可能想在调试视图中把那段推理文本展示出来。错误消息也各不相同:一个提供商返回有用的 insufficient_quota 字符串,另一个只返回一个没有响应体的裸 429。如果你在构建重试逻辑,把任何 429 或 5xx 都当作“退避并重试”来处理,而不是去解析供应商特有的文本。
这些都不是让你锁定在单一提供商的理由。它是让你保持错误处理通用、把模型选择当作变量的理由。
这就是 SiCore TokenWorks 的立场:在 https://api.token8341.com/v1 提供一个 OpenAI 兼容端点,当你在 GPT-4o、Claude、Gemini、DeepSeek、Qwen、ERNIE、Doubao、Spark 和 Pangu 之间切换时,模型字符串是唯一需要改变的东西。