← 返回资讯

用 SDK 还是直接调 HTTP:三条接入路线的取舍

2026-08-07

接第一家平台的时候,这根本不是个问题:文档给什么示例你抄什么,十分钟跑通,皆大欢喜。接到第三家就是问题了——你会发现代码里已经有三套客户端初始化、三种错误对象、三处流式解析,而且它们互相之间没有任何共同抽象。这时候再回头改,成本比一开始就想清楚高得多。

所以这篇不谈”哪种方式更好”,谈的是三条路各自的形状:换平台时你要改多少行,出问题时你能看到多少东西。这两件事才是长期成本的来源。

三条路各自长什么样

路线一:各平台的官方 SDK

每家平台自己发的客户端库。好处很直接:文档、示例、issue 区都围着它转,平台的特有能力(自家的部署形态、自家的任务模型、自家的扩展字段)通常第一时间出现在官方 SDK 里,而不会出现在通用兼容层里。

代价是每家一套。客户端的构造参数不一样,返回对象的字段名不一样,抛的异常类型不一样,流式的迭代方式也不一样。接两家的时候你还能忍,接到第四家,业务代码里就会长出一片 if platform == "xxx" 的分支,而且这些分支会渗透到调用方——上层想加个重试,得为每种异常类型各写一遍。

有一类平台是必须走这条路(或者说必须按它自己的范式走)的:接入范式跟 OpenAI 那套 Chat Completions 根本不同的。比如 Replicate 用的是自己的 predictions API,请求体是 version + input 的结构,默认是异步的——提交任务、然后轮询或者等 webhook 回调,也可以用 Prefer: wait 请求头切到同步模式。这种平台你不可能”改个 base_url 就切过去”,改的是调用范式本身。选型的时候,这个问题要比比价更早回答。

路线二:OpenAI SDK 指向兼容端点

多平台场景下的主流选择,也是我默认推荐的起点。绝大多数推理平台都提供 OpenAI 兼容端点,官方文档的口径高度一致:把 base URL 指过来、把 key 换掉、把模型 ID 换成它目录里的,其余代码不动。DeepInfra 的文档就明写了迁移是三处改动(改 base URL、换 token、指定其模型目录里的模型),Novita、Cerebras、Together AI 的文档也都是同样的说法。

这条路的收益是你的调用代码只有一套。切平台是配置层的事,不是代码层的事;单元测试、mock、日志格式、重试策略全部可以共用。Fireworks AI 还多给了一条路——它同时提供 OpenAI 兼容端点和 Anthropic SDK 兼容端点,存量 Anthropic SDK 代码也有低成本迁移路径,这是同类平台里少见的。

代价写在名字里:你只能用到”兼容部分”的能力。平台自己的扩展能力,兼容端点上不一定暴露;反过来,OpenAI 那套接口里的一些参数,平台也不一定支持。后面第三节专门讲这个。

路线三:裸 HTTP

自己拼 JSON、自己发请求、自己解析响应。这条路被低估了,我见过不少团队在两种场景下应该选它却没选:

一是排查问题。裸 HTTP 没有任何中间层,你发出去什么、收回来什么,一目了然。SDK 帮你做的事越多,出问题时要排除的可能性就越多——超时到底是谁的超时、重试到底重了几次、流式断在哪一层,这些在 SDK 里都得翻源码才能确认。

二是依赖敏感的运行环境。Serverless 函数要控冷启动体积,嵌入式或者边缘环境装不动一堆传递依赖,某些内网环境连不上包仓库——这些场景下一个标准库的 HTTP 客户端反而最省心。

代价是流式解析、超时分层、重试退避、错误码分流、连接复用这些全要自己写,而且写不好比用 SDK 更容易出事。这部分清单在第五节。

怎么选:按团队规模和平台数量

没有普适答案,但有几条经验规则:

情形建议理由
只接一家,且长期不打算换官方 SDK文档最全、示例最新,没必要给自己找抽象成本
接 2 家以上,都是 OpenAI 兼容OpenAI SDK + 配置切 base_url代码一套,切换是配置层的事
接的平台里有非 OpenAI 兼容范式的该平台单独走它自己的范式,其余走兼容强行统一会把异步任务模型硬塞进同步接口,得不偿失
一两个人的小团队、原型阶段OpenAI SDK省下来的时间比什么都值钱
有平台工程/中间件角色的团队兼容 SDK 为主 + 关键路径可降级到裸 HTTP需要精确控制的地方留一个逃生口
边缘/Serverless/受限依赖环境裸 HTTP体积和依赖是硬约束

