OpenAI接口从Completions到Responses的演进与开源兼容性解析

发布时间:2026/11/2 14:12:38
OpenAI接口从Completions到Responses的演进与开源兼容性解析

1. 接口换代这件事,为什么值得单独聊一次

如果你最近半年翻过 OpenAI 的官方文档,大概率会有一种"走错片场"的感觉:以前闭着眼睛都能写的chat.completions.create,现在文档首页推的却是responses.create。更让人犯嘀咕的是,社区里一边有人说"Completions 要废弃了赶紧迁移",一边又有人说"Responses 根本跑不通,还是老接口香"。信息互相打架,最后的结果就是——很多人干脆两个都不敢动,项目里那套老代码继续跑着,心里却一直悬着。

这篇东西就是想把这件事讲透。核心话题是OpenAI 接口规范从 Completions 到 Responses 的演进逻辑,以及围绕它衍生出来的一堆"开源兼容"问题。我会从接口设计的底层动机讲起,拆解 Responses 到底改了什么、为什么这么改,然后重点聊一个大家最关心也最容易被带偏的问题:那些号称"兼容 OpenAI"的开源实现、第三方网关、本地推理服务,到底兼容到了什么程度,哪些能无缝切换,哪些一碰就碎。

适合谁看?三类人。第一类是把 OpenAI 接口当成基础设施在用的应用开发者,你需要判断"要不要迁移、什么时候迁移、迁移成本多大"。第二类是在做 AI 网关、模型路由、多模型聚合的工程师,你需要搞清楚兼容层的边界在哪。第三类是对接口设计本身感兴趣的技术人,想理解一个被全球开发者依赖的 API 是怎么一步步演化的。不管你是哪一类,读完应该都能对"该用哪个接口、怎么用、坑在哪"有个清晰的判断。

先说结论性的判断,免得你看到一半才发现方向不对:Completions 不会明天就消失,但新能力基本只会往 Responses 上堆;开源兼容层对 Completions 的兼容度远高于 Responses,所以短期内"双接口并存"是常态。下面展开讲为什么。

2. Completions 接口的设计基因与历史包袱

2.1 从"补全一段文本"到"对话"的语义漂移

要理解 Responses 为什么出现,得先回到 Completions 的出身。最早的/v1/completions接口,本质是一个文本补全接口:你给它一段 prompt,它还你一段续写。这个设计非常纯粹,输入是字符串,输出是字符串,模型干的事就是"接着往下写"。那时候大家用它写代码、续写文章、做翻译,都是这个逻辑。

后来 ChatGPT 火了,对话成了主流用法,于是有了/v1/chat/completions。注意这个名字——它是在completions前面加了个chat,而不是重新设计一个接口。这个命名本身就暴露了它的历史包袱:它是在文本补全的骨架上,硬套了一层"消息数组"的皮。你传进去的不再是字符串,而是一个messages数组,每个元素有role和content。模型内部把这些消息拼成一个特殊格式的 prompt,再走一遍补全逻辑。

这个"套皮"设计在早期够用,但随着能力越来越复杂,问题就藏不住了。最典型的就是多轮对话的状态管理。在 Completions 里,模型是无状态的,每一轮你都得把完整的历史消息重新传一遍。对话越长,请求体越大,token 消耗越多,而且你还得自己维护这个历史数组,稍不注意就丢上下文或者重复发送。

2.2 那些年被"套皮"设计坑过的场景

我踩过最典型的一个坑是函数调用(Function Calling)。Completions 时代的工具调用,流程是这样的:你传tools参数,模型返回一个tool_calls,你执行完工具,再把结果以role: "tool"的消息塞回messages数组,重新发一次请求。整个链路里,工具调用的中间状态全靠你自己在客户端维护。

问题来了:如果模型一次要调三个工具,其中两个成功一个失败,你怎么把失败信息优雅地传回去?如果工具调用过程中你想插入一段系统提示,怎么保证不破坏消息顺序?这些在 Completions 里都没有原生支持,全靠开发者自己拼消息数组。我见过不少项目,光是为了处理工具调用的状态机,就写了上千行胶水代码。

