☰
OpenAI接口演进:从Chat Completions到Responses API的迁移与兼容指南
2026/10/7 5:56:47 网站建设 项目流程

最近折腾 OpenAI 的接口,遇到一个很典型的报错:[Error] Unexpected endpoint or method. (POST /chat/completions). Returning 2.当时第一反应是网关问题,结果查下来发现,是我本地装的新版 SDK 默认把请求路由到了/responses,而我指向的兼容服务只实现了老版的/chat/completions。这事看起来是个小坑,背后其实藏着一个值得聊清楚的话题:OpenAI 的接口规范到底在怎么演进,从 Completions 到 Responses 到底改了什么,那些号称“开源兼容 OpenAI 接口”的方案,到底兼容的是哪一层。

这条演进线不只是 OpenAI 自己的事。现在大量开源框架、本地推理引擎、API 网关都以“兼容 OpenAI 规范”作为卖点,但很多人没意识到,OpenAI 规范本身是个移动靶。你抄作业的时候,作业本已经换版本了。这篇就把我实际踩过的坑、翻过的源码、迁移时整理的参数对照,以及关于开源兼容的一些判断,一次说清楚。

1. 从 Completions 到 Responses:接口演进背后的设计逻辑

1.1 补全接口的朴素年代

OpenAI 最早对外开放的接口其实特别简单,就是 Completions,也就是常说的POST /v1/completions。那个年代的代表模型是text-davinci-003,请求体里塞一个prompt字符串,模型把它当成“前半句话”接着往下写。

resp = openai.Completion.create( model="text-davinci-003", prompt="写一首关于秋天的短诗", max_tokens=100 )

这个接口的设计思路是文本补全,不是对话。你想做多轮聊天,得自己把历史对话拼成一个长字符串塞进prompt,中间手工加分隔符,非常别扭。到了 2023 年,OpenAI 推出 Chat Completions,也就是POST /v1/chat/completions,消息被结构化成了messages数组,每个元素带role和content,system、user、assistant三种角色各司其职。这一步看着只是参数变了个形,实际上是产品形态从“文本续写”转向了“任务型对话”。

从技术实现角度看,messages数组本质上是把原先由开发者手工拼接的历史上下文,变成了一个结构化的输入协议。模型侧要做的事情其实差不多——把消息渲染成提示词再预测下一个 token,但 API 的用户体验完全不同了:角色分离让 system prompt、用户输入、模型历史回复都有了明确的边界,多轮对话和 few-shot 示例的维护成本大幅下降。

1.2 Chat Completions 的隐藏痛点

Chat Completions 火了之后,OpenAI 在这个接口上不断做加法:加了函数调用(Function Calling)、结构化输出(JSON Mode)、视觉输入、流式返回优化。接口活了很久,但使用过程中能明显感觉到一些设计上的别扭。

最典型的是工具调用的状态管理。在 Chat Completions 里,一次带函数调用的完整交互是这样的:第一轮请求带上tools,模型返回一个tool_calls,里面包含要调用的函数名和参数;开发者执行本地函数,把结果以role: "tool"的消息追加到messages里;然后再发一次请求,模型才给出最终文本。整个过程完全靠开发者自己维护对话历史、自己衔接多轮工具调用。如果模型连续调用多个工具,代码里就得反复拼接 messages、反复发起请求,非常容易出错。

还有一个让我印象深刻的坑:messages里如果出现role: "tool"的消息,必须严格带上对应的tool_call_id,否则直接 400。而某些兼容服务对这个校验做得时严时松,导致同一个 SDK、同一套代码,切换服务商后行为完全不一致。这说明 Chat Completions 的“状态机”其实是靠开发者在客户端手工维护的,协议层并没有提供官方状态管理。

1.3 为什么还会有 Responses API

到了 2024 年,OpenAI 又拿出一个叫 Responses API 的新接口,也就是POST /v1/responses。很多人以为这是 Chat Completions 的替代品,看到新接口第一反应都是“又来折腾人”。但仔细看设计,它更像是把 Chat Completions 和以前那个维护成本极高的 Assistants API 里的核心能力,合并成了一个更统一的接口。

Responses API 的核心变化是:一次请求拿回一个response对象,这个对象里的output数组可以包含多种类型的输出项,包括文本、函数调用、网络搜索引用等。工具调用的生命周期被协议层接管了,模型说要调用函数,你执行完把结果填回同一个 response 上下文里继续,整个过程有了明确的response_id作为状态锚点,不再需要你手动把整个 messages 历史一遍遍重传。

