你调模型,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_tokens 或 cache_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_reason、length时报警。 - 按任务类型设
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 要 10 条,模型给 3 条就停
- Prompt 太长输出质量下降
- 长 background 把任务藏起来了
- 输出很漂亮但没法执行
- 一个 prompt 塞太多任务
- AI 给列表不给执行
- AI 输出风格漂移
- 最后一句覆盖前面的指令
- Prompt 范围太宽
标签: #Prompt 工程 #排查 #llm-output #max-tokens #api-config #truncation