OpenAI兼容接口返回200却没内容:从JSON字段到SSE断流的排查路径

📅 2026/7/2 11:08:59
OpenAI兼容接口返回200却没内容:从JSON字段到SSE断流的排查路径
调用 OpenAI 兼容接口时最让人困惑的故障往往不是 401、404 或连接超时而是请求看起来“成功”了HTTP 状态是 200curl 也能收到字节客户端界面却没有任何答案。换一个客户端可能正常打开流式后又卡住日志里能看到 JSON业务代码读取的字符串仍然为空。此类问题如果只盯着状态码很容易在模型名、密钥和地址之间反复试错。真正需要检查的是一份完整的响应契约。HTTP 状态只回答请求是否按 HTTP 语义完成Content-Type 决定正文应该按 JSON、事件流还是其他格式解释应用字段决定文本、工具调用、拒绝信息和结束原因位于哪里传输层又可能把一个事件拆成多个网络分块。任意一层理解错误都可能把有效响应变成空白页面。本文不讨论某个服务一定支持哪些模型、价格或并发也不把“兼容”理解为所有字段完全相同。目标是建立一条可复现的排查路径先冻结失败证据再区分非流式和流式解析定位 UTF-8 分块、SSE 事件边界、代理缓冲、字段映射与超时预算最后用响应夹具把修复固化成回归测试。一、先冻结失败现场不要立即重试覆盖证据第一次出现空响应时最重要的动作不是刷新页面而是保存同一次请求在各层留下的最小证据。至少记录本地 trace_id、调用时间、客户端和解析器版本、最终主机与路径模板、模型字符串、stream 开关、状态码、Content-Type、响应字节数、首字节时间、首事件时间、总耗时和结束原因。提示正文与完整回答默认不记录密钥、Cookie 和用户资料必须脱敏。“同一次请求”是关键。很多团队把浏览器里的一次失败、网关日志里另一次成功和 curl 的第三次测试拼成一个故事最后得出错误结论。每次探针都要使用独立追踪标识并尽可能让客户端、代理和上游日志携带同一标识。若上游不支持自定义请求头也应在本地记录时间窗口、目标和响应请求 ID。原始正文不宜直接写入普通日志。可以保存受控样本状态码、媒体类型、总字节数、前后少量脱敏字符、正文哈希、事件数量和 JSON 顶层键。这样既能判断收到的是 HTML、JSON 还是事件流也不会把完整提示、回答或凭据扩散到监控系统。需要复现时再从受控存储读取脱敏夹具。还要同时记录“用户看到的现象”。空白、一直加载、瞬间结束、显示半句话、中文乱码、重复段落和报 Non-JSON 并不是同一个问题。把现象映射到首字节、事件数量、累计文本和完成信号通常能快速缩小范围零字节更接近传输问题有事件但累计文本为零更接近字段映射有文本却不结束则要检查完成事件和连接状态。建立证据快照时还应保存测试入口。浏览器页面、桌面客户端、SDK、工作流节点和直接 HTTP 请求可能经过不同代理、不同配置文件与不同解析器。只有入口也被记录才能解释“同一密钥在 A 正常、在 B 空白”。建议把入口名称、构建版本和配置摘要加入 trace 上下文并为每次人工复测保留明确步骤。复测不要使用真实长对话采用固定的短提示与确定的期望结构减少模型输出差异对判断的干扰。二、HTTP 200 只说明成功不能替代正文校验RFC 9110 对 200 的定义是请求成功响应内容的含义取决于请求方法。它没有承诺正文一定是客户端期待的 JSON更没有承诺某个业务字段一定包含文字。代理健康页、登录页、网关包装对象甚至空正文都可能伴随 200。客户端若看到 200 就直接访问固定字段错误会在更下游表现成 undefined、空数组或空字符串。第一道校验是状态与正文是否允许共存。HEAD、204 和 304 等情况本来就不应按普通正文处理对 POST 得到 200 时也要检查消息分帧是否表明存在内容。第二道校验是 Content-Type。application/json 应进入完整 JSON 解析路径text/event-stream 应进入 SSE 解析路径text/html 通常意味着命中了网页、错误页、WAF 或登录流程。不要为了“兼容更多服务”完全忽略媒体类型并猜正文。RFC 9110 明确说明 Content-Type 用于表达表示形式的媒体类型盲目嗅探会让不同响应落入错误解析器也可能引入安全问题。工程上可以为少量已知错误提供显式兼容分支但必须记录原始类型、匹配条件和退出方式不能把猜测当成默认路径。还要警惕状态被中间层改写。某些网关把上游 4xx 或 5xx 包装成自己的 200 JSON业务错误被放在 error、code 或 message 中另一些代理返回 200 HTML 错误页。判断响应来源时应比较 Server、Via、自定义请求 ID、Content-Type 与正文结构而不是只看最终数字。因此日志至少要形成三层结论HTTP 层是否成功、媒体层能否选择解析器、业务层是否满足字段契约。只有三层都通过才能把结果标记为“可消费成功”。否则应保留最早失败层让界面显示结构化错误而不是安静地返回空字符串。客户端还要验证 Content-Encoding 与实际解码路径。响应声明 gzip 或其他编码时中间层可能已经解压重复解压会报错中间层若修改正文却保留旧的 Content-Length也会产生截断或等待。调试时同时保存原始响应头和到达应用后的规范化头标注是哪一层完成了解压、分块重组和字符解码。不要仅从浏览器开发者工具截图推断后端容器实际收到的消息两条链路可能使用不同协议版本和代理规则。三、非流式 JSON 要先验证容器再读取文本非流式响应的优势是正文完整后再解析但“能 JSON.parse”仍不代表有答案。稳健的校验顺序应从外到内正文是否非空并能按 UTF-8 解码JSON 是否可解析且顶层类型正确是否为明确错误对象候选或输出数组是否存在目标项是否包含文本、工具调用、拒绝信息或结构化结果最后再读取结束原因与用量。下面的 JavaScript 示例故意不直接返回空字符串。它先验证媒体类型和顶层结构再把不同输出分支显式区分。字段名称必须依据实际接口资料调整不能把这段示例当作所有兼容服务的固定 schema。exportasyncfunctionreadNonStreamingResponse(response){constcontentTyperesponse.headers.get(content-type)||;constrawawaitresponse.text();if(!contentType.toLowerCase().includes(application/json)){thrownewError(Expected JSON, received${contentType||unknown});}if(raw.trim()){thrownewError(Response body is empty);}constbodyJSON.parse(raw);if(bodytypeofbodyobjectbody.error){thrownewError(Upstream error:${body.error.code||unknown});}if(!Array.isArray(body?.choices)||body.choices.length0){thrownewError(Missing non-empty choices array);}constmessagebody.choices[0]?.message;return{text:typeofmessage?.contentstring?message.content:null,toolCalls:Array.isArray(message?.tool_calls)?message.tool_calls:[],finishReason:body.choices[0]?.finish_reason??null,};}这里把 text 设为 null 而不是自动变成空字符串是为了保留“字段不存在”和“字段存在但内容为空”的差异。工具调用场景可能合法地没有普通文本拒绝或结构化输出也可能位于其他字段。如果适配器无条件执行 content || “”这些分支会被压扁成同一种空白现象。错误响应同样需要契约。为常见的 400、401、404、429 和 5xx 保存脱敏结构样本断言错误类型、参数名、请求 ID 和可重试性能够传到调用方。不要只测试成功夹具很多“200 空白”其实是网关改写后的错误对象而客户端没有检查 error 分支。数组与索引也不能想当然。某些响应可能有多个候选某个候选只有工具调用另一个才有文本也可能选择索引不是连续到达。流式和非流式适配器应使用明确的候选索引建立状态而不是永远覆盖 choices[0]。若产品只展示一个候选应在适配层写清选择规则并在日志保留候选数量、被选索引和各候选输出类型。这样出现“服务端有内容、界面无内容”时能判断是选择规则还是字段读取失败。四、地址必须区分主机、接口前缀和完整资源响应结构异常有时并不是解析器问题而是请求发到了错误资源。Base URL 在不同 SDK、客户端和插件里可能表示主机、版本前缀或完整端点。若调用方自动追加 /chat/completions而用户又填写了完整资源路径最终地址可能重复若调用方期待版本前缀却只拿到主机则可能命中网站首页或默认路由。排查时保存三列值用户输入、客户端拼接规则、最终出站 URL。本文事实白名单仅确认三个层级字符串主机 https://api.vectorengine.cn OpenAI 兼容接口前缀 https://api.vectorengine.cn/v1 以及 Chat Completions 完整资源 https://api.vectorengine.cn/v1/chat/completions 。这些字符串只说明地址层级不代表任何具体模型、价格、额度、流式格式或服务承诺。直接 HTTP 探针应使用完整资源SDK 配置通常更可能使用前缀但最终以该 SDK 或插件的文档为准。不要把在 curl 中可用的完整端点原样粘进所有 base_url 字段。反过来curl 若只请求接口前缀也不能证明聊天资源可用。重定向也会改变结果。301、302、307、308 对方法和正文处理存在差异跨主机跟随时还涉及认证头是否保留。生产配置应尽量使用最终稳定地址探针需要记录每一跳的状态、Location、目标主机和媒体类型。遇到证书主机名不匹配或跳转到陌生域名时应停止核对不能关闭证书验证继续试。如果需要查看与该地址层级相关的资料入口可在排查完成后阅读 https://178.nz/dn 。它不替代接口文档也不能作为未记录产品能力的依据。任何模型清单、速率、日志和隐私结论仍需单独的可核验来源。路径字符串之外还要核对 HTTP 方法和请求体。命中正确资源但使用错误方法可能得到 405请求被代理改成 GET 或丢失正文也可能落到默认页面。保存最终方法、请求 Content-Type、正文长度与非敏感字段集合可把“地址错误”和“请求形态错误”分开。对于会自动补尾斜杠的组件应比较补全前后是否发生重定向以及重定向后 POST 正文是否仍被保留。任何自动规范化都应有单元测试而不是依赖线上猜测。五、SSE 必须按事件边界解析不能按网络分块解析流式接口通常让客户端边生成边消费但网络读取返回的 chunk 只是任意大小的字节片段。一个 SSE 事件可能跨两个甚至多个 chunk一个 chunk 也可能包含多个完整事件以及下一个事件的一部分。把每个 chunk 直接当成一段 JSON 解析测试环境可能偶尔成功网络波动或中文内容一出现就会报截断。WHATWG 的 Server-sent events 规范定义了事件流的行与事件边界。常见数据以 data: 字段承载事件由空行结束连续 data 行需要按规则组合。客户端应先把字节流连续解码成文本再按换行维护缓冲遇到完整空行后才构造一个事件。注释、空字段、不同换行形式和连接关闭都需要明确处理。下面是一个简化的异步生成器只展示核心原则跨 chunk 保留文本缓冲以空行切分事件并组合 data 行。真实项目还要处理 CRLF、注释、event、id、retry、最大事件大小和取消信号。exportasyncfunction*parseEventStream(readable){constdecodernewTextDecoder(utf-8);letbuffer;forawait(constchunkofreadable){bufferdecoder.decode(chunk,{stream:true});bufferbuffer.replace(/\r\n/g,\n);letboundary;while((boundarybuffer.indexOf(\n\n))0){constblockbuffer.slice(0,boundary);bufferbuffer.slice(boundary2);constdatablock.split(\n).filter((line)line.startsWith(data:)).map((line)line.slice(5).replace(/^/,)).join(\n);if(data!)yielddata;}}bufferdecoder.decode();if(buffer.trim()!){thrownewError(Stream ended with an incomplete SSE event);}}解析器还应设置上限防止没有事件边界的无限缓冲耗尽内存。超过单事件大小、长时间没有完整事件或连接关闭仍有残留时应返回明确错误并记录缓冲长度与哈希。不要安静丢弃残留否则用户只会看到答案少了最后一段。SSE 解析还要处理心跳与注释行。以冒号开头的行通常用于保持连接并不承载业务数据若客户端把它当 JSON会产生周期性解析错误。相反完全忽略所有空 data 事件也可能丢掉协议定义的状态变化。实现时先解析字段再把业务负载交给上层适配器。事件 ID 与重连间隔是否使用应由产品需求和服务端契约决定不支持时明确忽略并记录不要部分实现后给人可恢复的错觉。六、中文乱码与偶发JSON错误要检查流式UTF-8解码UTF-8 中文字符通常由多个字节组成网络分块可能恰好从字符中间切开。若代码对每个 Uint8Array 单独执行一次非流式 decode解码器会把不完整序列替换成特殊字符替换符落在 JSON 字符串中会造成内容乱码落在结构位置则可能让 JSON.parse 失败。问题之所以“偶发”只是分块位置每次不同。正确做法是复用同一个 TextDecoder并在处理中间 chunk 时传入 stream: true让解码器保留未完成字节。流结束后再调用一次不带 chunk 的 decode 刷新内部缓冲。Node.js 的 Web Streams 还提供 TextDecoderStream可直接把字节流转换为连续文本流但事件边界仍需由 SSE 解析器处理。测试不能只用纯 ASCII。应准备包含中文、emoji、组合字符、反斜杠、换行和长文本的夹具并主动在每个可能字节位置切分。对于一个已知事件序列遍历不同分块方案后累计文本、事件数量与结束原因都应一致。只有这样才能证明解析器不依赖网络恰好给出的边界。乱码也可能来自错误的 Content-Type charset、代理重复转码或服务端本身输出无效字节。客户端应记录媒体类型参数、解码失败位置和原始字节片段的哈希不要把原始私人内容直接写日志。若启用 fatal 解码模式失败应转成结构化协议错误而不是崩溃或静默替换。还要区分“字符完整但 JSON 不完整”。即使 UTF-8 解码正确也不能在每次收到文本片段后立刻 JSON.parse必须先等到完整 SSE 事件。文本解码解决字节边界事件解析解决消息边界JSON 解析解决负载结构三步不能合并成一次猜测。测试分块时需要覆盖换行边界。回车与换行可能分在不同 chunkdata 前缀本身也可能被切开空行分隔符更可能跨块出现。解析器只能在连续文本缓冲上识别事件不能先对每块 split。建议把一条包含中文和多行 data 的完整事件编码为字节再在每一个字节位置切成两段执行测试随后加入三段和随机多段组合。若任何组合得到不同累计文本就说明实现仍依赖传输边界。七、上游持续发送但界面卡住要检查代理缓冲流式链路至少包含上游、反向代理或网关、应用后端和客户端。上游能生成事件不代表每一跳都会立即转发。代理可能为了压缩、吞吐或响应转换而缓冲若干字节某些网关直到缓冲区满或连接结束才下发用户看到的效果就从“逐字出现”变成“长时间空白后一次性显示”。定位缓冲需要四个时间点上游写出首事件、代理收到首字节、客户端收到首字节、客户端解析出首个完整事件。只有客户端总耗时没有意义。首字节很快但首事件很慢可能是事件未完成或解析器等待错误边界上游首事件很快、客户端首字节很慢更像中间层缓冲或排队。curl 可用于观察响应头和流式到达节奏。–no-buffer 减少 curl 自身的输出缓冲–include 显示响应头–write-out 输出统计值。以下命令只使用占位密钥和模型执行前必须替换为已核验值curl--show-error--silent--no-buffer--include\--requestPOSTREPLACE_WITH_VERIFIED_CHAT_ENDPOINT\--headerAuthorization: Bearer${VECTORENGINE_API_KEY}\--headerContent-Type: application/json\--data{model:REPLACE_WITH_VERIFIED_MODEL_ID,messages:[{role:user,content:reply with three short lines}],stream:true}\--write-out\nstatus%{http_code} first_byte%{time_starttransfer} total%{time_total}\n不要因为 --no-buffer 后仍卡住就断言上游没有流。该选项只影响 curl 的输出行为不能关闭远端代理缓冲。应对比同网络位置的直接上游探针与经过网关的探针并检查响应压缩、缓存、空闲超时、正文转换和 HTTP 版本。任何配置变更先在隔离环境验证不要在生产高峰直接关闭所有代理功能。连接中途断开时代理和客户端都要传播取消信号。若浏览器已关闭而后端仍读取上游资源会继续消耗若后端重试又没有幂等或请求去重用户可能收到重复片段。日志中应记录取消来源、最后完整事件序号和是否观察到完成信号。代理排查要避免一次修改多个变量。可以先保持请求不变只比较直连上游和经过网关的首字节、首事件与事件间隔再分别关闭响应转换、压缩或缓存中的一个选项。每次变更记录配置版本和回滚值。若直连与网关都正常而浏览器异常继续检查应用后端是否先把整个上游响应读完再返回。很多所谓“网关缓冲”其实发生在业务代码调用 response.text 或聚合流的那一行。八、超时不能只有一个数字要拆成阶段预算“请求 60 秒超时”无法说明卡在哪里。一个可靠的客户端至少区分连接超时、等待响应头或首字节的超时、等待首个完整事件的超时、事件间空闲超时和整次调用总时限。不同阶段对应不同责任方也对应不同重试策略。DNS、TCP 与 TLS 阶段失败时通常没有 HTTP 状态应保留底层错误类别和目标主机。收到响应头但长时间没有正文可能是上游排队、模型处理、代理缓冲或错误的 Content-Length。已经收到若干事件后触发空闲超时则要记录最后事件时间、累计字节和事件序号判断是上游停滞还是连接丢失。总时限用于限制整个调用占用的资源空闲时限用于识别流停止推进两者不能互相替代。长任务可能持续超过常规总时长但事件一直推进也可能连接一直保持却从未产生有效事件。产品界面应分别显示“正在等待首包”“正在接收”“已取消”和“超时失败”避免用户误以为按钮无效而重复提交。重试前先判断请求是否可能已被上游执行。传输中断不代表服务端没有生成或计费流式调用也可能已经输出部分文本。认证、路径、参数和结构错误不应自动重试429 或临时服务错误是否重试、等待多久必须依据服务方响应和文档。统一采用指数退避、次数上限、总预算与随机抖动并让取消信号终止后续重试。超时指标应按目标、模型标识、客户端版本、解析器版本、是否流式和失败阶段分组。只看平均总耗时会掩盖首事件长尾和少量连接失败。对空响应问题事件数量、累计文本长度和完成信号比例通常比平均耗时更有解释力。时间预算还需要与用户体验对应。等待首事件期间可以显示明确状态并允许取消已经收到内容后触发总时限应保留已验证片段并标记不完整而不是把整个回答清空。后台任务与交互式问答可采用不同预算但必须在配置中分开命名。测试时用可控服务器分别延迟响应头、首事件和中间事件确认触发的是预期错误类型并验证取消后套接字、读取循环和重试计时器都已释放。九、content为空不一定失败要检查非文本输出与完成信号很多适配器把“答案”硬编码为 choices[0].message.content。一旦返回工具调用、结构化对象、拒绝信息或不同输出类型content 可能为空或不存在但响应并非没有结果。解析器应先识别响应类别再决定哪一字段可展示不能用可选链加空字符串把所有差异吞掉。第一步检查错误对象。部分网关即使返回 200也会把错误放在顶层 error、code 或自定义字段中。第二步检查候选是否存在及其类型。第三步检查 message 下的普通文本、工具调用或其他明确分支。第四步检查 finish_reason 或等价完成字段确认输出是正常结束、长度受限、工具调用还是被拒绝。流式场景还要累计增量。某个单独事件没有文本很常见它可能只提供角色、元数据、工具参数片段或完成原因。客户端不能因为第一帧没有 content 就提前返回空白也不能只保留最后一帧。应按选择索引和输出类型建立状态机依次合并文本与结构化片段。连接关闭也不等于业务完成。解析器需要知道是否观察到协议规定的完成事件或结束标记。若连接在没有完成信号时关闭即使已经收到部分文本也应把结果标记为不完整并允许界面展示“收到部分内容”而不是伪装成正常结束。日志保存累计字符数、事件数和最后一个完成状态即可。对字段迁移保持显式版本适配。若客户端同时支持多种响应 API不要在一个函数里连续尝试十几个路径最后取第一个真值。为每种声明的协议建立独立适配器和契约测试未知结构直接报错并保存顶层键摘要。明确失败比错误地显示空字符串更容易修复。对于工具调用参数通常也是增量到达的字符串片段不能在每一帧上单独解析 JSON。适配器应按候选和工具调用索引累积函数名与参数直到收到明确结束状态后再做结构校验。若参数最终不完整应返回“工具参数截断”而不是空文本。结构化输出同样需要区分生成中的片段和完成后的对象。把所有非文本分支都映射成一段字符串会让上层无法安全决定是否执行工具或展示错误。十、日志要能定位问题也要避免保存提示与密钥排查空响应不需要记录所有正文。最小可观测字段包括 trace_id、时间、客户端版本、解析器版本、目标主机摘要、路径模板、HTTP 状态、Content-Type、Content-Encoding、响应字节数、事件数、首字节时间、首事件时间、总耗时、累计文本长度、结束原因和取消来源。URL 查询参数、Authorization、Cookie、完整请求体、完整回答和内部网络地址默认不记录。若需要比较请求是否一致可对规范化后的非敏感字段计算哈希需要保存失败响应时先脱敏再放入受访问控制、短保留期的样本库。公开 issue 只附结构、版本和最小复现不上传真实用户内容。跨组件追踪时字段名称要统一。客户端叫 request_id、网关叫 trace、上游又叫 x-request-id会让关联困难。建立一个内部 trace_id 并同时保存上游返回的请求 ID日志里明确记录父子关系。时钟也要同步否则毫秒级事件顺序可能被错误解释。指标与日志承担不同任务。指标用于发现空响应率、首事件长尾、解析错误率和未完成连接比例日志用于还原单次请求。不要把高基数字段、完整模型输入或用户标识塞进指标标签。告警触发后再根据 trace_id 查询受控日志。每次修改解析器都要评估日志变化。新增兼容分支若没有对应计数团队不知道它被触发多少次错误被转换为 fallback 后表面成功率可能上升实际空字符串率却恶化。把“协议未知”“媒体类型不符”“事件残留”“字段缺失”和“正常非文本输出”分成不同类别。日志采样也要有规则。成功请求只保留低比例结构指标协议错误和未完成连接可以提高采样率但仍不能突破脱敏边界。对正文摘要使用白名单而不是黑名单仅记录顶层键、类型、长度和哈希不尝试用正则从完整文本中删除秘密。访问失败样本库需要最小权限与审计过期自动清理。若团队无法保证这些条件就只保存可重新生成的合成夹具不保存真实响应。十一、用响应夹具和发布闸门防止空响应回归修复一次线上样本并不代表解析器稳定。应把脱敏后的响应转成夹具覆盖非流式成功、非流式错误、200 HTML、空正文、空候选、工具调用、合法空文本、标准 SSE、跨 chunk 事件、一个 chunk 多事件、跨块中文、连接提前关闭、缺少完成信号和超大事件。夹具不要依赖真实网络。测试输入是确定的字节序列与分块方案断言包括选择的解析器、事件数量、累计文本、工具参数、结束原因、错误类别和是否保留部分结果。对同一个 SSE 文本随机生成多种分块方式输出必须一致这类性质测试比固定三个 chunk 更容易发现边界缺陷。发布闸门至少包含三层静态检查保证示例和配置结构有效单元测试验证解码器、事件解析器和字段适配器端到端测试通过受控模拟服务器验证响应头、延迟、截断和取消。只有全部通过解析器版本才能进入灰度。生产灰度关注空响应率、解析失败率、首事件分布和连接未完成比例。故障复盘要写清直接原因和系统缺口。直接原因可能是把 chunk 当事件、对每块独立解码、代理缓冲或字段路径错误系统缺口可能是缺少跨分块夹具、没有媒体类型校验、日志不记录事件数量或发布前没有灰度。只修一行代码但不补测试同类问题仍会回来。最终验收不是“界面出现了一次文字”而是同一请求能在非流式和需要的流式路径中稳定满足契约错误能保留来源与类型取消和超时不会制造重复调用日志不泄露敏感内容真实失败样本已经进入回归集合。做到这些HTTP 200 却没有答案就不再是玄学而是一组可以逐层验证的工程条件。回归夹具还应绑定协议版本和来源。相同字段名在不同 API 或适配器版本中可能含义不同不能把所有样本扔进一个目录让测试自动猜。为每类契约设置明确标识、媒体类型、分块方案和预期结果升级时新增适配器版本旧夹具继续运行直到完成迁移并记录下线理由。这样既能防止新修复破坏旧客户端也能让团队知道某个兼容分支何时可以安全删除。把整套方法压缩成值班清单可以依次回答十二个问题最终请求去了哪个资源HTTP 方法与请求体是否正确状态码由哪一层产生Content-Type 与 Content-Encoding 是什么正文实际有多少字节非流式 JSON 是否完整流式文本是否连续解码SSE 是否按空行形成事件代理是否延迟首字节或首事件累计输出属于文本还是工具调用是否观察到完成信号失败样本是否已进入回归。任何一步没有证据就停在该步补采集不要同时改密钥、地址、模型和解析器。团队还应为“兼容”设定明确边界。兼容某个资源路径不等于兼容全部请求参数、响应字段、流式事件、工具调用和错误格式。接入评审要列出当前实际使用的能力只对这些能力建立契约与回归未验证能力标记为未知而不是默认支持。客户端升级、网关变更或上游版本切换时先用同一夹具和合成服务器完成差分再逐步放量。这样做不仅能解决今天的空响应也能在未来字段演进时快速判断是协议变化、适配器缺陷还是传输链路异常。最后要保留一个最小可运行探针。它使用固定短提示、占位密钥、经过核验的模型标识并同时测试非流式与流式路径输出只包含状态、媒体类型、事件数、累计字符数、结束原因和各阶段耗时。探针与业务客户端共享契约断言但不共享复杂界面状态。线上出现空白时先在同一网络位置运行探针探针也失败优先查上游与链路探针成功而业务失败优先查客户端配置、字段映射和渲染状态。清晰的分界能显著减少跨团队推诿和无效重试。参考资料OpenAI, API Overview, https://developers.openai.com/api/reference/overviewOpenAI, Chat Completions API Reference, https://developers.openai.com/api/reference/resources/chat/completionsOpenAI, Streaming API responses, https://developers.openai.com/api/docs/guides/streaming-responsesWHATWG, Server-sent events, https://html.spec.whatwg.org/multipage/server-sent-events.htmlMDN Web Docs, Using server-sent events, https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_eventsRFC Editor / IETF, RFC 9110: HTTP Semantics, https://www.rfc-editor.org/rfc/rfc9110.htmlRFC Editor / IETF, RFC 9112: HTTP/1.1, https://www.rfc-editor.org/rfc/rfc9112.htmlNode.js, Web Streams API, https://nodejs.org/api/webstreams.htmlNode.js, Stream, https://nodejs.org/api/stream.htmlcurl project, curl — How To Use, https://curl.se/docs/manpage.htmlIANA, Media Types, https://www.iana.org/assignments/media-types/media-types.xhtml