这才是 Responses 和 Chat Completions 最本质的区别:前者把“会话状态”从开发者手里收回到 API 层,后者则是一个无状态的请求-响应模型。这个变化对于简单对话场景没啥感觉,但一涉及多轮工具调用、长对话续写、复杂 Agent 编排,差别就非常明显了。

2. Responses API 核心设计拆解与开源兼容的真相

2.1 一个 response 对象解决多个输出

先看一个最简单的 Responses 请求长什么样。

from openai import OpenAI client = OpenAI(api_key="your-key") resp = client.responses.create( model="gpt-5", input="用一句话介绍 Responses API", ) print(resp.output_text)

注意input可以是字符串,也可以直接传消息列表。响应对象里,output是个数组,里面每一项都有自己明确的type。文本输出的类型是message,内容在content下面;函数调用输出的类型是function_call,参数在arguments里。API 还很贴心地提供了一个output_text便捷属性,把你需要从多个输出项里手动提取文本的脏活累活省掉了。

对比一下 Chat Completions 时代,要兼容多个工具同时返回、又要拿文本,得遍历choices[0].message.tool_calls和choices[0].message.content,然后自己组装。Responses API 把多输出的处理逻辑统一了,写起 Agent 编排来确实清爽很多。

2.2 状态锚点与续跑机制

Responses API 里最有含金量的设计是previous_response_id。它让你可以把一个多轮工具的上下文串在同一个会话里,不需要每次把整个消息历史重新发给服务端。

举个例子,用户问“帮我查一下今天北京天气,然后根据天气推荐穿搭”。Agent 先触发天气查询函数,拿到结果后,你要让模型继续生成推荐。Chat Completions 的做法是把工具结果追加到 messages 再发一次完整的请求;Responses 的做法是传入previous_response_id=上一步的response_id,再补上工具结果。服务端记住了上下文,请求体的体积大幅下降,长会话场景下的 token 消耗和延迟都会好一些。

这里有个容易忽略的细节:如果你使用truncation参数,可以控制上下文超长时的截断策略。过去在 Chat Completions 里,上下文超长只能自己手动裁剪 messages,裁坏了还会导致引用错乱。Responses 至少给了协议层的解决办法,虽然实际效果还需要看具体模型的表现,但设计上确实是朝“更可控”的方向走了。

2.3 开源兼容的真相:大家都在兼容哪一层

现在回到很多人关心的开源兼容问题。市面上遍地都是“兼容 OpenAI API”的开源项目和推理服务,比如 vLLM、llama.cpp、Ollama、LiteLLM、One API、New API 等。但“兼容 OpenAI”这句话含糊得不能再含糊。拆开看,至少有三层含义:

第一是 SDK 兼容层。你使用openai这个 Python/Node 包,把base_url改成某个本地服务的地址,同样一套调用代码就能跑通。这是大多数本地推理服务提供的兼容方式,vLLM 和 llama.cpp 的 OpenAI 兼容端点就是这一类。

第二是端点兼容层。服务端实现了/v1/chat/completions这个路径,并尽量按照 OpenAI 的请求和响应 schema 返回数据。这部分很多开源项目都做到了,但未必实现了/v1/responses。

第三是行为兼容层。不仅路径和 schema 对得上,流式事件、状态管理、错误格式也要一致。这一层能做到的项目就不多了,尤其是 Responses API 刚出来那会儿,大多数开源网关只同步了/chat/completions,/responses要么没有,要么实现得非常粗糙。

所以如果你把新版 SDK 的base_url指向一个只做了 Chat Completions 兼容的开源网关,然后调用client.responses.create(),网关不认识/responses路径,就会直接甩一句Unexpected endpoint or method。这不是代码写错了,而是你踩到了“兼容版本落后于 SDK 版本”的断层。

2.4 为什么开源项目跟不上 OpenAI 的换代速度

这里得替开源项目说句公道话。OpenAI 的接口规范迭代很快,尤其现在官方已经明确表示 Responses API 是未来方向,但 Chat Completions 也还在维护期、没被废弃。两边都要支持,意味着网关项目要维护两套 schema 映射、两套流式事件,工作量是双份的。