再比如多模态输入。Completions 后来支持了图片,但格式是content数组里塞image_url对象。这个设计能用,但很别扭——文本和图片混在一个数组里,顺序敏感,解析起来容易出错。而且音频、文件这些新模态加进来的时候,接口结构越来越臃肿。

还有一个更隐蔽的问题:推理过程不可见。Completions 返回的就是最终答案,模型"想了什么"你完全看不到。对于需要调试、需要审计、需要展示推理链的场景,这是个硬伤。虽然后来有reasoning相关的字段陆续加进来,但都是在原有结构上打补丁,不够系统。

2.3 为什么不能直接在 Completions 上继续改

有人会问:既然问题这么多,为什么不直接在 Completions 上迭代,非要搞个新接口?

答案在于兼容性诅咒。Completions 已经被全球无数应用依赖,任何破坏性改动都会引发灾难。你没法把messages数组改成别的结构,没法把工具调用的返回格式换掉,没法重新定义多模态的输入方式——因为一改,所有现存代码全挂。这就导致 Completions 只能做加法:加字段、加参数、加能力,但底层结构动不了。

加法的结果是接口越来越复杂,语义越来越混乱。同一个功能可能有多种实现方式,文档越写越厚,新用户上手成本越来越高。这时候,与其在旧地基上盖危楼,不如另起炉灶。Responses 就是那个"另起炉灶"的产物。

3. Responses 接口到底重构了什么

3.1 从"消息数组"到"事件流"的思维转变

Responses 最根本的变化,是把交互模型从**"请求-响应"改成了"请求-事件流"。在 Completions 里,你发一个请求,等一个完整响应,中间过程是黑盒。在 Responses 里,整个交互过程被拆成了一串事件(events)**,每个事件代表状态的一次变化。

这个转变听起来抽象,但影响非常实际。举个例子:在 Completions 里做流式输出,你拿到的是一个个delta,每个 delta 里有一小段文本。你只能看到"文本在往外蹦",但不知道模型是在思考、在调工具、还是在生成最终答案。而在 Responses 里,事件类型是明确的:有response.created、有response.output_text.delta、有response.function_call_arguments.delta、有response.completed。你能精确知道当前处于哪个阶段。

这个设计的好处是状态可观测。对于需要展示"AI 正在思考"、需要区分"推理内容"和"最终答案"、需要在工具调用时给用户反馈的场景,事件流提供了原生支持。你不用再靠猜或者靠 hack 来实现这些效果。

3.2 有状态会话:把历史管理从客户端搬到服务端

Responses 另一个重量级变化是有状态会话。前面说过,Completions 是无状态的,每轮对话你都得重传历史。Responses 引入了previous_response_id这个概念:你第一次请求拿到一个 response id,下一轮请求带上这个 id,服务端自动帮你把历史接上。

这个改动直接解决了好几个痛点。第一,请求体变小了。不用每次重传几十轮对话,省 token 也省带宽。第二,历史管理交给服务端,客户端逻辑简化。第三,多轮一致性更好,不会因为客户端拼错历史导致模型行为异常。

但这里有个坑必须提醒:有状态意味着服务端要存东西。如果你用的是第三方兼容实现,它可能根本不支持previous_response_id,或者支持得很粗糙。我实测过几个开源网关,有的直接忽略这个参数,有的虽然接受了但内部还是无状态处理,结果就是你以为接上了历史,实际上模型啥都不记得。这个后面会详细讲。

3.3 工具调用与多模态的原生整合

Responses 在工具调用上的设计,比 Completions 干净太多。在 Completions 里,工具调用是"模型返回 tool_calls,你执行,再塞回去"的三段式。在 Responses 里,工具调用是事件流的一部分:模型决定调工具时,会发出response.function_call_arguments.done事件;你执行完,把结果作为新的 input 传回去,继续同一个 response。

更关键的是,Responses 支持内置工具。比如网页搜索、文件搜索、代码执行这些,在 Completions 里你得自己接第三方或者自己实现,在 Responses 里可以直接声明tools: [{type: "web_search"}]之类的,服务端帮你搞定。这个能力对做 Agent 的团队来说是质变——以前要搭一整套工具编排框架,现在接口层面就支持了。

