Troubleshooting · 上下文窗口

prompt is too long / context_length_exceeded — 把上下文窗口塞满的,通常不是你刚发出去的那一句

30 秒版
  1. 限制是对整段对话,不是对你最后那句——每次请求都把前面全量重发,所以压垮它的往往是一句短话。
  2. /compact 压缩,不够再 /clear 重开;长期用 .claudeignore 把不该进上下文的目录挡掉。
  3. 失败的请求不会缩短历史,所以一炸就会连着炸——必须主动压缩,等是等不好的。

这个上限管的是整段对话,不是你最后那条消息。每一次请求都会把之前的所有东西重发一遍——先前的每一轮、读过的每一个文件、每一条工具结果。所以真正撑爆的那条消息,往往只有一行。而且有三种不同的上限,报错文字几乎一模一样;其中只有一种能靠少发输入解决。

你看到的报错

以下任意一种
API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long: <your-count> tokens > <limit> maximum"}}

{"error":{"message":"This model's maximum context length is <limit> tokens. However, your messages resulted in <your-count> tokens. Please reduce the length of the messages.","type":"invalid_request_error","param":"messages","code":"context_length_exceeded"}}

{"type":"error","error":{"type":"invalid_request_error","message":"input length and `max_tokens` exceed context limit: <input> + <max_tokens> > <limit>, decrease input length or `max_tokens` and try again"}}

{"type":"error","error":{"type":"invalid_request_error","message":"max_tokens: <your-value> > <limit>, which is the maximum allowed number of output tokens for <模型 id>"}}

这里的数字一律写成 <...>,因为它们每个模型、每个端点都不一样——只有你自己报错里的那组数字才值得看。

措辞也会变。有些端点把第三种缩短成 input length exceeds context limit,有些则去掉 JSON 外壳,只打印 prompt is too long。同一个上限,换个包装而已——两种写法都要搜。

前两种是同一个问题。第三种是这个问题再加上你给输出预留的空间。第四种是换了一身相似措辞的另一个上限,把输入缩小完全动不到它。

三个都被叫作「context length」的上限