更深层的问题是:Responses API 的很多能力,比如response_id状态管理、工具调用的多物品输出、web_search这类内置工具,是跟 OpenAI 的后端服务强绑定的。本地推理引擎靠一己之力在协议层模拟这些行为,成本极高。本地模型的推理引擎只需要“把 prompt 跑完,把 token 流式吐出来”,你要它同时维护会话状态、处理工具调用生命周期、输出结构化的多个 output item,这已经不是推理引擎的职责范围了,而是把 Agent 框架的工作下沉到了协议层。

所以我的结论是:在可预见的未来,开源生态的主流兼容层仍然会停留在 Chat Completions 上。Responses API 的完整兼容大概率只会出现在商业 API 网关或者专门做 Agent 基础设施的项目里。普通开发者面向开源本地模型时,继续使用 Chat Completions 反而是更稳妥的选择。

3. 从 Chat Completions 迁移到 Responses 的实操指南

3.1 最小改造:先看懂请求体的变化

如果确有必要迁移,建议从最小改动开始。先看一个最简单的对话场景在两种接口下的写法差异。

# Chat Completions 方式 resp = client.chat.completions.create( model="gpt-5", messages=[ {"role": "system", "content": "你是一个写作助手。"}, {"role": "user", "content": "帮我写一段产品简介。"} ], max_tokens=500 ) text = resp.choices[0].message.content # Responses 方式 resp = client.responses.create( model="gpt-5", input=[ {"role": "system", "content": "你是一个写作助手。"}, {"role": "user", "content": "帮我写一段产品简介。"} ], max_output_tokens=500 ) text = resp.output_text

肉眼可见的主要变化是:messages变成了input,max_tokens推荐换成max_output_tokens,返回的文本不再从choices[0].message.content里取,而是直接用output_text。如果只是简单对话,改到这一步就足够跑通了。

但要注意一个容易踩的细节:Responses API 对input的校验在某些版本里比 Chat Completions 更严格,尤其是工具调用相关的历史消息。你从 Chat 迁移过来时,如果 messages 里还残留着历史工具调用记录,建议先清掉再测试,否则可能遇到意料之外的 400 报错。

3.2 流式响应的差异,可能是最大的改造点

如果你在产品里用了流式输出,迁移的工作量会比想象中大。两种接口的流式事件结构完全不一样。

Chat Completions 流式解析的典型写法:

stream = client.chat.completions.create( model="gpt-5", messages=[{"role": "user", "content": "讲个故事"}], stream=True ) for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="")

Responses API 流式解析的写法:

with client.responses.stream( model="gpt-5", input="讲个故事", ) as stream: for event in stream: if event.type == "response.output_text.delta": print(event.delta, end="")

最大的区别是:Chat Completions 一切都在chunk.choices[0].delta里,而 Responses 流式会产生多种事件类型,包括response.output_text.delta、response.function_call_arguments.delta、response.completed等。这意味着你的流式解析逻辑不能再只盯着一个字段,而要根据事件类型分发处理。

我建议在迁移时先把事件类型打印出来看一遍,跑通一次真实的工具调用,看response.output_text.delta和response.function_call_arguments.delta是怎么穿插出现的,再动手写解析层。这样能少走很多弯路。

3.3 参数映射速查表

迁移时最实用的是一张参数对照表。我根据自己的实践和官方文档整理了下面这份,不敢说覆盖全部参数,但日常开发高频用到的都在里面。

场景Chat CompletionsResponses API
模型名modelmodel,同名
用户消息/历史messagesinput,结构兼容但校验更严
最大生成 tokenmax_tokensmax_output_tokens,旧参数仍可用但建议换新
采样温度temperaturetemperature,同名
随机种子seedseed,同名
停止词stopstop,同名
工具定义toolstools,结构基本相同
强制工具选择tool_choicetool_choice,同名
结构化输出response_formattext.format,需调整嵌套结构
流式开关stream=Truestream=True,事件结构不同
用量统计stream_options中的include_usage仍支持,但事件时机不同
会话续写手动拼接messagesprevious_response_id配合工具结果
单次请求多输出不支持,一个响应只对应一个角色output数组原生支持多输出项

需要特别提醒的是结构化输出。Chat Completions 里你传的是:

{"response_format": {"type": "json_schema", "json_schema": {...}}}

Responses API 里变成了:

{"text": {"format": {"type": "json_schema", "name": "my_schema", "schema": {...}}}}

这个嵌套层级的变化很容易被忽略,一旦写错,模型不会报错但会退化成普通文本输出。如果你依赖结构化输出,迁移时一定要重点测试这一块。

4. 迁移踩坑与排查实录

