AI API空响应contentreasoning流式响应OpenAI 兼容 API

AI API 返回空内容:content、reasoning 与响应格式排查

AI API 请求成功却没有文本?本文从 content、tool_calls、reasoning、finish_reason、流式响应和多协议解析入手,定位空字符串、null 与候选结果缺失。

AI API 返回空内容:content、reasoning 与响应格式排查

AI API 返回成功但正文为空的排查路径

AI API 返回 200 OK,Token 用量也不为零,但程序最终拿到的却是:

""
null
undefined
No content

这类问题不一定是模型“没有回答”。更常见的情况是:客户端读取了错误字段、模型正在调用工具、推理内容与最终文本被分开返回、流式事件没有正确拼接,或响应被安全策略和长度限制提前终止。

排查时不要只打印 content。先保留脱敏后的完整响应结构,再回答三个问题:

  1. 文本究竟位于哪个协议字段?
  2. 本轮是否本来就不应该返回普通文本?
  3. 响应为何结束,客户端有没有完整消费它?

本文提供一套适用于 OpenAI Chat Completions、Responses API、Claude Messages、Gemini 以及第三方兼容网关的排查方法。

先区分四种“空”

空字符串、null、缺失字段和无候选结果的区别

表现含义优先检查
content: ""字段存在但文本为空工具调用、长度限制、服务端转换
content: null本轮没有普通文本内容tool_calls、拒绝字段、协议定义
没有 content 字段读取路径可能不适用于当前 APIResponses、Claude、Gemini 的原生结构
choices / candidates 为空没生成可用候选安全策略、Prompt 反馈、网关异常
流式结束但累计文本为空事件被漏读或只返回非文本事件SSE 解析、事件类型、连接中断
有 reasoning、没有最终答案推理与答案分离,或生成提前结束输出 Token、结束原因、解析器

不要用下面这种逻辑覆盖所有 API:

const text = response.choices[0].message.content;

它只适用于特定 Chat Completions 响应,而且仍需处理 choices 缺失、contentnull、工具调用和拒绝等情况。

第一步:保存脱敏后的原始响应

排错时至少记录:

HTTP 状态码
Content-Type
API 路径
模型 ID
是否 stream=true
响应顶层字段名
choices / output / content / candidates 的数量
finish_reason / stop_reason / finishReason
usage
请求 ID

不要记录完整 API Key、Authorization、Cookie、用户隐私数据或未脱敏代码。原始响应也不等于可以整段写入生产日志;推荐只保留结构和必要诊断字段。

Node.js 中可以先安全打印结构:

console.dir(response, {
  depth: 6,
  maxArrayLength: 20,
});

如果 SDK 已经把响应转换成类对象,也可以用官方提供的序列化方法。不要只打印你认为应该存在的 content

OpenAI Chat Completions:先看 message 的完整结构

典型文本响应位于:

response.choices[0].message.content

message 不只包含文本。一个兼容客户端应同时检查:

const choice = response.choices?.[0];
const message = choice?.message;

console.log({
  content: message?.content,
  toolCalls: message?.tool_calls,
  refusal: message?.refusal,
  finishReason: choice?.finish_reason,
});

content 为 null,但有 tool_calls

当模型决定调用工具时,本轮核心输出可能是结构化工具请求:

{
  "message": {
    "role": "assistant",
    "content": null,
    "tool_calls": [
      {
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\":\"Shanghai\"}"
        }
      }
    ]
  },
  "finish_reason": "tool_calls"
}

这不是空响应。客户端应:

  1. 解析工具名和参数;
  2. 执行允许的工具;
  3. 把工具结果作为新消息发回;
  4. 再读取模型的最终文本。

如果 UI 只渲染 content,工具调用会被错误显示成“模型没回答”。

finish_reason 是 length

finish_reason: "length" 通常表示输出达到限制,最终答案可能尚未生成。推理模型尤其可能先消耗较多推理 Token,留给可见答案的空间不足。

处理方式:

  • 提高允许的输出 Token;
  • 缩短输入上下文;
  • 要求先给简短结论;
  • 检查模型使用的是 max_tokensmax_completion_tokens 还是 Provider 自定义字段;
  • 查看 usage 中可见输出与推理 Token 的细分(若服务提供)。

不要在不知道字段语义时同时发送多个 Token 上限参数。

finish_reason 是 content_filter

如果候选因安全策略停止,文本可能为空或不完整。应读取 Provider 返回的过滤、拒绝或安全元数据,而不是自动重复同一个请求。盲目重试通常不会改变确定性的策略结果。

Responses API:不要继续假设 choices[0]

Chat Completions、Responses、Claude 与 Gemini 的文本字段路径

