context_length_exceeded 怎么解决:Token 预算、历史裁剪与长对话排查实战
很多 AI 应用刚上线时调用正常,运行一段时间后却开始出现 context_length_exceeded、maximum context length、prompt is too long,或者在没有明确错误码的情况下直接返回 400。
常见现象包括:Dify 工作流第一次执行成功,连续对话后失败;Cursor 加入多个文件后回答突然变短;Cherry Studio 聊了几十轮后响应变慢;知识库检索内容一多,接口开始超时。
这类问题和 API Key 是否正确通常没有关系。真正需要检查的是:一次请求最终送进模型的输入有多大,以及给输出预留了多少空间。
本文不绑定具体模型或服务商,而是从 OpenAI 兼容接口的通用请求结构出发,说明如何计算上下文预算、裁剪历史消息、限制知识库片段,并给出 curl、Python 和 Node.js 示例。
一、context_length_exceeded 到底表示什么

大模型每次生成答案时,只能在一个有限的上下文窗口中处理内容。这个窗口不只包含用户刚输入的问题,还可能包含:
- 系统提示词;
- 历史对话;
- 当前用户消息;
- 工具定义与 JSON Schema;
- 知识库检索结果;
- 代码文件、网页或文档内容;
- 图片等多模态输入;
- 为本次回答预留的输出 Token。
可以把一次请求的预算简化为:
系统提示词
+ 历史消息
+ 当前问题
+ 工具定义
+ 检索片段
+ 附件内容
+ 预留输出
<= 模型上下文窗口
假设模型支持的上下文窗口为 C,输入估算为 I,希望最多生成 O 个 Token,那么应满足:
I + O <= C - 安全余量
工程上通常还会留出一定余量,用来吸收分词差异、工具调用参数和框架自动注入的内容。
不同模型使用的分词器可能不同,同一段中文、英文、代码或 JSON 产生的 Token 数也不相同,因此不能简单把“字符数”当成“Token 数”。
二、为什么短问题也会触发上下文超限
用户最后只输入“继续”,请求仍可能超限,因为真正发送的并不只有这两个字。
1. 框架自动携带全部聊天记录
不少聊天应用会把历史消息逐轮追加到 messages。单轮请求只有几百 Token,累计几十轮后就可能超过模型窗口。
2. 知识库一次返回过多片段
RAG 应用中,top_k 越大并不一定越准确。如果每个片段都很长,又没有去重和重排,检索结果可能占据大部分上下文,反而挤压当前问题和输出空间。
3. 工具定义和结构化输出被低估
函数调用、MCP 工具或复杂 JSON Schema 也会进入上下文。工具越多、参数说明越长,请求的固定开销越大。
4. 附件被重复加入
同一份代码、合同或报告可能同时出现在系统提示词、知识检索结果和用户附件里,内容重复但 Token 会重复计算。
5. 最大输出设置过大
部分接口会同时检查输入与最大输出预算。即使实际只想生成几百 Token,如果参数预留过大,也可能使总预算超过窗口。
6. 切换模型后仍沿用旧配置
不同模型的上下文窗口和最大输出上限可能不同。切换模型时如果没有同步调整历史长度、检索数量和输出上限,就会出现“原模型正常,新模型报错”。
三、先用 curl 建立一个可控的最小请求
排查工具或工作流之前,先直接调用接口。这样可以排除客户端自动拼接历史记录、附件和隐藏提示词的影响。
先设置环境变量:
export AI_API_KEY="替换为你的API_Key"
export AI_BASE_URL="填写你的OpenAI兼容接口Base_URL"
export AI_MODEL="替换为准确模型ID"
发送只包含一条消息的请求:
curl --request POST "$AI_BASE_URL/chat/completions" \
--header "Authorization: Bearer $AI_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"model": "'"$AI_MODEL"'",
"messages": [
{"role": "user", "content": "用一句话解释上下文窗口"}
],
"temperature": 0.2,
"max_tokens": 128
}'
如果最小请求成功,而 Dify、Cursor 或桌面客户端失败,说明 API Key、Base URL 和基础模型调用大概率没有问题,应重点检查工具实际组装的上下文。
如果最小请求仍然失败,则先按状态码处理:
- 401:检查认证;
- 404:检查路径和模型名;
- 429:检查限流与配额;
- 5xx:检查上游服务。
不要把所有 400 都直接判断为上下文超限,应读取响应体中的 error.code、error.type 和 error.message。
四、Python:估算消息体并从最旧对话开始裁剪
准确计算 Token 应优先使用目标模型对应的分词器。若当前服务没有提供分词器,可以先用保守估算做请求前保护,再结合接口返回的 usage.prompt_tokens 校准系数。
下面的示例完成三件事:保留系统提示词、始终保留最新用户请求、从最旧的普通对话开始删除。
示例中的估算函数不是精确分词器,但适合说明预算控制流程。
import json
import math
import os
import re
import requests
def estimate_tokens(text: str) -> int:
"""通用保守估算;生产环境应替换为目标模型分词器。"""
cjk = len(re.findall(r"[\u4e00-\u9fff]", text))
ascii_chars = len(re.findall(r"[\x00-\x7f]", text))
other = max(0, len(text) - cjk - ascii_chars)
return math.ceil(cjk * 1.5 + ascii_chars / 4 + other)
def message_tokens(message: dict) -> int:
content = message.get("content", "")
if not isinstance(content, str):
content = json.dumps(content, ensure_ascii=False)
return (
6
+ estimate_tokens(message.get("role", ""))
+ estimate_tokens(content)
)
def trim_messages(
messages: list[dict],
input_budget: int
) -> tuple[list[dict], int]:
if not messages:
return [], 0
system_messages = [
message
for message in messages
if message.get("role") == "system"
]
normal_messages = [
message
for message in messages
if message.get("role") != "system"
]
# 最新消息必须保留,避免裁剪后丢失当前问题。
kept = normal_messages[-1:] if normal_messages else []
used = sum(
message_tokens(message)
for message in system_messages + kept
)
if used > input_budget:
raise ValueError("系统提示词和当前问题已经超过输入预算")
# 从近到远补回历史消息,超出预算时停止。
for message in reversed(normal_messages[:-1]):
cost = message_tokens(message)
if used + cost > input_budget:
break
kept.insert(0, message)
used += cost
return system_messages + kept, used
def call_chat(messages: list[dict]) -> dict:
context_window = int(os.environ["AI_CONTEXT_WINDOW"])
reserved_output = int(os.getenv("AI_MAX_OUTPUT", "800"))
safety_margin = max(
512,
math.ceil(context_window * 0.1)
)
input_budget = (
context_window
- reserved_output
- safety_margin
)
trimmed, estimated_input = trim_messages(
messages,
input_budget
)
response = requests.post(
os.environ["AI_BASE_URL"].rstrip("/")
+ "/chat/completions",
headers={
"Authorization":
f"Bearer {os.environ['AI_API_KEY']}",
"Content-Type": "application/json",
},
json={
"model": os.environ["AI_MODEL"],
"messages": trimmed,
"max_tokens": reserved_output,
"temperature": 0.2,
},
timeout=(5, 60),
)
response.raise_for_status()
result = response.json()
result["local_budget"] = {
"estimated_input": estimated_input,
"messages_before": len(messages),
"messages_after": len(trimmed),
}
return result
这里要注意一个边界:不能只按消息条数裁剪。十条短对话可能比一条大型代码附件更小。
生产系统应统计真实 Token,并把附件、工具 Schema 和检索片段一起纳入预算。
五、不要只删除历史:先摘要,再保留关键事实
直接删除旧消息虽然简单,却可能丢失用户偏好、业务约束和前面已经确认的结论。
更稳妥的做法是把上下文分成三层:
- 稳定事实:用户身份范围、项目约束、输出格式、已经确认的决定;
- 滚动摘要:较早对话压缩成结构化摘要;
- 近期原文:保留最近几轮完整消息和当前问题。
滚动摘要不应写成一段模糊文字,可以使用固定结构:
目标:当前要完成什么
已确认:已经达成的结论
约束:不能改变的条件
未解决:仍需处理的问题
引用:必要的文件名、字段名、错误码
每次上下文接近阈值时,更新旧摘要,而不是继续叠加多个摘要。摘要本身也需要版本和长度限制,否则它最终也会变成新的超长历史。
六、Dify 工作流怎么控制上下文
Dify 中出现上下文超限时,先检查 LLM 节点最终接收的内容,而不是只看用户输入框。
建议按下面的顺序定位:
- 检查系统提示词是否粘贴了大段固定文档;
- 检查是否开启了对话记忆,以及保留了多少历史内容;
- 检查知识检索节点的
top_k、片段长度和重排结果; - 检查模板中是否重复引用了同一个变量;
- 检查工具定义、结构化输出 Schema 是否过长;
- 调低最大输出 Token,为输入留出空间;
- 在 LLM 节点前增加摘要或裁剪代码节点,并记录裁剪前后的长度。
知识库场景不要只追求更多片段。更合理的流程是:先召回,再去重和重排,只把与当前问题直接相关的少量片段交给模型。
对于长表格和长文档,可以先按章节生成摘要,再检索摘要对应的原文块。
七、Cursor 长对话和大文件怎么处理
Cursor 会对长对话和大型文件做摘要或压缩,但这不代表可以无限加入内容。
当文件显示为已压缩、显著压缩或未包含时,模型看到的内容可能已经不是完整原文。
实际使用中可以这样做:
- 一个聊天只处理一个明确任务,需求变化较大时新建聊天;
- 不要直接加入整个仓库,先限定目录、文件或函数;
- 大文件只引用与问题相关的行和符号;
- 把历史聊天中的关键结论整理成简短说明,再带入新会话;
- 让模型先列出还缺哪些文件,再逐个补充;
- 出现回答遗漏时,检查文件是否被压缩或未包含,而不是立刻重复发送全部代码。
“一次性提供所有上下文”看似省事,实际会降低有效信息密度。
对代码任务来说,入口函数、调用链、错误堆栈和相关配置通常比整个仓库更有价值。
八、Cherry Studio 的上下文数量和最大输出怎么设置
Cherry Studio 的对话设置中可以控制保留的上下文消息数量,也可以限制单次回答的最大 Token。
普通聊天可以先保留较少的最近消息;需要分步骤写作或连续推理时再适当增加。
如果出现长对话失败,可按以下步骤处理:
- 复制当前任务目标和已确认结论;
- 新建话题,把摘要作为第一条消息;
- 减少上下文消息数量;
- 将单次最大输出调整到实际需要的范围;
- 删除重复附件,只保留本轮需要的材料;
- 用短问题测试模型连通性,再逐步恢复必要上下文。
上下文数量是“消息条数”,不等于精确 Token。包含长代码、长文档或图片时,即使只保留几条消息,也可能占用很大空间。
九、Node.js 后端代理:在请求发出前统一做预算保护
如果多个前端、机器人和工作流共用一个 AI 接口,最好把上下文预算放到后端统一执行。
下面的 Express 示例限制消息体大小,并在发送前裁剪旧对话。
import express from "express";
const app = express();
app.use(express.json({ limit: "512kb" }));
function roughTokens(value) {
const text = typeof value === "string"
? value
: JSON.stringify(value);
const cjk = (
text.match(/[\u4e00-\u9fff]/g) || []
).length;
const ascii = (
text.match(/[\x00-\x7f]/g) || []
).length;
return Math.ceil(
cjk * 1.5
+ ascii / 4
+ (text.length - cjk - ascii)
);
}
function buildBudgetedMessages(messages, budget) {
const system = messages.filter(
(message) => message.role === "system"
);
const normal = messages.filter(
(message) => message.role !== "system"
);
const latest = normal.length
? [normal.at(-1)]
: [];
const kept = [...latest];
let used =
roughTokens(system)
+ roughTokens(latest);
if (used > budget) {
return {
messages: [],
estimatedInputTokens: used,
removedMessages: messages.length,
overflow: true,
};
}
for (
let index = normal.length - 2;
index >= 0;
index -= 1
) {
const cost = roughTokens(normal[index]);
if (used + cost > budget) {
break;
}
kept.unshift(normal[index]);
used += cost;
}
return {
messages: [...system, ...kept],
estimatedInputTokens: used,
removedMessages:
messages.length
- system.length
- kept.length,
overflow: false,
};
}
app.post("/api/chat", async (req, res) => {
const contextWindow = Number(
process.env.AI_CONTEXT_WINDOW
);
if (
!Number.isFinite(contextWindow)
|| contextWindow <= 0
) {
return res.status(500).json({
error: "CONTEXT_WINDOW_NOT_CONFIGURED",
});
}
const maxOutput = Math.min(
Number(req.body.maxOutput || 800),
2000
);
const safetyMargin = Math.max(
512,
Math.ceil(contextWindow * 0.1)
);
const inputBudget =
contextWindow
- maxOutput
- safetyMargin;
const budgeted = buildBudgetedMessages(
req.body.messages || [],
inputBudget
);
if (budgeted.overflow) {
return res.status(413).json({
error: "CURRENT_REQUEST_TOO_LARGE",
estimatedInputTokens:
budgeted.estimatedInputTokens,
});
}
const upstream = await fetch(
`${process.env.AI_BASE_URL.replace(/\/$/, "")}`
+ "/chat/completions",
{
method: "POST",
headers: {
Authorization:
`Bearer ${process.env.AI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: process.env.AI_MODEL,
messages: budgeted.messages,
max_tokens: maxOutput,
temperature: 0.2,
}),
signal: AbortSignal.timeout(60_000),
},
);
const body = await upstream.json();
if (!upstream.ok) {
return res.status(502).json({
error:
body?.error?.code
|| "UPSTREAM_REJECTED",
upstreamStatus: upstream.status,
});
}
res.json({
data: body,
context: {
estimatedInputTokens:
budgeted.estimatedInputTokens,
removedMessages:
budgeted.removedMessages,
},
});
});
app.listen(3000);
这个示例只保留近期消息。生产环境还应加入滚动摘要、真实分词器、检索片段预算和工具 Schema 预算。
裁剪行为也应返回给调用方,避免用户以为模型仍然看到了完整历史。
十、常见报错排查表
| 现象 | 常见原因 | 优先处理 |
|---|---|---|
context_length_exceeded |
输入与预留输出超过模型窗口 | 裁剪历史、检索片段和附件 |
prompt is too long |
系统提示词或拼接后的 Prompt 过大 | 输出最终请求长度,检查重复变量 |
max_tokens 参数错误 |
最大输出超过模型允许范围 | 按模型文档降低输出上限 |
rate_limit / 429 |
请求频率或 Token 吞吐超过限制 | 降低并发并做退避,不要只裁剪历史 |
timeout |
上下文过大、网络慢或模型处理时间长 | 先缩短输入,再检查分段超时 |
model_not_found |
模型 ID 错误或无访问权限 | 核对模型名,与上下文长度分开处理 |
401 / invalid_api_key |
认证头、API Key 或目标地址错误 | 用最小 curl 请求验证认证链 |
| 返回内容突然遗漏 | 文件或历史被工具压缩、摘要或移除 | 缩小任务范围并重新提供关键内容 |
十一、API Key 安全和日志记录
排查上下文问题时,经常需要记录请求信息,但不能把完整 API Key、用户文档和全部聊天内容直接写入日志。
建议记录:
- 模型 ID;
- 消息数量;
- 估算与实际输入 Token;
- 最大输出 Token;
- 检索片段数量;
- 附件数量与总长度;
- 裁剪消息数量;
- HTTP 状态码、错误码和请求 ID;
- 耗时与重试次数。
不建议记录:完整 API Key、身份证号、手机号、合同原文、未脱敏代码凭据和完整用户提示词。
API Key 应保存在服务端环境变量或密钥管理系统中,不要放进浏览器前端或公开仓库。
十二、企业场景需要增加哪些控制
团队统一接入大模型时,不能把上下文控制完全交给每个前端。建议在网关或后端服务中统一实现:
- 按模型维护上下文窗口和最大输出配置;
- 为系统提示词、历史、知识检索、工具和输出分别分配预算;
- 对超长文档做分块、摘要和分层检索;
- 为每次裁剪保留可审计记录,但不保存敏感原文;
- 监控真实
prompt_tokens与本地估算的偏差; - 对不同业务设置独立上限,避免单个请求占满 Token 吞吐;
- 用固定测试集验证裁剪后是否丢失关键事实。
上下文越长,并不代表答案一定越好。
企业应用更关注的是有效上下文:内容相关、没有重复、来源可追踪,并且给输出和工具调用留出足够空间。
十三、FAQ
1. context_length_exceeded 可以通过重试解决吗?
通常不能。相同请求不做修改,重试仍会超过窗口。应先减少输入或输出预算,再重新发送。
2. 只降低 max_tokens 就够了吗?
不一定。如果历史、附件和检索片段已经占满窗口,仅降低输出可能仍然失败。需要先拆分各部分预算,找到占用最大的来源。
3. 中文字符数可以直接换算成 Token 吗?
不能精确换算。不同模型和分词器结果不同,代码、空格、标点和 JSON 也会消耗 Token。
字符估算只能做预警,生产环境应使用对应分词器或接口返回的 usage 数据校准。
4. RAG 的 top_k 越大越好吗?
不是。过多低相关片段会增加成本、挤占上下文,还可能让答案偏离问题。
应结合召回、去重、重排和片段长度共同设置。
5. Cursor 已经自动摘要,为什么还会遗漏代码?
摘要和文件压缩只能保留模型判断的重要结构,无法保证每一行都存在。
遇到具体缺陷时,应单独提供相关函数、调用位置、错误堆栈和测试结果。
6. 新建 Dify 会话后恢复正常,说明是什么问题?
通常说明历史记忆或累计变量占用了过多上下文。
还应检查知识检索和系统提示词,避免新会话运行一段时间后再次超限。
7. 长上下文和 rate_limit 是同一类限制吗?
不是。上下文窗口限制单次请求可处理的内容;Token 速率限制约束一段时间内的吞吐。
缩短请求可能同时缓解两者,但排查时应根据状态码和错误信息分别处理。
总结

context_length_exceeded 的核心不是“对话轮数太多”,而是一次请求中系统提示词、历史消息、当前问题、工具定义、检索片段、附件和预留输出的总 Token 超过了模型窗口。
可靠的处理顺序是:
- 先用 curl 验证最小请求;
- 输出最终消息体和各部分预算;
- 历史消息采用“稳定事实 + 滚动摘要 + 近期原文”的分层结构;
- 知识库内容经过召回、去重和重排;
- 在后端统一执行裁剪、日志和安全控制。
这样做不仅能减少上下文超限,也能降低无效 Token 消耗,并让 Dify、Cursor、Cherry Studio 和自建应用在长任务中保持可预测的行为。
参考文档
- OpenAI API 请求调试与请求 ID:https://platform.openai.com/docs/api-reference/introduction
- Cursor 长对话摘要与文件压缩:https://docs.cursor.com/en/agent/chat/summarization
- Cherry Studio 上下文数量与最大输出设置:https://docs.cherry-ai.com/cherry-studio/preview/chat
更多推荐

所有评论(0)