再加一条:平台数量超过三家之后,真正该做的不是选 SDK,而是做一层自己的抽象。这一点第六节展开。

关于 OpenAI SDK 本身的用法(客户端构造、流式、异步、错误类型),可以看openai-python SDK 用法详解;兼容端点这个概念本身的边界,看OpenAI 兼容端点到底兼容了什么

“兼容”不等于”一样”

这是本文最想说清楚的一点。“OpenAI 兼容”是个营销上很省事、工程上很危险的词——它保证的是协议形状一致(路径、请求体结构、响应字段),不保证参数集合一致

好在有平台把边界写出来了。Groq 的官方文档里明确列出了它的 OpenAI 兼容端点不支持的参数

  • logprobslogit_biastop_logprobs
  • messages[].name
  • N(如果传,必须等于 1)
  • temperature 传 0 会被转换成 1e-8
  • 音频格式 vttsrt

这份清单值钱的地方在于,它列的每一项都对应一类真实会咬人的场景:

logprobs 算置信度的。 有一类做法是拿 token 的对数概率来判断模型这次回答有多”确定”,低于阈值就转人工、转检索、转更大的模型。这套逻辑在 OpenAI 上跑得好好的,切到不支持 logprobs 的端点上,拿不到这个字段,整条置信度链路直接失效——而且失效方式很可能不是报错,是你的置信度恒等于某个默认值,风控形同虚设。

n>1 采样择优的。 一次请求让模型生成多个候选,再用规则或者小模型打分挑一个,是提质量的常见手法。Groq 明确写了 N 必须为 1。这意味着同样的效果你得改成发 N 次请求——成本模型和并发模型全变了,不是改个参数的事。

追求确定性输出的。 很多人习惯把 temperature 设成 0 求”稳定”。Groq 的文档写了传 0 会被转换成 1e-8。这是个极小的正数,采样在数学上仍然存在随机性。如果你的测试用例是”同样输入必须得到同样输出”,这条会让你在切平台之后开始怀疑人生——因为绝大多数时候输出确实一样,偶尔不一样,属于最难复现的那类问题。

messages[].name 做多角色区分的。 多智能体或者多人对话场景里,有人用 name 字段标注发言人。这个字段不支持,可能是被忽略(更糟:静默丢信息),也可能是报错。

做转写字幕的。 音频不支持 vttsrt 输出格式,意味着字幕这一步得自己从时间戳结构里拼。

我要特别强调一句:**上面这份清单是 Groq 官方公开的,只对 Groq 成立。**其他平台各有各的兼容边界,可能更宽也可能更窄,本文不替它们断言。你要做的是去每一家的官方文档里找它们自己的”不支持参数”或”兼容性说明”章节——找不到这一章,就当作”边界未知”,用最保守的参数集起步,别把生僻参数放进生产路径。

工程上的应对办法,我一般是这么做的:

  1. 列出你实际用到的所有请求参数,一个不落,包括那些”顺手加的”。
  2. 每接一家新平台,拿这份列表去对它的文档,逐项确认。
  3. 对拿不准的参数,写一个最小验证脚本:只发一个请求,把参数带上,看它是报错、是忽略、还是生效。三种行为要区分对待——“忽略”是最危险的,因为没有任何信号。
  4. 把结果记成一张表,进代码库,别只存在某个人的脑子里。

base_url 这一行,是最容易翻车的一行

参数差异要跑起来才暴露,base_url 写错则是当场就挂,反而算好事。麻烦在于各家的路径写法真的不一样,抄的时候极容易漏掉一段:

平台OpenAI 兼容 base_url
Groqhttps://api.groq.com/openai/v1
DeepInfrahttps://api.deepinfra.com/v1/openai
Novitahttps://api.novita.ai/openai
Cerebrashttps://api.cerebras.ai/v1
Together AIhttps://api.together.ai/v1
Fireworks AIhttps://api.fireworks.ai/inference/v1
Nebius Token Factoryhttps://api.tokenfactory.nebius.com/v1/