4.1Unexpected endpoint or method的两种常见场景

开篇那个报错,我再展开说一下。POST /chat/completions返回Unexpected endpoint or method,本质上说明你的请求打到了一个不认识这个路径的服务上。我遇到过两种典型场景:

第一种是你把新版 SDK 的base_url指到了只支持 Chat Completions 的本地推理服务,同时代码里调用了client.responses.create()。SDK 拼出的路径是/v1/responses,而服务端根本没实现这个路由,于是返回Unexpected endpoint or method。这种情况的排查方法是:先确认 SDK 的base_url指向的服务到底暴露了哪些路径,直接curl /v1/responses看返回。

第二种是某些兼容网关自己配置错误,把/chat/completions的请求转发到了不支持该方法的后端。这种情况多在网关日志里能看到 405 或类似的原始错误。我的建议是排查时先把 SDK 层剥离掉,用 curl 直接打目标端点,看是路径不存在、方法不被允许,还是鉴权失败。

4.2 API Key 与鉴权相关问题的排查顺序

迁移到 Responses 后遇到 401 或 403,很多人第一反应是 API Key 问题,实际上我更建议按这个顺序排查:

先确认 Authorization 头有没有带对。新版 SDK 通常会用环境变量OPENAI_API_KEY作为默认 key,但如果你在代码里同时初始化了多个 client 实例,偶尔会出现 key 覆盖的问题。可以在代码里显式打印请求头,或者在网关日志里看收到的 Authorization 头是否符合预期。

再确认网关是不是把 Authorization 头正常透传了。有的兼容层会自己消费掉这个头,转发到上游时反而丢了,导致上游报 401。这种问题代码层面看不出任何异常,只能在网关日志和上游日志之间做比对。

最后才是确认 key 本身有没有过期、余额是否充足、模型权限是否覆盖。我自己踩过的坑是:旧项目里用了一个很早生成的 key,模型权限默认只在旧的模型列表里,换到新模型后返回 403,但换回gpt-4o就正常。这类问题在 OpenAI 官方接口里不常见,但在第三方兼容服务里出现频率不低。

4.3 Codex CLI 安装依赖缺失的排查

另外一个近期很多人遇到的问题:安装 Codex CLI 时报missing optional dependency @openai/codex-win32-x64。这其实是 npm 包机制导致的典型问题。Codex CLI 的二进制依赖是通过optionalDependencies声明的,npm 在安装时如果检测到当前平台的二进制包安装失败,为了避免整个安装失败,会静默跳过。

解决方式很简单,手动补装对应平台的包:

# Windows x64 平台 npm install @openai/codex-win32-x64 --save-optional # macOS 根据芯片选 arm64 或 x64 版本 npm install @openai/codex-darwin-arm64 --save-optional

但这个问题的根因往往不只是缺包。有时候是没装 Rust 工具链,导致 Codex CLI 某些功能在本地编译时失败;有时候是 npm 版本太老,对 optionalDependencies 的平台判断有问题。我建议安装前先看 Node 版本,最好保持在 LTS 版本,然后把 node_modules 整个删掉重新 install。不要把时间浪费在零散的报错排查上。

4.4 关于接口迁移的几个现实建议

最后分享几个我自己这段时间实操下来的判断,不一定对,但都是踩过坑换来的。

第一,不要因为换了 Responses API 就把所有代码一次性迁移。Chat Completions 短期内不会消失,官方也明确表示会保留兼容。我更推荐的做法是:新项目、新功能优先用 Responses,老接口暂时不动,让团队有个过渡期。

第二,面向开源本地模型的项目,先别急着迁。本地推理引擎的兼容层大多停留在 Chat Completions,你强行在 SDK 层用 Responses,最终还是要靠一个翻译层把它映射回 Chat Completions。这种多一层转换的架构,出了 bug 特别难排查。

第三,无论用哪个接口,都要尽早把“流式事件类型”的日志打出来。接口换代不可怕,可怕的是你的解析层只认一种结构。我在迁移流式逻辑时就发现,把事件类型打印出来看,比盲改代码快十倍。

写到这里,从 Completions 到 Responses 的技术脉络基本梳理清楚了。如果你只是做个简单对话产品,Chat Completions 还能继续用;如果你开始认真做 Agent、做多工具编排,Responses API 那套状态管理确实值得早点上手。至于开源兼容这一块,我的建议永远是:看文档不如看源码,看源码不如直接 curl 打一下目标端点,眼见为实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询