见山之后 Beyond the Mountain
Uncategorized /

Chat Completions 和 Responses API,到底差在哪

Chat Completions 和 Responses API,到底差在哪

Chat Completions 与 Responses API,到底差在哪:戒指封面

之前写《不用 GPT4,如何让你的 AI 助理更加智能》时,我用智谱 AI 和 LlamaIndex 做了一个知识库检索的例子。里面调用模型用到的 chat.completions.create(),熟悉 OpenAI 接口的朋友应该不陌生。把问题放进 messages,再从返回结果里取出回答,一次问答就完成了。

现在接入 OpenAI,还会遇到另一套接口——Responses API。它同样能聊天,也支持函数调用。那么,对于一个已经能正常回答问题的应用,换成 Responses 会有什么区别?

我们拿一个简单的技术问答助手来比较。先问它“什么是向量数据库”,接着追问,再让它查资料。随着需求一点点增加,两套接口要做的工作也就清楚了。

输入与输出

先不考虑历史消息和工具,只问一个问题。下面用 Python 分别调用两套接口,假设已经初始化了 OpenAI 客户端。

使用 Chat Completions:

chat = client.chat.completions.create(
    model="gpt-5.4",
    messages=[{"role": "user", "content": "什么是向量数据库?"}],
)
print(chat.choices[0].message.content)

换成 Responses:

response = client.responses.create(
    model="gpt-5.4",
    input="什么是向量数据库?",
)
print(response.output_text)

这两段代码里,最容易看见的变化是 messages 变成了 input,读取文字的方式也变了。Responses 的 input 还可以接收消息列表,原来的简单对话内容不必全部重写。

Chat Completions 围绕消息来组织输入。user 是用户的问题,assistant 是模型的回复,系统或开发者消息用来给规则;调用函数后的结果,则作为 tool 消息交回模型。返回时,候选结果放在 choices 中,每个候选包含一条 message。如果模型要调用函数,相关信息就放在这条消息的 tool_calls 里。

Responses 则把输出放进 output 数组。里面每一项都有自己的类型,官方称为 Item。比如普通回复是 message,函数调用是 function_call,推理模型还可能返回 reasoning。它们可以属于同一次响应,不能把数组里的几项理解成几份候选答案

Responses 的这些数据项,可以像图里这样按类型分开处理:

戒指把一次响应里的文字、工具调用和推理项分开处理

图中的三类材料只是示意,并非每次响应都会全部出现。

回到上面的代码,为什么直接打印 response.output_text 就能拿到文字?因为这是 SDK 提供的便捷属性,会把文本内容汇总起来。如果要处理工具调用,就需要按 output 中每一项的类型分别读取。直接拿 output[0] 当最终回答,遇到推理或工具调用时就容易出问题。

多轮对话

助手解释完向量数据库,我们接着问:“和关系型数据库比呢?”

这句话单独拿出来,模型并不知道要把什么和关系型数据库比较。使用 Chat Completions 时,应用需要把上一轮问题、模型的回答和这次追问,一起放进 messages

这也是 Chat Completions“无状态”的含义。它不会自动继承上一次请求,但只要应用把需要的历史带回来,照样可以多轮对话。

Responses 多提供了一种接法。第一次调用后保存返回的响应 ID,下一次通过 previous_response_id 引用它,服务端就能接上对应的上下文。仍然用刚才的问题,代码可以这样写:

rules = "用中文回答,先解释概念,再给一个简短例子。"

first = client.responses.create(
    model="gpt-5.4",
    instructions=rules,
    input="什么是向量数据库?",
)

second = client.responses.create(
    model="gpt-5.4",
    previous_response_id=first.id,
    instructions=rules,
    input="和关系型数据库比呢?",
)

看第二次调用,input 里只写了新问题,前一轮内容通过 first.id 接了回来。不过,这里为什么还要再传一次 instructions

因为使用 previous_response_id 接续时,上一轮顶层的 instructions 不会自动继承。希望助手每轮都用中文、按同样的规则回答,就需要明确重发。

戒指通过响应编号找回抽屉中的上轮历史,再处理新问题

图里的抽屉还保存着上轮历史,响应编号用来引用它。选 Responses 也可以继续自己维护历史。手动回传时,需要保留上一轮完整输出里必要的工具与推理 Items,不能只存下界面上的文字。需要持久保存并跨会话使用的对话对象,还可以选择 Conversations API。

存储策略也可以自行决定,store=False 可关闭响应存储。当然,接续对话只是方便管理历史,模型的上下文窗口仍然有限。

工具调用

能连续聊天以后,我们希望这个助手回答问题时还能查资料。比如从自己的知识库里检索相关内容,再交给模型解释。这就要用到工具调用了。

自定义函数

