
1. 接口演进背后的真实驱动力1.1 从补全到响应不只是改个名字OpenAI 的接口体系这几年变化挺大最早大家接触的都是/v1/completions那时候就是给一段 prompt模型接着往下写简单直接。后来/v1/chat/completions出来了把对话结构化了system、user、assistant 三种角色分工明确工具调用也塞了进来。再往后/v1/responses开始出现在一些新工具和 SDK 里很多人第一次看到这个路径是在配置本地代理或者第三方客户端的时候比如热词里提到的http://127.0.0.1:15721/v1/responses这种地址一看就是本地转发层在转发 Responses 格式的请求。这三个接口不是简单的版本号递进它们代表了三种不同的交互范式。Completions 是文本续写范式Chat Completions 是消息列表范式Responses 则是事件流范式。理解这个区别比记住哪个接口叫什么名字重要得多。1.2 为什么 OpenAI 要推 Responses核心原因有两个。第一Chat Completions 在处理多轮工具调用、流式增量、状态保持的时候协议本身越来越臃肿。每次工具调用都要把整个消息历史重新发一遍token 消耗大状态管理也麻烦。第二Agent 类应用爆发之后开发者需要一种更原生的方式来表达“模型思考-调用工具-观察结果-继续思考”这个循环Chat Completions 的tool_calls字段虽然能用但用起来很别扭。Responses 接口的设计思路是把一次交互当成一个“响应对象”里面包含输出项列表每个输出项可以是文本、工具调用、推理摘要等。流式模式下服务端会推送一系列事件比如response.created、response.output_item.added、response.output_text.delta、response.completed。这种事件驱动的设计天然适合 Agent 场景也方便前端做细粒度的 UI 更新。1.3 开源兼容的真实现状热词里有个很关键的词叫“开源兼容”。现在市面上大量第三方客户端、本地推理框架、代理层都在宣称自己兼容 OpenAI 接口但兼容的到底是哪个接口差别很大。很多工具只兼容/v1/chat/completions你把它指向/v1/responses就会报错比如热词里那个unexpected status 502 bad gateway: unknown error很可能就是代理层不认识 Responses 路径直接转发失败。还有一个热词是custom tools require mimo freeform responses lite mode这说明某些模型或平台在自定义工具场景下对 Responses 的格式有特殊要求不是所有兼容层都能正确处理。所以“开源兼容”这四个字在实际操作中要打很多折扣必须具体到接口路径、请求体结构、流式事件格式三个层面去验证。2. 三种接口的请求与响应结构拆解2.1 Completions 的极简结构/v1/completions的请求体非常朴素{ model: gpt-3.5-turbo-instruct, prompt: 写一段关于接口演进的说明, max_tokens: 256, temperature: 0.7, stream: false }响应也是线性的{ id: cmpl-xxx, object: text_completion, choices: [ { text: 接口演进的过程..., index: 0, finish_reason: stop } ], usage: { prompt_tokens: 12, completion_tokens: 80, total_tokens: 92 } }这种结构适合单轮文本生成比如补全代码、写摘要、做翻译。但它没有角色概念没有工具调用没有多轮状态所以在新一代应用里基本被 Chat Completions 取代了。2.2 Chat Completions 的消息列表范式/v1/chat/completions把输入变成消息数组{ model: gpt-4o, messages: [ {role: system, content: 你是一个严谨的技术助手}, {role: user, content: 解释一下 Responses 接口的优势} ], tools: [ { type: function, function: { name: search_docs, description: 搜索文档, parameters: { type: object, properties: { query: {type: string} }, required: [query] } } } ], stream: true }流式响应是一系列chat.completion.chunk对象每个 chunk 里带delta字段。工具调用的时候模型会返回tool_calls开发者需要把工具执行结果以role: tool的消息追加回去再发一次请求。这个“追加-重发”的循环就是 Chat Completions 在 Agent 场景下最麻烦的地方。2.3 Responses 的事件流设计/v1/responses的请求体看起来和 Chat Completions 有点像但字段名和语义变了{ model: gpt-4o, input: [ { role: user, content: [ {type: input_text, text: 帮我查一下今天的天气} ] } ], tools: [ { type: function, name: get_weather, description: 获取天气, parameters: { type: object, properties: { city: {type: string} }, required: [city] } } ], stream: true }注意几个关键差异messages变成了inputcontent从字符串变成了数组工具定义从嵌套的function对象变成了扁平结构。流式响应不再是 chunk而是事件event: response.created data: {type:response.created,response:{id:resp_xxx,status:in_progress}} event: response.output_item.added data: {type:response.output_item.added,output_index:0,item:{type:function_call,name:get_weather,arguments:{\city\:\北京\}}} event: response.completed data: {type:response.completed,response:{id:resp_xxx,status:completed,output:[...]}}这种事件流的好处是前端可以精确知道模型在干什么是在生成文本还是在调用工具还是在等待工具结果。对于构建复杂的 Agent UI这比解析 chunk 里的 delta 要清晰得多。2.4 三种接口的对比速查维度CompletionsChat CompletionsResponses输入结构prompt 字符串messages 数组input 数组角色支持无system/user/assistant/tool通过 input 项类型区分工具调用不支持tool_calls 字段function_call 输出项流式格式text_completion chunkchat.completion.chunk事件流状态保持无需重发历史支持 previous_response_id适用场景单轮文本生成多轮对话、简单工具Agent、复杂工作流这张表建议收藏配置第三方客户端的时候对着看能省很多排查时间。3. 开源兼容层的实现要点与踩坑记录3.1 兼容层到底在兼容什么很多开源项目说“兼容 OpenAI”其实只实现了 Chat Completions 的一个子集。真正要兼容 Responses需要做几件事第一解析input数组把里面的input_text、input_image、function_call_output等类型正确转换成内部表示第二生成符合规范的事件流事件类型和字段名不能错第三处理previous_response_id维护会话状态。我见过不少代理层收到/v1/responses请求之后直接把它转成/v1/chat/completions发给后端然后把 chunk 包装成事件返回。这种做法在简单文本生成场景下能跑通但一旦涉及工具调用或者多模态输入就会出问题。比如热词里提到的custom tools require mimo freeform responses lite mode就是因为工具调用的格式转换没做对导致后端拒绝请求。3.2 本地代理配置的典型问题热词里有个地址http://127.0.0.1:15721/v1/responses这通常是本地代理或者网关的监听地址。配置的时候有几个坑路径拼接错误有些客户端会在 base_url 后面自动加/v1如果你填的是http://127.0.0.1:15721/v1最终请求可能变成/v1/v1/responses直接 404。流式解析失败Responses 的事件流是 SSE 格式每行以event:和data:开头。如果代理层没有正确设置Content-Type: text/event-stream客户端可能收不到事件。超时设置Agent 场景下一次响应可能包含多轮工具调用耗时较长。默认 30 秒超时经常不够需要调到 120 秒以上。提示配置任何声称兼容 Responses 的客户端之前先用 curl 直接打一下/v1/responses确认返回的是事件流而不是错误页。这一步能排除掉一半的配置问题。3.3 502 错误的排查思路unexpected status 502 bad gateway: unknown error这个错误在本地代理场景下出现频率很高。502 意味着代理层无法从上游拿到有效响应。可能的原因上游地址填错了比如把/v1/responses发到了一个只支持/v1/chat/completions的服务。代理层没有正确处理 Responses 的请求体上游返回了 400但代理层把它包装成了 502。网络层问题比如本地端口被占用、防火墙拦截。排查顺序建议先看代理层日志确认请求有没有发出去再用 curl 直接打上游地址看原始返回最后检查请求体是否符合 Responses 规范。很多时候问题出在请求体里多了或少了字段比如input写成了messages或者tools的结构不对。3.4 工具调用格式的兼容性陷阱Chat Completions 的工具定义是{ type: function, function: { name: get_weather, parameters: {...} } }Responses 的工具定义是{ type: function, name: get_weather, parameters: {...} }少了一层function嵌套。很多兼容层在转换的时候忘了这一点导致后端解析失败。另外工具调用的返回格式也不同Chat Completions 用role: tool的消息Responses 用function_call_output类型的输入项。这些细节在实现兼容层的时候必须逐个对齐。4. 从零搭建一个最小可用的 Responses 兼容层4.1 整体架构设计假设你有一个只支持 Chat Completions 的后端想让它对外表现为 Responses 接口。整体架构可以这样设计客户端 - Responses 入口 - 请求转换器 - Chat Completions 后端 | | v v 事件流生成器 - 响应转换器 - 流式 chunk 解析器请求转换器负责把input数组转成messages把扁平的tools转成嵌套结构。响应转换器负责把 chunk 里的delta转成事件把tool_calls转成function_call输出项。事件流生成器负责按照 SSE 格式推送事件。4.2 请求转换的核心代码用 Python 写一个最小转换函数def convert_responses_to_chat(request: dict) - dict: messages [] for item in request.get(input, []): if item.get(type) input_text: messages.append({role: user, content: item[text]}) elif item.get(role): content item.get(content, ) if isinstance(content, list): text_parts [c[text] for c in content if c.get(type) input_text] content \n.join(text_parts) messages.append({role: item[role], content: content}) tools [] for tool in request.get(tools, []): if tool.get(type) function: tools.append({ type: function, function: { name: tool[name], description: tool.get(description, ), parameters: tool.get(parameters, {}) } }) return { model: request[model], messages: messages, tools: tools if tools else None, stream: request.get(stream, False) }这段代码处理了最常见的文本输入和工具定义转换。实际使用中还需要处理图片输入、function_call_output等类型但作为最小可用版本这些可以先跳过。4.3 事件流生成的关键细节SSE 格式要求每条消息以event:和data:两行组成末尾空行分隔。生成事件的时候要注意def emit_event(event_type: str, data: dict): return fevent: {event_type}\ndata: {json.dumps(data, ensure_asciiFalse)}\n\n事件顺序也很重要。一个典型的响应流程是response.created响应对象创建状态in_progress。response.output_item.added如果有工具调用先推送这个事件。response.function_call_arguments.delta工具参数增量。response.output_item.done工具调用项完成。response.output_text.delta文本增量。response.completed整个响应完成。如果顺序乱了客户端可能解析失败。比如有些客户端在收到response.completed之后就关闭连接如果这时候还有output_text.delta没推完文本就会丢失。4.4 状态保持的实现方式Responses 支持previous_response_id意思是客户端可以只发新消息服务端自动关联之前的上下文。实现方式有两种一种是在服务端存一个 response_id 到消息历史的映射另一种是把历史编码进 response_id 本身。第一种方式简单直接用一个字典或者 Redis 存就行response_store {} def handle_response(request): prev_id request.get(previous_response_id) history response_store.get(prev_id, []) if prev_id else [] new_messages convert_input_to_messages(request.get(input, [])) all_messages history new_messages # 调用后端... response_id generate_id() response_store[response_id] all_messages [assistant_reply] return response_id第二种方式适合无状态服务但实现复杂一般不建议。实际部署中第一种方式配合过期清理就够了。5. 常见问题速查与实操心得5.1 配置类问题速查表现象可能原因解决方法404 Not Foundbase_url 路径重复或缺失检查 base_url 是否已含/v1502 Bad Gateway上游不支持 Responses 路径确认上游接口类型必要时加转换层流式无输出Content-Type 不是 text/event-stream检查代理层响应头设置工具调用失败tools 结构不匹配对比 Responses 和 Chat 的工具定义差异超时中断默认超时太短调到 120s 以上或启用 keep-alive中文乱码未设置 ensure_asciiFalseJSON 序列化时指定编码5.2 实操心得先验证再集成我自己的习惯是任何新的接口或者兼容层先用 curl 验证再写代码集成。比如测试 Responses 接口curl -N -X POST http://127.0.0.1:15721/v1/responses \ -H Content-Type: application/json \ -H Authorization: Bearer sk-xxx \ -d { model: gpt-4o, input: [{role: user, content: [{type: input_text, text: 你好}]}], stream: true }-N参数关闭缓冲能实时看到事件流。如果这一步能拿到正确的事件再往客户端里配。如果这一步就报错那问题在服务端不用折腾客户端。5.3 实操心得日志要打全兼容层最怕的就是“静默失败”。请求发出去了返回了 200但内容不对。所以日志一定要打全请求体、转换后的请求体、上游返回的原始 chunk、生成的事件。我一般会在转换函数的前后各加一行日志把关键字段打出来。这样出问题的时候一眼就能看出是转换错了还是上游返回不对。注意日志里不要打完整的 API key 和用户敏感内容打前几位和后几位做标识就行。5.4 实操心得版本锁定很重要OpenAI 的接口规范还在演进今天能用的字段明天可能就变了。所以生产环境一定要锁定 SDK 版本和接口版本。比如openaiPython SDK不同版本对 Responses 的支持程度不一样。我一般会在requirements.txt里写死版本号升级之前先在测试环境跑一遍回归。另外第三方兼容层的版本也要锁。有些开源项目更新很快但兼容性测试跟不上新版本可能引入回归。锁定版本定期手动升级比自动追新要稳。5.5 关于“开源兼容”的理性预期最后说一点个人看法。现在很多项目宣传“完全兼容 OpenAI”实际用下来能完整兼容 Chat Completions 的就不错了Responses 的兼容更是参差不齐。所以选型的时候不要只看宣传语要看它的测试用例覆盖了哪些接口、哪些场景。如果项目里有tests/compatibility目录点进去看看比看 README 靠谱得多。如果实在找不到合适的兼容层自己写一个最小转换层也不难核心逻辑就是本文第 4 节那几百行代码。与其在多个不靠谱的兼容层之间来回切换不如自己掌控转换逻辑出了问题也好排查。