管的是什么会出现的报错文字真正有效的解法
输入/上下文窗口你在一次请求里发出的所有东西:系统提示、工具定义、先前的每一轮、每一条工具结果、每一个附上的文件prompt is too long · maximum context length · context_length_exceeded从对话里移掉内容
输出上限(max_tokens模型最多能写回多少max_tokens: <n> > <limit>调低 max_tokens。你的输入无关
输入+输出合计输入和你要求的 max_tokens 必须同时塞得进窗口input length and max_tokens exceed context limit两边动哪边都行——调低 max_tokens缩小输入

表格第三行是最让人意外的:一个请求可能在还没生出任何一个输出 token 之前就被拒绝,因为你给答案预留的空间,已经跟问题挤不下了。其实没有任何东西太长,是预留得太贪心。

为什么你「只发了一行」它也会炸

没有「单条消息」的上限这回事。每一次请求都会再带上整段对话,所以第 N 条消息的代价,是 1 到 N 的总和。最后那条消息不是原因,它只是压垮骆驼的最后一根稻草。

真正占住窗口的是这些,大致按「被低估的程度」排序:

  1. 工具结果。对一个还留着 node_modules 的仓库列一次目录、读一次 lock 文件、在整棵树上做一次没收敛范围的搜索。在对话记录里,这些看起来只是一行短短的输出。在请求里不是。
  2. 早先读过、之后一直没放掉的文件。第 2 步读的那个文件,到了第 40 步还在被重发。
  3. 会话一开始就自动加载的项目指示文件。你从来没看它们滚过屏幕,但它们在每一次请求里。
  4. 对话本身——大家第一个怪罪的那块,通常也是最小的那块。

有两个推论值得记牢:

  • 失败的请求不会让历史变短。这就是为什么一次溢出之后紧接着又一次:重试发的是同一份超大的请求内容。你得先移掉一些东西,再重试。
  • 窗口填得最快的时候,是一个顺利进行的长任务,不是一个难搞的任务。一大堆全都成功的小工具调用,就是最典型的样子。

按这个顺序排查

1 · 动任何东西之前,先读那两个数字#

上面每一种报错里,都同时有你的大小和上限。两者的比例决定策略:

  • 只超出一点 → 压缩对话(第 3 步)。
  • 超出好几倍 → 有不该进来的大东西被拉进来了(第 5 步)。
  • 光算输入明明离上限还很远,报错里却点名 max_tokens → 直接跳到第 6 步。

没看比例就开始猜,是这件事拖掉一个下午的原因。

2 · 先搞清楚你实际用掉多少#

先问客户端——它知道自己一直在发什么:

客户端内
/context

想在客户端之外拿到精确数字,可以请端点只数一个请求、不真的执行它:

终端
# <模型 id>:从 https://api.9coding.com/v1/models 里复制一个
curl -sS https://api.9coding.com/v1/messages/count_tokens \
  -H "Authorization: Bearer sk-9c-xxxxxxxx" \
  -H "content-type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"<模型 id>","messages":[{"role":"user","content":"paste the text you are about to send"}]}'

不是每个兼容端点都实现了这条路由。如果回 404,退而求其次做个数量级的估算:

终端
wc -c < the-file-you-are-about-to-paste.txt

粗略的尺度是:英文散文大约几个字符一个 token,源代码更少,中日韩文字则可能接近一个字一个 token。这个估算只能用来判断某个东西是不是明显太大——绝不能用来判断某个东西是不是刚好塞得下。

3 · 先压缩,再清空#

客户端内
/compact

它把对话记录换成一份摘要,然后接着做下去。要在你还没被逼到的时候做,不是被逼到之后——压缩本身得把历史再发一次才能摘要,而一段已经超过上限的对话,可能连这一次都发不出去。

4 · 压缩不够用时就清空#

客户端内
/clear

/compact 留下一份摘要,/clear 什么都不留。这个差别很重要:如果撑爆窗口的是某一条超大的工具结果,那它的摘要仍然是它的摘要,可能会把一大部分分量带到后面去。压缩完马上又出现同样的溢出,就改用清空,然后用三句话把任务重讲一次。

5 · 别让仓库把自己加载进窗口#

决定排除什么之前,先找出哪些东西大:

终端
du -sh -- */ 2>/dev/null | sort -h | tail -15
find . -type f -not -path './.git/*' | wc -l

然后把那几个惯犯完全挡在上下文外面:

.claudeignore
node_modules/
dist/
build/
.next/
vendor/
coverage/
logs/
*.min.js
*.map
package-lock.json
pnpm-lock.yaml
*.png
*.jpg
*.pdf
*.csv
把这份清单放进你的 ignore 文件——Claude Code 是 .claudeignore。在你依赖它之前,先确认你自己正在跑的那个客户端版本实际认的文件名与生效范围。

lock 文件和压缩过的打包文件值得特别点名:它们是单个文件,在目录列表里看起来人畜无害,却经常是整个仓库里最大的一段文字。

也顺便改一下你看文件的方式:能用带几行上下文的定点搜索,就别整个文件读;能读一段范围,就别读整个模块。每少读一个完整文件,就多留下一块窗口。

6 · 报错里点名 max_tokens 的话,调的是它,不是你的输入#

终端
# <模型 id>:从 https://api.9coding.com/v1/models 里复制一个
# 下面的 max_tokens 只是个随便取的探测值,不是文档写的上限
curl -sS -w '\n%{http_code}\n' https://api.9coding.com/v1/messages \
  -H "Authorization: Bearer sk-9c-xxxxxxxx" \
  -H "content-type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"<模型 id>","max_tokens":1024,"messages":[{"role":"user","content":"hi"}]}'

这种最小请求成功、你真正那个请求失败,问题就在输入。如果连这个也失败,就是 max_tokens 设得超过那个模型允许的值,怎么删都没用。

7 · 还是塞不下,就是任务本身太大#

拆开不是权宜之计,它就是解法。一个会话做一件任务,每件的产出写进文件、而不是留在对话里带着走,这样每次请求都很小,而且——最被低估的那一点——可以重跑。一个做了四小时、最后以溢出收场的会话,是重跑不了的。四个短的可以。

什么时候这不是你的问题

  • 同一份 prompt 在某个端点塞得下,在另一个端点塞不下。网关可能把窗口压在低于模型本身支持的上限。拿它跟模型文档写的上限比一比;两者对不上,那就是端点的策略,不是你发的内容。
  • 不管你删掉什么,它都在完全相同的大小失败。固定开销——系统提示、工具 schema——吃掉了一块你在对话里编辑不到的份额。把启用的工具数量减少。
  • 今天突然开始失败,而你的配置完全没动。可用窗口大小和默认 max_tokens 都是端点那一侧的配置,是会变的。
  • 连压缩本身都失败。这是唯一一种你真的没办法原地救回来的情况;开一个新的会话。

一个不公开自己实际上下文上限的端点,是你没办法规划长任务的端点——你只能靠撞上去,才知道天花板在哪。在你花一下午重写 prompt 之前,先去看运营方的状态页——我们的在状态页

提问题时请带上 request id——9Coding 的报错响应里都有一个,形如 (request id: 2026...)。这个 id 加上时间戳、模型名和完整报错文本,就能把那一次调用查出来。只说「用不了」的反馈没法排查。

不是同一个问题

429 表示请求大小没问题,是你发太多次了——请看 429 Too Many Requests。带 invalid_request_error、但报错里没有任何大小数字的 400,是请求格式有问题,不是请求太大——请看多轮对话里的 400 Bad Request。这两种都在别的页。

相关报错