LLM API协议抽象层演进:从Chat Completions到Responses
2026/6/20 3:54:08 网站建设 项目流程

我是一名在大模型平台层摸爬滚打八年多的工程师,日常要对接 OpenAI、Anthropic、Gemini、Azure、Bedrock 五类主流后端,维护过 17 个不同厂商/网关的 adapter,亲手写过 3 套统一 API 抽象层(从最简版 JSON Schema 映射,到带状态缓存的 runtime bridge,再到支持 tool calling trace 的 agent-aware gateway)。今天这篇不是教程,也不是文档翻译,是我把过去两年踩过的坑、被客户问到哑口无言的瞬间、深夜 debug 时突然顿悟的逻辑断层,全盘托出的一次复盘。

你可能刚接触 LLM API,看到/v1/chat/completions/v1/responses两个路径就懵了:这不就是换了个 URL?是不是 OpenAI 又在搞“接口升级焦虑”?又或者你已经上线了基于messages的对话系统,突然发现要加图像理解、要接函数调用、要让模型返回 JSON Schema 校验过的结构体——结果发现老接口越塞越臃肿,SDK 更新一次就崩一次,第三方网关适配表里你的服务标着“partial support”。这些都不是错觉,而是真实存在的抽象层撕裂。而我要说的,就是这场撕裂的来龙去脉、技术动因,以及——更重要的是——你在选型、设计、演进时,到底该信什么、该押什么、该防什么。

核心关键词其实就三个:OpenAI-compatible Chat CompletionsResponses API协议抽象层级。它们不是并列的三种选项,而是一条演进链上的三个坐标点:起点是“能跑通”,中间是“能兼容”,终点是“能生长”。如果你只记住一句话,那就记这个:messages是对历史的封装,input是对能力的声明;choices是对输出的采样,output是对结果的承诺。这句话背后,藏着所有协议差异的根因,也决定了你未来半年的开发效率、三个月的联调成本、甚至整个系统的可扩展寿命。接下来,我会用一个真实项目贯穿始终——我们为某金融 SaaS 客户做的智能投研助手,它既要解析 PDF 报告(多模态)、又要调用内部风控 API(tool calling)、还要按监管要求返回带字段级 schema 的 JSON(structured output),同时支持长周期策略推演(stateful workflow)。正是这个项目,让我彻底看清了为什么chat/completions在 2023 年还很顺手,到了 2024 年中却成了技术债的温床,而responses又为何不是“另一个接口”,而是整套运行时范式的重置。

1. 协议的本质:不是 URL,而是四层契约

很多人一上来就翻文档看 endpoint,这是最危险的起点。就像你买一辆车,只看车标和轮毂尺寸,却不去查发动机型号、变速箱协议、ECU 通信标准——等真要换轮胎、刷程序、连诊断仪时,才发现根本不是一回事。LLM API 的“协议”,从来就不是某个 URL 路径,而是一组隐性但强约束的工程契约,它由四个不可分割的层次共同构成:认证方式、请求体结构、响应体结构、流式事件模型。漏掉任何一层,接入成本都会指数级上升;只盯住其中一层(比如只看messages字段),则必然在后续迭代中反复返工。

1.1 认证方式:第一道门禁,也是最易被低估的兼容雷区

