标题:qwen3.7-max 报 400 context_length_exceeded 怎么办?删了聊天记录为什么还超限?
正文:
qwen3.7-max 报 400 context_length_exceeded 怎么办?删了聊天记录为什么还超限?
2026 年 10 月 22 日,我在一次本地调试中给采购 Agent 配置了 6 个工具。删掉最近几轮聊天后,请求 qwen3.7-max 仍返回 HTTP 400,应用侧记录的往返耗时为 186 ms。
下面是只保留排障字段的脱敏日志:
json
{
"date": "2026-10-22",
"environment": "local",
"model": "qwen3.7-max",
"tool_count": 6,
"http_status": 400,
"error_code": "context_length_exceeded",
"elapsed_ms": 186,
"response_has_generated_content": false,
"request_id": "[REDACTED]"
}
186 ms 只能说明响应较早返回,不能单凭耗时证明模型"压根没开始生成"。更准确的说法是:响应中没有生成内容,结合错误码,推测请求在生成前被网关或服务端的上下文校验拒绝。
继续随手删除几条消息通常没有用。发送请求前,应分别计算系统消息、工具定义、历史消息、当前输入、本轮工具结果和预留输出,再按明确的优先级裁剪。
可以先用下面的本地预算关系进行检查:
text
系统消息
+ 工具与请求 Schema
+ 历史完整回合
+ 当前输入
+ 本轮工具结果
+ 预留输出
≤ 本地配置的总预算
这是一种保守的客户端预算模型,不是所有接口都采用的固定校验公式。许多兼容接口会结合输入长度与请求的最大输出预算进行校验,但不同服务商也可能分别限制总上下文、最大输入、实际输出或最大输出上限。最终应以当前路由的文档、控制台限制和错误响应为准。
这里的 400 是 HTTP 客户端错误码,不是"上下文只有 400 token"。对于内容完全相同的超长请求,原样重试通常只会再次失败。
数据边界:按 2026 年 10 月 27 日随文模型目录核对,模型完整 ID 为
qwen3.7-max。现有资料没有给出该路由的上下文窗口、最大输出和价格,因此下文不填写未经证实的数值,也不把其他 Qwen 型号的参数直接套用到它身上。
为什么会出现这个问题
兼容接口的错误响应可能类似下面这样:
text
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"message": "The request exceeds the model's maximum context length. Please reduce the length of the messages or the maximum output tokens.",
"type": "invalid_request_error",
"param": "messages",
"code": "context_length_exceeded"
}
}
这只是示例。具体的 HTTP 状态码、message、type、param 和 code 可能被官方端点、API 网关或工作流平台重新封装,不能把上述结构当成固定格式。
我最初只统计了 messages 中的用户聊天,但实际请求里还可能包含:
- system 或 developer 消息;
- Function Calling、MCP 等工具定义;
- 工具参数的 JSON Schema;
- 结构化输出或响应格式 Schema;
- 历史工具调用及其返回值;
- 当前轮生成的工具结果;
- RAG 检索片段;
- 图片、文件或其他附件的文本化内容;
max_tokens或max_completion_tokens对应的输出预算。
先按现象选择处理方式
| 现象 | 优先处理 | 原因 |
|---|---|---|
| 减少历史消息后仍然 400 | 检查输出预算、工具定义和固定提示 | 超限项可能不在聊天记录中 |
| Agent 工具越加越容易报错 | 精简工具描述及参数 Schema | 顶层工具定义通常会随每次请求重复发送 |
| RAG 查询从第二轮开始失败 | 限制检索片段与工具结果 | 文档和工具返回值可能被带入后续上下文 |
| 长对话偶发超限 | 发送前执行 token 预算检查 | 字符数不能稳定换算成 token |
| 同一请求反复返回 400 | 停止原样重试,先修改请求 | 确定性的请求错误不会因等待而自动消失 |
如果请求链路包含 OpenRouter 或 ofox.io 这类聚合网关/中转方案,还应分别记录网关返回的 model、code、request_id 与客户端 token 估算,避免只根据 HTTP 400 判断超限发生在哪一层。
整个请求可以按下面的关系理解:
qwen3.7-max 是否存在额外的推理 token,以及服务端如何计算这部分占用,现有资料无法确认。应查看当前服务商的文档、响应字段和控制台限制,不宜凭其他模型的行为推断。
把六本账分开算
不要只记录一个总的 prompt_tokens。请求发送前,可以统一拆成下面六类:
system_tokens:system/developer 消息、角色规则和固定业务约束;tool_tokens:工具名称、描述、参数 Schema,以及请求级结构化输出 Schema;history_tokens:此前已经完成的用户、助手和工具交互回合,包括历史工具调用及其结果;current_tokens:当前用户输入、当前注入的 RAG 片段和附件内容;current_tool_result_tokens:当前尚未结束的回合中,新产生并准备回传给模型的工具结果;reserved_output_tokens:客户端计划留给本次回复的最大输出预算。
这里采用一种固定归属:
- 历史完整回合中的工具结果只计入
history_tokens; - 当前进行中回合的工具结果只计入
current_tool_result_tokens; - 同一段工具结果不能在两类中重复计算。
如果当前路由按总上下文限制校验,可使用下面的保守预算模型。设总上下文上限为 C,安全缓冲为 S:
text
可用输入预算 B = C - reserved_output_tokens - S
实际输入 I =
system_tokens
+ tool_tokens
+ history_tokens
+ current_tokens
+ current_tool_result_tokens
仅当 I ≤ B 时才发送
但这不是所有服务端的统一规则。如果某个路由分别设置"最大输入"和"最大输出",就应分别校验:
text
实际输入 ≤ 最大输入限制
请求的最大输出 ≤ 最大输出限制
如果服务端按其他方式计算,则应以该路由文档和实际错误响应为准。
C 也必须从当前服务商的模型文档、控制台或配置中读取。模型理论窗口与某个 API 路由实际开放的窗口未必相同。
计数器应尽量匹配服务端 tokenizer。不能简单用汉字数除以二,再把结果当成准确 token 数。代码、JSON、URL、自然语言和工具 Schema 的分词比例可能相差很大。拿不到准确 tokenizer 时可以保守估算,但日志中应明确标记为 estimated。
python
parts = {
"system": system_messages,
"tools": {
"tools": tools,
"response_format": response_format,
},
"history": completed_history,
"current": {
"message": current_message,
"retrieved_docs": retrieved_docs,
"attachments": attachments,
},
"current_tool_results": current_tool_results,
}
usage = {
name: count_serialized_tokens(value)
for name, value in parts.items()
}
usage["reserved_output"] = max_completion_tokens
usage["estimated"] = True
count_serialized_tokens() 只是占位接口。计数时最好针对最终序列化后的请求内容,而不是只计算业务层的原始字符串;随便选一个不匹配的 tokenizer,也不能保证准确计算 qwen3.7-max 的实际占用。
按优先级裁剪,不要直接截取数组尾部
只执行下面这行代码存在明显风险:
python
messages = messages[-8:]
在标准 Chat Completions 请求中,system 或 developer 消息通常就在 messages 数组头部,直接保留最后八条可能把它们误删。顶层 tools 通常仍然存在,因此请求可能同时出现"固定规则丢失"和"工具定义占用没有下降"两个问题。
此外,这种裁剪还可能保留孤立的 tool 返回值,却删除与之对应的 assistant tool call,导致消息结构非法。
更稳妥的做法是先分离固定消息,再按完整回合裁剪:
python
fixed_messages = extract_system_and_developer_messages(messages)
complete_turns = group_into_complete_turns(messages)
recent_turns = select_recent_turns(complete_turns, token_budget)
trimmed_messages = fixed_messages + recent_turns
我通常采用下面的裁剪顺序:
- 降低不必要的最大输出预算;
- 压缩本轮和历史工具结果;
- 对 RAG 文档去重,只保留与当前问题相关的片段;
- 把较旧的完整回合压缩成摘要;
- 保留最近若干个完整回合,而不是若干条孤立消息;
- 精简重复的 system/developer 规则;
- 最后再压缩工具描述和参数 Schema。
工具结果尤其容易膨胀,但不应优先按字符从中间硬切。硬截可能破坏 JSON、表格和关键字段,使后续模型收到不可解析或语义残缺的数据。
优先使用以下方法:
- 对对象执行字段白名单;
- 对列表分页,只返回当前页;
- 将大结果转换为结构化摘要;
- 保留错误码、状态、标识符等关键字段;
- 记录
truncated、原始长度、返回长度和下一页游标; - 只有无法结构化处理时,才使用保留头尾的字符级保险。
例如:
python
def compact_tool_result(result, allowed_fields=None, page_size=100):
if isinstance(result, dict):
fields = allowed_fields or result.keys()
data = {k: result[k] for k in fields if k in result}
return {
"data": data,
"_meta": {
"truncated": len(data) < len(result),
"original_field_count": len(result),
},
}
if isinstance(result, list):
page = result[:page_size]
return {
"data": page,
"_meta": {
"truncated": len(result) > page_size,
"original_item_count": len(result),
"returned_item_count": len(page),
"next_cursor": page_size if len(result) > page_size else None,
},
}
text = str(result)
limit = 12000
if len(text) <= limit:
return {
"data": text,
"_meta": {
"truncated": False,
"original_chars": len(text),
},
}
# 最后的字符级保险:保留头尾,并放进结构化容器中。
head_size = limit * 3 // 4
tail_size = limit - head_size
return {
"data": {
"head": text[:head_size],
"tail": text[-tail_size:],
},
"_meta": {
"truncated": True,
"original_chars": len(text),
"returned_chars": limit,
},
}
这里的 12000 只是字符级保险示例,不是 qwen3.7-max 的官方限制。实际阈值应根据序列化后的 token 计数和业务数据结构调整。
历史上下文可以采用"有限摘要记忆 + 最近完整回合":
python
context = []
context += fixed_messages
context.append(memory_summary)
context += recent_complete_turns
context += current_turn_messages
RAG 文档和本轮工具结果应放在前文定义的 current_tokens 与 current_tool_result_tokens 中单独计数,不要再重复算入历史。
摘要本身也不能无限增长。可以为摘要设置独立预算,将原始记录存入数据库,需要时再通过检索取回。对于合同、金额、日期、订单号和代码变更等内容,最好同时保留结构化事实,避免摘要丢失关键数字。
在 API 网关前增加预算检查
同一套 Agent 经过不同路由时,错误字段、模型限制和计费统计方式可能不同,但路由层不能凭空扩大模型或上游接口允许的上下文窗口。
这些产品也不宜全部归为同一类别:
- OpenRouter 与 ofox.io 可以作为聚合网关/中转方案进行比较,但两者支持的模型、错误字段、usage 统计和路由限制应分别以当前文档与控制台为准;
- Together AI 主要提供模型推理和 AI 云服务,不宜简单等同于聚合 API;
- 即使请求参数都使用
messages、tools和max_completion_tokens,不同路由对上下文窗口与输出预算的校验方式也可能不同。
无论使用哪一层,真正防止超长请求的关键仍是客户端预检、结构化压缩与裁剪。网关日志可以协助定位问题,但不能代替请求预算控制。
兼容客户端配置示例
在 OpenRouter 与 ofox.io 等聚合网关/中转方案之间配置兼容客户端时,建议把 base_url、API Key 和模型 ID 分开注入,并在日志中记录实际生效的路由配置。
下面是兼容客户端的配置示例,模型 ID 按 2026 年 10 月 27 日目录填写:
python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ["LLM_BASE_URL"],
)
MODEL = "qwen3.7-max"
预算检查最好放在最后一次请求组装和序列化之后。工具 Schema、默认 system/developer 消息、响应格式或工作流字段,可能要等业务代码执行完才被注入。
python
payload = build_final_payload()
while request_tokens(payload) > input_budget:
action = shrink_once(payload)
if action is None:
raise ValueError("context budget cannot be satisfied")
log_shrink_action(action, payload)
send(payload)
shrink_once() 每次应只执行一个可解释的动作,并记录:
- 裁剪对象;
- 裁剪原因;
- 裁剪前后的 token;
- 是否为估算值;
- 是否影响历史、工具结果或输出预算。
不要在循环里无条件删除消息。否则线上只能看到"请求成功了",却无法解释模型为什么突然丢失规则或上下文。
400 和 429 不要共用一套重试策略
这两个状态码经常被放进同一个重试中间件,但通常需要不同的处理方式:
429常见于速率或配额限制,可结合Retry-After和服务商文档执行退避;400 context_length_exceeded表示当前请求不符合上下文限制,等待不会使请求自动变短;- 如果网关使用其他状态码或错误码,还要结合
message、usage、响应头和服务商文档判断; - 经过网关时,应同时保存 HTTP 状态码、错误字段、模型 ID、请求的输出上限和各部分 token 估算;
- 日志应移除 API Key、业务文档正文和用户隐私,但不能只留一句"请求失败"。
建议至少记录以下字段:
json
{
"model": "qwen3.7-max",
"status": 400,
"code": "context_length_exceeded",
"request_id": "[REDACTED]",
"system_tokens": 1200,
"tool_tokens": 3500,
"history_tokens": 9200,
"current_tokens": 1800,
"current_tool_result_tokens": 2600,
"token_counts_estimated": true,
"max_completion_tokens": 4096
}
这些数值仅用于演示日志结构,不代表 qwen3.7-max 的官方限制或某次真实请求的 token 用量。
常见问题 FAQ
Q:qwen3.7-max 返回 400,是不是只能输入 400 token?
不是。400 是 HTTP 客户端错误状态码,与允许输入的 token 数没有这种对应关系。
如果响应中明确出现 context_length_exceeded,可以直接判断为上下文长度相关错误。若接口使用其他 code、文案或 HTTP 状态码,则还需要结合 message、usage、响应头和服务商文档判断。
Q:为什么删除 10 条聊天记录后仍然超限?
聊天记录只是请求上下文的一部分。system/developer 消息、工具 Schema、结构化输出 Schema、RAG 文档、附件、工具返回值和输出预算都可能占据大量空间。
应先按六类分项计数,再决定压缩哪一项,不要只按消息条数判断。
Q:把 max_tokens 调小能解决超限吗?
有可能,但取决于具体路由的校验规则。
如果服务端按"输入占用 + 请求的最大输出预算"进行校验,而且请求只是轻微越界,调小输出上限可能有效。如果输入本身已经超过该路由的最大输入限制,仍然必须裁剪输入。
参数名可能是 max_tokens,也可能是 max_completion_tokens,应以接口文档为准。
Q:context_length_exceeded 可以自动重试吗?
不能原样重试。程序可以在修改请求后自动重试,例如:
- 降低不必要的输出预算;
- 对工具结果执行字段筛选或分页;
- 减少 RAG 片段;
- 摘要旧的完整回合;
- 重新计算最终序列化请求的 token。
原样重试适合部分瞬时故障,不适合确定性的超长请求。
Q:字符数可以直接换算成 token 吗?
不能精确换算。汉字、英文、代码、JSON、URL 和工具 Schema 的分词比例不同。拿不到匹配的 tokenizer 时只能进行保守估算,并留出安全缓冲,同时在日志中标记计数结果为 estimated。
Q:为什么不能直接保留最后八条消息?
因为 system/developer 消息可能位于数组头部,直接取末尾可能把固定规则删除;同时还可能拆散 assistant tool call 与对应的 tool result。
应先分离固定消息,再按完整回合进行裁剪。
我的最终选择
我现在会把上下文当成有限资源,而不是把 messages 当成可以无限增长的数组。
具体做法是:
- 单独统计 system/developer 消息;
- 对工具定义和响应 Schema 做版本化管理;
- 历史对话使用有限摘要加最近完整回合;
- 历史工具结果统一归入历史回合;
- 本轮工具结果单独设置预算;
- 对 RAG、附件和工具输出执行字段筛选、分页与结构化摘要;
- 在最终序列化之后执行一次预算检查;
- 根据当前路由的文档和错误响应选择校验规则。
核心原则很简单:先把六本账算清楚,再决定压缩什么。
随手删除几条聊天记录只是碰运气。只有统一系统消息、工具定义、历史回合、当前输入、本轮工具结果和预留输出的归属与计数口径,服务才能长期稳定运行。