Responses API 迁移 OpenAI API Chat Completions

将 Chat Completions 迁移到 Responses API

把 OpenAI Chat Completions 代码移植到 Responses API 的分步指南:输入项、输出解析、状态、工具、流式传输,以及一套回滚方案。

AveMujica API 2 分钟阅读

从 Chat Completions 迁移到 Responses API,大体上是一项改名与结构调整的工作:POST /v1/chat/completions 变成 POST /v1/responses,messages 数组变成 input,系统提示词变成 instructions,而你读取的是 response.output_text 而不是 completion.choices[0].message.content。真正让人栽跟头的部分,是那些改变了行为而非仅仅改了名字的地方——输出项、有状态性、工具关联,以及结构化输出的字段。本指南按你实际应当动手的顺序走完迁移,并附带一套回滚方案,这样你就可以一次迁移一个端点,而不是一次性把一切重写。

你并非必须迁移。Chat Completions 仍受支持。当你想要推理连续性、服务端管理的会话状态、原生工具,或智能体循环能从中受益的更干净的类型化输出时,再迁移。如果你还在权衡,请先阅读 Responses API 与 Chat Completions。

第 0 步:建立一个可用于对比的基线

在动代码之前,先记录下当前端点上“正确”是什么样子。通过 Chat Completions 发送几条有代表性的提示词,并记录响应文本、token 用量和延迟。让它们经过你的网关,这样这些轨迹就会落到它的用量日志里。你之后会用相同的提示词去跑 Responses 并做比较。省掉这一步的迁移,一旦输出出现差异就会立刻沦为猜谜。

第 1 步:给端点改名并搬移系统提示词

最小的可用迁移只需三处编辑。把 SDK 指向 Responses 方法,把任何 system 消息搬进 instructions,并把单个用户轮次折叠进 input。

之前:

completion = client.chat.completions.create(
    model="gpt-5.6",
    messages=[
        {"role": "system", "content": "You are a release-notes summarizer."},
        {"role": "user", "content": "Summarize the changelog below:\n" + changelog},
    ],
)
text = completion.choices[0].message.content

之后:

response = client.responses.create(
    model="gpt-5.6",
    instructions="You are a release-notes summarizer.",
    input="Summarize the changelog below:\n" + changelog,
)
text = response.output_text

input 既接受纯字符串(如上),也接受一个带类型的项数组——当你需要多轮历史或工具结果时。先用字符串形态跑出一个通过的测试,然后只在你需要结构的地方去重塑它。

第 2 步:解析输出项,而不是 choices[0]

这是改变行为的一步。Chat Completions 给你的是 choices,索引 0 几乎总是你的答案。Responses 返回的是一个由带类型 项 组成的 output 数组——message、reasoning 和 function_call——而答案并不保证排在第一。推理模型可以在 message 之前发出一个 reasoning 项,而使用工具的一轮可以发出一个根本没有文本的 function_call。(你用来回传工具结果的 function_call_output 是一个由你构造的 输入 项,而不是这些被返回的项之一——把两者分清楚。)

便捷访问器 response.output_text 会处理常见的“只要给我文本”的情况。当你需要更多控制时,按 type 遍历:

answer, tool_calls = None, []
for item in response.output:
    if item.type == "message":
        answer = item.content[0].text
    elif item.type == "function_call":
        tool_calls.append(item)

同时更新你的用量统计:prompt_tokens/completion_tokens 变成 usage 对象上的 input_tokens/output_tokens。任何读取 token 计数的计费或仪表盘代码,在分阶段上线期间都必须同时处理这两组字段名。

第 3 步:决定如何管理会话状态

Chat Completions 是无状态的——你每次调用都重发完整历史。Responses 默认存储,并返回一个可供你串联的 id。从三种策略中选一种,并始终如一地应用:

策略做法何时使用
previous_response_id在下一次调用时发送先前响应的 id你想让服务端持有历史
手动传项把先前的 output 项回传到 input你想完全控制重发什么
无状态store: false 并自行重发历史零留存,或你已经自己管理着一份存储

有两个事实需要在规划时考虑。第一,instructions 不会跨 previous_response_id 携带,因此每一轮都要重发。第二,串联不会降低你的账单:链条中所有先前的输入 token 都按输入 token 计费。串联省下的是带宽和样板代码,而不是钱。若要真正降低上下文成本,请把迁移与提示词缓存结合起来。

第 4 步:移植工具调用与结构化输出

函数调用在迁移中得以保留,但接线方式变了。在 Chat Completions 中,你追加一条带 tool_call_id 的 role: "tool" 消息。在 Responses 中,你发送一个由 call_id 关联的 function_call_output 项:

client.responses.create(
    model="gpt-5.6",
    previous_response_id=response.id,
    input=[{
        "type": "function_call_output",
        "call_id": call.call_id,
        "output": tool_result,
    }],
)