看出规律了吗?没有规律。Groq 是 /openai/v1,DeepInfra 是 /v1/openai,两段顺序刚好反过来;Novita 干脆没有 /v1;Fireworks 中间多了一段 inference,抄漏了的表现是 404 而不是鉴权错误;Nebius 官方写法带末尾斜杠。

这件事之所以特别坑,是因为 SDK 会替你在 base_url 后面拼接口路径。你填的是”前缀”,SDK 拼上 /chat/completions 之类的后缀发出去。前缀错一段,最终 URL 就落在一个不存在的路径上,服务端返回 404 或者一个 HTML 错误页——而 SDK 拿到非预期响应之后抛的异常,往往跟”URL 写错了”这个真实原因毫无字面关系。我见过有人对着一个 JSON 解析失败的报错查了半小时鉴权。

可靠的办法是先 curl 验证,再倒推 SDK 配置

# 第一步:直接用完整 URL 打一发,确认路径是通的
curl -s -o /dev/null -w "%{http_code}\n" \
  -X POST "<平台文档给的 base_url>/chat/completions" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"<平台文档里的示例模型 ID>","messages":[{"role":"user","content":"hi"}]}'

看状态码分流:

  • 200:路径和 key 都对,可以把这个 URL 去掉 /chat/completions 之后填进 SDK 的 base_url。
  • 404:路径错了,回文档核对每一段,重点看有没有 inference 这类中间段、v1 在前还是在后。
  • 401 / 403:路径大概率是对的(请求打到了鉴权层),问题在 key 或者权限。
  • 400 / 422:路径和鉴权都通了,是请求体的问题——通常是模型 ID 写错。模型 ID 的命名风格各家差别也很大,扁平名、两段式、三段式都有,具体见模型 ID 命名规则

这个顺序的价值在于每一步只有一个变量。先把路径钉死,再谈鉴权,再谈请求体。base_url 更细的坑单独整理在base_url 配置常见错误

裸 HTTP 要自己补的五件事

决定手写 HTTP 之前,先看看这份清单,确认你愿意把这些都实现一遍:

1. 流式解析。 服务端事件流不是一行一个 JSON 那么简单:数据以 data: 前缀的块发送,一个 TCP 读取可能拿到半个块,也可能一次拿到三个块,所以你必须自己做缓冲和按分隔符切分,不能假设”读一次等于一条消息”。还要正确识别结束标志,并且区分”流正常结束”和”连接被掐断”——后者如果当成正常结束处理,用户会看到一段没头没尾的半截回答,而你的日志里一条错误都没有。

2. 超时分层。 一个总超时是不够的。连接建立、首字节返回、流式过程中两个数据块之间的间隔,这三种超时的合理值差着数量级:连接超时可以很短,首字节超时要留出模型排队和推理的时间,而流式场景下总超时几乎不该设——长回答本来就慢,设了会把正常请求砍断。真正要设的是”块间空闲超时”:多久没收到新数据算卡死。这一条写错,表现是”长文本生成总是在某个长度断掉”,很多人会误以为是模型的输出长度限制。

3. 重试与退避。 哪些错误该重试是个需要想清楚的问题:网络层错误、限流、部分 5xx 可以重试;请求体本身非法(4xx 里的语法/语义错误)重试多少次都是一样的结果,只是白烧配额。重试要带指数退避和抖动,否则一次抖动会让所有客户端同时回来,把刚缓过来的服务再打垮一次。还有个容易忘的:流式请求已经吐出去一部分再重试,会导致内容重复,要么放弃重试,要么在应用层做去重。

4. 错误码分流。 各家的错误码不完全一致,除了常见的 4xx/5xx,有的平台还有自定义码——Groq 的文档里就列了 498(Flex Tier 容量超限)和 499(调用方取消请求)这两个自定义码。你的分流逻辑如果只写了标准码,遇到自定义码会掉进 else 分支,然后按最保守的方式处理(通常是不重试直接失败),白白丢掉本可以恢复的请求。另外 Groq 官方明确写了 5xx 响应不计费,这类信息对做成本核对很有用。

5. 连接复用。 每次新建 TCP + TLS 握手的开销,在高频短请求场景下相当可观。SDK 一般默认帮你维护连接池,手写的时候如果每次都新建客户端对象,等于把这个优化丢了。这个问题的隐蔽之处在于:功能完全正常,只是慢,而且慢得很均匀,不容易归因。