多模态方面,Responses 把输入统一成了input数组,里面可以放文本、图片、文件,结构比 Completions 的content数组更清晰。而且输出侧也支持多模态,模型可以直接返回图片、文件,不用你再单独调别的接口。

3.4 一个对比表看清两代接口的差异

维度CompletionsResponses
交互模型请求-响应请求-事件流
状态管理客户端维护历史服务端维护(previous_response_id)
工具调用三段式,客户端编排事件流原生支持,含内置工具
多模态输入content 数组混排input 数组,结构更清晰
推理可见性有限,靠字段补丁事件流区分推理与输出
流式输出delta 文本流类型化事件流
兼容性事实标准,广泛兼容新接口,兼容层支持有限

这张表建议存下来,后面判断"某个开源实现能不能用"的时候,对着它逐项检查就行。

4. 开源兼容层的真相:兼容的是哪一层

4.1 "兼容 OpenAI"这句话的水分有多大

现在市面上大量开源项目、推理框架、API 网关都标榜"OpenAI Compatible"。但这句话的信息量其实极低,因为它没说兼容的是哪个 OpenAI 接口。是/v1/completions?/v1/chat/completions?还是/v1/responses?三者差别巨大。

我做过一轮实测,把常见的兼容实现分成三档:

  • 第一档:只兼容/v1/completions。这是最老的接口,实现最简单,很多轻量级推理服务只做到这一层。你用它做文本补全没问题,但对话、工具调用基本别想。
  • 第二档:兼容/v1/chat/completions。这是目前的主流,绝大多数开源网关、本地推理服务都停在这一层。对话、流式、基础工具调用能跑,但细节参差不齐。
  • 第三档:兼容/v1/responses。这一档目前很少,而且大多只实现了子集,事件流、有状态会话、内置工具这些核心特性往往缺失。

所以当你看到"OpenAI Compatible"时,第一件事就是问:兼容到哪一档?如果对方文档里只提 chat completions,那 Responses 相关的能力你就别指望了。

4.2 兼容层最容易"形似神不似"的几个点

即便都声称兼容 chat completions,实际表现也差很多。我踩过的坑主要集中在下面几个地方。

第一个是流式格式。OpenAI 的流式返回是 SSE(Server-Sent Events),每个 chunk 是data: {...}格式,最后以data: [DONE]结束。有些兼容实现虽然也返回 SSE,但 chunk 结构不对,比如缺少choices[0].delta的某些字段,或者finish_reason的时机不对。你的客户端如果严格按 OpenAI 的格式解析,就会出错。

第二个是工具调用的参数拼接。流式工具调用时,OpenAI 会把函数参数分多个 chunk 传,每个 chunk 是参数 JSON 的一个片段,你需要自己拼接。有些兼容实现直接一次性把完整参数传过来,或者拼接逻辑有 bug,导致你拿到的 JSON 不完整。这个坑很隐蔽,因为非流式下看不出来,一开流式就炸。

第三个是 token 计数和截断。OpenAI 的max_tokens、finish_reason: "length"这些行为,兼容实现经常对不齐。有的把max_tokens当摆设,有的截断后不返回正确的 finish_reason,导致你的重试逻辑失效。

第四个是错误码和错误结构。OpenAI 的错误返回有固定结构,包含error.type、error.code、error.message。兼容实现经常直接抛一个通用错误,你的错误处理代码就抓瞎了。

4.3 为什么 Responses 的兼容这么难做

Responses 兼容难,根本原因在于它的事件流模型。Completions 的兼容,本质是"把请求转成内部格式,把内部输出转成 OpenAI 格式",是个相对简单的适配层。而 Responses 的事件流,要求兼容实现真正理解并维护一套状态机:什么时候发response.created,什么时候发output_item.added,工具调用和文本输出怎么交错,previous_response_id怎么映射到内部会话……

这套东西不是简单转格式能搞定的,需要兼容层自己实现一套完整的事件编排逻辑。对于大多数开源项目来说,投入产出比太低——毕竟用 Responses 的人还不多,做深了也没多少人用。所以现状就是:Responses 的兼容层大多停留在"能返回个结果"的水平,事件流的精髓基本丢失。

5. 迁移决策:什么时候该动,什么时候该等

5.1 判断你的项目属于哪一类

