AI 学习笔记(六):多 Provider API 接入怎么做,OpenAI、Claude、DeepSeek 差异与统一封装
上一篇把单一 OpenAI 调用收口成了可复用服务层。
但项目只要再往前走一步,问题就会变成:如果我要同时接 OpenAI、Claude、DeepSeek,还能不能继续只写一套代码?
答案是:可以统一,但不能硬抹平。
这三家都有文本生成、工具调用、结构化输出能力,但请求入口、消息格式、上下文状态和高级特性并不完全一样。
真正稳的做法不是追求“一个巨大的通用请求体”,而是:统一你的业务接口,在 provider 边界做适配。
1. 先说结论:统一业务契约,不要统一底层细节
接多 provider 时,最容易走偏的一步,是先做一个“万能 messages 数组”,试图把所有平台都塞进同一个字段结构里。
短期看很省事,长期通常会出问题:
- OpenAI 现在更推荐走
Responses API - Claude 原生接口是
Messages API,system放在顶层,不在消息数组里 - DeepSeek 对 OpenAI 兼容度很高,但某些能力只在特定模型或 Beta base URL 生效
所以更稳的分层应该是:
- 业务层只关心:
- 我要文本还是结构化对象
- 需要哪些工具
- 超时、重试、日志、限流怎么统一
- Provider Adapter 关心:
- 具体请求长什么样
- 如何解析响应
- 哪些能力支持,哪些能力要降级
一句话总结:你要统一的是“我想让模型做什么”,不是“每家 SDK 长得一模一样”。
2. 三家最值得先看清的差异
| 维度 | OpenAI | Claude | DeepSeek |
|---|---|---|---|
| 主推荐接口 | responses.create |
messages.create |
OpenAI 兼容 chat.completions.create |
system 放哪 |
作为 input 里的 system 消息 |
顶层 system 字段 |
和 OpenAI 兼容,放 messages |
| 会话状态 | 可用 previous_response_id 串联响应 |
默认无状态,历史消息由你自己带 | 官方明确说明是无状态,需自行传历史 |
| 结构化输出 | text.format 支持 json_schema |
原生可做结构化输出,但接口形态不同 | JSON Output 可用;Strict function calling 仅 Beta |
| 工具调用 | 有 function calling,也有内建工具 | tools + tool_use / tool_result |
OpenAI 兼容 function calling |
| SDK 策略 | 官方 openai 包 |
官方 @anthropic-ai/sdk 包 |
可直接复用 openai 包配 baseURL |
这里有两个特别容易踩坑的点:
2.1 Claude 不要按 OpenAI 思维硬套
如果你直接用 Claude 原生 API,system 不是 messages 里的一个 role,而是单独字段。
另外它的工具调用响应里会出现 tool_use 内容块,回传工具结果时要发 tool_result,跟 OpenAI/DeepSeek 那种 function calling 心智模型并不完全一样。
2.2 DeepSeek “兼容 OpenAI”不等于“完全等同”
用 OpenAI SDK 配 DeepSeek 的 baseURL,大多数文本生成场景会很顺手。
但像 strict function calling、reasoner 模型限制、上下文缓存等细节,就不能默认按 OpenAI 的行为去猜。
3. 一套更稳的封装思路
我更建议把多 provider 封装成两层:
3.1 通用能力层
这层定义项目真正需要的最小接口,例如:
generateTextgenerateObjectcallToolsstreamText
再配上统一的:
timeoutmaxRetriestraceId/requestId- 错误分类
- provider/model 维度日志
3.2 Provider Adapter 层
每家只实现自己那部分差异:
OpenAIAdapter- 调
responses.create - 解析
output_text - 需要结构化输出时走
text.format
- 调
ClaudeAdapter- 调
messages.create - 从
content块中提取text - 工具调用按
tool_use/tool_result处理
- 调
DeepSeekAdapter- 继续用
openaiSDK - 但固定好
baseURL - 对 Beta 才支持的能力做显式开关
- 继续用
这套设计的关键不是“高级”,而是避免最低公分母抽象。
比如 Claude 有 Prompt Caching,DeepSeek 有默认开启的 Context Caching,OpenAI 有更偏原生的 Responses 能力。
这些都不应该被你的通用接口直接抹掉,而应该通过 providerOptions 这类扩展字段保留出口。
4. 一个可直接复用的最小示例
下面这个例子只统一“文本生成”,足够说明封装方向:
1 | import OpenAI from 'openai'; |
这段代码刻意没做太多抽象,只做两件事:
- 把三家的入口收敛成同一个
generateText - 保留
raw,方便后面拿 request id、usage、provider 特有字段继续扩展
这比一开始就设计“万能 SDK”更稳。
5. 真正上项目时,别只盯着调用成功
多 provider 接入里,真正决定稳定性的通常不是“能不能返回文本”,而是下面这些工程细节:
5.1 错误分类要带上 provider 维度
同样是 429,不同 provider 的限流节奏和恢复方式不一样。
日志里至少要带:
providermodelrequestIddurationMsretryableerrorType
5.2 结构化输出不要假设三家完全等价
OpenAI 的 Responses API 可以直接用 json_schema。
Claude 原生也能做结构化输出,但如果你走它的 OpenAI compatibility layer,有些 OpenAI 字段会被忽略或做兼容转换。
DeepSeek 的严格 function calling 目前又带 Beta base URL 限制。
所以这块更合理的做法通常是:
- 通用接口叫
generateObject - 每家单独实现
- 不支持 strict 的 provider 明确降级,而不是静默伪装支持
5.3 会话管理不要偷懒
OpenAI 已经有更强的响应链路能力,但 Claude 和 DeepSeek 这边,你仍然经常要自己管历史消息。
如果系统未来要做 Agent 或多轮工作流,建议尽早把“上下文拼装”从 provider 实现里拆出来,做成单独模块。
6. 什么时候该选哪一家
如果你只是想先跑通一套最小 AI 服务层,我的建议是:
- OpenAI 优先
Responses API、结构化输出、工具调用路线更完整,适合先把工程骨架搭起来。 - Claude 第二个接
它最能逼你把“不要把 provider 细节揉进业务层”这件事做对。 - DeepSeek 适合补成本和国产可用性维度
OpenAI 兼容入口上手快,但特性边界一定要单独测,不要只靠想当然。
总结
从单 provider 走到多 provider,真正的分水岭不是“多写几个 API Key”,而是你有没有把封装层次想清楚。
最值得记住的只有一句话:
统一业务能力接口,接受 provider 差异存在,在边界做适配,而不是强行把三家揉成一个假想标准。
这样做的好处是:
- 后续接第四家模型时改动最小
- 降级、路由、A/B 对比更容易做
- 结构化输出、工具调用、流式输出这些高级能力不会被最低公分母限制死
下一篇我会继续沿着这条线往前走:
把多 provider 差异进一步收口成一个可扩展的路由/降级服务层。