你调模型,回复以 “…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 Completions | choices[0].finish_reason | length |
| OpenAI Responses API | response.status + response.incomplete_details.reason | incomplete + max_output_tokens |
| Anthropic Messages API | response.stop_reason | max_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 月,各家的计数方式并不一样:
- OpenAI:
max_completion_tokens同时盖住 reasoning tokens 和可见输出。如果 reasoning 用掉了大半上限,你可能拿到一个incomplete/length结果、几乎没有可见文字,而且 reasoning 部分照样计费。OpenAI 建议刚上手 reasoning 模型时,给 reasoning 加输出至少预留 25,000 tokens,并用reasoning_effort(low/medium/high)控制隐藏开销。 - Anthropic:开了 extended thinking 时,
max_tokens必须大于你配置的 thinkingbudget_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_turn、max_tokens、stop_sequence、tool_use、pause_turn、refusal、model_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|>" 这样的哨兵串安全得多。
怎么确认已经修好
- 把原来失败的那个请求原样重跑。
- 断言 stop 字段是干净的:
finish_reason == "stop",或stop_reason == "end_turn",或status == "completed"。绝不能是length/max_tokens/incomplete。 - JSON 的话,解析输出并断言能正常 load。
- 把这个断言加进测试或告警,下次回归是自动被抓到,而不是被用户发现。
哪些情况可能不是你操作错了
用了 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 要 10 条,模型给 3 条就停
- Prompt 太长输出质量下降
- 长 background 把任务藏起来了
- 输出很漂亮但没法执行
- 一个 prompt 塞太多任务
- AI 给列表不给执行
- AI 输出风格漂移
- 最后一句覆盖前面的指令
- Prompt 范围太宽
标签: #Prompt 工程 #排查 #llm-output #max-tokens #api-config #truncation