模型输出被截断、报 context length 超限怎么解决?
调大模型接口时,你大概率会遇到两类报错:一类是回答"写到一半没了",另一类是请求直接被拒,提示 context length / maximum context length 超限。这俩不是一回事,修法也不同。下面直接说清楚怎么区分、怎么改。
两类问题的本质区别
输出被截断:请求发出去了,模型也回了,但内容在中间戛然而止。典型表现是在 JSON、代码、长文里突然断掉,末尾没有正常的结束符。根因几乎都是 max_tokens 设得太小,模型还没说完就被强制停了。
光靠肉眼很难稳定识别截断,更可靠的是看响应里的 finish_reason。当模型是因为达到 max_tokens 上限而停的,这个字段会返回 "length",正常结束则是 "stop"。把这个值记进日志,就能在监控里批量发现"被掐断"的请求,而不是等用户投诉:
fr = resp.choices[0].finish_reason
if fr == "length":
print("输出被 max_tokens 截断,需要调大上限或启用续写")
推理类模型有个额外坑:max_tokens 对它们来说是"思考过程 + 最终答案"的总预算,不是只给答案的。以 DeepSeek-R1 为例,官方明确 max_tokens 限制单次输出的总长度(含思考),默认 32K、上限 64K。如果你的任务需要长思考,却把 max_tokens 设得很小,模型会把额度全花在思考上,答案反而被挤没、表现为"思考了一大堆但没给结论"。这类情况同样看 finish_reason == "length" 来确认,然后整体调大 max_tokens,而不是只盯着答案长度。
context length 超限:请求根本没成功,服务端直接返回 4xx 错误(常见 400 或厂商自定义的 context_length_exceeded)。根因是"系统提示 + 历史消息 + 本次输入 + 工具定义"加起来的总 token 数超过了模型上下文窗口上限。注意:上下文窗口里既包含输入也包含输出预算,你设置的 max_tokens 也要占掉一部分额度。
一个实用的判断方法:看 HTTP 状态码和错误信息。能拿到部分响应、只是不全 → 输出截断;请求被拒、报错里出现 context / length / exceeded → 输入超长。
具体到各家报错文案:OpenAI 命中上下文上限时会返回类似 This model's maximum context length is 128000 tokens. However, you requested ... tokens ... 的提示,里面直接给出窗口上限和你的实际占用;Anthropic 则会报 max_tokens 或上下文相关错误,并在响应的 usage 字段里给出 input_tokens 等明细,方便你定位是哪一块吃掉了额度。把这段错误信息原样打进日志,比自己猜要准得多。
输出被截断:调大 max_tokens、检查 stop
最直接的解法就是调大 max_tokens。很多 SDK 的默认值偏低(DeepSeek 的 deepseek-chat 默认输出上限是 4K,R1 默认 32K 且包含思考过程),生成代码或长文时很容易撞到。
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="YOUR_BASE_URL",
)
resp = client.chat.completions.create(
model="YOUR_MODEL",
messages=[{"role": "user", "content": "写一份完整的后端接口文档"}],
max_tokens=8192, # 调大输出上限,别用默认值
)
print(resp.choices[0].message.content)
另一个容易被忽略的点是 stop 序列。如果你在代码里设了 stop=["\n\n"] 或某个分隔符,模型一旦输出该字符就立刻停。看到"截断位置总在特定字符处",先去查 stop 参数,而不是一味加 max_tokens。
resp = client.chat.completions.create(
model="YOUR_MODEL",
messages=[{"role": "user", "content": "生成 10 条测试用例"}],
max_tokens=4096,
stop=["```"], # 一旦输出 ``` 就停,可能导致内容不完整
)
上面这种写法在"让模型输出代码块"的场景很常见:本意是截断多余内容,结果把真正需要的代码也一起截了。stop 应该是"锦上添花的安全闸",不该承担主要的长度控制职责,长度交给 max_tokens 才稳妥。
还有一个坑:流式(stream)场景下,客户端如果自己写了"收到 N 个字符就断开"的逻辑,也会表现为截断。这类属于调用侧 bug,和模型无关。
context length 超限:砍输入,而不是加窗口
上下文超了,本质是"喂进去的东西太多"。优先做减法,而不是立刻换更贵的模型。
精简 system prompt。很多人把一大段背景知识、示例、规则全塞进 system,其实其中一半可以移到用户消息或外挂检索里。system 在每轮请求里都会重复占用额度,值得反复压缩。
截断 / 压缩历史。多轮对话里,旧消息可以只保留最近 K 轮,更早的内容做摘要后拼回。下面这段代码演示按"轮数"裁剪历史:
def trim_history(messages, keep_last_turns=6):
# messages 形如 [sys, u, a, u, a, ...]
if len(messages) <= keep_last_turns * 2:
return messages
# 始终保留第一条 system
sys_msg = messages[0] if messages and messages[0]["role"] == "system" else None
rest = messages[1:] if sys_msg else messages[:]
trimmed = rest[-keep_last_turns * 2:]
return ([sys_msg] + trimmed) if sys_msg else trimmed
history = trim_history(history, keep_last_turns=6)
优先用 RAG / 外挂检索,而不是把全部资料塞进上下文。很多"超长"需求其实是"在资料里找相关内容回答",这时正确做法是先检索出最相关的几段,只把这几段拼进 prompt。模型窗口再大,往里硬塞几千页文档既费钱又因 context rot 掉精度。把上下文留给真正需要模型当场推理的内容。
善用 prompt caching 降低重复成本。如果你的 system prompt 或常驻背景很长、且多轮复用,OpenAI、Anthropic 都支持对重复出现的前缀做缓存折扣(命中缓存的输入 token 价格能降一半)。这意味着"砍不掉的固定长上下文"可以通过缓存摊薄成本,但不解决窗口上限问题——超窗口照样报错。
换长上下文模型。当业务确实需要吞整本手册、整个代码库时,再考虑模型升级。各平台当前主流模型的窗口对比如下(数据来自各厂商官方文档):
| 平台 | 代表性模型 | 上下文窗口 | 单次最大输出 (max_tokens) |
|---|---|---|---|
| OpenAI | gpt-4o | 128K | 16,384 |
| Anthropic Claude | claude-sonnet-4-5(200K 窗口档) | 200K | 64K(Haiku 4.5)/ 1M 窗口模型可达 128K |
| DeepSeek | deepseek-chat (V3.1) | 128K | 8K(默认 4K) |
| DeepSeek | deepseek-reasoner (R1-0528) | 128K | 64K(含思考过程,默认 32K) |
Claude 的新版 Opus 5 / Sonnet 5 上下文窗口已到 1M token,但价格也更高。选型时别只看窗口大小,更要看"长上下文下的召回质量"——厂商文档里明确提到 context 越长、准确性会下降(context rot),所以能砍就砍。
把输入和输出当成同一个预算池来算,能少踩很多坑。上下文窗口是"输入 token + 你申请的 max_tokens"的总和上限,不是各占一份。比如 gpt-4o 窗口 128K,如果你本轮输入已经 120K,那 max_tokens 最多只能给到约 8K,再大就会在请求阶段直接报 context 超限——这时你以为是输出截断去加 max_tokens,反而把报错从"截断"变成了"请求被拒"。正确做法是先量输入占用,再倒推能留多少给输出:输入大就砍历史、缩 system,给输出腾地方。
请求前先估算 token 数,避免盲猜。OpenAI 和 Anthropic 都提供 token 计数能力,发请求前先算总占用,超了就提前裁剪,而不是等报错。
一次性可跑的完整示例
把上面几点串起来:从环境变量读 key、设合理的 max_tokens、超长时裁剪历史、捕获上下文超限异常并给出提示。
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
base_url="YOUR_BASE_URL",
)
def trim_history(messages, keep_last_turns=6):
if len(messages) <= keep_last_turns * 2:
return messages
sys_msg = messages[0] if messages[0]["role"] == "system" else None
rest = messages[1:] if sys_msg else messages[:]
trimmed = rest[-keep_last_turns * 2:]
return ([sys_msg] + trimmed) if sys_msg else trimmed
history = [{"role": "system", "content": "你是文档助手"}]
try:
history = trim_history(history, keep_last_turns=6)
resp = client.chat.completions.create(
model="YOUR_MODEL",
messages=history,
max_tokens=8192,
)
history.append({"role": "assistant", "content": resp.choices[0].message.content})
except Exception as e:
if "context" in str(e).lower() and "length" in str(e).lower():
print("上下文超限:进一步压缩 system 或历史后重试")
else:
raise
调试这类问题时,先用一段很短的输入跑通,确认 max_tokens 和 stop 行为符合预期,再逐步放大输入逼近真实场景。很多"偶发截断"其实是只在长输入下才触发——短输入测试通过会给你一个干净的基线,避免把输出截断和上下文超限两种 bug 混在一起排查。
快速排错表
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 回答中间断掉、缺结尾 | max_tokens 太小 | 调大到 8192 / 16384 |
| 截断总在特定字符处 | stop 序列命中 | 检查并移除/调整 stop |
| 流式输出提前结束 | 客户端截断逻辑 bug | 排查接收侧代码 |
请求被拒报 context_length_exceeded | 总输入超窗口 | 精简 system、裁剪历史 |
| 换模型后仍超限 | max_tokens 也占窗口 | 在窗口内给输出留预算 |
| 长文问答质量变差 | context rot | 缩短上下文或分块处理 |
配置检查清单
-
max_tokens按场景显式设置,不使用 SDK 默认值 - 确认没有误设
stop序列导致提前终止 - system prompt 已压缩到最小必要信息
- 多轮对话启用历史裁剪 / 摘要
- 选型前核对目标模型的上下文窗口与最大输出(见上表)
- 在窗口额度内为输出预留
max_tokens空间 - 生产环境请求前估算 token 数并做上限保护
- 流式场景排查客户端接收逻辑,避免自行截断