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


AI API 返回 200 OK,Token 用量也不为零,但程序最终拿到的却是:
""
null
undefined
No content
这类问题不一定是模型“没有回答”。更常见的情况是:客户端读取了错误字段、模型正在调用工具、推理内容与最终文本被分开返回、流式事件没有正确拼接,或响应被安全策略和长度限制提前终止。
排查时不要只打印 content。先保留脱敏后的完整响应结构,再回答三个问题:
- 文本究竟位于哪个协议字段?
- 本轮是否本来就不应该返回普通文本?
- 响应为何结束,客户端有没有完整消费它?
本文提供一套适用于 OpenAI Chat Completions、Responses API、Claude Messages、Gemini 以及第三方兼容网关的排查方法。
先区分四种“空”

| 表现 | 含义 | 优先检查 |
|---|---|---|
content: "" | 字段存在但文本为空 | 工具调用、长度限制、服务端转换 |
content: null | 本轮没有普通文本内容 | tool_calls、拒绝字段、协议定义 |
没有 content 字段 | 读取路径可能不适用于当前 API | Responses、Claude、Gemini 的原生结构 |
choices / candidates 为空 | 没生成可用候选 | 安全策略、Prompt 反馈、网关异常 |
| 流式结束但累计文本为空 | 事件被漏读或只返回非文本事件 | SSE 解析、事件类型、连接中断 |
| 有 reasoning、没有最终答案 | 推理与答案分离,或生成提前结束 | 输出 Token、结束原因、解析器 |
不要用下面这种逻辑覆盖所有 API:
const text = response.choices[0].message.content;
它只适用于特定 Chat Completions 响应,而且仍需处理 choices 缺失、content 为 null、工具调用和拒绝等情况。
第一步:保存脱敏后的原始响应
排错时至少记录:
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"
}
这不是空响应。客户端应:
- 解析工具名和参数;
- 执行允许的工具;
- 把工具结果作为新消息发回;
- 再读取模型的最终文本。
如果 UI 只渲染 content,工具调用会被错误显示成“模型没回答”。
finish_reason 是 length
finish_reason: "length" 通常表示输出达到限制,最终答案可能尚未生成。推理模型尤其可能先消耗较多推理 Token,留给可见答案的空间不足。
处理方式:
- 提高允许的输出 Token;
- 缩短输入上下文;
- 要求先给简短结论;
- 检查模型使用的是
max_tokens、max_completion_tokens还是 Provider 自定义字段; - 查看 usage 中可见输出与推理 Token 的细分(若服务提供)。
不要在不知道字段语义时同时发送多个 Token 上限参数。
finish_reason 是 content_filter
如果候选因安全策略停止,文本可能为空或不完整。应读取 Provider 返回的过滤、拒绝或安全元数据,而不是自动重复同一个请求。盲目重试通常不会改变确定性的策略结果。
Responses API:不要继续假设 choices[0]

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,却没有正文”通常有三种原因:
- 推理与最终答案使用不同字段或内容类型;
- 输出预算在生成最终答案前耗尽;
- 第三方网关转换了 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。
流式响应为什么最容易“看起来为空”

非流式请求一次返回完整对象;流式请求返回一串事件。空内容常来自客户端解析错误:
- 只读第一帧,而第一帧只有角色信息;
- 只识别
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 的“空内容”不是单一故障。最有效的排查顺序是:
- 保存脱敏响应结构;
- 确认 API 协议和最终 Endpoint;
- 区分空字符串、
null、缺失字段与无候选; - 检查工具调用、拒绝、安全反馈和多模态内容;
- 读取
finish_reason、stop_reason或finishReason; - 对照非流式与流式请求;
- 用最小请求区分服务端、网关和客户端解析问题。
不要把所有模型响应压缩成一个 content 字段。只要按协议解析内容类型,并把结束原因纳入业务逻辑,大多数“200 OK 但没有回答”的问题都能快速定位。


