news 2026/10/7 23:12:45

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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——在我当时调试的网关代码里,这是"协议路由匹配失败"的退出码,说明请求在进入业务逻辑之前就被拦下了。

我排障的顺序是这样的:

  1. 先 curl 探活:curl https://xxx/v1/models,正常返回模型列表,说明服务在线。
  2. 再 curl 打一次:curl https://xxx/v1/chat/completions -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}',结果直接返回 404 和这行报错。
  3. 翻服务端 OpenAPI 文档,发现 paths 里只有/v1/responses和/v1/models。

也就是说,这个服务的维护者只实现了 Responses API,把 Chat Completions 整个丢掉了。对于依赖旧协议的客户端来说,这不是"某个字段不兼容"的问题,而是整条路都被拆了。这也是接口代际更替时最典型、也最容易被忽视的坑:换协议不是改参数,是换入口。

1.2 三次转身:从文本补全到任务执行

回看 OpenAI 接口演进,其实是三个时代的三次转身。

第一代是 Completions 时代,端点POST /v1/completions。那时候的用法很朴素:把一段 prompt 扔进去,模型返回一段补全的 text,没有角色区分,没有系统提示词,更谈不上工具调用。典型调用长这样:

resp = client.completions.create( model="text-davinci-003", prompt="写一首关于秋天的短诗" ) print(resp.choices[0].text)

第二代是 Chat Completions,2023 年随 GPT-3.5 Turbo 一起出现,端点POST /v1/chat/completions。它把对话抽象成messages数组,每个消息带role(system/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}]input+instructions
最大输出长度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( model="gpt-4o-mini", messages=[{"role": "user", "content": "介绍一下自己"}] ) print(resp.choices[0].message.content)

新接口的响应体变了:

resp = client.responses.create( model="gpt-4o-mini", input="介绍一下自己" ) print(resp.output_text) # SDK 提供的便捷属性

resp.output_text是 SDK 为了方便取纯文本而做的快捷方式,底层其实是对output数组里message类型的 item 做拼接。如果你自己解析 JSON,会看到output是一个数组,每个元素可能有不同的type:message、function_call、reasoning。这带来的好处是,工具调用和推理过程不再是藏在 message 里的隐形结构,而是可以被程序直接遍历的显式 item。

3.3 流式事件:完全不同的事件协议

这块是迁移时最容易写错代码的地方。Chat Completions 的流式很简单:不断接收 chunk,每个 chunk 里有一个delta.content,拼起来就是完整文本。

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

Responses 的流式是一串事件,每个事件有明确的type:

stream = client.responses.create( model="gpt-4o-mini", input="讲个短故事", stream=True ) 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_calls:

