回答被截断在半句话:max_tokens 设太低(2026 修复)

模型回复中途断掉、JSON 没闭合、代码块缺反引号。绝大多数是 token 上限。怎么估算、按 SDK 检测、怎么恢复。

你调模型,回复以 “…as I was saying, the most important” 结束。没引号、没句号。或者 JSON parser 报错——输出停在 "score": 0.8, "reason" 后面就没了。或者代码块没收尾的三反引号。模型没糊涂,是预算用完了。你设的(或 SDK 默认的)token 上限卡停了生成,API 在那个位置截断。

最快修复:把输出 token 上限调大,重跑。OpenAI Chat Completions API 用 max_completion_tokens(旧的 max_tokens 已弃用,reasoning 模型会直接拒绝);OpenAI Responses API 用 max_output_tokens;Anthropic 用 max_tokens。设成你预期最长回复的大约 2 倍,然后确认 stop 字段回来是干净的(下面讲怎么看)。

截断是最容易确认、又最常被忽视的 bug 之一,因为可见输出”看起来差不多对”。下结论说”模型没懂”之前,先看 stop 字段。

先搞清楚:哪个 stop 字段能证明是截断

证明截断的那个字段,在每个 API 上名字不一样,截断对应的值也不一样。这是大多数人踩的坑,所以一定按你的 SDK 看对字段:

API / SDK要读的字段表示”被截断”的值
OpenAI Chat Completionschoices[0].finish_reasonlength
OpenAI Responses APIresponse.status + response.incomplete_details.reasonincomplete + max_output_tokens
Anthropic Messages APIresponse.stop_reasonmax_tokens(或 model_context_window_exceeded

如果你在 Anthropic 的 response 上读 finish_reason,啥都读不到——因为 Anthropic 把它叫 stop_reason。这个字段名不对应本身,就是截断常被漏掉的一个常见原因。

常见原因

1. token 上限用 SDK 默认值

OpenAI Python SDK 不会给 chat 请求强加一个很小的输出上限,但 Anthropic 的 max_tokens 是要你自己传的参数(官方 quickstart 用 max_tokens=1024),而且很多 wrapper 默认 1024 或 2048。长文生成会静默撞顶。

怎么判断:查你 SDK 版本对输出 token 字段的处理方式。代码没传就是默认值生效。

2. 多年前保守设置后再没调

2023 年给 chatbot 写了 max_tokens=500。现在同一个 client 拿来生成文章。这个数从没回头看过。

怎么判断:代码库里 grep max_tokens=max_completion_tokens=max_output_tokens=。每个值对照当前任务长度审一遍。

3. reasoning tokens 吃掉了预算(而且各家算法不一样)

对于 reasoning 模型(OpenAI GPT-5.5 的 Thinking/Pro 模式、Claude Opus 4.7 / Sonnet 4.6 的 extended thinking、Gemini 3.1 Pro),模型在产出任何可见文字之前,会花大量算力在隐藏的内部 reasoning 上。

截至 2026 年 6 月,各家的计数方式并不一样:

  • OpenAImax_completion_tokens 同时盖住 reasoning tokens 可见输出。如果 reasoning 用掉了大半上限,你可能拿到一个 incomplete / length 结果、几乎没有可见文字,而且 reasoning 部分照样计费。OpenAI 建议刚上手 reasoning 模型时,给 reasoning 加输出至少预留 25,000 tokens,并用 reasoning_effortlow / medium / high)控制隐藏开销。
  • Anthropic:开了 extended thinking 时,max_tokens 必须大于你配置的 thinking budget_tokens,因为 thinking 输出也算进 max_tokens。把 max_tokens 设成 thinking 预算加上你想要的可见答案长度。

怎么判断:看 usage.completion_tokens_details.reasoning_tokens(OpenAI)或 thinking 块和 usage(Anthropic)。reasoning tokens 远超可见输出 tokens,就是模型想了很多、说得很少。

4. streaming 把截断藏起来了

streaming 时 UI 边收边显示,stream 结束就结束,除非你专门做了 badge,否则不会看到”被截断”提示。用户看到半段回复以为模型说完了。