发送结果时缺少匹配的 call_id,是最常见的迁移错误之一,因此要在测试中对它加断言。结构化输出也要搬迁:response_format 变成 text.format,并且你要在 schema 上显式设置 strict: true——请验证你的 JSON Schema 在严格子集下仍能通过校验。(不要把工具定义里那种省略 strict、降级为非严格的行为搬过来;对于结构化输出,要显式。)细节见使用 JSON Schema 的结构化输出,可移植的定义见跨提供商的可移植工具调用。

第 5 步:更新流式的消费方

如果你做流式,传输层仍是服务器发送事件(SSE),但事件变了。Chat Completions 以带 choices[].delta 的 chat.completion.chunk 对象流式输出,并以 data: [DONE] 结束。Responses 流式输出语义化事件,例如 response.created、response.output_text.delta 和 response.completed。语义化事件更易于路由,因为每个都表明了自己是什么,但你现有的 [DONE]-与-delta 解析器在不做改动的情况下无法理解它们。两者的解析模式都在使用 SSE 流式传输大模型响应中。

第 6 步:在开关之后灰度上线,保留回滚

不要一次性把所有调用切过去。把端点选择放在一个功能开关之后,或在你的网关处按模型逐一路由,把上游注册在它后面,然后把一小片流量发给 Responses。用第 0 步捕获的基线来对比新的轨迹:答案在容差内一致、token 计数合理、延迟可接受。如果出现回退,回滚就是把开关切回 Chat Completions——而不是重新部署——因为客户端的差异被隔离住了。当这一片看起来健康时,再扩大它。

迁移不会改变什么

知道哪里可以放松是有帮助的。有几样东西原封不动地保留下来,所以你不应该去重写它们:

  • 鉴权。 同一个 API 密钥和 Authorization: Bearer 头照常工作;只有路径改变。
  • 模型标识符。 像 gpt-5.6 这样的模型在两个端点上叫法相同——你改的是请求信封,而不是模型。
  • 提示词内容。 你实际的指令和用户文本是相同的;它们只是从 messages 条目搬进了 instructions 和 input。
  • 温度和采样旋钮。 常见的生成参数保留其名称和含义。
  • 流式的传输层。 两个端点都通过服务器发送事件流式传输,所以你的 SSE 读取器保持不变;只有它解析的事件形态不同。

明白这一点能让迁移的范围可控。真正发生改变的行为面很小:输出结构、状态管理、工具结果字段,以及结构化输出的配置。其余一切都是机械式的搬移。当你估算工作量时,为这四块留出预算,把其余部分当作复制粘贴——这正是为什么按端点逐一上线是现实可行的,而不是一次性推倒重来。

读懂常见的迁移错误

大多数迁移 bug 会产生几种可辨认的症状。空的或缺失的文本,通常意味着你读了 output[0] 却落到了一个 reasoning 或 function_call 项上,而不是 message——请按 type 遍历或使用 output_text。结构化输出上与 schema 相关的 400,通常意味着你把 response_format 留在了原处,而没有切换到 text.format。停滞或报错的工具循环,通常意味着某个 function_call_output 发出去时没有匹配的 call_id。而一段会话在多轮之间“忘记”了自己的角色或语气,通常意味着你依赖了 instructions 会透过 previous_response_id 保持——它并不会,所以要重发它们。把这份简短清单放在你的 diff 旁边,能把令人困惑的失败变成快速的查表。

迁移清单

  • 端点已从 /v1/chat/completions 改为 /v1/responses。
  • system 消息已搬到 instructions,并在每个串联轮次重发。
  • 输出通过 output_text 读取,或按 type 遍历 output 项读取。
  • token 统计已处理 input_tokens/output_tokens。
  • 已选定状态策略:previous_response_id、手动传项,或 store: false。
  • 工具结果作为带匹配 call_id 的 function_call_output 发送。
  • response_format 已替换为 text.format;schema 已重新校验。
  • 流式消费方已针对语义化事件更新。
  • 上线由开关把控,并带一键回滚。

常见问题

我必须在某个截止日期前迁移吗?

Chat Completions 没有被排入日落计划,所以它并没有强制的截止日期。OpenAI 标记为弃用的端点是 Assistants API(于 2025-08-26 弃用,日落定于 2026-08-26),那是另一个不同的表面。迁移到 Responses 是为了它的能力,而不是出于紧迫感。

迁移后我的助手有时返回空文本。为什么?

你几乎可以肯定是读了 output[0],却撞上了一个 reasoning 或 function_call 项而不是 message。请按 type 遍历 output,或使用 response.output_text,它会替你跳过非文本项。

我能继续重发完整历史,而不用 previous_response_id 吗?

可以。设置 store: false,并把你的历史当作 input 项传入,就像你之前用 messages 那样。这让端点保持无状态,也是零留存要求下的正确选择。

先在一个端点上验证

拿一个非关键的端点,只应用第 1–2 步(改名加输出解析),并让你第 0 步的基线提示词经过它。如果文本一致,且网关用量日志里的 token 计数看起来正确,你就在自己的流量上证明了迁移机制,然后再去动状态、工具或流式。

参考资料