表面上看,Authorization: Bearer xxxx-api-key: xxx就是 header 名字不同,实则背后是三套完全不同的安全治理逻辑。

  • OpenAI-styleAuthorization: Bearer:这是 OAuth2 风格的 token 认证,token 本身携带 scope(如read:models,write:runs),服务端可做细粒度权限控制。它的优势是标准化程度高,几乎所有 HTTP client 库都原生支持;劣势是 token 生命周期管理复杂,需要 refresh flow,且无法与云平台 IAM 体系天然打通。

  • Anthropic 的x-api-key+anthropic-version:这里x-api-key看似简单,但它本质是静态密钥(static key),不带 scope,权限由密钥创建时绑定。而anthropic-version头部才是关键——它强制要求客户端声明所依赖的 API 版本语义(如2023-06-01),服务端据此决定是否启用新字段、是否兼容旧行为。这意味着:Anthropic 不是“向后兼容”,而是“版本契约锁定”。你用2023-06-01调用,哪怕服务端已升级到 v2.5,它仍会按旧版规则解析messages结构;但若你漏传此头,请求直接 400。我在给客户做 Anthropic adapter 时,就因忘记在 SDK 初始化时注入 version header,导致所有流式请求失败,debug 两小时才发现问题不在 SSE 解析,而在 header 缺失。

  • Gemini 的x-goog-api-key:Google 选择将 API key 直接暴露在 query string 或 header 中,这是其早期云服务的设计惯性。它不带任何权限上下文,完全依赖 key 本身的白名单配置。好处是极简,curl 一把梭;坏处是无法做动态权限升降级,且 key 泄露风险更高。更隐蔽的坑在于:Gemini 的 key 实际上是“project-level”而非“model-level”,同一个 key 可调用 Gemini Pro、Flash、Ultra,但不同 model 对contents.parts的字段校验严格度不同——Pro 接受纯文本,Ultra 却强制要求parts数组中每个元素必须带type字段。这导致我们最初用 Pro 测试通过的请求,在切换到 Ultra 时批量 400,原因竟是parts里少了一个{"type": "text"}包裹。

  • Azure OpenAI 的api-key+api-version:表面看和 Anthropic 类似,但api-version在 Azure 里是资源级概念,而非 API 功能级。它绑定的是整个 Azure Resource Provider 的 REST API 版本(如2023-12-01-preview),影响的是 URL 路径拼接逻辑(/subscriptions/{id}/providers/Microsoft.CognitiveServices/accounts/{name}/deployments/{deployment}/...),而不是模型输入语义。这意味着:你用2023-12-01-preview调用,服务端可能返回choices[0].message.content,但用2024-05-01调用,同一模型却可能返回response.choices[0].message.content(多了一层responsewrapper)。这不是 bug,而是 Azure 的资源管理范式使然。

  • AWS Bedrock 的 SigV4 签名:这才是真正的云原生范式。它不依赖静态 key,而是用 AWS IAM Role 的临时凭证(AccessKeyId + SecretAccessKey + SessionToken)对整个 HTTP request(包括 method、path、headers、body hash)进行 HMAC-SHA256 签名。签名过程需精确计算 canonical request,任何 header 大小写、空格、换行错误都会导致SignatureDoesNotMatch。我们曾因 Python SDK 默认在Content-Typeheader 后多加了一个空格,导致连续三天请求失败,日志里只显示403 Forbidden,没有任何具体错误码。直到用 AWS CLI 的--debug模式比对签名字符串,才定位到问题。SigV4 的代价是接入复杂,但收益是零信任架构下的强身份绑定——你的请求不仅是“有 key”,更是“由指定 IAM Role 在指定时间、用指定参数发起”。

提示:不要在 SDK 层硬编码认证逻辑。我们最终在统一网关里抽象出AuthStrategy接口,每个 provider 实现自己的sign(request)方法。OpenAI 实现为inject_bearer_header(),Anthropic 为inject_api_key_and_version(),Bedrock 则完整嵌入botocore.auth.SigV4Auth。这样当 Bedrock 新增invocationRoleArn参数时,只需更新 Bedrock strategy,不影响其他 provider。

1.2 请求体结构:字段名之下,是世界观的分野

