Responses API 与 Chat Completions:应该基于哪个构建
OpenAI Responses API 与 Chat Completions 的实战指南:请求结构、输出项、有状态性、推理、工具,以及何时该选择哪个端点。
如果你今天要开始一个全新的集成,请基于 Responses API(POST /v1/responses)构建,把 Chat Completions(POST /v1/chat/completions)留给那些已经存在且运行正常的代码。这两个端点解决的是同一个核心问题——发送一段提示词,得到一段模型回复——但它们对会话的建模方式不同。Chat Completions 把一轮对话视为扁平的 messages 列表,返回一个 choices 数组。Responses 则把一轮对话视为带类型的 input 项列表,返回一个带类型的 output 数组,其中可以并列容纳消息、推理和工具调用。这种结构上的差异就是全部要点,它决定了哪个端点更契合你的工作负载。
本文将从请求结构、输出结构、状态、推理和工具这几个方面对两者进行比较,然后给出一条无需猜测即可套用的决策规则。它不会宣布任何东西被弃用:Chat Completions 仍受支持,而且在各家提供商之间它仍是可移植性最强的形态。
请求结构:messages 与 input
Chat Completions 期望一个 messages 数组,其中每个元素都有一个 role(system、user、assistant、tool)和 content。一轮对话的一切——指令、历史、工具结果——都被粘合进这一个列表里。
from openai import OpenAI
client = OpenAI(base_url="https://your-gateway.example.com/v1", api_key="AVE_GATEWAY_API_KEY")
completion = client.chat.completions.create(
model="gpt-5.6",
messages=[
{"role": "system", "content": "You are a terse assistant."},
{"role": "user", "content": "Name three uses for an API gateway."},
],
)
print(completion.choices[0].message.content)
Responses 把这些关注点拆分开来。顶层的引导性内容移到 instructions,而本轮对话的内容移到 input,后者既接受纯字符串,也接受一个带类型的项数组。
response = client.responses.create(
model="gpt-5.6",
instructions="You are a terse assistant.",
input="Name three uses for an API gateway.",
)
print(response.output_text)
有两点值得立刻留意。第一,response.output_text 是一个便捷访问器,它会替你把文本输出拼接起来,取代 completion.choices[0].message.content 那条路径。第二,instructions 在串联的多轮之间并不会保持粘性——如果你用 previous_response_id 继续一段会话,前一次响应的 instructions 不会被带过来,因此每一轮都要重新发送它们。
输出结构:choices 与 output 项
Chat Completions 返回一个 choices 数组。借助 n 参数,你可以请求多个候选补全,每个都是一个独立的 choice。Responses 移除了 n,只返回单次生成,但这次生成是一个由 项 组成的 output 数组,每一项都有自己的 type。模型 返回 的项包括:
message—— 助手文本(以及任何拒绝回答)reasoning—— 推理摘要或加密的推理 tokenfunction_call—— 模型希望你去执行的工具调用
注意缺席的是什么:function_call_output 不是模型返回的项。它是一个 输入 项,由你构造并回传,用来把工具结果交给模型,以 call_id 关联。把这两个列表区分清楚——你读取的是输出项,你写入的是输入项——能避免一个常见的困惑来源,因为调用与它的结果分处这次交换的两端。
把每个 output 条目都当作消息来处理,是最常见的迁移错误。推理模型可能会在 message 之前发出一个 reasoning 项,而使用工具的一轮可能会发出一个 function_call,其中根本没有对用户可见的文本。请按 type 遍历 output,而不要假定索引 0 就是你的答案:
for item in response.output:
if item.type == "message":
print(item.content[0].text)
elif item.type == "function_call":
print("tool wanted:", item.name, item.arguments)
用量统计的用词也变了。Chat Completions 报告 prompt_tokens 和 completion_tokens;Responses 在 usage 对象上报告 input_tokens 和 output_tokens。任何解析 token 计数的仪表盘都需要处理这两种形态,因此一个把它们归一化为单一统计视图的网关,能让每一个客户端都省去维护两条代码路径的负担。
状态:客户端管理历史 与 服务端存储线程
使用 Chat Completions 时,会话归你所有:你在每次调用时都重发完整的 messages 历史,请求本身就携带了模型所需的一切。这种客户端管理的形态易于推理,也易于经过任何代理进行路由,这在很大程度上正是它成为行业通用语的原因。至于 OpenAI 是否 保留 这次交换的副本,则是与端点无关的另一个问题——它由 store 设置和你的账户默认值(可以为了记录日志而开启存储)决定,而不是因为 Chat Completions 本身就是“无状态”的。如果你必须确保上游不留存任何内容,就显式发送 store: false,而不要想当然地以为该端点什么都不保留。
Responses 默认存储。每次调用都会返回一个 id,你可以在下一次请求中传入 previous_response_id 来继续这条线程,而无需重发历史。你有三种方式来管理状态:
- 用
previous_response_id串联到一次已存储的响应, - 手动把先前的
output项回传到input中, - 用 Conversations API 处理更长生命周期的线程。
存储是一种便利,而非强制。设置 store: false 即可退出,这正是零数据留存工作流所需要的。有一个计费细节在做规划时很重要:即便你用 previous_response_id 串联,链条中所有先前的输入 token 都会按输入 token 计费。串联省去的是在网络上重发字节,它并不会让更早的上下文变得免费。
推理:Responses 拉开差距之处
偏好 Responses 最明确的理由是推理模型。由于推理是一等的 output 项类型,API 能够跨轮保留推理状态,而不是在两次调用之间将其丢弃。对于无状态的设置,你可以通过同时发送 store: false 和 include: ["reasoning.encrypted_content"] 来请求加密推理;API 会返回加密的推理 token,你在下一轮把它们回传,内容在内存中解密后随即丢弃。Chat Completions 没有等价的推理类型槽位,因此推理模型会在两轮之间失去这份连续性。
如果你的产品依赖多步推理、由推理模型驱动的工具循环,或长时间的智能体运行,那么 Responses 正是为此设计的端点。要更深入地了解其中涉及的参数,参见推理模型 API 参数。
工具与结构化输出
两个端点都支持函数调用和结构化输出,但字段名不同:
| 关注点 | Chat Completions | Responses |
|---|---|---|
| 端点 | /v1/chat/completions | /v1/responses |
| 轮次内容 | messages[] | input(字符串或项) |
| 系统提示词 | system message | instructions |
| 结果文本 | choices[0].message.content | output_text |
| 结构化输出 | response_format | text.format |
| 工具结果关联 | tool 消息上的 tool_call_id | 带 call_id 的 function_call_output |
| 多个候选 | n | 已移除(单次生成) |
| 原生工具 | 手动接线 | tools: [{ "type": "web_search" }] |
text.format 与 response_format 的替换很容易被忽略,一旦发错就会产生令人困惑的错误。对于结构化输出(Structured Outputs),请在 schema 上显式设置 strict: true,以便模型被约束到它上面。(另有一种省略 strict 的回退方式——先尝试严格强制,遇到不兼容的 schema 时降级为非严格——它适用于 Responses 的 函数/工具 定义,而不适用于 text.format 结构化输出;不要把两者混为一谈。)如果你依赖 JSON Schema 输出,在移植校验代码之前请先阅读使用 JSON Schema 的结构化输出。
工具循环在实践中有何不同
工具调用的往返,是两种形态感受差异最大的地方,因此值得并排看一看。在 Chat Completions 中,模型返回一条助手消息,你执行其中的 tool_calls,然后追加一条 role: "tool" 且带匹配 tool_call_id 的消息:
# Chat Completions: append a tool result as a message
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
在 Responses 中,模型发出一个 function_call 项,你把结果作为一个由 call_id 关联的 function_call_output 项回传——而不是作为消息:
# Responses: append a typed function_call_output item
client.responses.create(
model="gpt-5.6",
previous_response_id=response.id,
input=[{
"type": "function_call_output",
"call_id": call.call_id,
"output": result,
}],
)
在 Responses 中该关联键叫 call_id,在 Chat Completions 中叫 tool_call_id,发送结果时缺少匹配的 id 是一个常见的错误来源。由于 Responses 把调用与其结果保留为两个各自独立的类型项,智能体循环读起来更清晰:你再也不必从一条助手消息内部去拆解工具意图。跨提供商的可移植工具定义仍需要它自己的兼容层。
一条可套用的决策规则
你不必为此纠结。用这张表:
| 情形 | 端点 |
|---|---|
| 全新集成,尤其是智能体或推理模型 | Responses |
| 已有、运行正常且没有新需求的 Chat Completions 代码 | 保留 Chat Completions |
| 你今天需要在众多提供商之间获得最大可移植性 | Chat Completions(被广泛镜像的形态) |
| 你想要服务端管理的会话状态或原生工具 | Responses |
| 你想要零留存下的加密推理连续性 | 带 store: false + 加密推理的 Responses |
网关会让这个选择在两边都更划算。在同一个 base URL 之后,你可以把 Chat Completions 流量和 Responses 流量都路由到相同的上游,一次性注册好提供商,并在公开的定价页上发布每个模型支持什么、花费多少。如果之后你要迁移某个端点,客户端的改动很小,而路由保持原样。逐步的迁移路径见将 Chat Completions 迁移到 Responses API。
两者都支持流式,只是事件不同
两个端点都通过服务器发送事件(SSE)进行流式传输,但事件词汇不同。Chat Completions 以 chat.completion.chunk 对象流式输出,其中带 choices[].delta,并以一个字面量 data: [DONE] 结束。Responses 流式输出 语义化事件,例如 response.created、response.output_text.delta 和 response.completed,它们更易于路由,因为每个事件都表明了自己是什么。如果你要构建逐 token 的 UI,使用 SSE 流式传输大模型响应会带你走完解析两种格式的过程。
常见问题
既然有了 Responses,Chat Completions 现在被弃用了吗?
没有。Chat Completions 仍是受支持的端点,而且在非 OpenAI 提供商之间它仍是被镜像得最广的请求形态。被 OpenAI 标记为日落的是 Assistants API,而不是 Chat Completions。把新的智能体工作构建在 Responses 上,是因为它有结构化的输出项和推理支持,而不是因为 Chat Completions 要消失了。
我能像 Chat Completions 里的 n 那样,从 Responses API 得到多个候选答案吗?
不能通过单个 n 参数——Responses 每次调用只返回一次生成。如果你需要多个候选,就发送多次请求。如果你的 Chat Completions 代码依赖 n > 1,这是一处需要留意的真实行为变化。
previous_response_id 会降低我的 token 账单吗?
它减少的是网络上的字节,因为你不再重发历史,但它不会减少计费的输入 token:链条中所有先前的输入 token 都按输入 token 计费。请把串联当作一种人体工学与延迟特性,而不是成本优化。若要真正削减上下文成本,请看跨 OpenAI、Claude 和 Gemini 的提示词缓存。
多提供商的架构应该统一到哪个端点上?
如果你要路由到许多提供商,Chat Completions 今天仍是可移植性最强的最大公约数,而 Responses 是更丰富的 OpenAI 原生表面。网关能让你两者都保留:对外暴露每个客户端已经在说的形态,并按渠道逐一翻译。
用一个工作流同时跑两者
挑一个低风险的工作流,让它经过你的网关跑两遍——一次作为 Chat Completions 调用,一次作为 Responses 调用——然后在网关的用量日志中比较 output/choices 形态、token 统计和延迟。这一次并排对比,比任何规范摘要都更能告诉你哪个端点契合你的代码。