Responses API 的结果通常由 output 项组成,每一项又包含不同类型的内容。SDK 可能提供 output_text 这类便捷聚合字段,但原始 HTTP 结构并不是 Chat Completions 的 choices[0].message.content

稳健的解析器应按类型遍历输出,而不是只取固定数组位置:

function collectResponseText(response) {
  if (typeof response.output_text === 'string') {
    return response.output_text;
  }

  const parts = [];
  for (const item of response.output ?? []) {
    if (item.type !== 'message') continue;
    for (const content of item.content ?? []) {
      if (content.type === 'output_text' && content.text) {
        parts.push(content.text);
      }
    }
  }
  return parts.join('');
}

同一响应还可能包含函数调用、工具结果、推理项目或其他类型。只读第一个 output 项,很容易漏掉真正的文本。

如果第三方网关声称兼容 OpenAI,但只实现 Chat Completions,客户端改用 /responses 后可能返回 404、字段不完整或错误转换。此时应核对实际路径和网关支持矩阵。

Reasoning 不等于最终 content

“推理模型消耗了 Token,却没有正文”通常有三种原因:

  1. 推理与最终答案使用不同字段或内容类型;
  2. 输出预算在生成最终答案前耗尽;
  3. 第三方网关转换了 reasoning,但客户端只识别普通 content。

不同服务可能使用不同名称,例如 reasoning item、thinking block、reasoning_content 或 SDK 聚合字段。reasoning_content 并不是所有 OpenAI 兼容服务都遵循的通用标准字段,不能写死为唯一来源。

生产客户端应优先解析最终可见文本,把推理字段视为可选能力。不要把隐藏推理当作必须展示的正文,也不要假设每个 Provider 都允许返回原始思维链。

Claude Messages:content 是内容块数组

Claude Messages 的 content 通常是数组,而不是单个字符串:

{
  "content": [
    {
      "type": "text",
      "text": "The answer is ..."
    }
  ],
  "stop_reason": "end_turn"
}

解析时按 block 类型处理:

const text = (response.content ?? [])
  .filter((block) => block.type === 'text')
  .map((block) => block.text)
  .join('');

如果返回的是 tool_use block,本轮同样可能没有文本。启用扩展思考时,还可能出现 thinking 相关 block。客户端应检查 stop_reason,并完成工具调用循环,而不是把数组强制转换成字符串。

Gemini:检查 candidates、parts 和 finishReason

Gemini 原生响应的文本通常位于候选内容的 parts 中。SDK 的 .text 便捷属性可能替你完成拼接,但原始结构需要检查:

candidates[]
  └─ content
      └─ parts[]

当没有文本时,检查:

  • candidates 是否为空;
  • 每个 part 是文本、函数调用还是其他模态;
  • finishReason
  • Prompt 或候选安全反馈;
  • SDK 是否因安全结果拒绝返回 .text
  • 使用的是否为 Gemini 原生协议,而客户端却按 OpenAI 格式读取。

多模态响应也可能包含图片、函数调用或其他 part。不能默认第一个 part 一定有 text

流式响应为什么最容易“看起来为空”

SSE 流式事件从增量到完整文本的拼接与结束检查

非流式请求一次返回完整对象;流式请求返回一串事件。空内容常来自客户端解析错误:

  • 只读第一帧,而第一帧只有角色信息;
  • 只识别 message.content,没有读取增量 delta
  • 把每个 chunk 覆盖到变量,而不是追加;
  • 在工具调用事件出现时提前结束;
  • 没有处理 Responses API 的事件类型;
  • 把 SSE 的多行 data: 当成普通 JSON;
  • 遇到心跳、空行或结束标记就错误退出;
  • 代理缓冲或中断了流。

Chat Completions 的简化拼接示例:

let text = '';

for await (const chunk of stream) {
  const delta = chunk.choices?.[0]?.delta;
  if (typeof delta?.content === 'string') {
    text += delta.content;
  }

  // tool_calls 也需要按 index 累积参数片段
}

Responses API 则应按事件类型处理文本增量和完成事件。不要把两种流式格式交给同一个未经区分的解析器。

用非流式请求做对照

如果流式结果为空,临时把 stream 设为 false

  • 非流式正常:问题大概率在 SSE、SDK 消费方式或中间代理;
  • 非流式也为空:继续检查协议字段、工具调用、结束原因和输出预算;
  • curl 正常、应用为空:问题多半在应用解析层;
  • 直连正常、经过网关为空:检查网关转换与压缩/缓冲配置。

一套可复用的排查顺序

1. 确认 HTTP 层真的成功

检查状态码和 Content-Type。某些代理会返回 200 加 HTML 登录页或 WAF 页面,JSON SDK 随后可能吞掉错误或产生空对象。

2. 核对最终 Endpoint

