博客

Chat Completions 还是 Responses:为你的兼容端点选择它真正支持的 API 形态

迁移指南把差异归结为三处变化:端点、如何读取输出,以及如何携带状态。当你的端点声称兼容时,这些差异各自意味着什么。

Chat Completions 还是 Responses:为你的兼容端点选择它真正支持的 API 形态

同一批模型有两种 API 形态在流通,而对调用兼容端点的团队来说,有意思的问题不是纸面上哪个更胜一筹。而是你的端点实际实现了每种形态的哪些部分。官方的迁移指南把差异压缩成三处相关变化:把请求发到不同的端点、从不同的结构读取输出、选择应用如何在轮次之间携带状态。其余的一切都从这三处推出,而厂商落地页上的“compatible”并不能告诉你其中任何一处是否被忠实实现。

指南自己框定的三处变化

指南的总结值得引用,因为它界定了整个迁移的范围:

把迁移视为三处相关变化:把请求发到/v1/responses,从类型化的output数组读取输出,并选择你的应用如何在不同轮次之间携带状态。

第一处变化是机械性的。第二处是大多数代码崩掉的地方,因为这两种响应对象并不是同一主题的变体:在 Chat Completions 里你会收到一个choices数组,每个包含一个message,而 Responses API 返回一个类型化的response对象,带自己的id和一个由 Item 组成的output数组。有一个便捷辅助函数output_text,在只需要文本时用它抽取文本。

逐元素来看这些差异

迁移指南直接列出了差异:Chat Completions 对 Responses,下面不加评判地总结,构建时应以当时的官方页面为准:

| 关注点 | Chat Completions | Responses |
| --- | --- | --- |
| 端点 | POST /v1/chat/completions | POST /v1/responses |
| 输出 | choices,每个包含一个message | 类型化的 response对象,带有outputItems,外加一个output_text辅助函数 |
| 多次生成 | n把并行生成作为choices返回 | 没有n;每个请求只生成一份,需要候选就发多个请求 |
| 系统指引 | 消息列表里的一个 system消息 | 顶层的instructions,与input分开 |
| 输入 | 消息列表 | 一个字符串或一个消息列表 |
| 轮次之间的状态 | 你保存对话记录并重发 messages | previous_response_id、之前的输出 Items,或使用 Conversations API |
| 结构化输出 | response_format | text.format |
| 工具调用 | function 配置和返回的调用是同一种形态 | 请求配置与回传的调用都不同 |

有两行值得展开。关于状态,指南指出,store: true在轮次之间维护状态,同时保留 reasoning 和工具上下文,并且两个 API 的存储默认值不同,store: false可以在任一边把它关掉。关于多次生成,指南说得很明确:Chat Completions 可以返回多份并行生成,作为choices,使用n参数,而 Responses API 省略了这个参数,只返回一份生成。如果你的产品让用户在一次调用中从候选草稿里挑选,那这是对形态选择的约束,而不是细节。

一处限制,精确引用

有一个 tool calling 的限制在转述中常被扭曲,所以这里按迁移指南的原话给出:

从 GPT-5.4 起,Chat Completions 不支持使用reasoning_effort之外的取值none进行 tool calling。

按字面读这句话:它是一个关于 Chat Completions 从某一代模型开始的陈述,关于 tool calling 与非nonereasoning effort 组合的陈述。它不是“到处都不支持 tool calling”的断言,不是关于每个模型的断言,也根本不是关于兼容端点的断言。如果你的集成同时使用工具和 reasoning effort,这句话决定了你在 OpenAI 这一侧能用哪种形态,值得在围绕它设计之前查看当前页面。

兼容端点实际实现了什么

兼容端点通常先实现 Chat Completions,而覆盖 Responses 形态的程度各不相同,这就是为什么唯一可靠的答案来自测试,而不是兼容性对照表。方法并不光鲜:构造一个真实的 payload,覆盖你依赖的功能,包括一个 tool 定义和一个结构化输出字段,然后用两种形态各发一次。然后检查三件事。

第一,请求是否被接受。端点路径上的 404、字段名上的校验错误,或参数被静默丢弃,对你的规划而言含义相同,尽管只有其中一部分会明确报出来。

第二,响应结构是否与官方一致。字段名、数组嵌套,以及流式场景下的事件名。一个把文本放在与文档不同的键下的形态,就是会让你在最糟糕的时刻花一周调试的形态。

