news 2026/11/5 19:34:03

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 Chat Completions 到 Responses API:接口演进与迁移实战指南

1. 从 Chat Completions 到 Responses:一次接口规范的分水岭

聊到 OpenAI 的 API,很多人的第一反应还是那个经典的POST /v1/chat/completions。这个接口几乎成了大模型应用的事实标准,无论是接 GPT-4 还是本地跑 Llama 3,大家默认都是按这个姿势发请求。但到 2025 年再看,OpenAI 明显想把重心挪到新的Responses API上,也就是POST /v1/responses。我常被问到一个问题:这两个接口到底有什么区别?开源社区那些兼容层又是怎么做到“让你无感切换”的?这篇文章就专门拆这件事。

先说结论:Completions 是一条服务端生成的、流式返回的文本补全通道;Chat Completions 是它针对对话场景的升级版,输入 messages 数组,模型按角色轮流说话。而 Responses API 则是一个更统一的状态化接口,它把工具调用、多轮上下文、内置记忆和文件搜索都揉进了一个响应对象里,不再需要你在客户端手搓一大堆逻辑。从名字就能看出来,OpenAI 想要的不是“补全一段话”,而是“给出一个完整响应”。

为什么值得关心这事?因为如果你是做 AI 应用开发的,你的代码迟早要面对这个迁移。而且更微妙的是,开源社区的大量工具和框架仍然死守 Chat Completions 接口,这导致了一个非常实际的问题:服务商换了、模型换了、SDK 也换了,但你的请求体基本没变。这背后的“开源兼容”真相,比想象中要复杂,也更有意思。

1.1 为什么我直到今天还在用 Completions

先交代一下我的真实使用情况。我手上有几个线上服务,最早用的是text-davinci-003时代的普通 Completions 接口,后来 GPT-3.5 Turbo 出来之后就全面切到了 Chat Completions。说实话,切得很香,因为 Chat Completions 用 messages 数组表达系统提示词、用户消息、历史上下文确实比一条裸字符串要清晰得多。

但要说真正让我离不开 Chat Completions 的原因,其实是生态兼容。本地跑模型用的 Ollama、vLLM,甚至是一些开源的 API 网关,默认都实现了/v1/chat/completions路由。这意味着我只要把base_url从 OpenAI 改成局域网里的某个服务,SDK 不用换,代码几乎不用改,就能从云端 GPT 切到本地模型。这种无缝体验,直接影响了我的技术选型——模型可以换,但接口姿势最好别变。

所以当 OpenAI 推 Responses API 时,我第一反应不是“好厉害”,而是“咱能不能别折腾”。但实际用下来,我慢慢理解了这次演进背后的逻辑:Chat Completions 本身只是一个“对话文本”的接口,它没有原生的 agent 状态管理,没有内置的搜索工具,也没有统一的工具调用协议。你要做一个能连续处理任务的应用,就得自己在外面套一层循环,手动拼接上下文、手动解析 tool_calls、手动处理结果。Responses 想把这层循环收编进服务端。

1.2 Responses API 到底改了什么

用表格来看最直观。我整理了我自己在迁移过程中关注到的差异点:

维度Chat CompletionsResponses API
端点POST /v1/chat/completionsPOST /v1/responses
输入结构messages数组,每条有role和contentinput数组,支持字符串、消息或函数调用结果;也支持previous_response_id串联
输出结构choices[].message,内容是纯文本output数组,统一包含message、function_call、web_search等条目
工具调用通过tools定义,返回tool_calls,需手动拆解tools定义后,服务端直接把这些调用整理成独立的function_calloutput item
多轮上下文客户端必须把历史 messages 全部重新传一遍用previous_response_id引用上一次响应,不用重复传历史
内置能力不支持原生支持 Web Search、File Search、内置记忆(按账号维度)
流式输出choices[].delta增量output_text.delta增量,结构更扁平

最核心的变化,我觉得是previous_response_id。以前做多轮对话,我得把整个消息历史传过去,既费 token 又容易超长。现在的做法是先创建一条 response,然后用它的 id 作为下一次请求的previous_response_id,上下文直接被服务端接管。这在做持续对话和 agentic workflow 时省了很多事。

另一个容易被忽略的点是输出结构。Chat Completions 的 tool_calls 是嵌在 message 对象里的,解析时要做很多类型判断。Responses API 的输出只有两类核心条目:message和function_call,清晰得多。我在迁移工具调用逻辑时,代码量少了一半不止。

