1. 先别急着改最后一句:context_length_exceeded 的真实来源
你调用 API 时突然收到这样一段报错:
{ "error": { "message": "This model's maximum context length is 131072 tokens. However, you requested 296261 tokens (296261 in the messages, 0 in the completion). Please reduce the length of the messages.", "type": "invalid_request_error", "code": "context_length_exceeded" } }Anthropic 系接口的文案略有不同,但意思一样:prompt is too long: 215043 tokens > 200000 maximum。如果你用 LangChain 或 LangGraph 自己搭 Agent Harness,这个异常往往还会被包装成OutputParserException或者openai.BadRequestError: Error code: 400,看起来像是解析出错,其实根子还是上下文超限。
context_length_exceeded是什么?简单说,它是模型服务端在请求进入推理前做的一次长度校验:你这次请求里所有输入 token 加上你要求生成的最大 token,超过了这个模型能同时"看见"的窗口上限,于是直接拒绝。它能帮你做什么?它其实是一份免费的体检报告——报错里明确写了X tokens是模型上限、Y tokens是你实际请求量,还拆分了in the messages和in the completion,等于告诉你超限到底出在输入还是输出。
适合谁看?自己写 Harness、跑多轮对话、做工具调用 Agent、搞多 Agent 协作的开发者。尤其是那些第一反应是"是不是我这句话问得太复杂了"的人——我试过反复精简最后一句提问,结果 token 数纹丝不动,因为真正膨胀的是整个消息历史。这篇就按请求体、历史消息累积、max_tokens 设置三个角度,把超限来源一层层剥开,最后给你可复制的裁剪配置和 token 计数验证动作。
2. 接入前的准备:用 TaoToken 统一管理模型调用与 Key
排查这类问题,第一步不是改代码,而是先让请求链路变得可观测。如果你同时接了 OpenAI、Anthropic、以及各种兼容接口,每个 SDK 的报错格式、token 统计口径都不一样,排查成本会翻倍。我的做法是先用一个统一的接入层把模型调用收口,TaoToken 就是干这个的:它提供 OpenAI 兼容的 API 入口,你换 Base URL 和 Key 就能切换底层模型,报错格式和 token 统计口径保持一致,排查时不用在几套文档之间来回跳。
具体怎么拿 Key:打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台里创建 API Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理页面在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。API 根地址统一用 https://taotoken.net/api ,注意这个地址后面不加任何 UTM 参数,SDK 里填 Base URL 时就用它。
这里要强调一个排查前提:你必须能拿到每次请求的完整消息体。很多人排查失败,是因为请求是框架内部拼装的,自己看不到最终发出去的 JSON。所以接入时建议先写一个最小可复现脚本,手动构造 messages 数组,手动发请求,这样报错里的Y tokens才能和你手里的数据对上。如果你用 Claude Code 这类工具,它内部也会做上下文管理,报错文案可能是prompt is too long,排查思路一致,但你要去它的配置文件里找上下文窗口设置。
另外,如果你打算长期跑编码类 Agent 任务,可以考虑 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它更适合高频、长会话的场景。但无论用哪种方式,排查阶段都建议先用最小脚本把问题复现出来,别一上来就在复杂框架里改。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各语言 SDK 的 Base URL 填法。模型对话调试入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite ,可以先用它手动发一条超长消息,亲眼看看报错长什么样,建立直觉。
3. 可复制配置:请求体、历史裁剪与 max_tokens 三处下手
排查超限,核心是把"发出去的请求"变成可控的。下面给你三份可直接复制的配置片段,分别对应请求体构造、历史消息裁剪、以及 max_tokens 设置。
3.1 最小请求体:先看清你发了什么
先写一个不依赖任何框架的脚本,手动构造 messages 并打印 token 估算:
import tiktoken from openai import OpenAI client = OpenAI( base_url="https://taotoken.net/api", api_key="你的_TAOTOKEN_KEY" ) enc = tiktoken.get_encoding("cl100k_base") def count_tokens(messages): total = 0 for m in messages: # 每条消息有固定开销,粗略按 4 计 total += 4 total += len(enc.encode(m["role"])) total += len(enc.encode(m["content"])) return total messages = [ {"role": "system", "content": "你是一个代码助手。"}, {"role": "user", "content": "帮我分析这段日志。"}, ] print("输入 token 估算:", count_tokens(messages)) resp = client.chat.completions.create( model="gpt-4o", messages=messages, max_tokens=1024 ) print(resp.choices[0].message.content)这段代码的价值在于:报错里的Y tokens你能自己算出来,和官方统计对不上时,你就知道是估算口径问题还是真的有隐藏内容被塞进去了。
3.2 历史消息裁剪:滑动窗口 + 摘要压缩
最基础的是滑动窗口,只保留最近 N 轮:
def truncate_messages(messages, max_turns=10): system_msgs = [m for m in messages if m["role"] == "system"] other_msgs = [m for m in messages if m["role"] != "system"] # 每轮约含 user + assistant 两条 return system_msgs + other_msgs[-max_turns * 2:]更推荐的是摘要压缩,接近阈值时把早期对话压成一段摘要:
def compress_history(messages, model_client, threshold_tokens=100000): if count_tokens(messages) < threshold_tokens: return messages mid = len(messages) // 2 old_part, recent_part = messages[:mid], messages[mid:] summary_prompt = "请将以下对话历史压缩成简洁摘要,保留关键决策和结论:\n" + format_messages(old_part) summary = model_client.generate(summary_prompt) return [{"role": "system", "content": f"此前对话摘要:{summary}"}] + recent_part3.3 工具结果裁剪:超限大户常在这里
工具调用返回的大段内容,是很多人忽略的超限来源。读一个几千行日志、搜索返回几十条结果,原样塞进历史,几轮就爆:
def truncate_tool_result(result: str, max_chars=3000): if len(result) <= max_chars: return result return result[:max_chars] + f"\n...(内容过长,已截断,完整内容共{len(result)}字符)"3.4 max_tokens 设置:别指望它救输入侧
max_tokens控制的是模型这次生成回复的长度上限,它计入的是报错里的in the completion部分。如果你的超限来自in the messages,把 max_tokens 从 4096 降到 512 也毫无用处,因为输入侧一点没变。正确做法是:max_tokens 设一个合理值(比如 1024 到 4096),然后专门去处理输入侧的历史和工具结果。
如果你用 Claude Code 或类似工具,配置里通常有max_tokens和上下文窗口相关字段,三件套要写全:Base URL 填https://taotoken.net/api,Key 填你的 TaoToken Key,Model ID 填你实际用的模型名。缺一个都会导致请求行为不符合预期,排查时先确认这三项。
4. 验证请求:怎么确认修复真的生效
改完裁剪逻辑,不能只看"这次没报错"就完事,要主动验证。下面给你一套验证动作。
第一步,构造一个必然超限的请求,确认报错可复现:
big_text = "这是一段测试内容。" * 50000 # 故意撑大 messages = [ {"role": "system", "content": "你是助手。"}, {"role": "user", "content": big_text} ] print("估算 token:", count_tokens(messages)) # 发请求,预期收到 context_length_exceeded第二步,套上裁剪函数,再发一次,确认请求通过:
trimmed = truncate_messages(messages, max_turns=2) print("裁剪后 token:", count_tokens(trimmed)) resp = client.chat.completions.create( model="gpt-4o", messages=trimmed, max_tokens=512 ) print("成功:", resp.choices[0].message.content[:50])第三步,做一次长对话模拟,连续跑 30 轮,每轮打印当前历史 token 数,观察曲线是否被压住:
history = [{"role": "system", "content": "你是助手。"}] for i in range(30): history.append({"role": "user", "content": f"第{i}轮问题"}) history = compress_history(history, model_client, threshold_tokens=8000) print(f"第{i}轮后 token: {count_tokens(history)}")成功的结果是:token 数在阈值附近波动,而不是单调上升。如果它还在持续涨,说明你的压缩逻辑没被真正调用,或者工具结果没走裁剪函数。
验证模型行为是否正常,可以用模型对话入口 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 手动发一条裁剪后的消息,确认模型还能正常理解上下文,而不是因为裁剪过度"失忆"。
5. 常见报错对照排查:401、local proxy failed、reading choices、OAuth
排查超限时,你可能会撞上其他报错,容易混淆。下面按真实报错逐条对照。
401 Unauthorized:和超限无关,是 Key 或 Base URL 问题。检查三件套是否写全——Base URL 是否为https://taotoken.net/api,Key 是否复制完整(注意前后空格),Model ID 是否拼写正确。如果用了环境变量,确认变量真的被读取到。
local proxy failed / connection error:通常是网络层问题,不是 token 超限。先确认你的请求能到达https://taotoken.net/api,再检查 SDK 版本是否兼容。这类错误不会带context_length_exceeded字样,别往裁剪方向排查。
Error reading choices / choices 为空:可能是响应被截断或解析失败。先看原始响应体,确认是不是服务端返回了错误 JSON 而你的代码直接去读choices[0]。加一层判断:
if "error" in resp: print("服务端错误:", resp["error"]) else: print(resp["choices"][0]["message"]["content"])OAuth / 认证流程报错:如果你用 Claude Code 或 Codex 这类工具,认证方式可能不是简单 Key。以 Codex 的auth.json为例,里面存的是凭证信息,配置时要确保 Base URL、Key、Model ID 三件套和你的 TaoToken 账号一致。Claude Code 的 Anthropic 兼容接入,Base URL 同样填https://taotoken.net/api,具体字段参考接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
context_length_exceeded 本身:回到本文主线。先看报错里的in the messages和in the completion拆分。如果 messages 占大头,去查历史累积和工具结果;如果 completion 占大头,去降 max_tokens。两者都大,就两头一起处理。
一个容易踩的坑:有人看到报错就去改max_tokens,改完发现还是报错,就以为裁剪逻辑没用。其实是因为输入侧根本没动。记住,max_tokens只影响输出侧预算,输入侧超限必须靠裁剪或压缩。
6. 长期跑 Agent 的上下文管理建议
如果你只是偶尔调一次 API,上面几招够用了。但如果你在长期跑编码 Agent、多轮对话、多 Agent 协作,上下文管理应该作为架构的一部分提前设计,而不是等报错了再补。
第一,把裁剪逻辑封装成团队统一模块,比如一个ContextManager类,所有项目调用同一套策略。否则每个人各写各的,行为不一致,排查时互相甩锅。
第二,给消息打标记,区分"中间推理过程"和"最终结论"。ReAct 循环里失败的尝试、被放弃的方案,任务推进后就不需要完整保留:
def keep_only_conclusions(messages): return [m for m in messages if not m.get("is_intermediate_reasoning")]第三,多 Agent 场景下,子 Agent 只返回结构化结论,不要把完整中间过程传给主 Agent:
{ "summary": "已完成对XX模块的代码审查,发现3个潜在问题", "details_ref": "sub_agent_log_20260703_001" }详细日志单独存储,需要时再检索。这样主 Agent 的上下文不会被撑爆。
第四,压测阶段专门模拟长对话。本地开发任务简单,很难触发超限,生产环境用户可能连续聊几百轮。提前用脚本跑 100 轮以上,验证 token 曲线是否被压住。
第五,如果任务确实依赖大量历史,可以考虑切换到更大上下文窗口的模型作为兜底,但要清楚"塞得进去"不等于"用得好",过长上下文会有中间信息利用效率下降的问题。兜底是补充,不是替代精细化管理。
最后,如果你在编码类 Agent 上长期投入,Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 更适合高频场景;日常调试和验证模型行为,用模型对话入口 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 就够了。Key 管理和接入文档分别在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 和 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。把上下文管理做扎实,你的 Agent 才能从"跑几轮就爆"变成"能扛长任务"。