如果你的业务已经用 OpenAI SDK 写好了,迁到统一网关的成本比多数人想得低——协议兼容的意思是:请求体不变、返回结构不变、SDK 不变,变的只有地址、密钥和模型名。
但"能跑通"和"敢上线"是两件事。下面这份清单,是按真实迁移踩过的顺序排的。
一、迁移只需要改三个地方
以 Python SDK 为例,改动集中在这三行:
from openai import OpenAI
client = OpenAI(
base_url="https://<接入地址>/v1", # ① 换成网关地址
api_key="<网关分配的密钥>", # ② 换成网关密钥
timeout=30.0, # ③ 显式设置超时
max_retries=2,
)
resp = client.chat.completions.create(
model="<网关支持的模型名>", # 模型名按网关的清单填
messages=[{"role": "user", "content": "你好"}],
)
Node.js 的写法一一对应:baseURL、apiKey、timeout、maxRetries。
不要改业务层的调用逻辑——一旦开始为了适配网关去改 messages 结构或返回值解析,就说明这个网关的"兼容"是打折扣的,后面换供应商会更痛苦。
二、迁移前的协议对照清单
不是所有标着"OpenAI 兼容"的服务,都把能力做全了。按这张表逐项确认,缺哪项就在方案里标出来:
| 能力 | 端点 / 参数 | 迁移注意点 |
|---|---|---|
| 对话补全 | /v1/chat/completions | 最基础的一项,一般都能通 |
| 流式输出 | stream: true | 确认结尾有 [DONE],且不夹带非标准字段 |
| 工具调用 | tools / tool_choice | 部分网关不支持并行工具调用 |
| 结构化输出 | response_format | json_object 与 json_schema 支持度不一 |
| 向量化 | /v1/embeddings | 维度与模型绑定,换模型要重建索引 |
| 模型列表 | /v1/models | 用来校验账号实际可用范围 |
| 用量统计 | usage | 流式默认可能不返回,需显式开启 |
这张表建议直接做成验收用例:每项写一条最小请求,迁移前后各跑一遍,对比返回结构而不是只看"有没有报错"。
三、五个最容易踩的坑
坑 1:base_url 路径重复
最常见的一个。base_url 通常要写到 /v1,而 SDK 自己会拼上 /chat/completions。写成 https://xxx/v1/ 会得到 /v1//chat/completions;写成 https://xxx 则可能变成 /chat/completions 而 404。
判断方法很简单:打印 SDK 实际请求的完整 URL,而不是猜。
坑 2:超时和重试没配
SDK 默认超时往往很长(十分钟级别),网络抖动时会一直挂住;默认重试又会让你在限流时雪上加霜。生产环境至少显式设置 timeout(建议 20–60 秒,流式另设读超时)和 max_retries(建议 1–2 次),并在业务层加熔断。
坑 3:流式解析不完整
流式返回是 SSE,逐块到达。三个必须处理的细节:
delta.content可能为空(首块只有 role,末块只有 finish_reason);- 必须识别结束标记,不要把
[DONE]当正文拼进去; - 读超时要单独设,长输出时连接空闲不代表断了。
for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta.content
if delta:
yield delta
坑 4:参数名和模型能力差异
新版推理类模型把 max_tokens 改成了 max_completion_tokens,部分模型不支持自定义 temperature。如果代码里写死了一组参数,换模型就会报 400。
做法是维护一张模型能力表(支持哪些参数、上下文多长、是否支持工具调用),在网关层按模型裁剪参数,而不是让每个业务方各写一套 try/except。
坑 5:用量的口径不一致
流式请求默认可能不返回 usage,需要在请求里显式要求:
"stream_options": { "include_usage": true }
拿到之后还要确认三件事:缓存命中是否单列、失败请求是否计费、重试是否重复计费。否则你按 usage 做的成本看板,和月底账单永远对不上。
四、灰度迁移:先切 10% 流量
不要一次性全量切换。推荐四步:
- 影子流量:把真实请求复制一份发给新网关,只记录结果、不返回给用户,对比输出质量与延迟;
- 1% 灰度:按用户 ID 哈希取模分流,观察错误率和投诉;
- 10% → 50%:每一步稳定运行 24 小时再扩大;
- 全量:保留旧通道至少一周,随时可回切。
分流放在网关或配置中心,不要写进业务代码——回滚时才不需要发版。
五、上线后盯这五个指标
| 指标 | 为什么重要 |
|---|---|
| 请求成功率(按错误码分组) | 429 和 5xx 的处理方式完全不同,混在一起看不出问题 |
| p95 / p99 延迟 | 平均值会掩盖长尾,而用户只记得最慢的那一次 |
| 首 token 延迟 | 流式场景下,体感取决于第一个字什么时候出现 |
| 每请求 token 数 | 突然上涨通常意味着上下文或检索出了问题 |
| 单次业务动作成本 | 把 token 换算成"一次问答多少钱",才看得懂账单 |
六、回滚预案
回滚要能在一分钟内完成,且不需要发版。三个前提:
- 地址和密钥放在环境变量或配置中心,两套都常驻;
- 模型名走别名映射,回滚只改映射表;
- 有明确的回滚触发条件,比如成功率低于 99%、p95 翻倍、错误码集中在 401/402。
七、一个可切换的封装
把差异收敛到一个文件里,业务代码只调用 chat():
import os
from openai import OpenAI
BASE_URL = os.getenv("LLM_BASE_URL", "https://<接入地址>/v1")
API_KEY = os.getenv("LLM_API_KEY")
MODEL_ALIAS = {
"default": "<主模型>",
"fast": "<轻量模型>",
}
client = OpenAI(base_url=BASE_URL, api_key=API_KEY, timeout=30.0, max_retries=2)
def chat(messages, model="default", **kwargs):
return client.chat.completions.create(
model=MODEL_ALIAS.get(model, model),
messages=messages,
**kwargs,
)
切换供应商时,只改环境变量和别名表;业务代码一行不动。这也是为什么值得坚持"只依赖 OpenAI 兼容协议,不用私有字段"——它决定了你下一次迁移的成本。
结语
迁移本身不难,难的是迁移之后还能随时换回去。
把地址、密钥、模型名三样东西外部化,把协议能力做成验收用例,把回滚做成配置开关——做到这三点,选哪家网关就不再是一个需要赌的决定。