1.3 OpenAI 为什么敢动接口规范

这里有个很现实的商业问题:一个已经被全球开发者用烂了的接口,为什么非要改?我认为背后的逻辑有三层。

第一层是产品形态变了。OpenAI 越来越强调 agent 和助手(Assistant),而不只是“给你一段生成文本”。Chat Completions 太底层,助手需要的是会话、工具、记忆、检索这些高层抽象,Responses API 正好是这些能力的统一出口。本质上,这是从“AI 补全服务”走向“AI 代理服务”的转型。

第二层是生态话语权。谁的接口稳定,谁就掌握了开发者迁移成本和工具链指向。OpenAI 当然希望所有第三方框架都围绕它的最新规范来做适配,而不是让chat/completions这个老接口变成整个行业的“共同语言”。如果大家都不迁移,那它在兼容层上的话语权就会被削弱。

第三层是开发者体验。把多轮上下文和工具调用的复杂度收进服务端,能减少客户端样板代码,降低新手接入门槛。OpenAI 的数据显示,Responses API 在同等任务下可以减少大约 40% 的客户端调用逻辑,这对推广 agent 类应用很重要。

但这里就有意思了:Chat Completions 接口已经在开源生态里扎根太深了,不是 Oracle 说一句“改用新规范”就能翻篇的。于是我们看到了一种尴尬的并存状态——官方在推新东西,社区却在疯狂维护“老模拟层”。

2. 接口兼容的真相:开源社区是怎么“补位”的

很多人误以为“开源兼容”就是直接把 OpenAI 的 API 规范抄一遍。实际上,开源社区做的事更像是一种协议翻译。Chat Completions 是全世界用得最多的方言,但模型推理引擎各有各的脾气。兼容层的本质,是把各家的原生接口映射回这套“共同方言”。

我见过不少项目,核心业务依赖 OpenAI SDK,但底层可能跑的是 Ollama、vLLM、SGLang 甚至是一些国产模型的兼容网关。它们能做到“SDK 不变、请求体不变”,靠的就是那些默默无闻的兼容层。这些兼容层有的是官方实现,有的是社区用 FastAPI 现写的,质量参差不齐。

2.1 开源兼容层:一个 endpoint 映射的故事

以本地模型运行时 Ollama 为例。它在很早的版本里只提供自己的原生接口/api/chat,后来为了兼容 OpenAI 生态,专门加了一条路由/v1/chat/completions。我当时去翻它的源码,发现说白了就是一个适配器:把 OpenAI 的messages转成 Ollama 内部的messages,再把 Ollama 的生成结果包装成 OpenAI 风格的choices结构。这个转换并不复杂,但它解决了一个大痛点——前端用 openai SDK,只需改base_url指向http://localhost:11434/v1,代码不用动。

vLLM 也是类似的思路。它是高性能推理引擎,主打吞吐量,但它提供的 OpenAI 兼容服务器同样实现了/v1/chat/completions,甚至支持多轮对话、工具调用的一部分能力。虽然部分高级特性(比如结构化输出)做得不够全,但日常文本生成完全够用。

我自己的经验是,评估一个兼容层好不好,不要光看它支持多少接口,而要看你常用的那几个字段它能不能正确映射。比如logprobs、function_call、stream_options这些边缘功能,很多兼容层要么直接忽略,要么返回空值。生产环境踩到一个,排查起来非常费劲。

2.2 本地模型与 openai SDK 的“伪对齐”

这里要说一个坑:很多本地模型声称“兼容 OpenAI API”,但其实只是兼容了最基础的 Chat Completions 文本生成。一旦你用到tools、tool_choice、response_format,它们可能直接返回错误或者给出不符合规范的结果。

那些模型我没有点名批评的意思,这其实是个普遍现实。因为 OpenAI 的工具调用协议本身就是复杂契约,包括 JSON Schema 参数、严格模式、并行工具调用等。本地推理框架要做到完全对齐,需要同时实现函数匹配、参数约束解码和输出解析,难度不小。所以不少框架实际做的是“伪对齐”:它们能识别tools字段,但只返回一个你传进去的函数的名称,参数则靠模型自己瞎猜。

这导致了一个非常尴尬的局面:表面上是 OpenAI 格式,实际上是“半个 OpenAI 格式”。所以我在生产环境里,只要涉及工具调用,都会先做一轮兼容性冒烟测试,而不是信任“支持 OpenAI API”这种模糊的承诺。测试方法很简单:用同一个参数列表,分别请求 OpenAI 官方和本地服务,对比返回结构。不要求你的工具能完全跑通,但至少要保证结构能被 SDK 正确解析。