messagesinputcontents.parts这些字段名,绝非随意命名,而是各自代表一套对“模型如何被使用”的底层假设。

  • messages(OpenAI Chat Completions / Anthropic Messages):这是一个对话历史快照模型。它的设计哲学是:“模型的每一次推理,都是对一段完整对话历史的延续”。因此messages必须是数组,且顺序敏感;role字段(system/user/assistant)定义了每条消息的语义角色;content字段承载消息主体。这种结构天然适合聊天场景,但带来三个硬伤:

    1. 多模态表达笨重:要在content里塞图像,OpenAI 要求你写成{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},而 Anthropic 要求{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}}。两者字段名、嵌套深度、base64 编码位置全不同。我们的网关曾为此写了 80 行转换逻辑,只为把用户传来的image_base64字段映射到不同 provider 的正确位置。
    2. 工具调用语义模糊messages里怎么表示“请调用 weather_api 工具”?OpenAI 用tool_choice+tools数组 +assistant消息里返回tool_calls,Anthropic 则用tool_useblock +user消息里嵌入{"type": "tool_use", "id": "toolu_01", "name": "weather_api", "input": {...}}。前者是“模型建议调用”,后者是“用户指令调用”,语义鸿沟极大。
    3. 结构化输出被迫妥协:你想让模型返回 JSON,OpenAI 要求你写 system prompt “返回严格 JSON”,Anthropic 要求你用tool_resultblock 包裹,Gemini 则直接支持response_mime_type: "application/json"messages本身不提供 schema 声明能力,一切靠 prompt 工程 hack。
  • input(OpenAI Responses):这是一个模型执行单元模型。它的设计哲学是:“模型是一台可编程机器,input是喂给它的指令包,output是它执行后的产物”。因此input可以是 string(最简 case),也可以是 object(结构化指令),甚至可以是 array(多任务并行)。OpenAI Responses 的input支持两种形态:

    • 简单形态:"input": "帮我总结"—— 语义等同于messages: [{"role": "user", "content": "帮我总结"}],但去除了“对话”包袱;
    • 结构形态:"input": [{"role": "user", "content": [{"type": "text", "text": "帮我总结"}]}]—— 这里content变成了数组,每个元素是带type的块,为多模态(type: "image_url")、工具调用(type: "tool_use")、结构化输出(type: "json_schema")留出了干净的扩展槽位。
      关键突破在于:input不再预设“这是第几轮对话”,而是让平台侧通过previous_response_idsession_id来管理状态。这使得input成为真正意义上的“能力声明载体”。
  • contents.parts(Gemini Native):这是一个内容原子模型contents是数组(代表多轮交互),parts是每轮里的内容片段数组。parts中每个元素是一个part,必须带type字段(text/inline_data/file_data),且inline_datamime_typedata字段严格分离。这种设计让多模态成为一等公民:一张图、一段音频、一段文本,都是平等的part,没有主次之分。但代价是:它彻底放弃了messages的 role 语义,system指令只能塞进contents[0].parts[0].text,且无法指定作用域(是全局 system 还是仅对下一轮生效?)。我们在做 Gemini adapter 时,不得不自己实现system_prompt的注入逻辑——把它拆成textpart 插入contents[0],并确保后续user消息不覆盖它。

注意:inputcontents.parts都支持多模态,但哲学不同。input是“模型执行指令”,所以input里可以有{"type": "tool_use", "name": "search", "input": {"q": "LLM protocol"}}contents.parts是“内容片段”,所以 Gemini 里调用工具要用{"type": "function_call", "name": "search", "args": {"q": "LLM protocol"}},且必须放在contents数组的特定位置。前者是声明式,后者是命令式。

1.3 返回体结构:choicesoutput的范式之争