一个折中方案:自己做一层薄封装

三条路其实不是单选题。我的实际做法是:在自己的代码里定义一层业务语义的接口,对内可以是官方 SDK、可以是 OpenAI SDK、也可以是裸 HTTP。

关键在于”薄”和”业务语义”这两个词:

  • :这层不做业务逻辑,只做协议适配和统一。不要在这层写提示词拼装、不要写结果解析、不要写业务分支——那些属于上层。这层的职责边界是”把各家的差异吃掉”,超出这个范围就会变成第二个难维护的地方。
  • 业务语义:对外暴露的方法名要贴业务,不要贴协议。比如 摘要(文本) -> 结果 而不是 chat_completion(messages, temperature, top_p, ...)。后者只是把 OpenAI 的接口原样转发了一遍,换实现时该改的地方一处也没少。

这层封装至少要统一三样东西:统一的错误类型(把各家的异常映射成你自己的几类:可重试、不可重试、限流、鉴权失败),统一的用量口径(token 计数字段名各家可能不同,在这里归一,上层做成本统计才不用关心来源),统一的流式接口(对外就是一个字符串序列,内部谁解析的不重要)。

有了这层,多平台通道抽象才有地基——路由、降级、按成本选路这些能力都建立在”调用方不知道也不关心底下是谁”之上。如果调用方直接持有某家 SDK 的客户端对象,任何路由逻辑都无从下手。

再补一句实话:**只接一家的时候别做这层。**过早抽象是另一种浪费,而且第一版抽象几乎必然是错的——因为你只见过一家的形状。合理的时机是接第二家的时候,那时你才第一次看到”差异”长什么样。

不管走哪条路,先把请求打印出来

这一条能省掉的猜测量,超过本文其他所有建议加起来。

要能打印出实际发出去的请求 URL 和请求体(脱敏后)。 注意是”实际发出去的”,不是”你以为发出去的”——SDK 拼接过的完整 URL、序列化之后的 JSON 体、真正生效的超时值。很多问题在看到这两样东西的一瞬间就自明了:URL 少了一段、模型 ID 多了个空格、某个参数被 SDK 的默认值覆盖了、messages 里混进了一条空内容。

几个实施要点:

  • 脱敏是硬要求。Authorization 头、key、用户隐私内容都要处理。常见做法是 key 只留前后各几位,中间打码——保留首尾是为了能确认”用的是哪一把 key”,这在多环境多 key 的时候特别有用。
  • 做成开关,别常开。日志里全是请求体会把存储和检索都拖垮,也增加泄漏面。用环境变量或者动态配置控制,出问题时打开。
  • 顺手把响应头也记上。不少平台在响应头里返回限流相关的信息,出问题时这是最直接的证据。具体有哪些头、字段名叫什么,各家不同,以各自官方文档为准。
  • 别只在报错时打印。有些问题表现为”结果不对但没报错”,这类只有正常路径也有日志才查得出来。

自查清单

  1. 数一下你现在接了几家平台。超过两家但代码里没有统一抽象层的,把这件事排进计划。
  2. 列出你实际用到的全部请求参数,对着每家平台的官方文档逐项确认支持情况,记成一张表进代码库。找不到官方兼容性说明的,按”边界未知”对待,只用最基础的参数集。
  3. 对拿不准的参数写最小验证脚本,明确区分”报错""被忽略""生效”三种结果——被忽略的最危险。
  4. 换平台前,先用 curl 打通 /chat/completions,按 404 / 401 / 400 分流定位,再把验证过的前缀填进 SDK 的 base_url。
  5. 如果你手写 HTTP,逐条核对五件事是否都实现了:流式分块与结束标志、超时分层(尤其是块间空闲超时)、重试退避与流式重试的重复问题、错误码分流(含平台自定义码)、连接复用。
  6. 如果你依赖 logprobs 做置信度、依赖 n>1 做采样择优、或者依赖 temperature=0 求确定性,把这三条单独标成迁移风险项——它们在某些平台上不是”改个参数”能解决的。
  7. 给所有出站请求加上可开关的调试日志,打印脱敏后的完整 URL 与请求体,并确认脱敏逻辑本身被测试覆盖过。
  8. 做抽象层的话,确认它只做协议适配、对外暴露业务语义方法,并且统一了错误类型、用量口径和流式接口这三样。

相关阅读