回答被截断在半句话,因为 max_tokens 设太低

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

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

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

常见原因

1. max_tokens 用 SDK 默认值

OpenAI Python SDK 的 chat 默认 max_tokens 是无限(好),但 Anthropic 和很多 wrapper 默认 1024 或 2048。长文生成静默撞顶。

怎么判断:查你 SDK 版本的 max_tokens 默认值。代码没传就是默认值生效。

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

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

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

3. reasoning tokens 吃掉了预算

o1 / o3 / Claude extended thinking 这些模型,max_tokens 里大半被内部 reasoning 用掉,可见输出非常少。

怎么判断:API response 里有 usage.completion_tokens_details.reasoning_tokenscache_creation_input_tokens。reasoning tokens > 可见输出 tokens 就是模型想了很多说得很少。

4. streaming 把截断藏起来了

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

确定方法:streaming 接口仍会发一个最终事件带 finish_reason,看你的 client 有没有读出来。

5. JSON mode + 低 max_tokens = 非法 JSON

你开了 response_format={"type":"json_object"} 想保证合法 JSON。模型开始了合法对象,但中途 token 用完。parser 报错。

怎么判断:JSON parse error,输出 { 开头但没 } 结尾。finish_reason: length

6. 长 input + 小 max_tokens 总额

有些 API 限制 total tokens(input + output)。input 100k tokens、模型上下文 128k,留给 output 的只剩 ~28k。设 max_tokens=50000 会被静默 clamp 到剩余预算。

怎么判断:输出停在远低于你设的 max_tokens。看 API error 或 usage 日志。

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

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

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

最短修复路径

第 1 步:永远先看 finish_reason

啥都先做这个:

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

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

第 2 步:按任务定 max_tokens

粗略预算:

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

拿不准就往大设。只按真实生成的 token 计费。

第 3 步:reasoning 模型预算翻倍

# o1/o3 or Claude with thinking
resp = client.chat.completions.create(
    model="o3",
    messages=[...],
    max_completion_tokens=16000,  # 大约 8k reasoning + 8k visible
)

跑完看 usage.completion_tokens_details.reasoning_tokens 调到合适。

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

散文类,让模型接着写:

if finish_reason == "length":
    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 用重试拉高 max_tokens 重跑,别拼接——JSON 续写很脆。

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

streaming 时抓最后一个 chunk 的 finish_reason,给消息打 badge:

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

第 6 步:total-token cap 要算预算

input_tokens = count_tokens(messages)
model_window = 128_000
safety_margin = 1000
max_output = model_window - input_tokens - safety_margin
max_tokens = min(desired_output, max_output)

别把 max_tokens 设得比剩余 context 还高。

第 7 步:审 stop sequences

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

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

用了 managed wrapper(LangChain 这类)时,默认 max_tokens 可能是 wrapper 设的不是底层 SDK 设的。查 wrapper 文档——可能有隐藏 cap。

容易误判的情况

当成”模型糊涂”或”prompt 不清晰”。response 前半连贯、后半根本没生成,几乎一定是 max_tokens。永远先查 finish_reason

预防建议

  • 永远 log finish_reasonlength 时报警。
  • 按任务类型设 max_tokens,不要全局一刀切。
  • reasoning 模型按可见输出预期的 ~2x 分配。
  • streaming UI 显式暴露截断。
  • 用 JSON mode 时设 max_tokens = 预期 JSON 大小的 2x。
  • stop sequence 用明确的、不会撞正文的字符串。

FAQ

  • max_tokens 设很大有缺点吗? cost cap 和 latency cap——provider 会限制生成时长。其他没有,只按真实生成的 token 计费。
  • finish_reason: length 该自动重试吗? 散文续写——是。JSON——拉高 max_tokens 整个 prompt 重跑。

相关阅读

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