确认实际调用的是:

/v1/chat/completions
/v1/responses
/v1/messages
Gemini generateContent / streamGenerateContent

Base URL、协议和客户端必须匹配。可参考什么是 OpenAI 兼容 API

3. 打印响应结构,而不是只打印 content

记录顶层键、数组数量、内容类型和结束原因。避免在日志中保留敏感输入。

4. 检查非文本输出

依次查看:

tool_calls / function_call
refusal / safety feedback
reasoning / thinking
image or other modality

5. 检查结束原因与 Token 用量

如果是 length 或输出 Token 接近上限,先调整预算与上下文。如果是工具调用,执行工具循环。如果是安全策略,修改合法请求或按业务规则提示用户。

6. 用最小请求复现

移除工具、结构化输出、图片、长上下文和复杂系统提示,只保留:

{
  "model": "YOUR_MODEL_ID",
  "messages": [
    {"role": "user", "content": "Reply with exactly: OK"}
  ],
  "stream": false
}

最小请求正常后,再逐项恢复参数。

7. 比较 SDK 与原始 HTTP

SDK 可能重命名字段、聚合文本或隐藏底层事件。使用脱敏 curl 最小请求与 SDK 结果对照,可以快速区分服务端问题和客户端解析问题。

一个兼容多种结果的防御性思路

不要追求一个“万能字段”,而要明确协议并为非文本输出建模:

function parseModelResult(result, protocol) {
  if (protocol === 'openai-chat') {
    const choice = result.choices?.[0];
    return {
      text: choice?.message?.content ?? '',
      toolCalls: choice?.message?.tool_calls ?? [],
      finishReason: choice?.finish_reason ?? null,
    };
  }

  if (protocol === 'anthropic') {
    return {
      text: (result.content ?? [])
        .filter((x) => x.type === 'text')
        .map((x) => x.text)
        .join(''),
      toolCalls: (result.content ?? []).filter((x) => x.type === 'tool_use'),
      finishReason: result.stop_reason ?? null,
    };
  }

  throw new Error(`Unsupported protocol: ${protocol}`);
}

真实生产代码还应验证 Schema、保留请求 ID、区分可恢复错误,并为 Responses 与 Gemini 单独实现解析器。协议适配层比散落在 UI 里的可选链更容易测试。

通过 Nbility 排查空内容

使用 Nbility 模型广场复制准确 Model ID,并先确认模型支持的协议和能力。OpenAI 兼容客户端通常使用:

https://api.nbility.ai/v1

如果 Chat Completions 正常而 Responses 为空或报错,应检查客户端实际调用的 Endpoint。若只有工具调用或推理模型出现问题,请提供脱敏后的:

请求时间与时区
Model ID
API 路径
是否流式
finish_reason / stop_reason
响应顶层字段与内容类型
请求 ID

可通过 Nbility 工单提交这些信息。不要附上真实 API Key 或未脱敏业务数据。

FAQ

content 为 null 一定是错误吗?

不一定。模型发起工具调用时,本轮可能只有 tool_calls;其他协议也可能用内容块或输出项表达结果。先检查完整消息和结束原因。

为什么 usage 有输出 Token,却没有文本?

Token 可能消耗在推理、工具调用或未被当前解析器识别的内容类型上,也可能在最终答案前达到输出上限。检查 usage 细分、响应类型和结束原因。

reasoning_content 才是模型回答吗?

通常不是。它可能是某些网关的扩展推理字段,并非统一标准。最终用户答案应从当前协议定义的可见文本字段读取。

stream=false 正常,stream=true 为空怎么办?

重点检查 SSE 事件解析、增量追加、结束事件、中间代理缓冲以及 SDK 的异步迭代方式。

是否应该对空内容自动重试?

只有确认是暂时性上游问题时才有限重试。工具调用、安全过滤、协议解析错误和 Token 上限不会因盲目重试自动修复。

总结

AI API 的“空内容”不是单一故障。最有效的排查顺序是:

  1. 保存脱敏响应结构;
  2. 确认 API 协议和最终 Endpoint;
  3. 区分空字符串、null、缺失字段与无候选;
  4. 检查工具调用、拒绝、安全反馈和多模态内容;
  5. 读取 finish_reasonstop_reasonfinishReason
  6. 对照非流式与流式请求;
  7. 用最小请求区分服务端、网关和客户端解析问题。

不要把所有模型响应压缩成一个 content 字段。只要按协议解析内容类型,并把结束原因纳入业务逻辑,大多数“200 OK 但没有回答”的问题都能快速定位。

参考资料

相关文章

用 Nbility 跑通你的 Agent 工作流

获取 API Key,统一接入 OpenAI 兼容模型和开发工具。

管理 API Key