读取响应,是接入中最容易被忽略的环节,却是线上故障的高发区。

  • choices[0].message.content(Chat Completions):这是一个采样结果模型choices数组的存在,意味着模型可能返回多个候选(n > 1),message是其中一条生成结果,content是这条结果的文本主体。这种结构暗示:模型输出是“随机采样”的,content是概率分布的一个 realization。它天然适合“生成式问答”,但带来两个问题:

    1. 结构化输出解析脆弱:你想让模型返回 JSON,它却可能返回"{"status": "success", "data": [...]}"(带引号的字符串),或"```json\n{...}\n```"(带代码块包裹),甚至"{'status': 'success'}"(单引号)。因为content字段设计初衷就是文本,没有 schema 约束。我们曾为金融客户做财报分析,要求模型返回{revenue: number, profit: number},结果 30% 的响应因格式不规范导致 JSON.parse() 报错,不得不加 5 层 fallback:正则提取、trim 代码块、replace 单引号、try-catch、最后人工兜底。
    2. 工具调用结果混杂:当模型调用工具后,choices[0].message.content可能为空,而tool_calls字段在message下,tool_results又在下一轮messages里。你需要维护一个跨请求的状态机来拼接完整链路。这对长流程 agent 几乎是灾难。
  • output_text/output(Responses):这是一个确定性结果模型output_text是最简输出,保证是纯文本;output是结构化输出数组,每个元素是{"type": "message" | "tool_result" | "json_schema"}。关键区别在于:output不再是“采样结果”,而是“执行产物”。当你声明input里包含{"type": "json_schema", "schema": {...}}output里就会出现{"type": "json_schema", "value": {...}},且value字段保证是合法 JSON object,无需额外 parse。我们在投研助手项目中,将监管要求的字段 schema 直接作为input的一部分传入,output返回的value可直接序列化入库,错误率从 30% 降至 0.2%。
    更重要的是,output支持类型化区分:{"type": "tool_result", "tool_use_id": "toolu_01", "content": "25°C, sunny"}明确标识这是工具调用结果,而非模型生成文本。这让我们在 agent orchestrator 里可以精准路由:tool_result→ 调用下游 API → 生成新inputmessage→ 渲染给用户;json_schema→ 写入数据库。无需再靠正则匹配content字段里的关键词。

  • Gemini 的candidates[0].content.parts:这是内容片段模型candidates是模型生成的多个候选,content是其中一条的完整内容,parts是内容的原子切片。一个part可以是{"text": "温度是"},也可以是{"function_call": {"name": "get_weather", "args": {"city": "Beijing"}}}。这种设计让 Gemini 天然支持“混合输出”——一段文本 + 一个函数调用 + 一张图。但问题在于:parts顺序即渲染顺序,而function_call的执行结果不会自动插入parts,需要你手动 fetch 并 merge。我们在做实时天气播报时,发现 Gemini 返回的partsfunction_call在前,文本描述在后,但实际执行完工具后,需要把结果插回parts的对应位置,否则前端渲染错乱。这迫使我们在网关里实现了一套part的 patching 机制。

1.4 流式事件模型:SSE 不是银弹,WebSocket 才是未来

流式响应常被简化为“SSE vs WebSocket”,但真实世界远比这复杂。

  • OpenAI Chat Completions 的 SSE(Server-Sent Events):这是最成熟的流式方案。每个 event 是data: {...}\n\nchoices[0].delta.content是增量文本。优点是浏览器原生支持,Nginx/Apache 可直接代理;缺点是单向通信,无法从客户端向服务端发送控制指令(如“暂停”、“跳过当前 token”、“注入新 context”)。我们在做实时代码补全时,用户敲击速度远超模型生成速度,想实现“按键即中断上一轮、启动新请求”,SSE 无法做到,只能靠客户端 cancel 上一个请求,再发新请求,造成大量无效 token 浪费。

  • OpenAI Responses 的 SSE + WebSocket 双模式:OpenAI Responses 明确支持两种流式通道。SSE 用于简单文本流;WebSocket 则用于双向状态流。通过 WebSocket,客户端可以发送{"type": "input", "content": "继续分析"}{"type": "control", "action": "pause"},服务端也能实时推送{"type": "tool_result", "id": "t1", "content": "done"}。这才是真正支持 agent 交互的流式模型。我们在投研助手的“多轮策略推演”功能中,用 WebSocket 实现了:用户说“基于刚才的结论,模拟加息 25bp 的影响”,客户端发input事件;模型调用利率预测工具后,服务端推tool_result;客户端收到后,自动触发下一轮input,全程无页面刷新,延迟 < 200ms。

  • Anthropic 的 SSE withmessage_stopevent:Anthropic SSE 在末尾会发送一个event: message_stop,明确标识本次响应结束。这比 OpenAI 的data: [DONE]更语义化,便于客户端做精准的 loading 状态控制。但 Anthropic 不支持 WebSocket,所有控制指令(如stop_sequences)必须在初始请求体里声明,无法运行时动态调整。

  • Gemini 的 SSE withfinish_reason:Gemini SSE 的finish_reason字段(STOP/MAX_TOKENS/SAFETY)提供了更丰富的终止原因,便于客户端做差异化处理(如SAFETY触发时显示“内容受限”提示)。但 Gemini 的流式parts是逐个推送的,一个part可能包含多个 token,也可能只含一个,客户端需自行 buffer 合并。