2.3 第三方网关与多模型适配的现实方案

除了本地推理引擎,还有一些独立部署的“API 网关”项目,比如 LiteLLM、Higress、One API 这类。它们做的事情更上层:把多家模型供应商的接口统一成 OpenAI 格式,再暴露给内部应用。这样公司内部可以一个 SDK 接所有模型,灵活调度,不被单一厂商绑死。

LiteLLM 是我用得比较多的。它支持几百种模型提供商,核心原理就是每个 provider 写一个 adapter,把 OpenAI 格式的请求翻译成各家 API 的原生格式,再把返回结果翻译回 OpenAI 结构。它的配置方式很直观,一个 config.yaml 就能定义模型列表、密钥、转发策略。

但也有要付出的代价。网关层加了适配,必然带来额外的延迟和故障点。我自己测过,走 LiteLLM 转发比直连官方 API 平均多 20-50ms,在大多数场景可以接受。不过一旦网关自身挂掉,所有模型的请求都会失败,所以生产环境要给它做高可用。另外,各家模型的参数并不完全一致,比如 Anthropic 的max_tokens和 OpenAI 的max_tokens语义相似,但有些模型对temperature支持并不好。网关在这种情况下的做法通常是把不支持的参数悄悄剥离,结果可能是你给某个模型传了temperature=0,但它根本没启用,输出随机性反而很大。

所以我的建议是:网关适合做模型路由和权限管控,但要谨慎依赖它的“高级功能映射”。真正关键的业务逻辑,尽量直接对接底层厂商 API,不要把命都交给网关。尤其是 Responses API 这种状态化接口,很多网关还没适配好,现阶段强行走网关反而得不偿失。

3. 实操:从代码迁移到 Responses API

聊完兼容,说点实操。我最近把一个简单客服系统从 Chat Completions 迁到了 Responses API,整个迁移过程比想象中顺利,但也有不少值得注意的细节。这里我把核心迁移步骤和代码示例放出来,给大家做参考。

先说一个总原则:不要把 Responses API 当成 Chat Completions 的“升级版”,而要用新接口的思路重写调用逻辑。否则你很可能只是换了个端点,却还在手动传历史消息,白白浪费previous_response_id的优势。

3.1 最小可运行的迁移示例

先说安装依赖。官方 SDK 从某个版本开始同时支持两个接口,我的环境是openai>=1.40.0,Python 3.10+。

旧代码大概是这样的:

from openai import OpenAI client = OpenAI(api_key="sk-...") response = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "你是客服助手。"}, {"role": "user", "content": "我的订单几天能到?"} ], temperature=0.3 ) print(response.choices[0].message.content)

迁移到 Responses API 后,变成:

from openai import OpenAI client = OpenAI(api_key="sk-...") response = client.responses.create( model="gpt-4o", input=[ {"role": "system", "content": "你是客服助手。"}, {"role": "user", "content": "我的订单几天能到?"} ], temperature=0.3 ) print(response.output_text)

看起来变化不大,对吧?但注意几个细节:messages变成了input,choices[0].message.content变成了output_text。如果你的代码里到处都在访问choices,迁移时就要全面修改。

对于简单问答场景,这个迁移只需要换字段名。但真正复杂的是流式响应。Responses API 的流式结构是:

stream = client.responses.create( model="gpt-4o", input=[{"role": "user", "content": "讲个笑话"}], stream=True ) for event in stream: if event.type == "response.output_text.delta": print(event.delta, end="")

这里的event.delta就是增量文本,跟 Chat Completions 里choices[0].delta.content完全不同。SDK 内部已经做了事件解析,你只要判断事件类型即可。如果是从旧代码迁移,强烈建议先写一个封装函数,把流和普通模式都包进去,避免业务代码被接口类型污染。

3.2 关键字段差异:messages 到 input、temperature 到 top_p 的坑

我想重点吐槽两个地方。

第一个是input的灵活性。Responses API 的input不像messages那样只能传带 role 的消息,它还可以传纯字符串、函数调用结果条目,甚至可以用previous_response_id串上下文。这大大简化了 agent 状态管理。但问题是,很多框架的输入结构还是按messages来的,你不能无脑搬运。比如你之前有messages数组包含了assistant角色带tool_calls的消息,在 Responses API 里你需要把这种交互改写成多个独立的function_call和messageitem,格式不对会直接 400。