怎么判断:streaming 响应仍会在终止事件里带 stop 字段(OpenAI Chat 是最后一个 chunk 的 finish_reason,Anthropic 是 message_delta 事件里的 stop_reason,Responses API 是 response.completed / response.incomplete 事件)。看你的 client 有没有读出来。

5. JSON / 结构化输出 + 低上限 = 非法 JSON

你开了 JSON 或 Structured Outputs 想保证可解析的 JSON。模型开始了合法对象,但中途 token 用完。parser 报错。

怎么判断:JSON parse error,输出 { 开头但没 } 结尾,stop 字段是 length / max_tokens / incomplete。Structured Outputs 保证的是一个完整响应的 schema,并不能在生成被 token 上限切断时保护你。

6. 长 input + 剩余窗口太小

输出上限不能超过”上下文窗口减去你的 input”。input 100k tokens、模型窗口 128k,留给输出的只剩约 28k。设 max_completion_tokens=50000 要么被 clamp 到剩余预算,要么直接报错。

怎么判断:输出停在远低于你设的上限。Anthropic 上可能看到 stop_reason: model_context_window_exceeded;Responses API 看 incomplete_details.reason。查 usage 日志。

7. stop sequence 意外出现在正文里

你设了 stop=["END"](OpenAI)或 stop_sequences=["END"](Anthropic)。模型生成的一段里 “END” 是普通词。API 在那里截断。

怎么判断finish_reason: stop / stop_reason: stop_sequence,输出末尾正好是匹配 stop sequence 的词之前。

最短修复路径

第 1 步:每次调用都读对 stop 字段

啥都先做这个:log 它。字段名取决于你的 SDK(见上面的表)。

OpenAI Chat Completions:

resp = client.chat.completions.create(...)
choice = resp.choices[0]
if choice.finish_reason == "length":
    raise RuntimeError("Output truncated by token cap")

finish_reason 取值:stop(自然结束或 stop sequence)、length(撞上限)、content_filter(安全过滤)、tool_calls(函数调用)。

Anthropic Messages:

resp = client.messages.create(...)
if resp.stop_reason == "max_tokens":
    raise RuntimeError("Output truncated by max_tokens")

stop_reason 取值:end_turnmax_tokensstop_sequencetool_usepause_turnrefusalmodel_context_window_exceeded

OpenAI Responses API:

resp = client.responses.create(...)
if resp.status == "incomplete" and resp.incomplete_details.reason == "max_output_tokens":
    raise RuntimeError("Output truncated by max_output_tokens")

第 2 步:按任务定输出上限

粗略预算(可见输出 tokens,不含隐藏 reasoning):

  • 聊天回复:1000-2000
  • 短摘要:500
  • 文章(约 1000 词):4000
  • 文件级代码生成:8000
  • 多文件重构:16000+

拿不准就往大设。只按真实生成的 token 计费,所以没用到的上限余量不花钱。

第 3 步:reasoning 模型,单独给 reasoning 留预算

OpenAI 上,上限同时盖住 reasoning 和可见文字,所以要设得宽裕、并调 effort:

# GPT-5.5 (Thinking),走 Responses API
resp = client.responses.create(
    model="gpt-5.5",
    input=[...],
    reasoning={"effort": "medium"},   # low | medium | high
    max_output_tokens=25000,          # OpenAI 建议 reasoning + 输出 >= 25k
)

Anthropic 上,max_tokens 必须大于 thinking 预算:

# Claude Opus 4.7,开 extended thinking
resp = client.messages.create(
    model="claude-opus-4-7",
    thinking={"type": "enabled", "budget_tokens": 8000},
    max_tokens=16000,   # 必须 > budget_tokens;给可见答案留约 8k
    messages=[...],
)

跑完看 usage.completion_tokens_details.reasoning_tokens(OpenAI)或本次的 usage(Anthropic)调到合适。

第 4 步:检测到截断就续写

散文类,让模型从断点接着写:

