DeepSeek API 迁移评估:从 OpenAI 切换前先梳理代码改动点

如果把业务从 OpenAI API 切换到 DeepSeek API,最危险的一句话是:“模型名和 base_url 改一下应该就行了吧。”

这句话危险,不是因为底层一定复杂,而是因为迁移成本取决于一个还没确认的前提:两个服务在协议层的差异有多大。接口兼容不是一个“是/否”变量,而是请求、响应、错误、限流、扩展能力这五层差异的累加。在拿到两份官方文档并完成最小实验之前,任何成本估算都是猜测。

本文不预设 DeepSeek API 的具体行为。下面这套框架,用来在切换前把代码里所有可能受影响的点找出来,再逐项去官方文档和测试环境确认。

1. 先给存量代码做静态扫描

扫描不是为了立刻改代码,而是为了建立调用点地图。重点找四类位置:

  • 配置入口:API Key、Base URL、模型名、超时时间是否集中管理;
  • HTTP 客户端:业务代码是否直接发起请求,还是统一经过 SDK;
  • 响应解析:应用从哪里抽取文本、用量、结束原因;
  • 错误分支:哪里对错误码或异常类型做了重试、降级、告警。

扫描产出是一张清单:模块路径、调用方式、关键参数、响应依赖、错误依赖。后续改动量估算都基于这张表,而不是基于模糊记忆。

2. 协议差异核查清单

对照两个服务文档前,先把要核对的层面列全。需要特别说明的是,OpenAI 目前提供 Chat Completions 与 Responses 两套 API,两者在请求路径、消息结构和响应字段上并不相同。DeepSeek 当前主要兼容 Chat Completions 协议,因此下文以 Chat Completions 为基准展开;若你的代码基于 Responses API,迁移前需额外确认目标服务是否提供对应端点。

  • 认证方式:API Key 放在哪个 Header;格式是否一致。
  • 资源路径:要调用的端点路径;是否与既有接入点一一对应。
  • 请求体字段:模型字段名、消息结构、参数字段和取值范围。
  • 响应结构:文本内容在对象中的嵌套位置;字段是否可能为 null;结构与原系统的哪些字段关联。
  • 错误对象:HTTP 状态码、错误体的字段层级、错误码枚举。
  • 流式返回:SSE 事件格式、结束标记、是否携带用量信息。
  • 扩展能力:Function Calling、JSON 模式、异步并发等使用项。

建议用一个三列映射表:原有调用方式、目标服务文档要求、是否影响现有代码。目标服务文档如果不存在同名能力,不要假设行为一致。

3. 先用原始 HTTP 探针验证

不要第一步就接入目标服务 SDK。SDK 会把响应解析成结构化类型,隐藏原始协议细节,而这些细节正是迁移中最容易出错的部分。

用一个极简 Python 探针,不绑定任何具体 SDK:

import os
import json
import requests

base_url = os.environ['API_BASE_URL'].rstrip('/')
endpoint = os.environ['COMPLETIONS_ENDPOINT']
api_key = os.environ['API_KEY']

# 这里放目标服务官方文档中的最简请求样例
request_body = json.loads(os.environ.get('REQUEST_BODY', '{}'))

resp = requests.post(
    url=f'{base_url}{endpoint}',
    headers={'Authorization': f'Bearer {api_key}'},
    json=request_body,
    timeout=30,
)

print('status:', resp.status_code)
print('headers:', json.dumps(dict(resp.headers), indent=2))
print('body:', resp.text)

API_BASE_URLCOMPLETIONS_ENDPOINTAPI_KEYREQUEST_BODY 放入环境变量后运行。注意,不同服务对补全端点的路径定义可能不同,例如 OpenAI 的 Chat Completions 路径为 /v1/chat/completions,而 DeepSeek 的兼容端点路径需以其官方文档为准。建议在环境变量中显式配置完整端点路径,例如:

export API_BASE_URL="https://api.deepseek.com"
export COMPLETIONS_ENDPOINT="/chat/completions"  # 以官方文档为准
export API_KEY="your-key"
export REQUEST_BODY='{"model":"deepseek-chat","messages":[{"role":"user","content":"Hello"}]}'

第一次不需要追求业务输出,只需要确认三件事:服务端是否接受请求;状态码是否符合预期;原始响应中是否存在业务所需字段。

4. 响应解析单独收口

先把原始响应完整打印出来,再决定解析层怎么改。不要直接复制原代码的解析逻辑。

建议加一个内部函数:

def extract_text(response_dict):
    # 根据目标服务实测响应结构调整字段名
    # 业务代码不感知底层字段差异
    raise NotImplementedError

如果项目在多个地方访问底层响应字段,切换前先补这一层。否则每个调用点都可能改一遍,而且容易漏。

5. 错误映射要单独做

请求逻辑可以很快改完,错误逻辑才是常见的隐蔽成本。现有重试、告警往往依赖原服务错误体中的某些字段;目标服务的错误结构一旦不同,这些逻辑会悄悄失效。

建议造出这些错误场景并分别记录状态码和错误体:

  • 认证信息无效;
  • 请求参数缺失;
  • 模型不存在;
  • 配额不足或欠费;
  • 并发超限。

然后维护一个“上游错误 → 内部错误类型”的映射表,统一修改错误处理模块。不要假设 429、500 这些语义一定相同,也不要预设错误码。

6. 用兼容层隔离风险

一个可行的做法是,在业务代码与具体 API 之间放一个薄接口:

class CompletionClient:
    def __init__(self, config):
        self.config = config

    def chat(self, messages, **kwargs):
        # 切换前后,只改这个类的内部实现
        raise NotImplementedError

这个类的价值不是设计好看,而是让新老接入方式可以短期并存。灰度期间可以按请求来源或内部标记决定走哪条链路。

7. 测试与灰度切换

回归测试不要一开始就用真实账号大量调用。可以准备一组固定输入,先在两个服务上分别跑,比较状态码和响应字段是否稳定。把解析差异整理成 diff,之后再进入代码修改。

灰度阶段要注意可回退性。base_url、认证凭证、模型名应该全部做成配置,而不是散落到代码中。一旦发生异常,要能通过配置切回原服务,而不是重新发布代码。

结论

从 OpenAI API 切换到 DeepSeek API 的成本,不取决于宣传中的“兼容”程度,而取决于你自己的代码对响应结构、错误结构、流式和扩展能力的依赖深度。

正确的迁移顺序是:先静态扫描出调用点,再用探针拿原始响应,接着做字段映射与错误映射,最后通过配置灰度切换。只有走完这一步,你才算真正知道这是一次低风险配置调整,还是需要预留两到三周的改造工程。