第二个是 temperature 和 top_p 的语义差别。虽然两个接口都有这两个参数,但 Responses API 对它们的处理更严格。OpenAI 官方建议在同一个请求里不要同时修改 temperature 和 top_p,否则会报错或不生效。这一点在 Chat Completions 时代其实是“不报错但行为不可预期”,到了 Responses API 变成了“直接告诉你别这么干”。我一开始没注意,同时传了两个参数,结果某个模型直接返回参数冲突错误。这个坑算是白送的教训。

另外,Responses API 的temperature默认值跟 Chat Completions 不完全一致。在部分模型上,默认值从 1.0 变成了 0.8 之类的,导致我迁移后感觉输出风格有细微变化。所以如果希望保持一致,最好显式传递原来的参数值。

3.3 踩坑实录:unexpected endpoint or method 的错误排查

这个错误估计不少人都见过:请求发出去,服务端返回一条 JSON 错误,提示[error] unexpected endpoint or method. (post /chat/completions)。我第一次遇到时一脸懵,因为我的代码明明是标准 Chat Completions 调用。

后来定位才发现,问题并不在我的代码,而在中转服务或兼容层。这种错误通常出现在以下几种情况:

  • 你配置的base_url指向了某个不支持/chat/completions路径的服务,但它依然接收了请求,路由没匹配上,于是抛出这个错误。
  • 你的 SDK 版本太新,默认请求路径发生了变化,某些网关还在用老路径。
  • 你用了 API 密钥配置工具,实际请求被转发到了另一个 provider,而那个 provider 没有实现该 endpoint。

排查步骤我建议按顺序做:

  1. 打印实际请求 URL 和 payload,确认base_url、path、method是否符合预期。
  2. 直接用 curl 发一个最小请求,绕过 SDK,看服务端返回什么。
  3. 检查你用的 SDK 版本,看它是否把请求发到了其他端点(比如 Responses API 的路径被硬编码)。
  4. 如果走了网关,查看网关日志,看它到底把请求转给了哪个上游服务。

我的一个实际案例是:公司内部网关把 OpenAI 的 SDK 请求转发到本地 vLLM,但网关配置里 vendor 类型写错了,把 vLLM 识别成了另一种模型服务,导致路径映射失败。改完 vendor 配置就好了。这个错误本身并不难,但它提醒我:兼容层的排查,要从链路视角看,不能只盯着应用代码。

4. 常见问题与排查技巧

这一部分把我在实际迁移和日常使用中遇到的典型问题整理成了一份速查表。很多问题看似是代码问题,其实根因在接口规范理解不到位,或者兼容层配置不对。

4.1 错误速查表

错误信息/现象可能原因解决方案
unexpected endpoint or method. (post /chat/completions)兼容层/gateway 不支持该路径,或 vendor 类型配置错误检查网关路由配置、SDK base_url、使用 curl 直连验证
invalid_request_error: 'messages' is a required property向 Responses API 请求时传了旧格式 payload确认 endpoint 是/chat/completions还是/responses,切换对应的请求体
Unsupported value: 'messages'SDK 把旧接口请求体发到了新接口更新 SDK 到最新版,并按 Responses API 格式重写
ResponseError: parameter 'temperature' conflicts with 'top_p'Responses 接口拒绝同时调整两个采样参数只保留其中一个,或显式设为相同值
流式输出没有内容监听的事件类型不对确认事件名是否为response.output_text.delta
本地模型走 OpenAI SDK 时 tool_calls 解析失败兼容层未完整实现工具调用协议降级为文本模式,或换用支持工具调用的推理框架
多轮对话上下文丢失使用了 Responses API 但忘记传previous_response_id改为传递该字段,合并回最近一次响应 ID

除此之外,还有一个常见问题:模型返回 400 但错误信息是空对象。这种情况多半出在请求里带了非法字段,比如把 Chat Completions 的function_call参数原样搬到 Responses API。后者不再使用这个字段,而是把它拆成了tools下的tool_choice。排查时可以把请求体里的未知字段全部去掉,逐项加上,很快就能定位。

4.2 开源兼容层的正确打开方式

给准备用开源兼容层的朋友三个建议。

第一,明确你的需求边界。如果只是文本生成、简单对话,任何兼容层都够用。但如果你要 tool calling、结构化输出、JSON mode,就必须先确认兼容层对这些能力的支持程度。很多项目 README 里写“In development”,翻译过来就是“别指望”。

