
LLM API 调用中的 Context Length Exceeded 错误:修复、权衡与模型选择

你的 LLM API 调用刚刚返回:
{
"error": {
"message": "This model's maximum context length is 128000 tokens. However, your messages resulted in 142837 tokens.",
"type": "invalid_request_error",
"code": "context_length_exceeded"
}
}这意味着你的输入(系统提示词 + 对话历史 + 用户消息)超过了模型的上下文窗口。请求在生成任何 token 之前就被拒绝了。
与速率限制错误(关于请求量)不同,上下文长度错误关于的是请求大小。修复方法不是放慢速度——而是减少输入、重构请求或切换到上下文窗口更大的模型。
要点速览
- Context length exceeded = 你的输入对于模型的 token 窗口来说太大了。
- 你有四个选项:截断、摘要、分块或切换模型。
- 每个选项在成本、质量和延迟上有不同的权衡。
- 使用下面的决策表为你的工作负载选择正确的方案。
- 对于生产系统,在用户看到错误之前就处理它——而不是之后。
快速修复清单
在选择策略之前,先检查这些常见原因:
| 检查项 | 要找什么 | 快速修复 |
|---|---|---|
| 对话历史过长 | 多轮聊天中 messages 数组无限增长 | 裁剪最旧的消息或实现滑动窗口 |
| 系统提示词过大 | 详细指令占用了上下文预算 | 压缩系统提示词或将静态指令移到引用中 |
| 重复内容 | 相同上下文被多次注入(RAG、工具结果) | 发送前去重 |
| 大型工具/函数结果 | 工具调用返回了巨大的 JSON 或文本块 | 在添加到上下文之前截断或摘要工具输出 |
| 不必要的元数据 | 只需几个字段却传入了完整对象 | 只提取相关字段 |
如果这些快速修复都不适用,你需要一个结构化的方案。
决策表:截断 vs 摘要 vs 分块 vs 切换模型
| 策略 | 工作原理 | 质量影响 | 成本影响 | 延迟影响 | 最适合 |
|---|---|---|---|---|---|
| 截断 | 丢弃最旧的消息或裁剪输入 | 可能丢失重要上下文 | 减少输入 token = 更低成本 | 更快(更少输入) | 长历史的聊天应用;近期上下文最重要的场景 |
| 摘要 | 使用更便宜的模型将先前上下文压缩为摘要 | 有损——摘要可能遗漏细节 | 额外一次摘要 API 调用,但主调用更小 | 增加一次额外调用 | 有累积状态的 Agent 工作流;知识密集型对话 |
| 分块 + 合并 | 将输入分成块,分别处理,合并结果 | 可能丢失跨块上下文的风险 | 多次调用 = 更高总成本 | 更慢(串行或并行分块) | 文档处理、长文本分析 |
| 切换模型 | 使用上下文窗口更大的模型 | 通常中性或更好 | 大上下文模型通常每 token 更贵 | 可能有差异 | 当输入无法在不丢失关键信息的情况下缩减时 |
策略 1:截断——丢弃最不重要的部分
截断是最简单的方案。移除输入中最旧或最不相关的部分。
聊天的滑动窗口
def sliding_window(messages: list, max_tokens: int, system_prompt: str) -> list:
"""在 token 预算内保留系统提示词 + 最近的消息。"""
# 始终保留系统提示词
result = [{"role": "system", "content": system_prompt}]
token_count = count_tokens(system_prompt)
# 从最新到最旧添加消息
for msg in reversed(messages):
msg_tokens = count_tokens(msg["content"])
if token_count + msg_tokens > max_tokens:
break
result.insert(1, msg) # 插入到系统提示词之后
token_count += msg_tokens
return result截断何时效果好
- 近期上下文最重要的多轮聊天
- 需要时可以重新检索的 RAG 管道
- 可以按顺序处理的批处理
截断何时危险
- 早期指令影响后续行为的 Agent 工作流
- 完整性很重要的法律或合规场景
- 丢弃步骤会改变答案的多步推理
策略 2:摘要——在不丢失含义的情况下压缩
摘要使用更便宜、更快的模型来压缩先前上下文:
async def summarize_context(messages: list, client) -> str:
"""将较旧的消息压缩为摘要。"""
context_text = "\n".join(
f"{m['role']}: {m['content']}" for m in messages
)
response = await client.chat.completions.create(
model="gpt-4o-mini", # 用更便宜的模型做摘要
messages=[{
"role": "user",
"content": f"将这段对话上下文总结在 500 token 以内。"
f"保留关键决策、事实和待办事项:\n\n{context_text}"
}],
max_tokens=500
)
return response.choices[0].message.content成本对比:摘要 vs 截断
| 方案 | 主模型的输入 token | 额外 API 调用 | 总成本 |
|---|---|---|---|
| 不管理(触发错误) | 不适用——请求被拒绝 | 0 | 浪费的延迟 + 重试成本 |
| 截断到 80K token | 80K | 0 | 80K 输入的基础成本 |
| 摘要旧上下文 | ~10K(摘要)+ 40K(近期)= 50K | 1 次摘要调用(约 $0.01) | 更低的主调用成本 + 小额摘要成本 |
摘要增加了一次便宜的调用,但通常能显著减少主调用的输入 token——对于昂贵的模型,这可以实现净成本下降。
策略 3:分块和合并——用于长文档
当输入是单个长文档(而非对话)时,分块通常是正确的方案:
async def process_long_document(
document: str,
question: str,
client,
chunk_size: int = 50000
) -> str:
"""通过分块处理长文档。"""
chunks = split_into_chunks(document, chunk_size)
chunk_results = []
for i, chunk in enumerate(chunks):
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"分析文档的第 {i+1}/{len(chunks)} 块。\n"
f"问题:{question}\n\n"
f"块内容:\n{chunk}"
}]
)
chunk_results.append(response.choices[0].message.content)
# 合并块结果
merge_response = await client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"将这些部分分析合并为最终答案。\n"
f"问题:{question}\n\n"
f"部分结果:\n" + "\n---\n".join(chunk_results)
}]
)
return merge_response.choices[0].message.content分块的权衡
- 优点:可处理任意长度的输入
- 缺点:跨块上下文会丢失
- 缺点:多次 API 调用 = 更高的总成本和延迟
- 缓解:块之间重叠 10-20% 以保留边界上下文
策略 4:切换到上下文更大的模型
如果你的输入确实需要保持大体量,切换到上下文窗口更大的模型:
| 模型家族 | 典型最大上下文 | 何时使用 |
|---|---|---|
| GPT-4o | 128K token | 大多数工作负载的默认选择 |
| Claude Sonnet/Opus | 200K token | 当你需要超过 128K 时 |
| Gemini Pro | 1M+ token | 非常长的文档、完整代码库 |
| Gemini Flash | 1M+ token | 成本敏感的长上下文任务 |
使用路由网关进行模型选择
与其硬编码模型选择,你可以使用根据输入大小路由到合适模型的网关:
from openai import OpenAI
client = OpenAI(
api_key="your-evolink-key",
base_url="https://api.evolink.ai/v1"
)
# 让 Smart Router 根据你的工作负载选择
response = client.chat.completions.create(
model="evolink/auto",
messages=your_messages
)生产模式:发送前预检查
不要等 API 拒绝你的请求。在发送前检查输入大小:
import tiktoken
def check_context_length(messages: list, model: str, max_tokens: int) -> dict:
"""预检查消息是否在模型的上下文窗口内。"""
encoder = tiktoken.encoding_for_model(model)
total_tokens = sum(
len(encoder.encode(m["content"])) for m in messages
)
if total_tokens > max_tokens:
return {
"fits": False,
"total_tokens": total_tokens,
"excess": total_tokens - max_tokens,
"suggestion": "truncate" if total_tokens < max_tokens * 1.5
else "summarize_or_switch"
}
return {"fits": True, "total_tokens": total_tokens}这避免了被拒绝请求浪费的延迟,让你在用户看到错误之前就应用正确的策略。
相关文章
- 修复 OpenRouter 429 "Provider Returned Error" — 当问题是速率限制而非输入大小时
- AI API 超时:原因、重试模式与回退设计 — 当长输入导致超时而非拒绝时
- 如何减少 Agent 工作负载中的 429 错误 — 在管理请求大小的同时管理请求量
- 2026年最佳生产可靠性AI API平台 — 选择能处理模型路由的平台
常见问题
"Context length exceeded" 是什么意思?
它意味着你的输入(系统提示词 + 消息 + 任何注入的上下文)包含的 token 超过了模型最大上下文窗口允许的数量。请求在任何生成开始之前就被拒绝了。
Context length exceeded 和速率限制错误一样吗?
不一样。速率限制错误(429)关于请求量——在一个时间窗口内请求太多。上下文长度错误关于请求大小——单个请求太大。它们需要不同的修复方法。
截断和摘要哪个更好?
截断更简单、更便宜但会丢失信息。摘要保留含义但增加额外的 API 调用并引入压缩失真。对于近期最重要的聊天历史使用截断;对于累积上下文很重要的 Agent 工作流使用摘要。
使用更大的模型能避免 context length 错误吗?
可以,但有成本代价。上下文窗口更大的模型(Gemini 1M+、Claude 200K)可以接受更大的输入,但每 token 通常更贵。计算质量改进是否值得额外的花费。
如何在发送请求前计算 token 数?
tiktoken 库,或使用提供商的 token 计数端点。确切的 token 数取决于模型的分词器,所以请为你的目标模型使用正确的编码器。应该在应用代码还是网关层面处理这个错误?
两者都要。应用代码应预检查输入大小并应用合适的策略(截断、摘要、分块或切换)。路由网关可以额外选择上下文窗口足够大的模型来处理你的请求。