第三,错误是否可读。在每种形态里故意发一个无效字段,看错误是指出了这个字段,还是塌缩成一个笼统的失败。这就是五分钟修好和花一下午二分定位的区别。

本站对兼容集成普遍采用的那套清单纪律,在集成清单里。并且要从真正会调用端点的那个环境跑测试:在公司代理或 TLS 检查层之后,路径重写和响应头处理会改变你实际在跟谁说话,正如企业网络指南针对一类相关问题所描述的那样。

按约束选,而不是按偏好选

诚实的选法是列出你的产品真正需要什么,再看哪种形态能提供:

  • 需要一次调用拿到多个候选?那就是n参数,按指南它存在于 Chat Completions,而不在 Responses。留在 Chat Completions,或者改动产品交互。
  • 需要配合previous_response_id的托管状态?那就是 Responses。但要把指南的提醒一起带上:previous_response_id不会带上上一个响应的顶层instructions,所以稳定的 instructions 必须在每次请求时重发。
  • 需要text.format结构化输出如文档所述?那就是 Responses;Chat Completions 的字段是response_format。两个字段名不能互换,结构化输出指南覆盖了不同兼容厂商之间形态的差异。
  • 需要在端点之间最大可移植性?使用两种形态都支持的子集,并自己管理状态,而不是依赖任一 API 的记忆。

有一条能拯救代码库的规则:不要在同一个客户端里混用两种形态的字段名。response_format发给 Responses,或text.format发给 Chat Completions,不是聪明的兼容技巧;而是一个在你碰到的每个端点上表现都不同的 bug。

迁移,三步

每一步都有一个验收点,而验收点正是防止半迁移客户端上线的关键。

第一步:端点。把路径改成/v1/responses并拿到响应对象。验收标准:一个纯文本请求返回类型化响应,带有id,并且output_text包含预期文本。

第二步:如何读取输出。把choices[0].message访问替换为逐项读取output数组,或在纯文本场景下用output_text。验收标准:你的抽取函数对纯文本产出与之前相同的最终文本,并在涉及工具或 reasoning 时正确识别非文本项。工具调用是最锋利的一角:请求侧的 function 配置和响应侧返回的调用在两个 API 之间形态都不同,function calling 指南端到端走了一遍;运行工具循环时生产侧的纪律在tool calling 指南里。

第三步:状态。明确决定:自己重发之前的 items、使用previous_response_id并重发 instructions,还是用持久的 conversation 对象。验收标准:多轮对话产生的后续行为与你的 Chat Completions 基线一致,包括进程重启之后。如果你做流式响应,同时要对齐事件名;事件序列的差异在streaming 指南里。

常见失败

这些在匆忙迁移的集成里反复出现:

  1. 把response_format发给 Responses 形态的端点,然后断定该端点不支持结构化输出。
  2. 仍然在choices的响应对象里读取outputitems。
  3. 假设n仍然有效,结果产品期望多份生成时却静默只拿到一份。
  4. 把previous_response_id当成“API 记住了一切”,包括它并不会带过来的 instructions。
  5. 假设两种形态在其他方面完全相同,然后在生产里发现工具调用的差异。
  6. 把“文档说支持这个”读成“我的端点支持这个”。文档描述的是官方 API;你的端点是另一份实现。

一条工程提醒

API 形态不是网络细节。它会渗进你的类型定义、日志字段名、测试夹具和值班手册。在写客户端之前就定下形态,记录你依赖它的哪些部分,当第二个端点进入画面时,重跑一次 payload 测试,而不是假设第一个答案依然成立。

清单

  1. 用真实 payload 确认你的端点接受哪些形态,而不是靠兼容性声明。
  2. 每种形态固定一套类型和字段名,不要混用。
  3. 对齐结构化输出字段:response_format或text.format,绝不在同一个客户端里两者都用。
  4. 对齐工具定义与工具调用的形态,请求侧和响应侧都要。
  5. 选定状态策略,并写下它不会带过来什么。
  6. 如果你做流式,对齐 streaming 事件名。
  7. 把每个真实 payload 用两种形态各发一次,并把结果留作集成测试。
  8. 从生产环境里真正会调用端点的那个环境跑测试。

这些都不会让任一形态成为普遍正确的答案;它让你的选择成为关于你的端点的答案。本站所记录平台在端点层面的具体细节在其文档里,并且这里引用的每个字段名、参数和事件名,都应在构建时以当时的官方页面核对。