要不要迁移到 Responses,不能一刀切,得看你的项目类型。我把它分成三类。

第一类:纯文本对话应用。比如客服机器人、写作助手、问答系统。这类应用 Completions 完全够用,迁移到 Responses 的收益主要是"历史管理简化"和"推理可见性",但如果你现在的历史管理没出问题,迁移的紧迫性不高。可以等 Responses 生态更成熟再说。

第二类:Agent / 工具调用密集型应用。比如自动化工作流、多工具编排、需要内置搜索或代码执行的场景。这类应用强烈建议尽早评估 Responses。因为工具调用的原生支持、内置工具、事件流的可观测性,对这类应用是实打实的效率提升。你在 Completions 上写的那些工具编排胶水代码,在 Responses 上可能直接省掉。

第三类:多模型 / 多供应商架构。如果你的系统要同时对接 OpenAI、开源模型、其他厂商,那迁移决策要更谨慎。因为 Responses 的兼容性差,你一旦深度绑定,切换成本会很高。这种情况下,保持 Completions 作为统一抽象层可能更稳妥,把 Responses 的特性作为可选增强。

5.2 迁移成本的真实构成

很多人低估了迁移成本,以为"改个接口名、调下参数"就完事。实际成本主要在三个地方。

一是数据结构重构。Completions 的messages数组和 Responses 的input数组,结构不一样。你的消息构造、历史管理、工具结果回传,这些逻辑都要重写。如果代码里到处散落着messages.push(...),那改动量不小。

二是流式处理重写。前面说过,Responses 是类型化事件流,你的流式解析逻辑要从"解析 delta 文本"改成"解析事件类型并分发处理"。这是个架构级改动,不是改几行能搞定的。

三是测试和回归。接口行为变了,你的测试用例、mock、断言都要跟着改。而且 Responses 的一些行为(比如事件顺序)可能和 Completions 不完全一致,需要重新验证。

我的建议是:先做一个小范围试点,挑一个非核心功能迁移到 Responses,跑通全链路,把坑摸清楚,再决定要不要全面铺开。别一上来就全量迁移,风险太大。

5.3 双接口并存的过渡策略

现实一点讲,未来一两年内,双接口并存会是大多数项目的常态。你需要一套策略来管理这种并存。

我的做法是在应用层做一层抽象,把"对话"、"工具调用"、"流式输出"这些概念抽象成内部接口,底层可以切换 Completions 或 Responses 实现。这样迁移的时候,只改底层适配器,上层业务代码不动。这层抽象会增加一些前期成本,但长期看非常值——尤其是当你还要对接多个供应商的时候。

具体实现上,可以定义一个LLMClient接口,包含chat()、stream()、call_tool()这些方法,然后写CompletionsClient和ResponsesClient两个实现。业务代码只依赖LLMClient。这样 Responses 成熟了,切过去就行;某个供应商只支持 Completions,也能兼容。

6. 实操中那些文档不会告诉你的细节

6.1 事件流解析的正确姿势

Responses 的事件流解析,有几个细节文档讲得不清楚,但实操中很关键。

第一,事件类型要穷举处理,但要有兜底。Responses 的事件类型会随版本增加,你不能只处理已知类型,遇到未知类型要能优雅忽略而不是崩溃。我的做法是维护一个已知类型集合,未知类型记日志但不中断。

第二,response.completed不一定是你以为的那个时机。在工具调用场景下,模型可能先发一个response.completed,然后你回传工具结果,又触发新一轮。所以不能简单地把response.completed当成"整个交互结束",要结合上下文判断。

第三,文本 delta 和推理 delta 要分开处理。如果你把推理内容也当最终答案展示给用户,体验会很怪。要区分output_text.delta和推理相关的事件,只把前者展示给用户。

6.2 previous_response_id 的隐藏限制

previous_response_id用起来很爽,但有几个限制要知道。

一是有效期。服务端存的历史不是永久的,超过一定时间或一定轮数可能就失效了。你不能假设一个 response id 永远有效,要有降级方案——比如失效时回退到手动传历史。

二是跨会话隔离。不同用户的 response id 不能混用,否则会串上下文。这个在多人应用里要特别注意,做好 id 和用户的绑定。