if truncated:  # finish_reason == "length" / stop_reason == "max_tokens"
    cont = call_llm(messages + [
        {"role": "assistant", "content": partial_output},
        {"role": "user", "content": "Continue exactly where you left off. Do not repeat."}
    ])
    full_output = partial_output + cont

JSON 或工具调用,用更高的上限把整个 prompt 重跑,别拼接——JSON 续写很脆,半截的 tool_use 块也拼不回去。

第 5 步:streaming UI 显式暴露截断

streaming 时抓终止事件里的 stop 字段,给消息打 badge:

{message.truncated && <span className="warn">Response was truncated. Request more?</span>}

第 6 步:受窗口约束的上限要算预算

input_tokens = count_tokens(messages)
model_window = 1_000_000   # 截至 2026 年 6 月,Opus 4.7 / Sonnet 4.6 / Gemini 3.1 Pro 标准窗口
safety_margin = 1000
max_output = model_window - input_tokens - safety_margin
output_cap = min(desired_output, max_output)

别把输出上限设得比剩余 context 还高。注意应用内聊天窗口不等于 API 窗口:ChatGPT Plus 应用内大约携带 320 页上下文(完整 1M 在 API 侧或 $200 的 Pro 档),所以走 API 和往网页 UI 里粘贴的表现是不一样的。

第 7 步:审 stop sequences

stop sequence 必须是正文里几乎不会出现的字符串。"\n\n" 在散文里危险,像 "<|END|>" 这样的哨兵串安全得多。

怎么确认已经修好

  1. 把原来失败的那个请求原样重跑。
  2. 断言 stop 字段是干净的:finish_reason == "stop",或 stop_reason == "end_turn",或 status == "completed"。绝不能是 length / max_tokens / incomplete
  3. JSON 的话,解析输出并断言能正常 load。
  4. 把这个断言加进测试或告警,下次回归是自动被抓到,而不是被用户发现。

哪些情况可能不是你操作错了

用了 managed wrapper(LangChain 这类)时,默认上限可能是 wrapper 设的不是底层 SDK 设的,而且 wrapper 可能还在给一个现在要求 max_completion_tokens 的 reasoning 模型传旧的 max_tokens。查 wrapper 版本和文档——可能有隐藏 cap 或过时的参数名。

容易误判的情况

当成”模型糊涂”或”prompt 不清晰”。回复前半连贯、后半根本没生成,几乎一定是 token 上限,不是 prompt。永远先查 stop 字段。

预防建议

  • 每次调用都 log stop 字段,length / max_tokens / incomplete 时告警。
  • 按任务类型设输出上限,不要全局一刀切。
  • reasoning 模型要给隐藏 reasoning 留预算(OpenAI:算进上限里,起步接近 25k;Anthropic:max_tokens 大于 budget_tokens)。
  • streaming UI 显式暴露截断。
  • 用 JSON / Structured Outputs 时,把上限设成预期 JSON 大小的约 2 倍。
  • stop sequence 用明确、不会撞正文的字符串,绝不要用单换行。

FAQ

  • 我的代码还在 OpenAI 上用 max_tokens,现在报错了,变了什么? OpenAI 在 Chat Completions 上弃用了 max_tokens,改用 max_completion_tokens,而且 reasoning 模型会直接拒绝旧名字。改字段名。在更新的 Responses API 上字段叫 max_output_tokens
  • 为什么 Anthropic 忽略我的 finish_reason 检查? Anthropic 不返回 finish_reason。它返回 stop_reason,截断对应的值是 max_tokens(不是 length)。按对应的 SDK 查对应的字段。
  • 把上限设很大有缺点吗? 延迟和最坏情况账单——provider 会限制生成时长,失控的长回复更贵。但只按真实生成的 token 计费,没用到的余量是免费的。
  • 该自动重试截断吗? 散文续写——是。JSON 或工具调用——拉高上限把整个 prompt 重跑,别拼半截响应。
  • 回复很短,但 stop 字段说正常结束了,怎么办? 那就不是截断,是 prompt 或模型行为问题。看下面相关阅读里”列表提前结束""格式问题”那几篇。

相关阅读

标签: #Prompt 工程 #排查 #llm-output #max-tokens #api-config #truncation