以自定义函数为例,应用先告诉模型有哪些函数、各自接收什么参数。Chat Completions 在需要工具时返回函数名和参数,应用执行函数,再把结果作为 tool 消息带回,模型才能继续回答。执行失败怎么办、要不要重试、什么时候结束,也都需要应用处理。这一套已经可以用来做 Agent。

换成 Responses 后,自定义函数的执行仍然归应用负责。模型返回 function_call Item,应用执行自己的检索代码,再回传 function_call_output,二者通过 call_id 对应。

也就是说,即使你把函数定义发给了模型,真正去查知识库的代码仍在你的服务里。访问权限、执行失败和防重复执行这些事情,也不会因为换了接口就自动解决。

内置工具

那 Responses 能帮我们省掉哪些工作?这里需要把自定义函数和平台托管的内置工具分开看。

如果让助手查网页,可以直接启用内置的 web_search。比如我们想查 OpenAI 对旧接口的最新说明:

response = client.responses.create(
    model="gpt-5.4",
    tools=[{"type": "web_search"}],
    input="查找 OpenAI 对 Chat Completions 的最新说明,附来源。",
)

这时模型需要搜索,由平台执行搜索并让模型根据结果继续回答。我们省掉了自己接搜索服务、包装结果,以及推进这部分工具循环的工作。文件搜索、托管代码执行也有对应能力,但仍要按工具要求准备数据或配置容器。

戒指分别在应用侧与平台内检索资料,说明两类工具的执行位置

左边的戒指在应用侧检索资料,右边则在平台内完成搜索。自定义函数在两套接口中都由应用执行;图中的内置网页搜索指 Responses 的 web_search

Chat Completions 也有通过专用搜索模型联网的路径,所以不能把区别简单归纳成“一个能搜索,一个不能搜索”。Responses 把多种内置工具放进了统一的调用与输出结构;自己写的业务函数,两边都需要应用参与执行。

Token 计费与上下文压缩

回头看第二轮问答,我们只传了新问题和响应 ID,请求确实短了。那之前那段历史,还算不算钱?

仍然计费。 响应 ID 帮我们接回了历史,模型处理这次问题时依然需要读取相关上下文。官方文档明确说明,响应链中的历史输入 token 仍会计费。

所以,请求传了多少内容和模型实际处理多少 token,需要分开算。Chat Completions 与 Responses 不单独收一笔“接口费”,模型 token 按所选模型计价。启用搜索、文件存储或代码执行,还要考虑对应费用;缓存命中也会影响实际成本。

如果对话很长,可以使用 compaction,把之前的上下文压缩成后续能够继续使用的表示,减少下一阶段携带的 token。这需要主动调用压缩端点,或者配置服务端压缩阈值。只换成 Responses、只传一个响应 ID,都不会自动启用压缩。

压缩后的效果也需要验证。比如我们给助手定下的规则还在不在,之前提供的关键信息有没有影响后续回答。具体能省多少费用、降低多少延迟,要拿自己的任务测试,不能套用别人的一个节省比例。

接口迁移与选择

前面的一次问答,只改几处写法就能完成。但应用如果已经接了工具、结构化输出或流式显示,迁移时还要检查解析代码。常见的变化可以放在一起看:

接入位置Chat CompletionsResponses
输入messagesinput
文本读取choices[0].message.contentSDK 的 output_text
函数定义定义放在 function 对象内函数名与参数直接放在工具对象上
函数结果关联tool_call_idcall_id
结构化输出配置response_formattext.format
流式文本choices[].deltaresponse.output_text.delta 事件
多个候选结果受支持模型可使用 n不支持 n,需要分别请求

比如流式处理,原来逐块读取 delta,换成 Responses 后就要看事件类型。文字增量和工具参数增量要分开处理。界面只显示文字,可以少处理一些事件,但负责执行工具的后端不能把其他事件都丢掉。

还要把模型限制一起考虑进去。Responses 可以在后续调用中继续利用受支持的推理上下文;当前迁移指南特别注明,从 GPT-5.4 起,Chat Completions 的工具调用不支持 reasoning_effortnone 以外的值。如果需要推理和工具配合,这个限制就会直接影响选择。

对于已有项目,我的建议是先看现在缺什么。Chat Completions 仍然受支持,如果调用稳定、历史由应用自己管理,又没有需要接入的新能力,就不必只因为出现了新接口而重写。还要适配多家服务的项目,也得分别确认参数支持情况,迁移会多出一轮兼容与回归测试。

新项目可以按照 OpenAI 的建议,从 Responses 开始。已有项目想试,不妨先迁一条具体流程,比如把手工接入的网页搜索换成内置搜索,再用同一组问题比较回答、耗时和费用。这样才能知道,省下的维护工作值不值得这次改动。