1. 接口演进背后的真实驱动力
1.1 从补全到对话,再到响应式编程
如果你在过去两年里写过任何跟大模型对接的代码,大概率经历过这么一条路径:最开始用/v1/completions,给一段 prompt,模型续写一段文本,简单直接。后来 ChatGPT 火了,大家开始用/v1/chat/completions,消息变成数组,角色分 system、user、assistant,多轮对话有了结构。再后来,OpenAI 推出了/v1/responses,很多人第一反应是“又来一套新接口,是不是又要重写一遍”。
我一开始也是这个反应。但真正把 Responses 接口接进项目、跑通几个实际场景之后,我的判断变了:这不是简单的接口换皮,而是 OpenAI 在重新定义“一次模型调用”到底应该包含什么。Completions 时代,一次调用就是“给文本、拿文本”;Chat Completions 时代,一次调用是“给消息列表、拿一条消息”;Responses 时代,一次调用变成了“给一组输入和工具、拿一个结构化的响应对象,里面可能包含文本、工具调用、推理过程、引用来源”。
这个变化的核心驱动力,是模型能力本身在变。早期的模型只会续写,你给它什么它接什么。后来的模型会对话,但你得自己管理上下文。现在的模型会推理、会调工具、会多步执行,如果接口还停留在“一问一答”的层面,开发者就得在应用层写大量胶水代码来拼装这些能力。Responses 接口本质上是在把“编排”这件事从应用层往 API 层挪。
1.2 为什么不是直接升级 Chat Completions
这里有个很多人会问的问题:既然 Chat Completions 已经能用,为什么不直接在上面加字段,非要搞一个新端点?
我自己的理解是,Chat Completions 的消息模型有一个根本性的限制:它假设一次调用的输入和输出都是“消息”。但当你引入工具调用、代码执行、文件检索这些能力之后,输入和输出就不再是纯粹的消息了。工具调用的结果是一个结构化的对象,推理过程是一段内部状态,引用来源是一组元数据。这些东西硬塞进 message 数组里,会变得非常别扭。
Responses 接口的做法是引入一个更通用的“输入项”和“输出项”概念。输入可以是消息、可以是工具结果、可以是文件引用;输出可以是文本、可以是工具调用请求、可以是推理摘要。这种设计让接口在面对多模态、多工具、多步执行的场景时,扩展性明显更好。
从实际迁移的角度看,如果你的应用只是简单的单轮问答,Chat Completions 完全够用,没必要急着换。但如果你在做 Agent、在做需要多步工具调用的流程、在做需要保留推理链的复杂任务,Responses 接口带来的结构清晰度是实打实的。
1.3 开源兼容层的真实处境
热词里出现了“开源兼容”“cline openai compatible 配置”这些词,说明很多人关心的是:我用的开源工具、我自建的兼容层,能不能跟上这个演进。
现实情况是,开源生态对 Responses 接口的支持是分层的。第一层是官方 SDK,Python 和 Node 的 SDK 基本都跟上了,直接调client.responses.create就行。第二层是各种兼容网关和代理层,这一层参差不齐,有的只实现了 Chat Completions 的转发,遇到 Responses 请求就直接 502。第三层是下游应用,比如各种编辑器插件、CLI 工具,它们往往依赖兼容层提供的接口,兼容层不支持,它们就用不了。
我踩过的一个典型坑是:本地跑了一个兼容服务,配置里写的是支持 OpenAI 接口,结果用 Responses 格式请求过去,返回unexpected status 502 bad gateway: unknown error,排查半天发现是兼容层根本没实现这个路由。所以下面我会专门用一节来讲怎么判断一个兼容层到底支持到什么程度。
2. 核心概念拆解与实操要点
2.1 Completions、Chat Completions、Responses 三者到底差在哪
先把三个接口的核心差异用一张表说清楚,这样后面讲迁移和兼容的时候不容易乱。
| 维度 | Completions | Chat Completions | Responses |
|---|---|---|---|
| 输入形态 | 单个 prompt 字符串 | messages 数组 | input 数组,支持多种类型 |
| 输出形态 | 文本补全 | 单条 assistant 消息 | 结构化响应对象,含多种输出项 |
| 多轮对话 | 需手动拼接 | 原生支持 | 原生支持,且可混合工具结果 |
| 工具调用 | 不支持 | 支持 function calling | 支持,且工具结果作为输入项 |
| 推理过程 | 无 | 部分模型有 | 有独立的推理输出项 |
| 状态管理 | 无 | 无 | 支持 previous_response_id 串联 |
| 典型场景 | 文本续写、补全 | 对话、简单工具调用 | Agent、多步任务、复杂编排 |
这张表里最值得关注的是最后两行。previous_response_id这个机制意味着你不需要每次把完整历史都传回去,服务端会帮你维护上下文。这对长对话和长任务来说,token 消耗和代码复杂度都会明显下降。
2.2 输入项与输出项的设计逻辑
Responses 接口里,input 是一个数组,数组里的每一项可以是不同角色和类型。最常见的几种:
{"role": "user", "content": "..."}用户消息{"role": "assistant", "content": "..."}助手消息{"type": "function_call_output", "call_id": "...", "output": "..."}工具执行结果{"type": "reasoning", ...}推理项(通常由服务端返回,也可回传)
输出侧,response 对象里有一个output数组,里面同样可能是文本项、工具调用项、推理项。这种对称设计的好处是,你可以把上一轮的输出直接作为下一轮的输入的一部分,不需要做格式转换。
我实际用下来,这个设计在写 Agent 循环的时候特别省事。以前用 Chat Completions 写工具调用循环,你得从tool_calls里提取参数、执行、再把结果包成role: tool的消息塞回去。现在工具调用结果本身就是一种输入项,直接 append 到 input 数组里就行。
2.3 工具调用的新写法与注意事项
Responses 接口的工具调用和 Chat Completions 有几个关键区别,不注意的话很容易踩坑。
第一,工具定义的位置变了。Chat Completions 里 tools 是顶层参数,Responses 里也是顶层参数,但工具的 schema 结构略有不同,特别是strict字段的行为。如果你从旧接口迁移,建议先把工具定义单独抽出来做一次校验。
第二,工具调用的返回处理变了。Chat Completions 里你要遍历choices[0].message.tool_calls,Responses 里你要遍历response.output找type为function_call的项。拿到call_id和arguments之后,执行你的函数,然后把结果作为function_call_output输入项传回去。
第三,多工具并行调用的处理。Responses 接口在一次响应里可能返回多个 function_call 项,你需要全部执行完,把多个 output 项一起传回去。这里有个细节:传回去的顺序最好和返回的顺序一致,虽然文档没强制要求,但实测下来顺序乱了偶尔会出现模型理解偏差。
注意:工具执行结果里的 output 字段必须是字符串。如果你返回的是 JSON 对象,记得先序列化。我见过有人直接塞 dict 进去,结果报类型错误,排查了半天。
2.4 推理项的处理与保留策略
推理项是 Responses 接口里比较新的东西。对于支持推理的模型,响应里会包含type为reasoning的输出项。这些项默认可能只返回摘要,具体取决于你的配置。
我的建议是:如果你在做需要可解释性的场景,比如调试 Agent 的决策过程,把推理项保留下来很有价值。但如果你只是要最终结果,保留推理项会增加 token 消耗和存储成本。可以在请求里通过参数控制推理项的详细程度。
还有一个实操细节:当你用previous_response_id串联多轮时,推理项的处理是服务端自动完成的,你不需要手动回传。但如果你是手动管理 input 数组,就需要决定是否把推理项也放回去。实测下来,对于大多数任务,不回传推理项对结果影响不大,但回传能让模型在复杂任务上表现更稳定。
3. 实操过程与核心环节实现
3.1 从 Chat Completions 迁移到 Responses 的完整步骤
假设你有一个现有的 Chat Completions 调用,想迁移到 Responses。我按实际迁移顺序拆一遍。
第一步,替换客户端调用。Python SDK 里,client.chat.completions.create换成client.responses.create。参数名从messages换成input,从max_tokens换成max_output_tokens。注意max_tokens在 Responses 里已经废弃,用新的名字。
第二步,转换消息格式。Chat Completions 的{"role": "user", "content": "hi"}在 Responses 里可以直接用,但如果你用的是多模态内容数组,结构会略有不同。文本部分从{"type": "text", "text": "..."}变成{"type": "input_text", "text": "..."},图片部分从{"type": "image_url", ...}变成{"type": "input_image", ...}。这个变化不大,但漏改会报错。
第三步,处理系统消息。Chat Completions 里 system 是 messages 里的一条。Responses 里推荐用顶层的instructions参数,而不是把 system 塞进 input。实测下来,用instructions参数在多数模型上效果更稳定。
第四步,调整工具调用循环。前面讲过,输出解析和输入回传的格式都变了,需要重写循环逻辑。
第五步,处理响应解析。Chat Completions 里你取choices[0].message.content。Responses 里你要遍历response.output,找到type为message的项,再取里面的content。如果只想要纯文本,可以写一个辅助函数把 output 数组里的文本项拼起来。
下面是一个最小可运行的迁移示例,用 Python SDK:
from openai import OpenAI client = OpenAI() # 旧写法 # resp = client.chat.completions.create( # model="gpt-4o", # messages=[{"role": "user", "content": "用一句话解释什么是递归"}] # ) # print(resp.choices[0].message.content) # 新写法 resp = client.responses.create( model="gpt-4o", instructions="你是一个简洁的技术助手。", input=[ {"role": "user", "content": "用一句话解释什么是递归"} ] ) # 提取文本输出 text_parts = [] for item in resp.output: if item.type == "message": for c in item.content: if c.type == "output_text": text_parts.append(c.text) print("".join(text_parts))这段代码跑通之后,你就有了一个最小的 Responses 调用骨架。接下来把工具调用、多轮串联逐步加上去。
3.2 工具调用循环的完整实现
工具调用是 Responses 接口最能体现价值的地方,我把完整循环写一遍。
import json from openai import OpenAI client = OpenAI() tools = [ { "type": "function", "name": "get_weather", "description": "查询指定城市的天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"], "additionalProperties": False }, "strict": True } ] def get_weather(city): # 实际项目里这里调真实 API return json.dumps({"city": city, "temp": 22, "condition": "晴"}) input_items = [ {"role": "user", "content": "北京今天天气怎么样?"} ] while True: resp = client.responses.create( model="gpt-4o", input=input_items, tools=tools ) # 把模型输出全部加入输入,保持上下文 input_items += resp.output # 检查是否有工具调用 tool_calls = [item for item in resp.output if item.type == "function_call"] if not tool_calls: break for call in tool_calls: args = json.loads(call.arguments) result = get_weather(args["city"]) input_items.append({ "type": "function_call_output", "call_id": call.call_id, "output": result }) # 提取最终文本 for item in resp.output: if item.type == "message": for c in item.content: if c.type == "output_text": print(c.text)这个循环里有两个关键点。一是input_items += resp.output这行,它把模型的输出项(包括工具调用请求)原样加回输入,这样模型在下一轮能看到自己刚才请求了什么。二是工具执行结果用function_call_output类型追加,call_id必须和请求里的对应上。
提示:如果你的工具执行可能失败,建议在 output 里返回错误信息而不是抛异常,让模型自己决定怎么处理。实测下来,模型对工具报错的容错能力比想象中好。
3.3 多轮对话与 previous_response_id 的使用
如果你不想手动维护 input 数组,可以用previous_response_id。每次请求带上上一次的 response id,服务端会自动把历史接上。
resp1 = client.responses.create( model="gpt-4o", input=[{"role": "user", "content": "我叫小明"}] ) resp2 = client.responses.create( model="gpt-4o", previous_response_id=resp1.id, input=[{"role": "user", "content": "我叫什么?"}] )这种方式的好处是代码简洁,token 消耗也可能更低,因为服务端可以做一些优化。但要注意,previous_response_id有有效期,而且如果你在中间手动改了 input,可能会和之前的历史冲突。我的建议是:简单对话用previous_response_id,复杂 Agent 循环用手动管理 input 数组,控制力更强。
3.4 兼容层配置与验证方法
热词里“cline openai compatible 配置”和那个 502 报错,说明很多人卡在兼容层上。我分享一套验证方法。
首先,确认你的兼容层版本。很多兼容层项目在 README 里会写支持的接口列表,但实际实现可能滞后。最可靠的方法是直接发一个最小 Responses 请求过去,看返回什么。
curl -X POST http://127.0.0.1:15721/v1/responses \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-key" \ -d '{ "model": "gpt-4o", "input": [{"role": "user", "content": "ping"}] }'如果返回 404,说明路由没实现。如果返回 502,说明路由有但后端转发失败,可能是上游不支持或者配置错误。如果返回 200 但结构不对,说明是部分实现。
对于 Cline 这类工具,配置里通常有一个“API Provider”选项,选 OpenAI Compatible 之后填 Base URL 和 API Key。如果工具本身用的是 Responses 格式,而你的兼容层只支持 Chat Completions,就会出问题。解决办法要么是升级兼容层,要么是在工具里切换到 Chat Completions 模式(如果支持的话)。
我自己的做法是:本地兼容层只用来做 Chat Completions 的转发,Responses 相关的实验直接连官方端点。这样职责清晰,不容易混。
4. 常见问题与排查技巧实录
4.1 高频报错与对应排查路径
下面这张表是我在实际项目中遇到过的典型问题,按报错信息整理。
| 报错信息 | 可能原因 | 排查路径 |
|---|---|---|
| 502 Bad Gateway | 兼容层未实现 Responses 路由 | 检查兼容层版本和路由配置 |
| 400 invalid input type | 输入项 type 写错 | 对照文档检查 input_text/output_text |
| 400 unknown parameter max_tokens | 用了旧参数名 | 换成 max_output_tokens |
| 工具调用无返回 | output 未正确回传 | 检查 call_id 是否匹配 |
| 推理项缺失 | 模型或配置不支持 | 确认模型是否支持推理输出 |
| 上下文丢失 | previous_response_id 过期 | 改用手动 input 管理 |
502 那个问题特别常见,因为很多兼容层是基于旧版接口写的,新路由没加。遇到 502 不要先怀疑网络,先确认兼容层到底支不支持。
4.2 工具调用不返回结果的三种情况
工具调用是出错重灾区,我总结三种典型情况。
第一种,模型返回了 function_call,但你执行完把结果传回去之后,模型没有继续生成文本,而是又发起了一次同样的调用。这通常是因为 output 格式不对,模型没识别到这是工具结果。检查type是不是function_call_output,call_id是不是和请求一致。
第二种,模型一次返回多个 function_call,你只处理了第一个。这会导致循环卡住或者结果不完整。记得遍历所有 function_call 项。
第三种,工具参数解析失败。如果模型返回的 arguments 不是合法 JSON,json.loads会抛异常。建议加一层 try-except,解析失败时把原始字符串作为错误信息传回去,让模型重试。
4.3 迁移过程中的兼容性陷阱
从 Chat Completions 迁移过来,有几个容易忽略的差异。
系统消息的处理。旧代码里 system 是 messages 的第一条,迁移后如果还这么写,虽然多数情况能用,但和instructions参数同时存在时行为可能不一致。建议统一用instructions。
停止序列。Chat Completions 的stop参数在 Responses 里行为有变化,部分模型不再支持自定义停止序列。如果你的逻辑依赖 stop,需要重新测试。
流式输出。Responses 的流式事件类型和 Chat Completions 完全不同,事件名从chat.completion.chunk变成了一系列response.*事件。如果你有流式解析代码,这部分需要重写。
token 计数。Responses 的 usage 字段结构变了,输入输出 token 的统计口径也可能不同。如果你做成本监控,记得更新解析逻辑。
4.4 独家避坑经验
最后分享几条文档里不会写、但实际会遇到的坑。
第一条,strict: True的工具定义对 schema 要求很严。additionalProperties必须显式设为 False,所有 required 字段必须在 properties 里定义,嵌套对象也要遵守。不满足的话请求会直接报错,而不是降级处理。
第二条,Responses 接口对 input 数组里的空项比较敏感。如果你动态构建 input,注意过滤掉 None 或空 dict,否则可能报类型错误。
第三条,previous_response_id和手动 input 不要混用。我试过在带 previous_response_id 的请求里又传了完整历史,结果模型看到了重复的上下文,回答变得很奇怪。二选一,别混。
第四条,兼容层的超时设置。Responses 接口因为可能涉及多步推理,响应时间比 Chat Completions 长。如果你的兼容层或反向代理有默认 30 秒超时,复杂任务很容易被截断。建议把超时调到 120 秒以上。
第五条,本地调试时如果遇到custom tools require mimo freeform responses lite mode这类提示,说明你用的兼容层对自定义工具有特殊模式要求。这种提示通常出现在非官方实现里,解决办法是查该兼容层的文档,看是否需要开启某个开关,或者换用官方 SDK 直连。
我在实际项目里迁移完一轮之后的最大体会是:Responses 接口不是给所有人准备的。如果你的场景就是简单问答,留在 Chat Completions 完全没问题。但如果你在做 Agent、在做需要工具编排的复杂流程,早点迁过去,后面省下的胶水代码和维护成本是值得的。迁移过程中最花时间的不是接口调用本身,而是工具调用循环和兼容层的适配,这两块建议单独排期,不要低估。