大模型 API 输出被截断怎么办:先看 finish_reason,再按原因逐条修

2026-10-13 105 0

先别改参数,先看 finish_reason

输出断了,绝大多数人第一反应是把 max_tokens 调大。但如果断的原因不是 Token 上限,调大它一点用都没有,还会白白增加成本和失败概率。所以第一步是读响应体里的那个状态字段。

OpenAI 兼容协议里是 choices[0].finish_reason,Anthropic 协议里是 stop_reason。它只有几种取值,每一种对应一个完全不同的方向:

  • length:模型撞到了 Token 上限被强制停住。这是最常见的截断。
  • stop:模型自己认为写完了。如果你看到的正文明显不完整,那多半是命中了 stop 参数。
  • content_filter:内容被安全策略拦截,跟长度无关。
  • 压根没拿到完整响应体,或者 stream=True 时连接中途静默断开:这不是模型截断,是链路超时或代理断开。

把这四种分清楚,后面的排查才有意义。下面按这个顺序讲。

finish_reason 是 length:三个上限,改错地方没用

length 意味着请求里设的最大输出值不够用了。这时候要动的是 max_tokens(较新的接口里改名为 max_completion_tokens,含义相同,都表示这一次调用允许生成多少 Token)。

但调大它之前,先确认两件事,因为有两个天花板卡在上面:

  1. 模型自身的单次最大输出上限(Max Output Tokens)。每个模型有自己的架构上限,常见的有 4096、8192、16384 这类档位,或者更高。请求里填得再大也不会突破它。
  2. 总上下文窗口。提示词长度加上输出长度,两者之和不能超过模型上下文。如果你的输入本身就占了很大一截,输出额度自然被压得很小——这时候先把输入瘦身,比调参数有效。

所以正确的顺序是:先看模型规格里的上下文窗口和最大输出,再决定 max_tokens 填多少。查规格的地方就是模型页——比如 NexAIX 的模型清单里每个模型都标注了上下文与输出边界,先确认你用的那个模型的数字,再回到代码里改。

推理模型:思维链也在花你的输出额度

o1、o3、DeepSeek-R1 这类带推理过程的模型有个坑:模型内部生成的思维链(reasoning tokens)是计入 completion_tokens 的,也就是说它和最终正文共用同一份 max_completion_tokens 配额。

后果很直观:额度设得偏小,模型推理过程先把额度烧完,正文要么只剩半句,要么直接是空的。表现上你会觉得“JSON 不完整”“回答只有开头”——看起来像网络问题,其实是额度问题。

处理办法只有一个:给推理类模型留出明显大于普通模型的输出额度。同一条提示词,普通对话模型够用的数值,换到推理模型上往往不够。

内容确实超过单次上限:用续写循环,而不是硬调参数

有一种情况调参数确实解决不了:你要生成的内容本身就比模型单次最大输出还长(比如一次写完几十页文档)。这时候标准做法是续写循环(Continuation Loop)。

思路很朴素——模型停在哪里,就从哪里接着写:

  1. 正常发起请求,拿到结果。
  2. 检查 finish_reason。如果等于 length,说明是被截断的,不是写完的。
  3. 把已生成的这段文本作为一条 assistant 消息拼回 messages,再追加一条 user 指令,例如“继续,从上一句结尾接着写,不要重复已有内容”。
  4. 反复执行,直到模型不再返回 length,或者你设的条件达成(写到了结束标记、达到目标长度、循环次数到上限)。

伪代码大致是这样:

full = ""
messages = [{"role": "user", "content": prompt}]
for _ in range(MAX_ROUNDS):
    resp = client.chat.completions.create(
        model=MODEL, messages=messages, max_tokens=MAX_OUT
    )
    choice = resp.choices[0]
    full += choice.message.content
    if choice.finish_reason != "length":
        break
    messages.append({"role": "assistant", "content": choice.message.content})
    messages.append({"role": "user", "content": "继续,接着上一段结尾写,不要重复。"})