实操心得:不要在应用层直接解析 raw SSE stream。我们封装了StreamParser类,统一处理:event type 识别、data 解析、JSON parse 错误恢复、[DONE]/message_stop识别、finish_reason映射。对于 WebSocket,我们实现了ResponseChannel类,封装连接管理、心跳保活、reconnect 逻辑、message type router。这些抽象让上层业务代码完全不用关心底层传输细节。

2. OpenAI Responses API:不是接口升级,而是运行时重构

很多团队把responses当作chat/completions的“v2 版本”,这是最大的认知偏差。chat/completions是一个功能接口(feature interface),解决“如何让模型生成文本”;responses是一个运行时接口(runtime interface),解决“如何让模型作为一个可编排、可观察、可扩展的计算单元运行”。理解这一点,是判断何时该迁、如何迁移的前提。

2.1 为什么chat/completions的抽象已到极限?

chat/completions的设计基因刻在 2022 年:GPT-3.5 Turbo 刚发布,市场焦点是“聊天机器人”。它的核心假设非常清晰:

  • 输入 = 一段对话历史(messages
  • 输出 = 一条新的 assistant 消息(choices[0].message
  • 能力 = 文本生成(content

这个假设在单一文本问答场景下坚如磐石。但当产品形态进化,它开始处处掣肘:

  • 多模态场景:你要传一张财报截图,messages里塞{"type": "image_url", ...}是 workaround,不是 first-class 支持。messagescontent字段本意是文本,硬塞二进制数据违背设计直觉。更糟的是,messages数组的每个元素都必须有role,而图像本身没有“角色”,强行赋予user角色,语义失真。

  • 工具调用场景chat/completionstool_calls是模型“建议”调用,tool_results是用户“提供”结果,二者割裂。模型无法知道工具调用是否成功,用户也无法告诉模型“这个工具结果不可信,换一个”。整个链路是开环的。

  • 结构化输出场景chat/completions没有 schema 声明机制。你只能靠 prompt engineering 诱导模型输出 JSON,但模型不保证格式,也不校验字段。当金融客户要求revenue字段必须是 number 类型,chat/completions给你返回"revenue": "123.45"(字符串),你就得在应用层做类型转换,一旦转换失败,整个流程中断。

  • 状态化工作流场景chat/completions要求你把全部历史messages传上去,10 轮对话就是 10 个对象,token 开销巨大。更致命的是,历史是“只读快照”,模型无法修改历史中的某一条消息(比如修正上一轮的错误事实),只能生成新消息覆盖。这导致长链路推理中,错误会像滚雪球一样累积。

这些不是小问题,而是chat/completions的抽象模型与新需求之间的范式冲突。OpenAI 没有选择在老接口上打补丁(比如加multimodal_input字段、structured_output_schema字段),而是另起炉灶,用responses构建一个全新的运行时契约。

2.2responses的三大运行时支柱

responses不是堆砌新功能,而是重建三个底层支柱:输入声明、输出承诺、状态契约

支柱一:输入声明(Input Declaration)

responsesinput字段,本质是一个能力声明 DSL(Domain Specific Language)。它不再问“你有什么历史”,而是问“你希望模型执行什么任务”。这个 DSL 支持四种原语:

  • 文本任务"input": "帮我总结"—— 最简声明,等价于messages: [{"role": "user", "content": "..."}]
  • 多模态任务"input": [{"role": "user", "content": [{"type": "text", "text": "分析这张图"}, {"type": "image_url", "image_url": {"url": "..."}}]}]——contentpart数组,每个part是独立的能力单元,type字段是能力类型声明。
  • 工具调用任务"input": [{"role": "user", "content": [{"type": "tool_use", "id": "t1", "name": "search", "input": {"q": "LLM protocol"}}]}]——tool_use是一个声明,告诉模型“请调用 search 工具”,id是本次调用的唯一标识,用于后续tool_result关联。
  • 结构化输出任务"input": [{"role": "user", "content": [{"type": "json_schema", "schema": {"type": "object", "properties": {"revenue": {"type": "number"}}}}]}]——json_schema是一个声明,告诉模型“请输出符合此 schema 的 JSON”,服务端会强制校验,不合规则报错,不返回垃圾数据。

这种声明式设计,让input成为真正的“能力蓝图”。你在写代码时,不再是拼接messages数组,而是构造一个InputTask对象,它的parts属性是一个Part[],每个PartTextPartImagePartToolUsePartJsonSchemaPart的实例。这种面向对象的建模,让代码可读性、可测试性、可扩展性大幅提升。

支柱二:输出承诺(Output Contract)

responsesoutput字段,是一个类型化结果契约。它不承诺“返回一段文本”,而是承诺“返回一个由若干确定类型部分组成的数组”。每个output元素都有type字段,目前支持:

  • "message":纯文本输出,value字段是 string
  • "tool_result":工具调用结果,tool_use_id字段关联input中的idcontent字段是工具返回的原始数据
  • "json_schema":结构化输出,value字段是严格符合 schema 的 JSON object
  • "error":执行错误,codemessage字段提供调试信息

这个契约的关键在于类型安全。当你在 TypeScript 中定义OutputItemunion type:

type OutputItem = | { type: "message"; value: string } | { type: "tool_result"; tool_use_id: string; content: any } | { type: "json_schema"; value: Record<string, any> };

你的 IDE 就能根据item.type自动推导item.valueitem.content的类型,编译期就能捕获item.value.length(当typetool_result时)这类错误。这在chat/completions时代是不可想象的——choices[0].message.content永远是 string,但你永远不知道它里面是纯文本、JSON 字符串、还是代码块。

支柱三:状态契约(State Contract)

responses引入了previous_response_idsession_id两个字段,构建了一个平台侧状态管理契约

  • previous_response_id:指向上一次responses请求的id字段。服务端可以用它检索之前的output,从而实现“基于上一轮结果的续写”。例如,上一轮output里有{"type": "tool_result", "tool_use_id": "t1", "content": "25°C"},这一轮input就可以直接引用t1的结果,无需客户端再传一遍。
  • session_id:一个客户端生成的 UUID,用于标识一个长期会话。服务端可以基于session_id维护会话状态(如用户偏好、上下文缓存、工具调用历史),而无需客户端每次传全量messages。这对于移动端尤其重要——网络不稳定时,messages数组太大容易丢包,而session_id很小,重传成本低。

这个契约把状态管理从“应用侧负担”变成了“平台侧服务”。你在写投研助手时,不再需要在 Redis 里存一个巨大的messages数组,只需存一个session_id和几个关键response_id,状态同步开销降低 90%。

2.3responses如何解决chat/completions的经典痛点?

用投研助手的真实案例,对比两个接口的实现差异:

场景:用户上传一份 PDF 财报,要求“提取营收、净利润、毛利率,并用表格展示,同时调用内部风控 API 校验数据异常”

  • chat/completions实现

    1. 第一步:messages: [{role: "user", content: [{"type": "file_url", "url": "pdf_url"}]}]→ 模型返回content: "已解析,营收 100M,净利润 20M,毛利率 30%"
    2. 第二步:客户端从content里用正则提取数字,构造新messages[{role: "user", content: "调用风控 API 校验:revenue=100000000, profit=20000000, gross_margin=0.3"}]
    3. 第三步:模型返回tool_calls: [{id: "t1", function: {name: "risk_check", arguments: "{...}"}}]
    4. 第四步:客户端执行risk_check,拿到结果,再构造新messages[{role: "tool", tool_call_id: "t1", content: "{result: true}"}]
    5. 第五步:模型返回最终表格。
      总耗时:5 轮 API 调用,平均延迟 2s/轮,总延迟 10s+;错误点:步骤 1 的正则可能失效,步骤 2 的数字格式可能错,步骤 4 的 tool result 可能丢失。
  • responses实现

    1. 单次请求:input: [{role: "user", content: [ {type: "file_url", url: "pdf_url"}, {type: "json_schema", schema: {type: "object", properties: {revenue: {type: "number"}, profit: {type: "number"}, gross_margin: {type: "number"}}}}, {type: "tool_use", id: "t1", name: "risk_check", input: {}} ]}]
    2. 服务端自动:解析 PDF → 提取结构化数据 → 校验 schema → 调用risk_check工具 → 合并结果 → 返回output: [ {type: "json_schema", value: {revenue: 100000000, profit: 20000000, gross_margin: 0.3}}, {type: "tool_result", tool_use_id: "t1", content: {risk_level: "low"}} ]
      总耗时:1 轮 API 调用,延迟 3s;错误点:0 —— schema 校验在服务端完成,tool_resulttool_use由服务端自动关联。

这就是responses的威力:它把原本需要客户端 orchestrate 的 5 步,压缩成服务端 atomic execution 的 1 步。input是声明,output是承诺,session_id是状态锚点。这不是接口变短了,而是抽象层级变高了。

2.4 为什么responses不是“为了流式”?一个被严重误解的点

网上充斥着“responses是流式接口,chat/completions是非流式”的说法,这是彻头彻尾的误读。真相是:流式能力与 endpoint 路径无关,只与stream参数和底层传输协议有关

  • chat/completions支持stream: true,返回 SSE,choices[0].delta.content是增量文本。
  • responses同样支持stream: true,返回 SSE,output数组是增量推送的(先推{"type": "message", "value": "正在"},再推{"type": "message", "value": "分析"})。
  • 两者也都支持stream: false,返回完整 JSON。

那么区别在哪?在于流式内容的语义粒度

  • chat/completions的流式是token 粒度:每个 event 是一个或几个 token,delta.content是字符串增量。你无法知道这个 token 是属于message还是tool_calls,因为delta只有content字段。
  • responses的流式是output item 粒度:每个 event 是一个完整的output元素。第一个 event 可能是{"type": "message", "value": "正在分析"},第二个是{"type": "tool_result", "tool_use_id": "t1", "content": "25°C"}。客户端可以精准地按type分发:message→ 渲染到 UI,tool_result→ 调用下游,json_schema→ 更新 state。

这才是responses流式的价值:语义化流式(Semantic Streaming)。它让流式不再只是“更快看到开头”,而是“更早获得可执行结果”。在投研助手里,用户上传 PDF 后,UI 立即显示正在解析文档...messageevent),1 秒后显示风控校验通过tool_resultevent),3 秒后渲染出结构化表格(json_schemaevent)。整个过程是渐进式、可感知、可交互的,而不是等待 3 秒后一次性弹出所有内容。

2.5 为什么 `

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询