最近好几个项目团队都来问我同一个问题:OpenAI 的接口规范演进到了一个新阶段,从最早的 Completions 到统治生态的 Chat Completions,再到现在的 Responses API,到底要不要跟着切?开源社区说的"OpenAI 兼容"到底兼容的是哪一层?为什么明明照着官方文档写,还是会撞上unexpected endpoint or method (post /chat/completions)这种报错?
这篇文章我就把自己实际迁移、排查、对接开源网关的经验完整梳理一遍。不吹不黑,从协议演进逻辑讲到底层兼容真相,再给出可以直接抄走的切换方案和避坑清单。适合正在做 LLM 应用、维护 API 网关、或者本地跑推理服务的工程师参考。
1. 为什么要从 Completions 演进到 Responses
1.1 Completions 时代的底层逻辑
早期的 OpenAI 接口规范演进其实非常直白:就是/v1/completions一个端点打天下。你传一个prompt,模型给你续写出一段纯文本。请求体大概长这样:model、prompt、max_tokens、temperature、top_p、n、stop、logprobs,返回结构是choices[].text。这套设计在 GPT-3 时代是合理的,因为那时候产品形态就是"输入一段话,输出一段文本"。
但问题也很明显:它没有角色概念,没有 system、user、assistant 的区分。你要做多轮对话,就得自己在业务层把历史消息拼接成一个超长 prompt 塞进去。拼接本身倒还好,真正麻烦的是截断和上下文管理——你永远要自己数 token,小心翼翼地保留最近的若干轮对话,生怕把系统指令挤出上下文窗口。用一句话概括:它像一个没有"记忆"的输入法,只负责把光标后面的内容接下去,至于前面聊过什么,模型并不知道。
还有一个隐性成本:因为没有统一的消息结构,每个接入方拼 prompt 的方式都不同,有人用Q: ... A: ...分隔,有人用Human: ... Assistant: ...分隔。这种混乱直接导致同一批模型在不同产品里的效果参差不齐,调优经验也无法沉淀。所以当 OpenAI 决定拥抱对话场景时,接口层必须有一次彻底的重构。
1.2 Chat Completions 的统治地位
2023 年 3 月,gpt-3.5-turbo发布,同时带来了/v1/chat/completions。这个接口最核心的资产是messages数组:system、user、assistant三种角色,由服务端统一处理多轮对话的上下文格式。开发者不需要再手动拼接历史了,直接把消息列表扔给接口,token 管理、角色区分这类脏活全都收归服务端。
这个设计的统治力强到超乎想象。Chat Completions 发布之后,几乎所有开源推理框架都把"OpenAI 兼容"默认实现为/v1/chat/completions。vLLM、Ollama、LM Studio、llama.cpp 的服务端,清一色优先对齐这个端点。原因很简单:这是需求最密集、调用最频繁、生态里真实流量最大的协议。
于是出现了一个很有意思的现象:很多开发者现在提到"OpenAI 接口",脑子里第一反应就是/chat/completions。他们直接跳过了 Completions 时代,以为 OpenAI 接口天生就是 messages 数组这套格式。这个"潜意识的默认"恰恰是后面理解 Responses API 时容易绕弯的原因——你以为接口演进是技术层面的优化,实际上它是产品形态和生态风向的写照。
1.3 Responses API 想解决什么问题
2024 年 OpenAI DevDay 上,Responses API 正式亮相,端点变成/v1/responses。我第一反应是:命名变了,是不是只是把 messages 改成了别的字段?真去读文档才发现,它的野心比我想的大得多。
Chat Completions 虽然解决了对话格式统一的问题,但只覆盖了"对话"这个最基础的产品形态。一旦你开始做 Agent,问题就来了:工具调用需要两段式协作,先让模型输出tool_calls,业务层执行工具,再把结果拼回消息列表发第二轮;一次完整的多轮工具调用流程,可能要在客户端维护一个不断膨胀的上下文。代码写起来繁琐还是小事,真正麻烦的是上下文管理非常容易出错——函数结果拼错位置、消息顺序乱掉、截断策略写错,任何一个低级失误都会让 Agent 行为变得不可控。
Responses API 的思路是:把 Agent 需要的通用能力直接收进接口层,而不是让每个开发团队自己造轮子。它把工具调用做成了服务端原生的执行机制,引入状态变量让多轮会话可以在接口层面串联,同时内置了 web search、file search、code interpreter 这类开箱即用的能力。用我自己的话说:它不再是一个"补全端点",而是一个"Agent 运行时"。这也是为什么这一节的标题强调"想解决什么问题"——它的目标用户不是写聊天机器人的团队,而是写复杂工作流、多工具编排的团队。
2. Responses 接口到底改了什么:协议细节对比
2.1 端点与请求体的关键差异
先把最直观的差异列出来,我直接对比两个端点的请求结构。
| 维度 | Chat Completions | Responses |
|---|---|---|
| 端点 | POST /v1/chat/completions | POST /v1/responses |
| 对话输入 | messages数组 | input,可以是字符串、消息数组或 item 数组 |
| 角色字段 | system / user / assistant | 支持system / developer / user / assistant,developer用于更细粒度的指令分级 |
| 工具声明 | tools独立字段,function_call单独配置 | 直接内联在tools数组中,工具调用由服务端统一调度 |
| 返回结构 | choices[].message | output数组,内含多种类型的 item |
| 多轮状态 | 客户端自己拼历史 | previous_response_id直接串联历史上下文 |
这里要单独说一下input。很多想从 Chat Completions 迁过来的同学第一反应是把messages改名为input就完了,这是最大的误区。Responses 的input支持三种形态:一个普通字符串表示"直接给我这段文本的响应";一个简化消息数组表示多轮对话;一个更完整的 item 数组,允许你传message、function_call、function_call_output等更丰富的结构化内容。第三类 item 形态才是 Agent 场景下真正用得到的东西,它让工具调用结果不再依赖笨拙的字符串拼接。
2.2 响应结构与状态语义
Chat Completions 的返回是一层choices包裹着message,拿到message.content基本就够用了。Responses 的返回是output数组,数组里的每个元素都有各自的type:message表示自然语言回复,function_call表示需要调用工具的请求,reasoning表示推理过程的中间产物,还有web_search、file_search、code_interpreter这类内置工具的触发结果。
我刚开始看这个结构的时候觉得比原来复杂,用久了反而觉得清晰——因为它把 Agent 生命周期里的所有事件统一成了"响应项",你可以像处理日志流一样遍历output,按类型分发到不同的处理逻辑。官方 SDK 还给message类型提供了.output_text这种便捷属性,直接拿到纯文本回复,不需要自己去嵌套数组里挖内容,这个细节对迁移体验的改善很大。
另一个值得关注的语义升级是状态追踪。Chat Completions 时代,多轮对话的上下文完全靠客户端组装,服务端无状态;Responses 引入了previous_response_id,把前一轮响应的 ID 传给下一次请求,服务端就能直接串联上下文。这意味着复杂的多轮 Agent 流程可以在接口层维护记忆,客户端不需要每一轮都回传全部历史消息。设计上确实优雅,但要注意:只有使用官方 API 时才能享受这个特性,任何本地模型和第三方兼容层大概率不会实现它。
2.3 兼容层要理解的关键点:Responses 不是"字符串换数组"
我见过一些团队在规划网关改造时,把 Responses 当成一次简单的字段映射:messages换成input,message.content取output_text,以为写个转换函数就完事。如果只做这些,你实现的是"看起来像 Responses 的薄壳",不是真正的 Responses API。
Responses API 的核心价值在服务端的行为编排:工具调度的生命周期、内置搜索和代码执行、推理过程管理、上下文状态串联,这些能力全部发生在 API 服务端。开源网关要做完整兼容,意味着你不仅要把请求翻译过去,还要实现背后那套调度逻辑。这也是为什么我判断,真正值得投入的做法是:在网关层做一个"协议映射桥",对外同时暴露/chat/completions和/responses两个端点,内部把/responses请求翻译成若干次工具调用和消息传递,而不是在字段层面做静态转换。
3. 开源兼容的真相:为什么大家都在"兼容 Chat Completions"而不是"兼容 Responses"
3.1 一个让很多人懵掉的报错:unexpected endpoint or method(POST /chat/completions)
我在多个项目的部署现场都见过这个报错:某个开源 CLI 工具或应用在连接自建服务时,输出类似[error] unexpected endpoint or method. (post /chat/completions). returning 2,紧接着程序退出。很多人的第一反应是"OpenAI 又改了协议",但实际查下来,99% 的情况跟 OpenAI 无关,是你连接的网关没实现这个路由。
举个我排查过的例子:某个团队把 OpenAI 兼容网关部署在内网,客户端工具的 base_url 配的是网关根地址,工具内部自动拼接路径/chat/completions,但那个网关版本只实现了/v1/completions和/v1/embeddings,根本没有/chat/completions这个路由,所以服务端返回了 unexpected endpoint 错误。还有些情况是 base_url 写重复了,比如本地服务监听路径是/v1/chat/completions,客户端又把base_url配成了http://host/v1,最终拼接出/v1/v1/chat/completions这种鬼路径,同样触发这个报错。
排查思路其实很简单:第一步直接用 curl 打一次目标端点,看真实返回内容;第二步检查 base_url 的拼接规则,确认没有重复路径段;第三步确认请求头和认证信息符合服务端预期。先手动打出一次 200,再去调客户端,这是排查一切 OpenAI 兼容层连接问题的最有效路径。
3.2 开源项目为什么总是"慢半拍"
很多人问:为什么 Responses API 都出来这么久了,开源推理框架还没大力跟进?我的看法是:开源项目对接口的兼容从来不是按官方文档的重要性排序,而是按真实流量的分布排序。
绝大多数开源 LLM 应用框架在调用模型时,走的都是/v1/chat/completions。比如 LangChain、LlamaIndex 这类中间件,默认的模型适配器就是 Chat Completions;主流 Agent 框架虽然支持工具调用,底层也是通过 messages 数组模拟对话历史。对这些框架来说,Responses API 是一个"在本地推理场景里用不上、在远端 API 场景里依赖官方密钥"的协议。本地推理框架没有动力去实现一个只有接入官方服务才能发挥全部能力的接口——服务端的工具调度和内置搜索是它们没法在本地复刻的东西。
而且兼容一个新接口的成本被严重低估。你以为只是新增一个路由,实际上要处理请求解析、流式输出格式、错误语义、字段映射、多类型 item 的序列化与反序列化,还要为每个模型的行为差异写测试。对一个靠社区维护的项目来说,这是一笔不小的投入。维护者更理性的选择是:先把 Chat Completions 的流式、工具调用、各类模型的兼容性打磨到极致,再观望 Responses API 的生态接受度。
3.3 兼容真相的底层规律:接口兼容与流量对齐
如果你长期维护 API 网关类项目,会发现一个规律:所谓"OpenAI 兼容",本质上是一份与真实流量对齐的契约,不是对官方文档的完整复刻。
社区公认的"OpenAI 兼容最小集"通常是三件套:/v1/models、/v1/chat/completions、/v1/embeddings。把这三个接口做稳定,绝大多数开源应用就能跑起来。至于/v1/completions,很多新项目干脆不实现了,因为生态里已经没有新流量往那里去;/v1/responses则被排在更长远的规划里,等用户真的开始在自建服务上调用它,维护者才会把它加上。
这个规律对你的直接指导意义是:如果你要为团队做技术选型,不要只看某个网关"宣称兼容 OpenAI",要查它实际实现的是哪几个端点;如果你自研网关,建议按照真实流量分布来排优先级,把 Chat Completions 和 embeddings 做到位,Responses 作为增量能力预留扩展点。接口兼容不是做慈善,而是做成本更低的生意。
4. 迁移实操:从 Chat Completions 切换到 Responses
4.1 快速识别自己的代码依赖了哪些能力
迁移之前,先对自己的调用方式做一个分类诊断。我的判断清单大概是这样的:
- 项目只有简单的多轮聊天,不涉及工具调用、不长上下文:留在 Chat Completions 完全没问题,它依然被官方完整支持,迁移收益很小。
- 项目涉及工具调用和 Agent 编排,但代码量不大,上下文管理还能控制住:可以选择迁移到 Responses,享受服务端的工具调度和状态串联。
- 项目重度依赖 web search、file search、代码解释器这类内置能力:应该直接切 Responses,这些能力在 Chat Completions 里没有原生支持,自己实现性价比极低。
- 项目跑在本地模型或第三方兼容层上:别急着切 Responses,先确认你的服务端真的实现了这个端点,否则就是给自己埋坑。
我的建议是:把"当前代码里最疼的那个点"作为迁移的决策依据。如果只是因为没有用过新接口就想切,那通常说明不值得切;如果是被工具调用的两段式协作烦透了,那 Responses 的价值是实实在在的。
4.2 两段可直接抄的代码示例
下面用官方 Python SDK 展示最典型的写法差异,先看旧的 Chat Completions:
from openai import OpenAI client = OpenAI() # 自动读取 OPENAI_API_KEY resp = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "你是资深的架构师"}, {"role": "user", "content": "对比一下 Completions 和 Responses 的差异"}, ], ) print(resp.choices[0].message.content)再看对应的 Responses 写法:
from openai import OpenAI client = OpenAI() resp = client.responses.create( model="gpt-4o", input=[ {"role": "developer", "content": "你是资深的架构师"}, {"role": "user", "content": "对比一下 Completions 和 Responses 的差异"}, ], ) print(resp.output_text)两段代码看起来都挺干净,但背后的语义完全不同。第一个例子里的messages是客户端把历史消息全量提交;第二个例子里的input可以是一个字符串,也可以是一条消息数组,还可以是包含工具调用结果的结构化 item 数组。resp.output_text更是省去了遍历output数组的步骤,直接拿到最终的文本输出。切换初期先掌握这两个差异点,就可以跑通 80% 的基础对话场景。
4.3 迁移中的常见坑:响应的解析、流式事件、重试语义
迁移过程中最容易踩的坑集中在三个地方。
第一个是响应解析。你在 Chat Completions 里一直用的choices[0].message.content在 Responses 里不存在了,如果直接照搬会拿到None。正确做法是用 SDK 提供的output_text,或者自己遍历output数组,按item.type == "message"过滤再取内容。
第二个是流式接口的差异。Chat Completions 的流式事件名和数据增量格式,跟 Responses 的流式格式完全不同。如果你在业务代码里硬解析流式事件,迁移时必须同步更新解析逻辑。官方 SDK 的流式写法虽然也封装了不少,但事件类型变了,你的事件分发器还是要改。
第三个是重试语义。Responses API 的某些内置工具调用发生在服务端,耗时可能比普通的单轮对话长得多。按原来的超时和重试策略,很容易把明明还在正常执行的请求误判为超时,然后重复提交。我在实际项目中就把超时时间从 30 秒调到了 120 秒,并且为previous_response_id串联的请求专门设计了去重逻辑,这个问题才算解决。
还有一个我强烈推荐的做法:在网关层做"双协议出口"。对外保留/chat/completions给旧客户端,新增/responses给新客户端,两边走同一套调度逻辑。这个方案能大幅降低迁移风险,至少不用在同一天逼所有应用全部切换。
5. Codex 信号与工具链演进
5.1 Codex CLI 与 ChatGPT 登录
顺着接口演进这条线,必然会看到 Codex 的出现。Codex 是 OpenAI 官方推出的命令行编码代理,把"对话 + 代码执行 + 文件修改"集成到一个代理式的工作流里。首次使用时的引导就是welcome to codex,然后让你sign in with ChatGPT完成授权。它不再是你 IDE 里的自动补全插件,而是一个能直接跑在代码仓库旁边、自己读文件、自己执行命令的代理。
我实际用下来的感受是:它代表了接口规范演进的一个信号——OpenAI 已经不再满足于提供"文本补全能力",而是在提供"完整的执行环境"。这对开发者的意义在于,你将来选型时,不能只看模型的对话能力,还要关注它背后的接口层和执行工具链是否是一体的。
5.2 一个真实的安装坑:missing optional dependency @openai/codex-win32-x64
我在 Windows 环境装 Codex 时踩过一个很典型的坑:npm install过程中提示missing optional dependency @openai/codex-win32-x64. reinstall codex: npm install。看字面意思就知道,这是 npm 安装可选平台依赖时,Win32 对应的平台二进制包没有成功拉取,导致命令行工具无法运行。
排查和解决并不复杂:先查node_modules/@openai目录里是不是缺了codex-win32-x64这个包;然后清理 npm 缓存重新安装;如果还不行,就手动执行提示里的npm install @openai/codex-win32-x64补装。这类问题在网络波动或镜像源同步不及时时很常见,不用慌。
这个坑也侧面反映了一个趋势:OpenAI 正在把它的工具链打包成完整的桌面级分发物,而不再只是一串 HTTP 接口。对开发者的建议是,使用这类工具前先确认平台支持情况和安装网络的稳定性,别等报错再查。
5.3 从接口规范到工具链生态的启示
把 Responses API 和 Codex 放在一起看,逻辑就清楚了:OpenAI 的接口规范演进,本质是把"模型能力封装层"和"面向开发者的执行环境"绑定在一起。接口层面的抽象粒度越来越粗,行为越来越多,客户端要自己写的逻辑越来越少。
这对我们选技术栈有一个明确的提醒:以后选择网关或框架时,别只看"它支不支持 OpenAI 品牌",要看它对新接口的跟进速度、对工具调用的支持程度、对流式协议的完整覆盖。OpenAI 兼容这个词正在变得不够用,准确的说法应该是"兼容到哪个协议版本、覆盖到哪些行为语义"。
6. 我的实战避坑清单与最终建议
6.1 接口选型决策表
直接给一张可以参考的决策表,按场景选接口:
| 场景 | 推荐接口 | 理由 |
|---|---|---|
| 纯多轮聊天、简单文本生成 | Chat Completions | 生态最成熟,兼容性最好,迁移收益小 |
| 工具调用、Agent 编排 | Responses 或 Chat Completions + functions | Responses 更省心,但如果兼容层不支持就留在后者 |
| 重度依赖 web search / file search | Responses | 这些能力在 Chat Completions 里没有原生实现 |
| 本地模型、自建推理服务 | Chat Completions | 本地服务多优先兼容此端点,Responses 容易踩空 |
| 服务端状态串联、长上下文多轮 | Responses | previous_response_id省去客户端维护历史的成本 |
这张表的核心逻辑是:不要因为新而选新,要因为"解决当前最疼的问题"而选新。
6.2 key 获取与调用环境的安全提醒
接口无论怎么演进,调用凭证的管理永远是绕不开的一环。API key 请直接从官方平台的安全页面创建,不要在代码里硬编码,更不要提交到 Git 仓库。我见过不少项目把 key 直接写在前端代码里,等于把钱包密码贴在了门口。
共享 key 和不明来源的中转 key 也建议敬而远之。你无法控制其在传输链路中是否被记录,一旦出现异常消耗或安全事故,责任很难说清。如果你所在区域的网络访问存在问题,先确认自己的调用环境是否符合服务商的支持范围,再用企业层面采购的合规网关或官方支持的服务入口,而不是依赖灰色渠道。拿不到合规环境的时候,最稳妥的选择是把服务部署在合规区域,再通过内部网络调用。
6.3 给开源维护者的一点建议
如果你在维护推理服务网关或中间件,我的建议是:把 Chat Completions 的流式和工具调用稳定性当作基本盘,这两个能力直接影响主流框架的接入体验;Responses 的兼容则用"协议翻译映射层"来应对,对外暴露新端点,内部复用已有的消息处理和工具调度逻辑,避免为单个新协议重写一套引擎。
接口兼容的本质是"服务端能力和客户端预期之间的契约"。如果你想减少后续迭代的维护成本,最值得做的事是把内部的请求语义抽象成一份统一的中间表示,让新的后端协议只做适配层,不碰核心逻辑。这样无论是接 Responses,还是接未来可能出现的新协议,成本都能控制在可接受范围内。
最后说一点个人体会。我在实际项目中验证过一条稳妥的路径:先让网关对外同时暴露chat/completions和responses两个端点,内部通过映射层共享调度逻辑;新项目直接走 Responses,老项目继续走 Chat Completions,两边并行,等到所有流量都验证没问题再做全面切换。踩过unexpected endpoint or method那个报错之后,我养成了一个习惯:部署任何自称 OpenAI 兼容的服务,第一件事就是拿 curl 打一个真实请求验证路由,别信文档,直接看返回状态码。希望这篇文章帮你避开我踩过的这些坑,也帮你更清醒地判断接口演进的方向——协议总在变,但底层需求永远是那三件事:更少的客户端状态、更稳的工具调度、更完整的执行能力。