两个容易踩的点:一是衔接处要检查,模型有可能重复上一段最后一句;二是循环必须有硬性退出条件,否则一个反复返回 length 的请求会一直烧额度。

比续写更省事的替代方案是在 Prompt 层就把任务拆开:长文按章节分次生成,长 JSON 按字段模块分批输出。结构化输出场景下,分块生成的结果通常比一次生成再拼接更稳定、也更好校验。

拿不到完整响应体、流式中断:问题不在参数,在超时

如果日志里根本看不到完整的 JSON 响应,或者开了 stream=True 之后数据流中途静默停住、客户端直接抛异常,那跟 Token 没关系,是连接被掐掉了。

长文本生成耗时本来就长。默认超时往往偏短(很多 SDK 的默认值只有几分钟甚至更短),反向代理侧还有一个独立的读超时(Nginx 的 proxy_read_timeout 之类),任何一层先到点,连接就断。

两个动作:

  • 开启 stream=True。流式输出让数据持续到达,连接不容易被判为空闲,同时你能边收边处理。
  • 显式把客户端超时调大,长文本场景常见做法是 120 秒到 300 秒,或用 SDK 提供的流式专用超时参数。

需要说明的是:本站网关针对单次长连接的具体默认超时时长并未在公开文档中写明,所以不要假设它一定够长。稳妥的做法是以客户端超时设置为准,并在生产上线前用你实际的最长请求跑一次真实压测;如果你确实需要确认网关侧的行为,走官网工单问清楚。超时排查的逐段定位方法,可以看从 504、524 到 APITimeoutError 的排查顺序。

内容不完整但 finish_reason 是 stop

这种情况说明“模型认为自己写完了”,那就要从别处找原因:

  • stop 参数误触。如果你传了 stop=["\n\n"] 之类的停用词,正文里一旦出现这个序列,生成就会被切断。先把它去掉,或者换成不会自然出现在正文里的特殊标记。
  • content_filter。内容被安全策略拦截,长度设置再大也没用,需要调整输入内容本身。

这两类都属于“看起来像截断,实际不是”,如果一直按 Token 上限的方向查,会一直在原地打转。这类混合症状的完整排查顺序,可以参考从 401、404 到超时、截断按顺序定位。

改到最后还是要回到配置本身

排查收尾时,别忘了截断有时是配置迁移的副产品:换端点、换 SDK 版本之后参数名变了(max_tokens 改成 max_completion_tokens),或者客户端底层的超时默认值跟你原来用的那套不一样。

如果只是把应用接到一个新的 OpenAI 兼容端点,改动通常只有一处——base_url。例如 NexAIX 的端点就是 https://api.nexaix.net/v1,模型名和其余参数沿用官方 SDK 的写法,接入文档和获取 Key 的入口在官网导航里。这样迁移时不会引入额外的参数差异,截断问题也更容易定位到底是模型因素还是客户端因素。

一句话对照

  • length + 数值不够 → 调大 max_tokens / max_completion_tokens,但不要超过模型最大输出,也不要让输入加输出超出上下文窗口。
  • length + 推理模型 → 思维链在吃额度,给更大输出配额。
  • length + 内容就是超长 → 续写循环或任务分块,别硬调参数。
  • 没有完整响应 / 流式中断 → stream=True,把客户端超时调到 120 秒以上。
  • stop 但内容缺失 → 检查 stop 参数;content_filter → 改输入。

相关文章

Node.js 调用 AI API:openai SDK 与原生 fetch 的接入、流式输出、超时重试和错误处理
Python 调用 AI API:从客户端初始化、改 base_url 到流式输出和重试
AI API 接入教程:Python、Node.js、LangChain 与原生 HTTP 的 base_url、流式、超时和限流配置
AI API 超时怎么解决:从 504、524 到 APITimeoutError,按链路逐段排查
AI API 故障排查:从 401、404、429 到超时、截断和降配怀疑,按顺序定位
模型上下文窗口对比:窗口与输出上限怎么算,截断、延迟和限流怎么避

评论(0)

暂无评论

发布评论