三是兼容层可能不支持。前面提过,很多兼容实现忽略这个参数。如果你依赖它做历史管理,换到兼容层就会出问题。所以要么确认兼容层支持,要么自己维护历史作为兜底。

6.3 工具调用结果回传的格式陷阱

Responses 里回传工具结果,格式和 Completions 不一样。Completions 是往messages里塞role: "tool"的消息,Responses 是往input里塞特定类型的 item。这个格式如果搞错,模型会"看不懂"工具结果,表现为重复调用工具或者胡言乱语。

我踩过一次坑:把工具结果按 Completions 的格式塞进 Responses 的 input,结果模型完全无视了工具返回的内容,继续按自己的想象回答。排查了半天才发现是格式问题。所以迁移时,工具结果回传这块一定要对照文档仔细改,别想当然。

7. 兼容性测试:怎么验证一个实现到底行不行

7.1 一套可复用的测试清单

与其听别人说"兼容",不如自己测。我整理了一套测试清单,覆盖从基础到进阶的能力,你可以拿它去验证任何声称兼容 OpenAI 的实现。

测试项验证内容通过标准
基础对话单轮文本问答返回结构正确,内容合理
多轮对话历史传递模型能引用前文
流式输出SSE 格式chunk 结构正确,以 [DONE] 结束
工具调用(非流式)tools 参数返回 tool_calls,结构正确
工具调用(流式)参数分片拼接拼接后 JSON 完整可解析
多模态输入图片理解能正确描述图片内容
错误处理非法参数返回标准错误结构
截断行为max_tokensfinish_reason 正确
有状态会话previous_response_id历史正确接续
事件流类型Responses 事件事件类型齐全,顺序合理

这套清单跑一遍,一个实现的真实兼容度就清楚了。别嫌麻烦,踩坑的成本比测试高得多。

7.2 遇到不兼容时的降级方案

测试发现不兼容怎么办?不是所有情况都要换实现,有些可以降级处理。

流式格式不对:如果只是 chunk 结构小差异,可以在客户端做一层适配,把对方的格式转成标准格式。这个适配层写一次,到处能用。

工具调用不支持:如果兼容层不支持工具调用,可以在应用层用 prompt 工程模拟——让模型输出特定格式的文本,你解析后执行。虽然不如原生优雅,但能用。

有状态会话不支持:这个最简单,回退到客户端维护历史,每次请求带上完整历史。功能上没损失,只是费点 token。

事件流不完整:如果兼容层只返回最终结果不返回事件流,那就当非流式用,或者用轮询模拟进度。体验差一些,但功能能保证。

降级方案的核心思路是:把 Responses 的高级特性当成"增强",而不是"必需"。这样即使兼容层拉胯,你的应用也能跑。

8. 我对这套接口演进的一点个人判断

聊了这么多技术细节,最后说点我自己的观察。

接口这东西,从来不是纯技术问题。Completions 能成为事实标准,不只是因为它设计得好,更因为它出现得早、生态大、大家都用。Responses 设计上确实更先进,但它要面对的是"如何让已经用惯 Completions 的人愿意迁移"这个非技术难题。从目前的开源兼容现状看,这个迁移会比我最初预想的慢。

我的实际做法是:新项目直接用 Responses,老项目保持 Completions 但做好抽象层。新项目没有历史包袱,直接用新接口能吃到工具调用、事件流这些红利。老项目迁移成本高,没必要为了迁移而迁移,等 Responses 生态更成熟、兼容层更完善了再动。

还有一个判断:未来一两年,"兼容 OpenAI"这个标签会从"兼容 chat completions"逐渐转向"兼容 responses"。现在做兼容层的团队,如果还没开始支持 Responses,建议尽早排期。因为一旦 Responses 成为主流,兼容层的竞争点就会从"能不能对话"变成"事件流完不完整、有状态会话支不支持"。这个窗口期不会太长。

至于那些还在纠结"要不要学 Responses"的开发者,我的建议很简单:先跑通一个最小 demo。不用管迁移,就写个几十行的脚本,调一次 Responses,看看事件流长什么样,工具调用怎么回传。跑通了,你自然就知道它适不适合你的场景了。光看文档是看不出门道的,动手跑一遍比什么都强。