ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenAI接口演进:从Chat到Responses兼容实践

OpenAI接口演进:从Chat到Responses兼容实践 最近在排查内部网关日志时又看到一行让人血压升高的报错unexpected endpoint or method. (POST /chat/completions)。这不是我第一次遇到它也不会是最后一次。说实话OpenAI 的接口规范演进速度比大多数人的认知要快从最早的 Completions到一度成为行业默认标准的 Chat Completions再到 2024 年下半年开始主推的 Responses API三个名字、三套请求结构表面都叫OpenAI 接口底层逻辑完全不是一回事。这篇文章不打算复读官方文档而是把这几轮演进的设计意图、对现有代码的真实冲击以及开源生态为什么至今死守/v1/chat/completions的底层原因梳理一遍。如果你正在做 LLM 应用接入、自建私有化网关或者要给开源模型套一层 OpenAI 兼容接口这篇文章应该能帮你少踩几个坑。我会从那个具体的报错开始讲因为它恰恰是接口规范演进过程中各种断层的一个缩影。1. 先解决那个让人头疼的 unexpected endpoint or method1.1 这个报错到底是谁抛出来的unexpected endpoint or method这种返回格式并不是 HTTP 标准状态码而是 OpenAI 风格网关自己设计的一种错误响应。它通常出现在两种情况一种是请求路径与网关路由不匹配比如你本来要打/v1/chat/completions但因为 base_url 拼接问题实际打到了/chat/completions少了版本前缀网关不认识这个路由就直接把这个请求原样拒绝掉另一种是目标服务根本没有实现这个端点比如自建网关只做了/v1/responses的转发逻辑而上层 SDK 还在走老接口一个 POST 打过去自然就撞上了unexpected endpoint or method。我先给一个最常用的验证命令遇到类似问题不要急着改代码先用 curl 直接打目标服务看看它到底认不认这个路径curl https://api.openai.com/v1/chat/completions \ -H Authorization: Bearer $OPENAI_API_KEY \ -H Content-Type: application/json \ -d {model: gpt-4o-mini, messages: [{role: user, content: hello}]}如果这段 curl 能正常返回说明官方端点没问题问题出在你的客户端配置上如果服务端也返回同样的unexpected endpoint or method那基本可以确定是你的请求路径拼错了或者你打的服务压根没这个路由。实际工程里最常见的场景反而不是官方 API而是公司内部网关底层用 vLLM 起模型网关层只实现了/v1/chat/completions结果上层某个新框架默认调用client.responses.create一个/v1/responses请求过来网关没有对应处理逻辑直接给你一个 404 或这个unexpected endpoint or method。这里有个很实用的排查技巧先确认你的 SDK 调用方法对应哪个路径再确认 base_url 最后该怎么拼。OpenAI 官方 Python 和 Node SDK 的默认 base_url 都带/v1所以client.chat.completions.create请求的是/v1/chat/completionsclient.responses.create请求的是/v1/responses。如果你自己包装了一层 HTTP 客户端手动拼 base_url 时少写了/v1就会出现这种让人摸不着头脑的报错。看到这个报错第一反应不要急着改代码先用 curl 直接打目标服务确认它到底支持哪些路由。很多排查到最后根本不是 SDK 的问题而是网关路由表的问题。1.2 三个接口名其实是三个时代接口改名不是拍脑袋Completions、Chat Completions、Responses分别对应 OpenAI 产品能力的三个时代。Completions 时代模型做的是续写你给一段 prompt它返回一段文本没有对话、没有角色、没有工具。Chat Completions 时代模型开始用 messages 组织上下文user、system、assistant 这些角色成为协议的一部分function calling 也在这个时期加入。Responses 时代则更进一步OpenAI 想统一文本、工具调用、文件检索、网页搜索、多模态输入等一堆能力给 Agent 应用一个更完整的接口。这三个时代不是简单的版本升级而是产品定位的迁移。早期用 Completions 的人心态是我在用一个高级文本生成器用 Chat Completions 的人心态是我在做对话系统到了 Responses 时代心态变成了我在搭一个会自己调用工具的智能体。心智模型变了接口设计自然要跟着变。如果你还拿 Chat Completions 时代的编程习惯去套 Responses API会觉得它很多余、很多字段不知所谓反过来如果你已经习惯了 Responses API 的输出结构再回去写 Chat Completions 的工具循环又会觉得重复劳动太多。1.3 演进背后的驱动力从补全到智能体更根本的驱动力是应用形态变了。最初的 API 是给完形填空用的产品交互基本是用户输入一段话拿到一段输出。但到了 GPT-4 时代大家开始做多轮对话、做插件调用2024 年以后又流行 Agent模型不光要回答问题还要自己决定调用哪个工具、处理工具返回结果、继续推理。Chat Completions 的 messages 数组叠加 tools 参数确实能实现这些但客户端要手动维护每一轮历史还要自己写工具执行循环代码量一大各种边界问题就来了。OpenAI 显然看到了这个趋势。从 Assistants API 开始它就在尝试把会话状态和工具循环往服务端收拢Responses API 则是把这些能力协议化、标准化让任何语言的 SDK 都能通过同一个端点访问。换句话说接口演进不是工程团队闲得没事干而是 Agent 应用对协议提出了新的需求更少的历史传输、更统一的事件流、更完整的工具抽象。理解了这层驱动力再看 Responses API 里那些看起来陌生的字段就会顺眼很多。2. 拆开看Chat Completions 和 Responses 到底差在哪2.1 Completions 的续写思维已经退场的旧协议在聊两个新接口之前值得先看一眼那个已经退场的 Completions 协议。它请求的核心字段是prompt响应核心字段是choices[0].text。没有 messages没有 role没有 tools模型看到什么就续写什么。当时的 text-davinci-003、code-davinci-002 都是这个套路你也很难用这套协议做真正意义上的对话产品因为多轮上下文完全要自己拼到 prompt 里角色信息根本无处安放。后来 GPT-3.5-turbo 和 GPT-4 时代到来聊天模型成了主力Completions 就变成了 legacy 接口。OpenAI 官方也建议迁移到 Chat Completions。现在它基本已经退出主流只有一些老代码还在维护。如果你在旧项目里见过这种prompt字段别奇怪那是接口规范演进留下的时代印记。2.2 Chat Completions 凭什么成为事实标准回过头看Chat Completions 的成功几乎是必然的。它把对话拆成最朴素的 messages 数组每个元素带一个 rolesystem 给指令、user 提问题、assistant 给回复、tool 放工具结果。这个结构足够简单简单到任何后端服务都能轻松映射又足够表达对话场景的完整语义多轮上下文就是按时间顺序排列 messages不需要额外的状态管理。它还在工具调用上做了一个巧妙的抽象模型如果决定调用函数返回的 message 里会带tool_calls客户端执行完函数后把结果作为 roletool 的消息放回 messages再发一轮请求。这个客户端循环模式虽然原始但非常透明开发者能完全掌控每一轮发生了什么。我见过很多团队基于这套模式做了复杂的 Agent 编排系统跑得也很稳。更重要的是Chat Completions 的请求结构可以直接用 curl 验证协议不依赖任何特定平台能力。对于开源社区来说能用 curl 测通的接口就是好接口。所以 vLLM、Ollama、llama.cpp 这些开源推理引擎不约而同选择先实现/v1/chat/completions不是没有道理的。2.3 Responses 的新设计统一输出、有状态、内置工具Responses API 的官方定位是下一代 Agent 接口它相比 Chat Completions 做了几个关键变化。第一个是统一输出结构Chat Completions 返回的是choices[0].message而 Responses 返回一个output数组数组里的元素可以是 message、reasoning、function_call、web_search_call、file_search_call 等不同类型。这样设计的好处是一个响应里可以混合出现思考过程工具调用文本输出客户端可以按类型一一处理而不是靠猜。第二个关键变化是有状态。Chat Completions 是无状态的每次请求都要把完整历史传进去Responses 支持previous_response_id服务端帮你存着会话状态下一次请求只要带上上一个响应的 id就能接着往下聊。用个不严谨但好懂的说法Chat Completions 像每次把剧本全文递给演员Responses 只是告诉服务器接着上次演。这对长对话、Agent 多轮任务特别有意义因为省掉了大量的历史 token 传输。第三个变化是内置工具。Responses API 直接支持web_search、file_search、code_interpreter、computer_use这类托管工具你不需要自己实现搜索逻辑、文件解析逻辑只要在请求里声明工具类型OpenAI 平台就会帮你把结果塞回响应。这是 Chat Completions 时代完全做不到的也是开源生态很难追赶的点。当然对于自定义函数Responses API 仍然保留了 function call 的模式本质上还是客户端执行、结果回传但整体协议表达比 Chat Completions 干净很多。2.4 一张表看三个接口的对位差异维度Completions旧Chat Completions现状Responses API新端点/v1/completions/v1/chat/completions/v1/responses请求核心prompt字符串messages数组inputinstructions输出核心choices[].textchoices[].messageoutput[]数组角色体系无system / user / assistant / toolmessages 里保留另有 instructions状态管理无状态无状态每次传全量历史支持previous_response_id服务端状态工具调用不支持toolstool_calls客户端循环内置托管工具 function call流式事件文本增量choices[].deltaresponse.output_text.delta 等事件开源兼容现状已淡出事实标准几乎全兼容实验性多数网关未实现这张表应该能帮你快速定位自己处在哪个时代。很多团队嘴上说着我们用的是 OpenAI 接口实际上用的是 Chat Completions真正跑到 Responses API 的多半是重度 Agent 应用或者用了官方最新的框架。3. 官方为什么推 Responses又为什么不废掉 Chat Completions3.1 Responses 是给 Agent 场景设计的一体化协议官方推 Responses 的动机本质上是在为 Agent 时代补协议。Chat Completions 设计于 2023 年那时候的应用主要是聊天和内容生成但 2024 年以后OpenAI 的模型能力开始走向会使用工具、会搜索、会操作电脑的智能体包括后来推出的 Codex CLI 这类命令行编码代理也把工具调用和长任务状态管理作为核心需求。旧的 Chat Completions 协议虽然能靠客户端循环硬撑但撑得很勉强尤其是多步骤工具调用、长会话回溯这些场景代码复杂度会直线上升。所以 OpenAI 推出了 Responses本质上是把原来 Assistants API 里的 thread、run 概念做了协议化收敛到一个/v1/responses端点里。它让 Agent 开发者可以用一套接口完成多轮对话 工具调用 内置搜索 文件检索 结构化输出的组合而不用在 Chat Completions 和 Assistants API 之间来回折腾。对我个人来说最直观的感受是用 Responses API 写 Agent 代码样板代码明显少了处理工具调用的逻辑也更统一。3.2 官方的兼容态度旧协议继续活新能力给新协议OpenAI 在发布 Responses API 时并没有要求大家立刻弃用 Chat Completions。官方文档也明确表示Chat Completions 依然是可用且长期支持的接口大量存量应用都跑在上面强制迁移会造成巨大的生态扰动。这个策略我非常认可新协议负责吸引新场景旧协议负责承接存量生态两条腿走路。但要注意OpenAI 把一些新能力只给了 Responses API比如内置的 web_search 和 file_search 工具Chat Completions 想做这些只能自己接第三方搜索服务或自建向量检索。也就是说旧接口不会死但旧接口 新能力这条路是走不通的。如果你要的只是稳定的对话功能Chat Completions 完全够用如果你要的是托管化 Agent 能力那就必须上 Responses API。3.3 SDK 里的暗坑方法名决定路径路径决定兼容这个坑我踩过不止一次值得单独拿出来说。OpenAI 官方 SDK 同时保留了client.chat.completions.create和client.responses.create两个方法它们请求的路径完全不同。前者打/v1/chat/completions后者打/v1/responses。如果你的应用接的是自建网关而网关只实现了/v1/chat/completions那上层框架一旦用了client.responses.create立刻就会得到unexpected endpoint or method或者 404。更隐蔽的是很多 Agent 框架内部默认走 Responses API比如官方开源的 Agents SDK 就是把 Responses 作为默认协议。你以为你把 API Key 和 base_url 都配置好了结果框架在你看不见的地方调用了/v1/responses自建网关根本不知道这个路由。遇到这类问题最快的定位方式是在网关层加打印日志把所有进来的请求路径和 method 记下来一眼就能看出是哪个端点触发了报错。4. 开源兼容的真相为什么大家死守 /v1/chat/completions4.1 开源推理引擎的协议选择逻辑如果只看热度你以为开源生态会紧跟 OpenAI 一起拥抱 Responses API。但现实是你打开 vLLM、SGLang、Ollama、llama.cpp 这些主流项目的文档翻到的 OpenAI 兼容接口基本都是/v1/chat/completions有些顺带支持/v1/completions但很少有把/v1/responses作为一等公民来做的。原因不复杂。第一需求决定供给用户拿开源模型部署私有服务绝大多数是通过 OpenAI SDK 的client.chat.completions.create来对接的项目方自然优先做这个。第二实现成本差异巨大Chat Completions 的协议核心就是 messages 数组后端只要把 messages 拼进模型的聊天模板采样生成文本再按 OpenAI 格式返回就行整个链路非常短而 Responses API 牵扯到服务端状态存储、工具事件流、内置工具调度对推理引擎来说这些全是额外的平台级工作。4.2 聊天模板决定了接口映射的天然成本还有一个经常被忽略的技术细节开源模型的 chat template 和 Chat Completions 的协议字段天生就是对得上的。比如很多开源模型使用或兼容 ChatML 格式的聊天模板system、user、assistant 这些角色在模板里都有明确标记工具调用和工具结果也有对应的模板表达方式。换句话说Chat Completions 的协议字段几乎就是模型训练和推理模板的外层表示映射成本极低。反过来看 Responses API它引入了instructions、previous_response_id、output数组、各种事件类型这些不是模型模板层面的概念而是平台应用层的抽象。开源推理引擎如果要原生支持 Responses API等于要在自己项目里再造一套会话管理和工具编排系统这已经超出了推理引擎的职责范围。所以不是开源项目没技术能力做兼容而是它们普遍认为这块的成本收益不划算。4.3 兼容网关的三种姿势虽然开源引擎本身不做 Responses API但生态里有很多网关项目在做协议转换它们的思路大致可以分成三种。第一种是直通网关只做路由和鉴权上游和下游协议保持一致最简单也最稳定。第二种是转换把各家供应商不同的原生协议统一转换成 Chat Completions 格式再暴露给上层LiteLLM、one-api 这类项目就是这么干的这也是 Chat Completions 成为事实标准的重要推手。第三种是伪装也是我们这里最关心的客户端想要/v1/responses后端只有/v1/chat/completions网关就在中间把 Responses 请求映射成 Chat Completions 请求再把 Chat Completions 响应或流式事件翻译回 Responses 格式。这种方案确实是能用的但要注意它只能模拟协议层模拟不了 OpenAI 的托管工具。web_search这类内置工具在开源后端里没有对应实现网关最多把它降级成普通文本回复或者映射到一个自建搜索函数。4.4 开源追不动 Responses 的真实原因我把真实原因总结成四句话。第一托管工具无法本地化web_search、file_search、code_interpreter 这些能力绑定 OpenAI 平台服务开源项目就算实现了协议外壳也没有背后的搜索索引和沙箱执行环境。第二有状态会话需要服务端存储Chat Completions 是请求-响应一次算完Responses 却要维护previous_response_id对应的历史状态这对自托管部署的运维能力提出了额外要求。第三协议变化节奏太快Responses API 本身还在演进output 里的 item 类型和事件类型一直在增加兼容层维护起来很累。第四点最关键商业动机不足。开源模型的使用者要的是私有化、可掌控、低成本的推理能力不是 OpenAI 的托管生态。Chat Completions 已经足够支撑绝大多数应用除非有一天业界主流框架都强制要求 Responses API否则开源项目没有动力去追这个新协议。所以开源不兼容 Responses不是技术上的绝对不可能而是一个基于成本收益的理性选择。5. 自己动手做一个 Responses 到 Chat Completions 的兼容层5.1 先判断该让谁兼容谁在写任何映射代码之前先想清楚你属于哪种情况。如果你的上层 Agent 框架已经绑定了 Responses API底层是 vLLM 或 Ollama 这种只支持 Chat Completions 的开源引擎那你的唯一出路就是在网关层做一次协议转换把/v1/responses翻译成/v1/chat/completions。反过来如果你的应用是自己写的没有强依赖 Responses那最简单的方式是统一用 Chat Completions完全没必要引入新的协议层。还有一个常见的误区是改 SDK 参数硬凑。有人会在代码里手动把 Responses 请求体拆开转成 Chat Completions 的 messages再把返回值拼回去。这个思路没错但放在业务代码里做会非常脏而且每个接入方都这么搞一遍后期维护会痛不欲生。正确做法是把协议转换下沉到网关层业务方对协议差异无感知。5.2 排查 unexpected endpoint 的完整链路如果现在你眼前就有一个unexpected endpoint or method报错按这个顺序排查速度最快。第一步确认 SDK 调用方法看看代码里到底调的是client.chat.completions.create还是client.responses.create对应到期望路径。第二步检查 base_url确认它是否包含/v1有没有被网关前缀改写。第三步用 curl 分别请求这两个端点直接看目标服务返回什么这一步能区分官方不可用和自家网关不认识两种错误。第四步看网关日志是不是请求根本没到后端就被路由拦截了。可能原因现象解决方向base_url 少了/v1官方 API 也报 unexpected endpoint拼接路径补上/v1SDK 方法用错自建网关报 404 / unexpected endpoint改用对应的 client 子客户端网关未实现/v1/responses只有 responses 请求失败在网关加协议转换层上游模型不支持 tools 字段chat completions 请求报参数错误检查模型是否开启 tool 支持这套排查链路我每次遇到类似问题都会走一遍基本十分钟内能定位到根因。5.3 一个最小可用的映射方案下面给一个最小可用的 Responses 到 Chat Completions 的映射伪代码用 FastAPI 写核心思路是把instructions和input转成 messages然后转发给上游 Chat Completions 服务再把上游响应包装成 Responses 格式。from fastapi import FastAPI, Request import httpx app FastAPI() UPSTREAM_CHAT_URL http://127.0.0.1:8000/v1/chat/completions app.post(/v1/responses) async def responses_to_chat(req: Request): body await req.json() messages [] if body.get(instructions): messages.append({role: system, content: body[instructions]}) inp body.get(input, []) if isinstance(inp, str): messages.append({role: user, content: inp}) else: for item in inp: if item.get(type) message: messages.append({ role: item.get(role, user), content: item.get(content, ), }) tools [t for t in body.get(tools, []) if t.get(type) function] payload {model: body.get(model), messages: messages} if tools: payload[tools] tools async with httpx.AsyncClient() as client: resp await client.post(UPSTREAM_CHAT_URL, jsonpayload) data resp.json() text data[choices][0][message].get(content) or return { id: resp_mock, object: response, status: completed, output: [ { type: message, role: assistant, content: [{type: output_text, text: text}], } ], }这段代码只覆盖最简单的非流式、无工具调用场景但它把映射的核心思路讲清楚了Responses 的input/instructions拆成 Chat Completions 的messages上游返回的choices[0].message.content包装成 Responses 的output数组。实际生产里要考虑模型字段映射、工具调用循环、流式 SSE、错误格式兼容等。对于工具循环更完整的做法是在适配层里循环第一次调 Chat Completions 拿到tool_calls适配层执行对应函数把结果以 roletool 追加到 messages再调第二轮直到模型不再返回tool_calls。最后把整个循环的最终文本和中间的工具调用过程按照 Responses 的 output 数组规范拼装出来。如果还想支持previous_response_id可以在 Redis 里维护一个response_id - messages 列表的映射下次请求过来直接读取历史续接。5.4 流式映射最容易翻车的三个点流式场景是协议兼容层最容易翻车的地方我有三个切身体会。第一个是事件名不对齐Chat Completions 的流式 chunk 是choices[].deltaResponses API 的流式是response.output_text.delta、response.completed这一类事件直接透传肯定不行必须把 chunks 聚合成输出片段后再翻译成 Responses 事件。第二个是工具调用的流式表达。Chat Completions 里工具调用的 delta 是按 index 拆开拼装function.name和function.arguments的前端需要自己拼接Responses 事件则有自己的call_id和事件序列。做映射的时候如果不自己维护拼接缓冲区很容易出现工具参数被截断、事件顺序错乱的问题。第三个是结束事件。Responses 客户端通常期待一个明确的response.completed事件或者等 SSE 流结束如果你只是把 Chat Completions 的finish_reason透传过去客户端可能一直等不到终止信号。我个人的建议是流式映射不要做边收边转这种极致优化宁可攒一小段缓冲再输出也要保证事件顺序和终止语义完整。5.5 要不要迁移我的判断标准说句实话Chat Completions 和 Responses API 在未来很长一段时间内会共存。我自己的判断标准很直接如果你的应用就是聊天、补全、简单 RAGChat Completions 够用且稳继续用不用折腾如果你在做多工具 Agent、需要内置搜索和文件检索、希望少传历史省 token那直接上 Responses API别在旧接口上硬憋。我个人在实际项目里是两边都保留的内部网关同时暴露/v1/chat/completions和/v1/responses底层开源模型走 Chat Completions 协议上层 Agent 框架走 Responses 协议中间靠一层轻量映射把两边串起来。这套组合跑下来很稳也让团队不用在换协议和换框架之间做二选一。接口演进是趋势但兼容层不是耻辱它是大型系统演进过程中最正常的工程现实。
返回列表