第二,搭建一个兼容性测试矩阵。我第一次接本地模型跑生产时,写了一个约 20 个请求的自动化测试脚本,覆盖纯文本、流式、多轮、工具调用、超长上下文、错误参数等场景。每次升级兼容层或换模型,先跑一遍脚本再上线。这个习惯帮我避开了很多线上事故。

第三,不要依赖兼容层去“压平”所有差异。比如某些模型不支持system角色消息,兼容层可能会把它偷偷拼到第一条 user 消息里,结果指令遵循效果变差。这种差异,最好在应用层做适配,而不是指望兼容层做魔法。

4.3 个人经验:什么时候该追新、什么时候该守住老接口

最后聊聊我的个人选择策略。我现在维护的项目里,大概分成三类:

第一类是稳定运行的老业务,上下文逻辑都在客户端,历史 messages 自己管理。这类项目我不建议迁到 Responses API,因为迁移收益很小,还要付出改造和测试成本。守住 Chat Completions 没有任何问题,OpenAI 短期内不会砍掉它。

第二类是新建的 agent 项目,需要多轮工具调用、状态管理、记忆能力。这类项目直接用 Responses API,因为官方正在把全部新能力都堆在这里,继续用老接口等于绕远路。

第三类是深度依赖开源本地模型的项目。这类项目还是要以兼容层为准,如果你用的推理框架对 Responses API 支持不到位,那响应式接口连用都用不了。现实是 vLLM 这类项目目前主要精力还在完善 Chat Completions 兼容上,Responses 的完整实现仍然需要时间。

说到底,接口规范背后的演进和兼容,不只是一个技术话题,更是一个生态博弈的问题。OpenAI 想确立新标准,开源社区想保留通用协议,两边拉扯过程中,真正受益的是我们这些开发者——你有选择权,可以站在新接口的台阶上看未来,也可以守在老接口的舒适区里看热闹。

我个人目前的选择是:生产环境老业务继续跑 Chat Completions,新 agent 项目用 Responses API 小步试点,本地实验模型则继续走兼容层。等 Responses API 的工具链完善了,再逐步把老业务迁过去也不迟。技术选型这件事,最忌盲目追新,也最忌固步自封。了解清楚演进方向,再决定自己走哪条路,这才是最稳妥的做法。

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

12V锂电池保护板原理与DW01+8205A实战设计

1. 为什么12V锂电池组必须配保护板——不是“可有可无”,而是“生死线”你手头那块标称12V、实际由3串或4串锂电芯组成的电池组,很可能正处在一场无声的慢性崩溃中。我见过太多人把12V锂电池当铅酸电池用:直接接上12V灯带、12V风扇、12V车载冰…

作者头像 李华
网站建设 2026/11/4 20:08:31

昇腾310P NPU转码实战:FFmpeg硬件加速性能提升10倍

1. 为什么我要折腾NPU转码这件事做视频处理这行的朋友应该都有体会,FFmpeg 是绕不开的基础设施。不管是做点播平台的转码流水线,还是本地批量压缩素材,甚至是给监控系统做实时流处理,FFmpeg 几乎无处不在。但问题也很明显&#xf…

作者头像 李华
网站建设 2026/10/28 13:53:50

PHP运行机制解析:从SAPI到CLI与FPM的完整生命周期

1. PHP一次请求的完整生命周期:从模块初始化到脚本结束1.1 五种生命周期阶段详解很多PHPer写了好几年代码,问起"PHP是怎么跑起来的",第一反应是"浏览器发请求,服务器执行PHP,返回HTML"。这个说法没…

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

F28377D性能优化:TMU硬件加速三角函数与RAM执行提升控制中断余量

最近在TMS320F28377D上做并网逆变器的控制算法,主控中断周期定在50微秒,里面要过三相锁相环、park变换、反park变换以及SVPWM的角度计算。这些环节看起来不复杂,但三角函数调用次数一多,中断就明显顶不住。一开始我以为是算法流程…

作者头像 李华
网站建设 2026/10/31 2:13:51

1/2馈线制作与连接:阻抗匹配、屏蔽完整性与连续性三大核心

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 2:03:17

RK3588平台RKMPP硬解H264与RGA转BGRA8888完整实践指南

最近在RK3588平台上做视频接入相关的开发,核心任务是把摄像头推上来的H264码流实时解码成BGRA8888格式,供上层算法框架和显示模块消费。整套链路最开始我直接用FFmpeg软解,结果在1080p60多路接入的场景下CPU被抓得干干净净,后来切…

作者头像 李华