从Chat Completions到Responses API:接口演进与迁移指南

发布时间:2026/10/7 13:06:33
从Chat Completions到Responses API:接口演进与迁移指南 1. 从一次线上事故说起POST /chat/completions 为什么突然就 404 了先讲一个我真实踩过的坑。某个周一早上运维群突然飘红线上服务的错误日志里刷满了下面这行[error] unexpected endpoint or method. (post /chat/completions). returning 2看到这个报错的第一反应是“网关挂了”因为我们的服务通过内部统一接入层调用模型接口怀疑是某个节点配置失效。但排查了一圈接入层健康检查完全正常健康检查用的不是/chat/completions这个路径。真正的原因是在一次例行升级中接入层背后的模型服务已经切换到了 OpenAI 最新一代的Responses API而这个 API 默认路由是/v1/responses压根不认/v1/chat/completions。旧请求打到新接口上自然就给你一个 404。那次事故之后我把 Completions 和 Responses 两代接口从协议设计、参数映射、流式事件、开源生态兼容几个维度完整梳理了一遍。今天这篇就是那次排查的复盘也是给所有还在用chat.completions写业务代码、或者正准备接新接口的开发者一份避坑指南。不管你是做 Agent 应用、做开源模型网关还是只写过一个调用 GPT 的脚本这篇文章应该都能帮你少走几个月的弯路。2. Completions 时代为什么会有这个接口它解决了什么2.1 从文本补全到对话生成的范式转换说 Responses 之前得先把 Completions 为什么会出现讲清楚。OpenAI 早期最著名的接口是Text Completions也就是POST /v1/completions输入一段前缀文本模型给你续写后面的内容。它本质上就是一个“文本补全”工具模型不知道你在“对话”你只是给它一段话它尽量接得自然。在 GPT-3 那个年代这个接口本身够用因为模型能力有限大家做的也都是文本生成、摘要、分类这类任务。但 ChatGPT 出来之后情况完全变了开发者需要的是真正的多轮对话模型要能记住前面说过的话还要能区分“系统指令”和“用户消息”。用纯文本补全去做多轮对话意味着你每次都要手动把整个聊天历史拼成一大段字符串再塞进 prompt 里。做一两次没问题做着做着就发现分隔符怎么加谁说的话怎么标记上下文太长怎么办这些问题如果全部让开发者自己处理效率太低了。于是 OpenAI 在 2023 年 3 月推出了Chat Completions接口路径是POST /v1/chat/completions。它最大的变化是把“一段文本”升级成了“一组消息”消息带明确的role字段可以是system、user、assistant每个角色对应不同的语义。模型不再需要从一串纯文本里猜测哪句是问题、哪句是回答而是基于消息序列直接生成回复。这个设计今天看起来平平无奇但在当时是一次很关键的接口范式转变从“补全一段话”变成了“完成一轮对话”。2.2 Chat Completions 的核心机制与使用体验我记得第一次用chat.completions.create写多轮对话时最大的感受是“心智负担确实降低了”。你不需要再手动拼接上下文只需要把历史消息按顺序传给messages参数就行。当时的典型调用长这样from openai import OpenAI client OpenAI(api_keysk-...) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个擅长写技术文章的助手。}, {role: user, content: 帮我构思一篇关于接口演进的博文提纲。}, {role: assistant, content: 好的我建议从以下三个角度展开……}, {role: user, content: 第三个角度再细化一下。}, ], temperature0.7, max_tokens1024, ) answer response.choices[0].message.content这里面的几个参数后来几乎成了行业标准temperature控制随机性max_tokens限制生成长度messages表示对话上下文。大量开源项目、商业产品、模型聚合平台都围绕这套协议做封装。哪怕到今天你去看市面上的开源模型网关核心协议仍然普遍是 Chat Completions道理很简单生态太庞大了迁移成本太高。2.3 补丁式演进留下的隐患但 Chat Completions 也有它的历史包袱。最明显的一点它本质上是“无状态”的。每次请求都要把完整对话历史重新传一遍服务端不帮你保存任何状态。对话短还好一旦上下文积累到几万 token每次请求的传输成本和计费成本都会显著上升。第二点麻烦的是工具调用Function Calling。函数调用功能是后来以“补丁”形式加到 Chat Completions 里的体现在请求里是tools参数响应里是tool_calls字段。这个设计能用但用起来很别扭你必须先发一轮请求拿到工具调用参数再自己执行函数把结果以roletool的消息塞回对话然后再发一轮请求让模型基于工具结果生成最终回答。整个流程需要你手动维护循环在 Agent 场景下代码很快就变得又长又绕。第三点是响应结构不统一。文本生成、工具调用、日志概率、用量统计全部塞在一个choices数组里不同场景下你要解析的字段完全不同。流式场景下还得处理多种delta内容。用着用着你会发现Chat Completions 更像是“为对话而生的接口”而不是“为 Agent 而生的接口”。当应用从简单的聊天升级到多步骤任务执行时它的边界感就出来了。3. Responses API为什么 OpenAI 选择推翻重做3.1 与其继续打补丁不如重新定义协议2024 年之后OpenAI 做了一个让很多开发者惊讶的决定推出新一代Responses API路径是POST /v1/responses。这个决定背后的逻辑其实很清晰——Agent 应用已经成为主流而 Chat Completions 在设计之初并没有考虑 Agent 场景。继续在旧协议上打补丁会越补越乱干脆基于 Agent 运行的核心需求重新设计一套接口。我在实际使用中对 Responses API 最直观的感受是它把“一次模型调用”从一个单纯的文本生成操作扩展成了一个“可组合的智能任务单元”。简单来说你现在调用一次接口模型可以自主决定调用工具、搜索文件、执行代码然后给出最终结果。所有这些都发生在同一次请求-响应的生命周期里不需要开发者手动拼接多轮调用来模拟“推理-行动-观察”的循环。3.2 请求参数与输入形态的核心差异如果拿 Chat Completions 的经验直接套 Responses API第一反应会是“字段名怎么全变了”。Responses API 的请求参数里不再叫messages而是叫input不再叫max_tokens而是叫max_output_tokens工具参数不再叫tools而是改成了tools下的细分类型可以是function、web_search、file_search、code_interpreter等内置工具。这些改名的背后不只是命名习惯的变化而是语义的升级。举个例子input参数既可以直接传一个字符串也可以传消息数组还可以传previous_response_id。这个previous_response_id是 Responses API 最核心的亮点之一它允许你把上一次响应的 ID 直接传给下一次请求服务端会帮你维护整个对话上下文你不需要再手动拼接历史消息。也就是说接口终于从“无状态”走向了“有状态”。from openai import OpenAI client OpenAI() # 第一轮创建对话 first client.responses.create( modelgpt-4o, input帮我列一下搭建个人博客的步骤。, ) # 第二轮直接引用上一轮的 response id无需重发历史消息 second client.responses.create( modelgpt-4o, input第二步再展开说说, previous_response_idfirst.id, ) print(second.output_text)这段代码看起来普通但它解决了一个真实痛点在多轮对话场景下不需要每次把整段历史重新发送。官方提供previous_response_id机制之后上下文管理从“客户端拼字符串”变成了“服务端存状态”。如果你做过长对话应用应该能体会这个改动省了多少事。3.3 内置工具与结构化输出带来的 Agent 能力跃升Responses API 的另一个变化是工具生态的“开箱即用”。在 Chat Completions 时代要接入联网搜索、文件解析、代码执行这些能力你需要自己找第三方服务、自己写中间层。Responses API 直接把web_search、file_search、code_interpreter做成了内置工具只需要在请求里声明response client.responses.create( modelgpt-4o, input搜索一下最新的 Python 版本信息并总结成三条要点。, tools[ { type: web_search, name: web_search, } ], ) print(response.output_text)模型会在内部完成搜索、阅读、总结的完整流程最终输出整理好的答案。这种“工具即参数”的设计对 Agent 开发者来说非常友好你不必为了一个搜索功能单独对接一套 API。结构化输出方面Responses API 支持在text配置里声明format可以用json_schema强制模型输出符合指定 JSON Schema 的结果response client.responses.create( modelgpt-4o, input从这段文本里提取商品名称、价格和库存。, text{ format: { type: json_schema, name: product_info, schema: { type: object, properties: { name: {type: string}, price: {type: number}, stock: {type: integer}, }, required: [name, price, stock], }, strict: True, } }, )注意这里的format是嵌在text配置里的。我当时第一次看到这个写法也愣了一下不是直接传response_format而是把格式约束放在输出文本的配置项里。这个设计其实更自洽因为它把“输出什么格式”和“输出什么内容”绑定在了一起。3.4 流式事件机制的变化流式输出也是重灾区。Chat Completions 的流式响应是一串choices[].delta增量Responses API 则改成了一组语义化的事件流。常见的事件类型包括response.created、response.output_text.delta、response.completed、response.failed等等。用 Python SDK 的方式差别很大with client.responses.stream(modelgpt-4o, input写一首关于夏天的小诗) as stream: for event in stream: if event.type response.output_text.delta: print(event.delta, end)如果你维护过流式输出的前端就会知道事件化设计对渲染和中断恢复都友好得多。Chat Completions 流里几乎没有“状态边界”而 Responses API 每个事件自带类型你完全可以基于事件类型做不同的 UI 逻辑。4. 开源兼容的真相为什么大量项目仍然停留在 Chat Completions4.1 “兼容”到底兼容的是什么聊到开源兼容必须先厘清一个概念兼容分很多层。第一层是“官方协议兼容”也就是 OpenAI 官方同时维护两套协议旧的 Chat Completions 处于Deprecated状态但还能用。第二层是“客户端 SDK 兼容”OpenAI 官方 Python SDK 和 Node SDK 已经默认把client.chat.completions和client.responses同时保留不存在二选一。第三层是“开源生态兼容”这才是最复杂的。我观察到的现象是到今天大量的开源项目、第三方模型聚合平台、自建网关核心 API 依然是 Chat Completions。原因非常简单——生态惯性。Chat Completions 的协议已经成了事实标准几乎所有的国产模型、开源模型、云厂商的兼容接口都以它为准。你做一个网关项目如果不支持/chat/completions那几乎等于不兼容整个生态。4.2 开源项目适配 Responses 的真实成本有朋友问过我“既然 Responses API 更好为什么开源项目不赶紧切过去”我通常会反问一句“你试试把一个大型项目从 messages 改成 input 和 previous_response_id 需要多久”这个迁移不是改字段名那么简单。背后牵扯到请求日志、流式解析、错误处理、工具调用循环、上下文管理、计费统计每一层都要跟着变。而且 Responses API 的“状态化”设计虽然对调用方友好对网关类项目反而不友好网关的核心价值就是把无状态请求转发到后端你要它维护previous_response_id的映射关系等于要求网关保存每个会话的服务端状态这正是很多网关架构里最不想碰的东西。还有一个很实际的问题大量开源模型并没有真正实现 Responses 协议的完整语义。模型服务商可以轻松做一个/v1/chat/completions兼容端点因为它只是把请求转成提示词但要做/v1/responses兼容端点至少要处理内置工具调用、状态化上下文、事件流这些机制这不是简单映射能做到的。所以你会看到很多“兼容 Responses”的项目其实是把 Responses 请求翻译成 Chat Completions 请求再转发这又回到了兼容层这个老话题。4.3 官方与开源之间的动态博弈官方在推 Responses API 的同时也在有意识地降低接入门槛。比如Agents SDK原Swarm的演进版里把 Responses API 作为默认后端协议还配套了Codex CLI、OpenAI Agents SDK等开源工具。这些工具本身依赖 Responses 的previous_response_id和内置工具机制。有个细节我记得很清楚某次我用 npm 安装 Codex CLI 时报了一个missing optional dependency openai/codex-win32-x64. reinstall codex: npm in的错误。当时第一反应是“依赖装漏了”后来发现是平台相关的二进制包没拉全。重装了对应平台包之后才正常。类似的兼容问题在开源生态里太常见了——工具链更新得太快示例文档跟不上。这些现象背后有个值得思考的趋势OpenAI 正在把“协议层”和“生态层”解耦。协议层由官方定义生态层则通过开源项目来推进。Responses API 本身吸收了开源 Agent 框架中“循环-工具-事件”的设计思路反过来又通过官方 SDK 反哺开源项目。所谓“开源兼容”本质上就是在这个双向互动中形成的。5. 两代接口的逐字段对照与迁移实战5.1 关键参数映射表如果你决定从 Chat Completions 迁到 Responses最省力的方式是先做一张字段映射表把代码里的参数名逐一对应起来。我整理了一份常用对照Chat CompletionsResponses API说明messagesinput消息序列也可传字符串或 response idmodelmodel未变化max_tokensmax_output_tokens输出长度上限temperaturetemperature语义一致toolstoolsResponses 支持内置工具类型tool_choicetool_choice语义略有差异response_formattext.format结构化输出新的表达方式streamstream语义一致useruser语义一致seedseed语义一致top_ptop_p语义一致frequency_penaltyfrequency_penalty语义一致presence_penaltypresence_penalty语义一致stoptext.stop停止符也移到了 text 配置里5.2 文本对话场景的最小迁移示例先看一段最简单的消息补全迁移。旧接口是这样写的response client.chat.completions.create( modelgpt-4o, messages[ {role: system, content: 你是一个技术翻译助手。}, {role: user, content: 把这句话翻译成英文接口规范演进。}, ], ) print(response.choices[0].message.content)迁移到 Responses API 之后等价的写法是这样response client.responses.create( modelgpt-4o, instructions你是一个技术翻译助手。, input把这句话翻译成英文接口规范演进。, ) print(response.output_text)注意两个细节第一system消息变成了instructions参数作为独立的配置项传进去第二获取回答内容的路径从response.choices[0].message.content变成了response.output_text。这两个差异是迁移过程中最容易踩的坑。你如果只把messages改成input忘了处理instructions和output_text代码会直接报错。5.3 多轮对话迁移与 previous_response_id 的正确用法多轮对话的迁移最能体现新协议的优势。旧协议下你需要手动维护messages数组history [ {role: system, content: 你是智能客服。}, {role: user, content: 我想退货。}, ] # 第一次调用 r1 client.chat.completions.create(modelgpt-4o, messageshistory) history.append({role: assistant, content: r1.choices[0].message.content}) # 用户继续说话 history.append({role: user, content: 怎么申请退货}) # 第二次调用依然要把整个 history 传一遍 r2 client.chat.completions.create(modelgpt-4o, messageshistory)新协议下你可以省掉大部分历史拼接逻辑# 第一次调用 r1 client.responses.create( modelgpt-4o, instructions你是智能客服。, input我想退货。, ) # 用户继续说话直接引用 response id r2 client.responses.create( modelgpt-4o, input怎么申请退货, previous_response_idr1.id, ) print(r2.output_text)这个写法在服务端会自动关联上下文省掉了messages数组的维护成本。但要提醒一句previous_response_id是“引用关联”不是“无限追溯”它有长度上限。长对话场景下你还是需要定期做摘要压缩或者重置会话不能指望一个 ID 串起几个小时的对话。5.4 工具调用的迁移差异工具调用是迁移中最容易出 bug 的部分。Chat Completions 时代的函数调用需要你走一个“两段式”流程# 第一段让模型决定是否调用工具 resp client.chat.completions.create( modelgpt-4o, messages[{role: user, content: 北京现在几点}], tools[{ type: function, function: { name: get_current_time, description: 获取指定城市的当前时间, parameters: { type: object, properties: { city: {type: string} } } } }], ) tool_call resp.choices[0].message.tool_calls[0] # 第二段执行函数把结果回传给模型 result get_current_time(tool_call.function.arguments) resp2 client.chat.completions.create( modelgpt-4o, messages[ {role: user, content: 北京现在几点}, resp.choices[0].message, {role: tool, tool_call_id: tool_call.id, content: result}, ], ) print(resp2.choices[0].message.content)Responses API 里工具调用的“循环”由接口自己管理你只需要把function工具传给tools然后提供execute_function回调def execute_function(name, arguments): if name get_current_time: return get_current_time(arguments[city]) raise ValueError(fUnknown function: {name}) response client.responses.create( modelgpt-4o, input北京现在几点, tools[{ type: function, name: get_current_time, description: 获取指定城市的当前时间, parameters: { type: object, properties: { city: {type: string} } }, }], ) print(response.output_text)当然这里要说明一下官方 Python SDK 的responses接口里工具的实际执行还是需要你自己在回调函数里写逻辑但整个“模型要求调用 - 工具结果返回模型”的循环由 SDK 和协议帮你编排好了你不用手动构造第二轮的 messages。对比下来代码的复杂度降低非常明显。5.5 结构化输出与 JSON 解析的迁移注意点对话应用很少用到结构化输出但后端服务、自动化脚本、数据清洗场景非常依赖。Chat Completions 的写法resp client.chat.completions.create( modelgpt-4o, messages[{role: user, content: 提取信息……}], response_format{type: json_object}, )注意旧接口用json_object只能保证输出是 JSON不保证符合某个具体结构要真正约束结构还得用json_schema并配合tool_choice强制调用。Responses API 的做法简洁得多就在text.format里声明 schema配合strict: true让输出严格匹配定义。对于那些需要“解析结果直接入库”的场景这个特性非常实用。6. 常见问题与排查技巧实录6.1 Error 404 unexpected endpoint or method 的完整排查思路回到开头那个坑。unexpected endpoint or method (post /chat/completions)这个错误表面上是“路径不存在”实际上背后有好几种可能。我整理了一份排查清单检查你所调用的服务端版本如果是 OpenAI 官方接口Chat Completions 目前仍然可用不会返回 404如果你用的是自建网关或第三方接入层那么网关很可能已经升级到了只支持 Responses API 的版本。检查 Base URL 是否指向了错误的服务很多团队会维护多个环境开发环境切到了新网关生产环境还没切代码里写死了某个环境的地址就会间歇性出现这个报错。检查 SDK 版本旧版openaiSDK 默认请求路径里有chat/completions新版 SDK 已经同时支持两类接口但如果你手动构造 HTTP 请求路径写错的情况太多了。检查代理层路由规则如果网关做了路径转发/v1/chat/completions和/v1/responses可能被路由到不同的后端节点节点能力不一致时旧的路径就会返回这个错误。顺带说一句如果你用的是社区维护的“聚合类网关”这个问题更常见因为这些项目往往以 Chat Completions 为统一协议但新版本可能会加入 Responses 兼容入口配置没跟上就会出现路由错乱。遇到这种 404不要急着怀疑模型服务挂了先确认“路径是否被当前版本支持”这一步往往就能定位问题。6.2 从 choices 到 output响应结构差异导致的解析失败迁移过程中第二个高频问题是解析失败。旧代码里到处是response.choices[0].message.content换了新接口之后choices直接不存在了。Responses API 的响应体是output数组里面元素有type区分可能是message、function_call、web_search_call等等。如果你要拿纯文本答案最方便的是response.output_text这个快捷字段。我的建议是迁到新接口之后在代码里加一个“响应结构断言”函数先把响应里的字段和类型打印出来再写解析逻辑避免凭记忆猜字段名。这个习惯帮我少踩了很多坑。6.3 新接口下模型拒绝服务的常见原因Responses API 的报错信息比 Chat Completions 更明确但有些错误仍然很容易让人懵。比如提示parameter input is required多半是你把input拼成了inputs又比如previous_response_id无效通常是会话已经过期或者 response id 来自不同模型这个 ID 是跟模型绑定的不能用 A 模型的响应 ID 续接 B 模型的上下文。另外还要注意Responses API 对某些旧参数做了严格校验比如logprobs的配置方式变了。你在旧接口里传的logprobsTrue, top_logprobs3在新接口里需要写成logprobs配置块。遇到unknown parameter这类错误时基本可以判定是 SDK 版本过旧升级到最新版再试。6.4 计费与用量统计的口径变化用量统计也是迁移后容易被忽略的一环。Chat Completions 的usage字段很简单就是prompt_tokens和completion_tokens。Responses API 的usage更细包含input_tokens、output_tokens、total_tokens还多了一个cache_read_input_tokens之类的字段。如果你原本用日志统计成本迁移后解析usage的字段名需要同步修改否则账单数据会直接对不上。有个容易踩的坑是previous_response_id续接对话时input_tokens计算口径和重新传历史消息时不一样。用 ID 续接通常更省钱因为它加载的是服务端缓存的上下文而不是重新逐字计费。但你如果同时传了input和previous_response_id计费规则又不一样了。我用下来最稳妥的方式是尽量二选一要么传input全文要么传previous_response_id不要两者混用账单才好解释。6.5 流式输出场景的兼容性问题如果你的项目依赖流式输出Chat Completions 和 Responses 的差异感受特别明显。旧接口的流里只有data: {choices: [{delta: {content: ...}}]}这种结构新接口的事件流有明确的类型比如response.output_text.delta表示文本增量response.function_call_arguments.delta表示工具参数的增量。直接按旧逻辑解析新接口的流一定会出问题。我的建议是流式场景中先打印事件类型列表把真实的事件序列看一遍再写解析逻辑。别直接靠文档猜实际跑一遍比看文档更可靠。另外如果你是做 Server-Sent EventsSSE转发的网关Responses API 的事件类型更丰富转发时需要做“事件类型白名单”否则会把一些内部事件透传给前端导致前端解析报错。7. 写在最后兼容是妥协选择才是关键坦白说我并不是让你立刻把全部代码切到 Responses API。对于纯文本问答、简单聊天机器人、以及依赖大量开源生态组件的项目Chat Completions 依然够用而且兼容性最好。真正建议尽快迁移的是这几类场景多步骤 Agent、需要联网搜索/文件解析/代码执行的应用、以及受够了手动拼上下文的长对话产品。我个人在实际操作中的体会是迁移不用搞“一刀切”。可以在新模块里先用 Responses API把旧模块留在 Chat Completions中间留一个兼容层做参数转换等稳定之后再做全量替换。我甚至见过一种做法用 OpenAI 官方 SDK 的同时在代码里写一个抽象工厂只暴露chat()和response()两个方法底层具体调用哪个接口由配置决定。这样切换只需要改配置项不需要改业务逻辑。最后再分享一个小技巧无论你用哪个接口调试时第一时间打印完整的响应体尤其是response.model_dump()或者response.json()。现在的 SDK 做了一层又一层封装很多问题的根源是“你以为你调的是 A 接口实际上封装层已经偷偷转成 B 请求了”。把原始报文拉出来看一眼比对着文档猜半天高效得多。这套接口演进背后的逻辑其实很简单当场景从“对话”转向“任务”协议就必须跟着变。Chat Completions 完成了它的历史使命Responses API 正在定义下一个时代的接口形态。作为开发者我们能做的就是理解变化背后的原因然后在合适的场景做合适的选择。