ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenAI接口演进:从Chat Completions到Responses的迁移指南

OpenAI接口演进:从Chat Completions到Responses的迁移指南 上周调试一个内部工具时日志里突然冒出一行非常眼熟的报错[error] unexpected endpoint or method. (post /chat/completions). returning 2。当时的第一反应是网关又抽风了翻了一下配置才发现根本不是参数问题——我指向的新服务压根没注册/v1/chat/completions这个路由它只暴露了/v1/responses。那一刻我意识到OpenAI 接口规范从 Completions 到 Chat Completions、再到 Responses 的这场演进已经不是技术圈里的概念讨论而是实打实砸到了我们这些写业务代码、维护开源网关的人头上。这篇文章就把我最近踩过的坑、翻过的源码、迁过的代码一起整理出来。它适合三类人还在用chat/completions接口、对 Responses API 一头雾水的开发者维护开源兼容网关、被各种协议映射折腾得头疼的工程师以及想理解为什么 OpenAI 要折腾一套新接口的读者。我会从那次报错讲起把三代接口的设计逻辑、字段差异、兼容层真相、迁移改法一次说清楚。1. 一声报错背后的接口代际更替1.1 这次不是参数问题是路被拆了回到那个报错本身。unexpected endpoint or method. (post /chat/completions)这一段字面意思很直白服务器不认识这个地址。但真正让我警觉的是后面那个returning 2——在我当时调试的网关代码里这是协议路由匹配失败的退出码说明请求在进入业务逻辑之前就被拦下了。我排障的顺序是这样的先 curl 探活curl https://xxx/v1/models正常返回模型列表说明服务在线。再 curl 打一次curl https://xxx/v1/chat/completions -d {model:gpt-4o-mini,messages:[{role:user,content:hi}]}结果直接返回 404 和这行报错。翻服务端 OpenAPI 文档发现 paths 里只有/v1/responses和/v1/models。也就是说这个服务的维护者只实现了 Responses API把 Chat Completions 整个丢掉了。对于依赖旧协议的客户端来说这不是某个字段不兼容的问题而是整条路都被拆了。这也是接口代际更替时最典型、也最容易被忽视的坑换协议不是改参数是换入口。1.2 三次转身从文本补全到任务执行回看 OpenAI 接口演进其实是三个时代的三次转身。第一代是 Completions 时代端点POST /v1/completions。那时候的用法很朴素把一段 prompt 扔进去模型返回一段补全的 text没有角色区分没有系统提示词更谈不上工具调用。典型调用长这样resp client.completions.create( modeltext-davinci-003, prompt写一首关于秋天的短诗 ) print(resp.choices[0].text)第二代是 Chat Completions2023 年随 GPT-3.5 Turbo 一起出现端点POST /v1/chat/completions。它把对话抽象成messages数组每个消息带rolesystem/user/assistant一下子让聊天机器人这个场景变得极其自然。过去三年里几乎所有 LLM 应用框架——LangChain、LlamaIndex、各种 RAG 工具——默认对接的都是这个接口。第三代就是 Responses API端点是POST /v1/responses。官方最初的定位很明确它不是 Chat Completions 的简单升级而是为 Agent 场景设计的统一接口。对话、函数调用、推理过程、多模态输入全部收敛到一个入口里输出也不再是一段文本而是一组结构化的 item。这里有个背景很多人没注意到OpenAI 从 2024 年底开始把新模型、新能力优先放到 Responses API 上开放。我实际遇到过不止一次某个新模型只在/v1/responses端点上可用/v1/chat/completions拿不到。也就是说Chat Completions 并没有被官方一纸公告判死刑而是在新功能缺席中慢慢进入维护态。对于在兼容层上做二次开发的团队来说这个信号比任何公告都重要。2. 解开三代接口的设计逻辑2.1 Completions 时代一次没头没尾的文本续写我常把 Completions 比作复印机一张纸进去一张纸出来中间没有交流。你给它一段话它返回这段话最合理的续写。这个设计在 GPT-3 的年代够用因为那时候模型的主业确实是预测下一个 token聊天只是其中一种玩法。但它的局限很快就暴露了。首先是无法表达角色——没有 system 消息想让模型扮演某个专家只能把要求硬塞进 prompt 里。其次是多轮对话极其别扭你要自己拼历史记录拼完一长串再当 prompt 发出去。最后是工具调用完全没有立足之地想让模型调 API 得靠请输出 JSON这种脆弱约定。这些问题本质上是接口设计跟随模型能力走——模型只能做文本续写时接口也只能是文本续写。2.2 Chat Completions把对话历史打包成消息数组Chat Completions 解决的是对话这个核心场景。它的请求体就是一个消息数组{ model: gpt-4o-mini, messages: [ {role: system, content: 你是一个严谨的助手}, {role: user, content: 今天天气怎么样} ] }这套设计的好处是直观、好调试生态工具接受度高。但它有一个被很多人忽略的硬伤无状态。每次请求客户端都必须把完整对话历史塞进 messages 数组对话越长请求体越大token 消耗越高。到了 Agent 场景——模型要调工具、要读结果、要再决策——开发者要在客户端手工维护一个越来越长的消息列表并反复将中间工具输出转为 assistant/tool 消息塞回去。我为这事写过不下三百行胶水代码每次跑多轮工具调用都感觉自己在给模型手搓上下文记账本。2.3 Responses为 Agent 时代设计的任务式接口Responses API 的核心转变是把发消息变成了派任务。请求里不再是单纯的消息数组而是{ model: gpt-4o-mini, instructions: 你是一个严谨的助手, input: 今天天气怎么样, tools: [ {type: function, name: get_weather, description: 获取天气, parameters: {...}} ] }注意几个区别系统提示词变成了instructions对话内容变成了input工具变成了一等公民。更重要的是响应体不再是choices[0].message而是一个output数组里面可以有多种类型的 item——普通文本消息、函数调用、推理过程摘要等。这背后是一套全新的设计哲学让服务端替你维护任务状态。Responses 支持用previous_response_id把多轮调用串联起来服务端知道这次请求是上一次的继续不需要客户端把所有历史重复发送。对于工具调用密集的 Agent 应用来说这个改动直接砍掉了大量客户端状态管理代码。也正因为如此OpenAI 自己在 Codex CLI、Deep Research 这类重 Agent 产品里全部是基于 Responses 协议在跑。3. Responses API 到底改了什么请求、响应与事件流3.1 端点和请求体的逐字段差异先看一张我整理的对应表这张表比我当年对着文档翻半天总结出来的要清楚得多维度Chat CompletionsResponses端点POST /v1/chat/completionsPOST /v1/responses消息/提示messages: [{role, content}]inputinstructions最大输出长度max_tokensmax_output_tokens系统提示词messages 里塞 system 消息独立的instructions字段工具定义type: function包裹function.name顶层name/description/parameters多轮上下文客户端拼 messagesprevious_response_id或 input items输出结构choices[0].messageoutput数组item 化流式事件choices[0].delta.content累积response.output_text.delta等事件流关键是max_tokens改成max_output_tokens这件事比想象中坑人。它不只是改个字段名而是max_tokens在部分新模型里被解释为总的生成 token 预算max_output_tokens则严格限制输出长度。我见过团队迁移后输出被莫名截断查了半天发现是旧参数被新接口默默忽略了。3.2 响应结构从 choices 数组到 item 列表旧接口拿到响应后标准取法长这样resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 介绍一下自己}] ) print(resp.choices[0].message.content)新接口的响应体变了resp client.responses.create( modelgpt-4o-mini, input介绍一下自己 ) print(resp.output_text) # SDK 提供的便捷属性resp.output_text是 SDK 为了方便取纯文本而做的快捷方式底层其实是对output数组里message类型的 item 做拼接。如果你自己解析 JSON会看到output是一个数组每个元素可能有不同的typemessage、function_call、reasoning。这带来的好处是工具调用和推理过程不再是藏在 message 里的隐形结构而是可以被程序直接遍历的显式 item。3.3 流式事件完全不同的事件协议这块是迁移时最容易写错代码的地方。Chat Completions 的流式很简单不断接收 chunk每个 chunk 里有一个delta.content拼起来就是完整文本。stream client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 讲个短故事}], streamTrue ) for chunk in stream: if chunk.choices[0].delta and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end)Responses 的流式是一串事件每个事件有明确的typestream client.responses.create( modelgpt-4o-mini, input讲个短故事, streamTrue ) for event in stream: if event.type response.output_text.delta: print(event.delta, end)除response.output_text.delta之外还有response.created、response.output_item.added、response.function_call_arguments.delta等事件。对 Agent 应用来说这套事件协议是真正的杀器你可以监听函数参数正在传回来的事件边接收边决定下一步动作而不必像旧接口那样等整段流结束再解析。代价就是如果只是做个简单的聊天机器人这套事件流确实显得重。3.4 工具调用从夹带到一等公民旧接口定义工具时函数信息被包在function子对象里读取结果时要先判断finish_reason是不是tool_calls再遍历message.tool_callstools [{ type: function, function: { name: get_weather, description: 获取城市天气, parameters: { type: object, properties: {city: {type: string}}, required: [city] } } }] resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 北京天气怎么样}], toolstools ) tool_call resp.choices[0].message.tool_calls[0] print(tool_call.function.name, tool_call.function.arguments)新接口里工具定义的层级更扁平调用结果也直接出现在 output 数组里tools [{ type: function, name: get_weather, description: 获取城市天气, parameters: { type: object, properties: {city: {type: string}}, required: [city] } }] resp client.responses.create( modelgpt-4o-mini, input北京天气怎么样, toolstools ) for item in resp.output: if item.type function_call: print(item.name, item.arguments)我个人的体会是在 Chat Completions 里写工具调用感觉像是在跟接口协议斗智斗勇在 Responses 里写工具调用感觉才是在正常写业务代码。二者对 Agent 框架的友好程度完全不在一个量级。4. 开源兼容层的兼容到底兼容了什么4.1 兼容层在做什么所谓 OpenAI 兼容层本质是一个协议翻译器。它拿 OpenAI 的接口格式当普通话负责把普通话翻译成各家后端的方言——Anthropic 的 Messages 格式、Google Gemini 的格式或者反过来把本地推理服务的方言包装成普通话。在这条赛道上有几类典型项目通用网关比如 LiteLLM、Portkey、Cloudflare AI Gateway。它们统一接收 OpenAI 格式的请求再转发给几十家模型供应商。本地推理框架vLLM、Ollama、llama.cpp 都直接暴露 OpenAI 兼容端点让本地模型能被标准 OpenAI SDK 调用。企业自建中间层很多团队内部会写一个薄薄的网关用来做模型切换、鉴权、限流和审计。这些项目的共同选择是把 OpenAI 接口格式当作天然标准。原因很简单生态工具——LangChain、LlamaIndex、各种开源 Agent 框架——默认都靠 OpenAI 格式对接你的网关不支持 OpenAI 格式就等于被整个工具生态拒之门外。4.2 为什么绝大多数兼容层只做了 Chat Completions这是我在排障路上最深刻的认知兼容 OpenAI这句话的含水量极高。很多项目声称兼容 OpenAI翻它的源码其实只实现了/v1/chat/completions一个端点有些甚至连流式都没做全。原因不复杂。Chat Completions 的 messages 结构足够简单映射到 Anthropic 和 Gemini 很直接——角色映射一下、内容塞进去就行。但 Responses API 引入了instructions、outputitem、previous_response_id这些概念映射到其他家就很尴尬Anthropic 没有任务状态的概念Gemini 也没有输出 item 列表一说。大部分兼容层在处理 Responses 请求时只能把它降级成把 instructions 塞进 system、把 input 塞进 user、再按普通 chat 请求转发响应回来再硬掰成 output 数组。这个降级再硬掰的过程就是我在文章开头那个unexpected endpoint or method报错的根源——网关根本没实现/v1/responses的路由自然直接拒绝。所以排查这类问题第一步永远是确认你对接的网关到底注册了哪些路由。翻它的 OpenAPI 文档或者直接看源码里的路由列表比反复检查客户端代码有效率得多。4.3 兼容是有损的那些被悄悄降级的参数即使网关已经支持/v1/responses兼容也不等于无损转译。我实测过的兼容层里有几类典型的降级行为能力直通情况常见降级表现文本生成基本正常无temperature/top_p大多数直通部分自建网关只转发不生效工具调用部分支持不支持并行工具调用只支持单函数response_format结构化输出看实现JSON Schema 被降级成请输出 JSON的提示词流式事件差异极大不转发function_call_arguments事件只给最终文本previous_response_id大多数不支持只能退化为手动拼历史最危险的是第二种和第五种参数被网关吃掉但不报错。你以为模型在用结构化输出其实模型只是在靠提示词硬撑你以为客户端在流式监听工具参数其实事件根本不会来。这种静默降级比直接报 404 更坑因为它会让你的程序在看起来正常的状态下悄悄变笨。4.4 迁移前必须做的四步检查基于这次排障我总结了一套接入任何兼容网关前的检查清单探活端点直接curl打/v1/responses看是不是 404。这一步 10 秒就能筛掉大部分只兼容 chat的网关。翻 OpenAPI 文档确认 paths 里有没有/v1/responses以及 tool_choice、stream 这些参数在 schema 里是不是可选项。看响应结构真实发一次带工具调用的请求打印完整响应 JSON确认output数组里function_callitem 是否按预期出现。做小流量对比同一批请求分别走新旧接口对比输出质量和耗时。很多降级问题在这个环节才会暴露。这套检查其实适用于任何接口迁移本质思路就一句话不要相信兼容这个词要相信你真实发出的那一次请求。5. 迁移实战从 chat.completions 到 responses 的代码改法5.1 最小改动一次普通对话最简单的非流式对话代码差异已经很明显。旧写法resp client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个简洁的助手}, {role: user, content: 用一句话介绍你自己} ] ) print(resp.choices[0].message.content)新写法resp client.responses.create( modelgpt-4o-mini, instructions你是一个简洁的助手, input用一句话介绍你自己 ) print(resp.output_text)两个注意点。第一input字段可以直接传字符串也可以传[{ role: user, content: ... }]这种消息数组形式兼容性做得很好。第二.output_text是 SDK 1.x 提供的便捷属性如果手动解析响应 JSON要自己遍历output数组把所有message类型的content拼起来。5.2 流式对话的迁移流式改法我已在上文给出这里补充一个实用经验不要用if event.type ...写一长串分支建议把事件按类型做成字典分发handlers { response.output_text.delta: lambda e: print(e.delta, end), response.function_call_arguments.delta: lambda e: buffer.append(e.delta), } for event in stream: handler handlers.get(event.type) if handler: handler(event)这样新增事件类型时只需加字典项不用动主循环。更重要的是不要在迁移初期一次性处理所有事件。先监听response.output_text.delta把文本流跑通再逐步加工具调用相关的事件每加一个就验证一遍。事件流模式下一次处理太多新概念容易把问题搅在一起。5.3 工具调用的迁移要点工具定义的层级变化我已经用代码展示过。这里再强调一个坑函数参数格式的兼容性。旧接口里parameters字段在function下一层新接口里直接在顶层。如果你的代码里写了个build_tools()函数迁移时最容易漏改的就是这个嵌套层级——我见过不止一次工具定义发过去了但模型始终不触发工具调用最后发现是网关把name当成了function.name来读自然匹配不上。另外新接口默认支持parallel_tool_calls并行工具调用一次输出里可能同时出现多个function_callitem。如果你的 Agent 逻辑假设一次只有一个工具调用迁移后要特别小心别只取output数组里第一个function_call就完事。5.4 多轮上下文的两种处理方式Responses 的多轮处理官方给了两种思路。第一种是previous_response_id串联适合服务端有状态的会话场景。你把第一次请求返回的response.id保存下来第二次请求带上resp1 client.responses.create(modelgpt-4o-mini, input我叫张三) thread_id resp1.id resp2 client.responses.create( modelgpt-4o-mini, input我刚才说我叫什么, previous_response_idthread_id )第二种是手动把历史组装成 items 数组适合无状态或需要跨会话恢复的场景。这里要注意function_call调用结果要作为带call_id的 item 传回去否则模型不知道这个工具结果对应哪一次调用。input[ {role: user, content: 北京天气怎么样}, {type: function_call, call_id: call_xxx, name: get_weather, arguments: {\city\:\北京\}}, {type: function_call_output, call_id: call_xxx, output: 晴25度} ]我个人建议新项目直接采用第一种它省掉的状态管理代码不是一点半点存量系统如果数据库里已经存了完整 messages 历史可以先走第二种平滑过渡不必强改存储结构。6. 从 Codex CLI 看接口会往哪里走6.1 Codex CLI 为什么必须用新接口如果你用过 Codex CLI 这类开源命令行 Agent就能直观体会到接口演进背后的真实驱动力。它让你在终端里用自然语言下指令模型可以自己读文件、执行命令、改代码、跑测试——一次任务里可能发生几十次工具调用。这个场景把 Chat Completions 的痛点放大得非常明显每次工具调用都要拼接一长串历史消息上下文越来越长而中间那些工具输出对最终答案往往没什么用但你又不能不带。Responses API 的outputitem 化 流式事件 状态串联几乎就是为这种Agent 循环量身定做的。模型每执行一步工具客户端通过事件流实时拿到function_call的参数然后把function_call_output传回去整个循环像流水线一样干净。我可以理解为什么 OpenAI 要把新能力优先堆在 Responses 上因为他们的自家产品确实在拿这套协议跑重活。6.2 安装实测missing optional dependency 的插曲提到 Codex CLI顺手说一个刚踩过的小坑。在 Windows 上通过 npm 安装时我遇到过这样的报错missing optional dependency openai/codex-win32-x64. reinstall codex: npm install...这个报错的本质是 npm 的 optional dependency 机制Codex 按平台分发二进制包Windows 对应的是openai/codex-win32-x64这个包但安装时它没下载成功。npm 对 optional dependency 的策略是装不上就跳过所以整个安装流程不会失败但实际运行时组件缺失。解决方案通常两步先执行npm install openai/codex-win32-x64 --save-optional补装对应平台的二进制依赖如果还不行就清理 node_modules 后重装并确保 npm 版本不要太旧。这个坑和接口演进没关系但它是 Agent 工具链里很典型的平台相关依赖问题遇到的人不少顺手记一下。6.3 对普通开发者的建议聊完这些我给不同角色的开发者几条不太一样的建议。如果你的场景是聊天机器人、内容生成、普通 RAGChat Completions 短期内完全够用不必恐慌式迁移。官方对它的维护会持续很久存量生态也不会一夜消失。但你得知道新模型、新能力的推送顺序一定是 Responses 优先别哪天想用某个新模型时才发现旧端点拿不到。如果你在做 Agent、工具调用、多步任务处理或者准备在开源网关之上做协议转换我建议直接拥抱 Responses。它省掉的状态管理不是锦上添花而是结构性优势。而且迁移成本没有想象中高——字段对应关系就那么几张表最复杂的部分反而是流式事件和工具 item 的处理。如果你在维护开源兼容层我的建议只有一条尽早把/v1/responses路由和支持矩阵做出来。生态工具对 OpenAI 新接口的适配速度比你想象得快兼容层如果一直停留在 Chat Completions迟早会变成旧协议的翻译官。而且 Responses 的 item 化设计其实更方便做多后端映射——先统一成中间表示再翻译到各家格式比在 messages 数组上打补丁干净得多。我在实际维护网关和脚本的过程中最深的体会是接口演进这种事最好顺着官方设计意图走而不是在旧接口上不断打补丁。Completions 到 Chat Completions 是从续写到对话Chat Completions 到 Responses 是从对话到任务。每一次升级背后都是模型能力边界的一次扩张。下次再看到unexpected endpoint or method这类报错时先别急着骂网关去翻一下它到底实现了哪个版本的协议——很多问题的答案其实就写在路由表里。
返回列表