tools = [{ "type": "function", "function": { "name": "get_weather", "description": "获取城市天气", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"] } } }] resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "北京天气怎么样?"}], tools=tools ) 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( model="gpt-4o-mini", input="北京天气怎么样?", tools=tools ) 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 迁移前必须做的四步检查

基于这次排障,我总结了一套接入任何兼容网关前的检查清单:

  1. 探活端点:直接curl打/v1/responses,看是不是 404。这一步 10 秒就能筛掉大部分"只兼容 chat"的网关。
  2. 翻 OpenAPI 文档:确认 paths 里有没有/v1/responses,以及 tool_choice、stream 这些参数在 schema 里是不是可选项。
  3. 看响应结构:真实发一次带工具调用的请求,打印完整响应 JSON,确认output数组里function_callitem 是否按预期出现。
  4. 做小流量对比:同一批请求分别走新旧接口,对比输出质量和耗时。很多降级问题在这个环节才会暴露。

这套检查其实适用于任何接口迁移,本质思路就一句话:不要相信"兼容"这个词,要相信你真实发出的那一次请求。

5. 迁移实战:从 chat.completions 到 responses 的代码改法

5.1 最小改动:一次普通对话

最简单的非流式对话,代码差异已经很明显。旧写法:

resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是一个简洁的助手"}, {"role": "user", "content": "用一句话介绍你自己"} ] ) print(resp.choices[0].message.content)

新写法:

resp = client.responses.create( model="gpt-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(model="gpt-4o-mini", input="我叫张三") thread_id = resp1.id resp2 = client.responses.create( model="gpt-4o-mini", input="我刚才说我叫什么?", previous_response_id=thread_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 对普通开发者的建议

聊完这些,我给不同角色的开发者几条不太一样的建议。

如果你的场景是聊天机器人、内容生成、普通 RAG,Chat Completions 短期内完全够用,不必恐慌式迁移。官方对它的维护会持续很久,存量生态也不会一夜消失。但你得知道,新模型、新能力的推送顺序一定是 Responses 优先,别哪天想用某个新模型时才发现旧端点拿不到。

如果你在做 Agent、工具调用、多步任务处理,或者准备在开源网关之上做协议转换,我建议直接拥抱 Responses。它省掉的状态管理不是锦上添花,而是结构性优势。而且迁移成本没有想象中高——字段对应关系就那么几张表,最复杂的部分反而是流式事件和工具 item 的处理。

如果你在维护开源兼容层,我的建议只有一条:尽早把/v1/responses路由和支持矩阵做出来。生态工具对 OpenAI 新接口的适配速度比你想象得快,兼容层如果一直停留在 Chat Completions,迟早会变成"旧协议的翻译官"。而且 Responses 的 item 化设计其实更方便做多后端映射——先统一成中间表示,再翻译到各家格式,比在 messages 数组上打补丁干净得多。

我在实际维护网关和脚本的过程中,最深的体会是:接口演进这种事,最好顺着官方设计意图走,而不是在旧接口上不断打补丁。Completions 到 Chat Completions 是"从续写到对话",Chat Completions 到 Responses 是"从对话到任务"。每一次升级,背后都是模型能力边界的一次扩张。下次再看到unexpected endpoint or method这类报错时,先别急着骂网关,去翻一下它到底实现了哪个版本的协议——很多问题的答案,其实就写在路由表里。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 23:10:23

企业搜索范式迁移:RAG与关键词检索实战拆解

企业搜索这个领域,过去十几年其实一直处在“能用”和“好用”之间反复横跳。传统的关键词检索——就是大家熟悉的 BM25、Elasticsearch 那套——胜在简单、快、可控,但一到“用户其实想问个问题,而不是查几个词”的场景就露馅。这两年 RAG&am…

作者头像 李华
网站建设 2026/10/7 23:10:01

松下A6伺服全闭环调试实战:从光栅尺选型到参数配置避坑指南

做精密设备调试这些年,我碰到最多的一个局面就是:伺服半闭环看着哪都正常,电机编码器反馈也一直很稳,可只要把千分表打在工作台上,定位误差就是下不来。丝杠有螺距误差、联轴器有扭转、导轨有间隙,这些东西…

作者头像 李华
网站建设 2026/10/7 23:07:54

Agent降本50%:X-Router自演进模型路由与昇腾适配实战

1. 从Token账单说起:为什么路由层才是Agent降本的关键战场做Agent开发的人都有一个共同的痛:模型调用成本像滚雪球一样越滚越大。一个稍微复杂点的多轮对话Agent,跑一天下来Token消耗量能让人心惊肉跳。我见过不少团队,功能做得挺…

作者头像 李华
网站建设 2026/10/7 23:06:18

Java校园二手交易平台SSM源码详解:从环境部署到二次开发

简介:这是一份基于JSP/Servlet与StrutsHibernate架构的校园二手交易平台Java源码,面向高校在校生、Java Web学习者或毕业设计开发者,解决校园内二手物品信息发布、浏览与交易管理等需求。系统采用B/S模式与MySQL 5.0数据库,具备面…

作者头像 李华
网站建设 2026/10/7 23:06:16

企业智能体平台落地难?五种实现路径与工程治理实战

1. 企业智能体平台落地的真实困境过去一年我参与过三个企业级智能体平台的选型与落地项目,从制造业的售后知识助手,到金融行业的合规审查流程,再到零售集团的销售辅助工具,几乎每一个项目在POC阶段都跑得挺漂亮,但一到…

作者头像 李华
网站建设 2026/10/7 23:05:55

Keria赛后采访解读:LCK赛区如何维持英雄联盟强国地位与辅助位进阶

1. 从一句赛后采访说起:Keria这句话到底在说什么 如果你只看标题,可能会觉得这不过是一句普通的赛后客套话。但如果你真的追过LCK、追过T1这几年的比赛,就会明白Keria说出“很高兴能让韩国继续被称为英雄联盟强国”这句话时,背后压…

作者头像 李华