最近在折腾Agent项目,翻各种Issue的时候发现一个很有意思的现象:真正让开发者头疼的往往不是Agent的规划能力、工具调用的逻辑,而是那个看不见摸不着的Context。社区里搜索量最高的几个问题几乎全跟上下文相关,比如“context is too large and auto-compaction could not recover this”,还有那个经典的“API Error: 400 this model's maximum context length is 1048576 tokens”。
这些问题听起来是报错,本质上是你对Context的理解出了问题。我最早做Agent的时候也栽在这里,以为Context就是一个可以无限塞东西的垃圾桶,直到连续被几家模型厂商的上下文窗口限制教育了好几轮,才老老实实把Context当成一门正经的工程学问来研究。
这篇文章不打算讲那种“Context是模型输入的全部内容”的教科书定义,而是直接聊聊在Agent开发里Context到底是怎么工作、怎么膨胀、怎么爆掉的,以及我实测下来真正管用的几种治理方案。不管你是刚开始学Agent开发的初学者,还是已经在生产环境里被上下文溢出折磨过一轮的工程老手,这篇都值得花十分钟看一遍。
1. 先搞明白:Agent的Context到底是什么
1.1 从一次最简单的对话看Context
很多人以为Context就是聊天记录,这个理解对了一半。实际上一轮完整的LLM调用中,Context包含了系统提示词(System Prompt)、用户输入、历史消息、工具定义、工具调用结果,以及任何通过RAG或记忆模块塞进来的外部知识。
我举个例子你就明白了。你问模型“帮我查一下今天的天气”,如果你的Prompt里不包含任何历史信息,模型只知道你现在问了一句话。但如果你前面还说过“我在北京”,那模型就能结合这个背景信息回答得更准确——这个背景信息就是Context。
在Agent场景里,情况更复杂。Agent通常要干一件具体的事,比如“帮用户写一份周报”,这背后可能涉及读取用户的项目文档、查看本周的代码提交记录、调用AI工具生成摘要、再一步步组装成完整的周报。这个过程中的每一个中间步骤产生的数据,都得通过Context传给模型,否则模型根本不知道你前面做了什么。
所以Context实际上是模型的“工作记忆”,它承载的是当前任务有关的全部信息。对于Agent来说,Context就是它的工作台,工作台太小,活就干不了。
1.2 Context窗口:模型的工作台大小是硬边界
Context窗口(Context Window)是模型能够接收的最大Token数量,这个数字是硬件和模型架构决定的硬性限制。你给模型发一条请求,模型看到的所有内容加在一起(包括系统提示、历史记录、你的新消息、模型的回复),占总Token数不能超过这个上限。
现在主流模型的情况大概是这样:很多大模型的上下文窗口是128K到256K,有些达到了1M,但注意,这是“最大支持”的长度,实际使用时效果会随着上下文增长而下降。这里的1M Token其实不是给你随便用的,模型在处理超长上下文的时候,注意力机制的计算量和内存占用都是非线性增长,哪怕模型没有报错,推理速度也会慢到让人崩溃。
有个很形象的类比:上下文窗口就像一张办公桌。你可以在上面铺满各种文件,但桌子只有这么大,铺得越多,你找东西越费劲,而且其他人(注意力头)帮你找东西的时候也更慢。如果文件多到超出桌面,那就不是效率问题了,是直接不让你干活。
1.3 Agent场景下Context的膨胀速度远超你的想象
我们做个简单的算术题。普通聊天场景下,一次对话大约消耗几百到几千Token,这没什么压力。但Agent的场景不一样,特别是循环型Agent(Agent Loop),每一轮循环都要做“推理 -> 调用工具 -> 拿到结果 -> 再推理”的操作。
假设一个Agent的任务是“分析一份PDF报告并生成摘要”,我们来估算一下消耗:
- 系统提示词 + 工具定义:大约2000 Token
- PDF文件内容:假设10万字,约5万到8万 Token
- Agent第一轮调用:读取文件,返回文件摘要,消耗约5000 Token
- 第二轮调用:基于摘要做分析,调用一个外部API获取补充数据,API返回了2万字符的结果,消耗约1万 Token
- 后续几轮:进行归纳、生成最终报告,每一步都要带上前面所有的对话历史
跑完整个流程,这个Agent轻松吃掉了8万到10万 Token。如果任务再复杂一点,比如需要多轮工具调用、多次重试,那轻轻松松就能突破20万 Token。
这就是为什么网上那么多人遇到“context is too large”的报错,不是模型不行,是Agent天生就是个吃Context的大户。
2. Context在Agent里的组成拆解
2.1 Token、注意力与“工作记忆”的关系
要理解Context,绕不开Token这个概念。Token是模型处理文本的最小单位,可以粗略理解为一个单词的一部分,中文里大约1个汉字相当于0.6到1个Token,英文里一个单词约1到2个Token。不同模型的Token化规则不一样,但基本逻辑相同。
模型处理Context的核心机制是注意力机制(Attention),它在处理每个词的时候,会去“看”整个上下文里所有词,给每个词分配一个注意力权重。这意味着上下文越长,模型每次推理需要处理的计算量越大,对重要信息的“注意力”也会被稀释。
我经常用一个比喻:Context是你的工作记忆,Token是工作记忆的容量单位,注意力是你实际能同时盯住的细节数量。工作记忆越大,你能处理的任务越复杂,但如果工作记忆太大,注意力就被分散了,你可能记住了一些没用的细节,反而忽略了关键信息。
所以在设计Agent的时候,不是Context越大越好,而是“够用且聚焦”最好。一个塞满各种无用信息的超大Context,反而会让模型做出质量更差、甚至前后矛盾的回答。
2.2 一个Agent请求里到底塞了什么
如果你用OpenAI或Anthropic的API,去看一个实际的请求体,你会发现Message数组里通常包含这么几类元素:
- System Message:系统提示词,告诉模型它的角色、任务目标、行为规范。这部分是常量,每次请求都带。
- User Message:用户当前输入的指令。
- Assistant Message:模型之前的回复。
- Tool Call和Tool Result:模型发起的工具调用记录以及工具返回的结果,这在Agent场景里是Context膨胀的主要来源。
还有一部分Context不是通过Message数组传的,而是通过System Prompt间接带入,比如你在提示词里塞入的RAG检索结果、数据库查询结果、文件内容摘录等。
这里有个关键点:在很多主流API格式里,Tool Result会被注入到后续的对话上下文中,而且通常是以独立消息的形式。如果一个Agent执行了10次工具调用,每次工具返回5000个字的结果,那仅工具调用的上下文就吃了一万Token,这部分是很多新人根本没意识到的隐形消耗。
另外要提醒的是,不同的模型框架对“上下文”(Context)这个词的使用很混乱。LangChain里有个ContextWindow参数,有些框架里memory也叫context,还有的叫Context Manager,实际含义基本围绕“怎么管理和传递上下文”展开。但底层逻辑都一样,你最终塞给模型的那个东西有多大,才是真正的Context大小。
2.3 上下文长度是怎么被一点点吃掉的
大部分人在算Context消耗的时候只算了“历史消息”,忽略了以下几个隐藏的大头:
第一是系统提示词。看起来很少,但如果你在里面放了大量规则、Few-shot示例、JSON Schema定义,很容易就上千Token。我见过一些开发者的系统提示词写了两三千字,光是这部分的Token就占了不小比例,而且累积在每一轮请求里。
第二是工具定义。现在主流Agent框架里,工具是通过Function Calling机制暴露给模型的,每个工具描述包含工具名称、参数Schema、用途说明。如果一个Agent挂了10个工具,每个工具的描述几百Token,那光工具定义就占了好几千Token。这部分还是每轮都存在的,不是一次性消耗。
第三是历史累积。Agent运行时间越久,对话轮次越多,历史消息累加得就越快。如果设计不合理,Agent跑了几十轮之后,光历史消息就吃掉了几万Token。
第四是中间结果。工具返回、代码执行输出、文件读取结果,这些东西如果原样塞回Context,膨胀速度非常夸张。一个数据库查询返回了500行记录,就有大几千Token,一个API响应带了完整JSON,又是几千Token。
这四块加在一起,基本就是Context膨胀的完整图景。所以治理Context,本质上就是治理这四类内容的体积和留存策略。
3. Context溢出:最常见的“Agent死亡”现场
3.1 两种典型的溢出错误长什么样
在Agent开发和实际运行中,最常遇到的是两类报错。
第一类是模型层面直接拒绝你:“This model's maximum context length is 1048576 tokens. However, your messages resulted in 1100000 tokens.” 这是硬性超限,就是你塞进去的内容超过了模型窗口。这类报错很好理解,也很好确认,就是计算一下你实际发送的Token数就知道了。
第二类是框架层面的自动压缩失败:“context is too large and auto-compaction could not recover this”。这类报错更坑,因为它发生在框架处理环节。比如你用的是Codex、Claude Code、Cline这类编码Agent,框架有一套自己的上下文管理系统,通常会做自动摘要压缩(Auto-Compact)。当上下文超过阈值时,它会把旧消息压缩成摘要,释放空间。但如果压缩后还是超出限制,或者压缩过程中出现了无法处理的结构(比如某个工具结果太大导致压缩逻辑失败),就会抛出这个错误。
我见过不少开发者陷入一个误区:以为报错里出现“context is too large”就一定是模型窗口的问题,于是拼命换更大上下文的模型,或者疯狂调框架参数。结果发现问题根本不是出在“窗口不够大”,而是“框架的压缩逻辑根本不完善”。
3.2 为什么“自动压缩”有时候救不回来
很多人对Auto-Compact有一个过高的预期,以为它是万能的。实际上自动压缩的核心逻辑很简单:把较早的对话消息替换成长度更短的摘要。这个逻辑在纯聊天的场景下很好用,因为对话消息内容冗余度高,摘要一下损失不大。
但在Agent场景下,自动压缩经常失效,原因有三。
第一,Agent的对话历史里藏着大量的工具调用记录和工具执行结果。这些结果不一定能被摘要算法很好地概括,比如一个JSON格式的API响应、一段代码执行输出,被压缩成摘要后,模型可能就丢失了关键数据,导致后续步骤无法继续。
第二,压缩本身的Token开销不容小觑。如果要压缩的上下文已经非常长,生成摘要的模型输出也是一大段内容,这些内容本身也要占Context空间。如果压缩前的上下文已经接近窗口上限,压缩生成的新Token可能反而让总长度没降多少,甚至可能不降反升。
第三,很多框架的压缩逻辑是按“消息数组”处理的,如果某一条消息本身超长(比如工具返回了30万Token),压缩算法根本没法改变这一条消息的体积。这时候你真的只能从源头控制每条消息的大小。
所以把Auto-Compact当成救命稻草是危险的。真正稳妥的做法是在Agent的设计阶段就做好Context治理,让上下文没有机会膨胀到需要压缩的程度。
3.3 排查Context问题的四个步骤
当你遇到上下文相关的报错时,我建议按这个顺序排查:
第一步:确认报错来源。是模型API返回的错误,还是Agent框架自己抛出的错误?如果是前者,问题出在窗口太小或请求过大;如果是后者,问题往往出在框架的上下文管理策略上。
第二步:计算当前上下文的Token构成。大多数框架都有Debug模式或者可以查看Token统计的接口,如果没有,就手动估算一下各个组件的Token量级。重点看系统提示词、历史消息、工具结果三块谁占了大头。
第三步:检查是否触发了自动压缩,以及压缩后的结果。如果你看到“auto-compaction could not recover”这种错误,去检查框架日志里压缩前后的Token变化,确认压缩是否真的有效、压缩后是否丢失了关键信息。
第四步:分析上下文的增长模式。是每一轮都在线性增长,还是某一次操作突然激增?如果是后者,大概率是某个工具返回了超大结果,或者某个循环出现了无限膨胀。
排查完之后,再决定采用下一节说的哪类治理方案。
4. 把Context管起来的四类实战方案
4.1 摘要压缩:最廉价也最容易被低估的兜底
摘要压缩是最朴素、也是绝大多数框架内置的兜底方案。原理很简单:每隔N轮对话或者当Token数超过阈值时,把早期历史消息交给模型生成一段摘要,然后用摘要替换原标题消息。
这种方法适合Context膨胀主要来自闲聊式历史对话的场景。对于Agent来说,摘要压缩要小心处理工具调用历史,因为很多中间步骤的细节可能对后续决策至关重要,无脑摘要会丢失关键上下文。
我自己的做法是把历史消息按类型分开处理:普通对话消息可以做摘要,但工具调用的入参和出参倾向于保留原始内容(或者做结构化截断),因为模型后续可能要基于这些信息进行调整和反思。
实操上,摘要压缩的核心参数是触发阈值和摘要频率。建议触发阈值设为窗口上限的50%到60%,不要太晚。如果窗口是128K,差不多到70K左右就该考虑压缩了,给后续操作留出足够余量。
要注意的是,摘要本身也会累积,多轮摘要之后,摘要又会变成新的长上下文。所以更长期的做法是下面要说的滑动窗口和分层记忆,摘要压缩只能算临时止血。
4.2 滑动窗口与消息老化:让对话“滚动”起来
滑动窗口是最常用也最好理解的Context管理方式。核心思想是只保留最近N轮消息,超过N轮的一律丢弃或归档。这有点像消息队列的先进先出。在纯聊天的场景下,效果出奇的好,因为最近的对话通常与当前意图最相关。
在Agent场景里,滑动窗口要重点考虑两个问题:第一,系统提示词和工具定义不受滑动窗口限制,它们每轮都在;第二,有些关键信息是“全局状态”,绝对不能因为时间久远就被丢弃。比如用户的目标、项目的核心约束、安全规则等,这些应该放在系统提示词里,而不是混在对话历史中。
我见过不少失败的Agent设计,就是因为把关键约束放在了早期的User Message里,导致滑动窗口滚了几轮之后,模型完全忘了用户最初的指令,自己发挥得越来越离谱。解决方法是把核心指令同时固化到System Prompt中,确保无论窗口怎么滚动,这些信息都在。
如果框架支持,可以给消息设置“优先级”或“持久化”标记,让重要消息跳过滑动窗口的淘汰逻辑。没有这个能力的框架,就需要你在每次调用前手动重组Message数组,这其实就是一种轻量级的上下文编排。
4.3 RAG替代堆砌:把上下文从“存储”变成“检索”
很多Agent犯的毛病是:为了确保模型“知道”某个信息,就把所有相关资料全部塞进Context里。这是对Context最大的浪费。正确做法是引入RAG(Retrieval-Augmented Generation),先通过检索从外部知识库中挑出真正相关的片段,再塞给模型。
RAG的本质是把模型从“全部记住”变成“按需查询”。你的知识库可以有几十万条文档,但每次构造请求时,只把检索出来的3到5个相关片段放进Context。这样上下文体积从“全量资料”缩减到“精准摘录”,效率和效果都能提升。
在Agent中,RAG有两种接入方式。一种是Agent的某个工具本身就是“检索工具”,Agent决定何时去查资料;另一种是在每轮请求前,系统自动根据当前对话状态做检索,把结果作为上下文注入。第二种方式更可控,也更节省Token。
实操建议是:把RAG检索的Top-K控制在3到7之间,不要贪多;检索片段做截断处理,每个片段限制在500到1000个Token。这比一次性塞进来一万个Token的文档要优雅得多。另外一定要给检索加缓存,同一任务多次跑的时候,不要重复检索浪费时间和Token。
4.4 分层记忆:短期、工作与长期记忆分开管
这是我做Agent这么久,觉得最接近“正规军”的做法。所谓分层记忆,就是把信息按生命周期分成不同层级,各层级用不同的存储和调用策略。
第一层是令牌级上下文,对应模型的窗口,说白了就是当前正在处理的信息,每一轮调用都会变化。第二层是会话级记忆,对应的是当前会话周期内需要保存的信息,比如用户偏好、当前任务状态、之前做过的关键决策。这一层可以用滑动窗口加持久化的方式实现。第三层是用户级长期记忆,对应跨会话的信息,比如用户的历史偏好、过去项目的结论、常用的工具配置。这一层一般存到向量数据库或普通数据库里,在相关场景下检索回来注入上下文。
分层的好处是各层级的治理策略可以独立优化。会话级记忆可以用摘要压缩,用户级长期记忆用RAG检索,令牌级上下文做窗口限制。不会像把所有信息都堆在同一个Context里那样,牵一发动全身。
我在实际项目里用的框架组合是:核心Agent代码自己管理短期窗口,中间结果写入本地SQLite做会话级存储,长期知识用向量数据库做RAG。整体下来,同样一个任务,Token消耗比之前“全量堆叠”的方案下降了60%以上,模型输出质量反而更稳定。
5. 设计Agent时最容易被忽略的Context陷阱
5.1 别把历史对话全部倒给模型
很多Agent框架默认会把所有历史消息原样传给模型,这是最省事但也是最低效的设计。如果你仔细看官方文档,会发现几乎所有框架都支持对话消息的筛选和裁剪,但很少有人主动用。
我个人的经验是:历史消息中真正有用的通常只有最近3到5轮,再早的内容要么已经被执行完不再重要,要么其中的关键信息已经沉淀到系统Prompt或者摘要里了,留着纯属浪费。
有一种情况需要特别注意:当Agent在修改代码或操作文件时,较早轮次里的文件内容可能是旧版本,如果后续模型还引用这些旧内容,会产生“上下文污染”,让模型以为文件还是旧的样子。这种时候宁可丢弃旧消息,也不要让它干扰模型的判断。
所以我的建议很直接:不要用框架默认的无限历史模式,主动设置保留轮次,必要时连工具返回结果一起裁剪。
5.2 工具返回结果比对话历史更值得留
前面已经说过,Agent场景里Context膨胀最大的元凶往往是工具返回结果。很多工具,比如代码执行器、数据库查询器、网页抓取器,返回的都是大段结构化文本,动不动就几千上万字。
处理工具返回结果的核心思路是“能截断就截断,能摘要就摘要,能不进上下文就不进”。数据库查询返回500行记录?不,你只需要让模型看到前20行加一个行数统计。网页抓取返回了整页HTML?不,先做内容提取,去掉标签,再截断到核心段落。API返回了完整JSON?不,把无关字段删掉,只保留模型真正需要决策的字段。
更高级的做法是:在工具内部就设置“返回值压缩”,让工具自己决定返回什么。比如做一个代码执行工具,它不返回完整输出,而是让Agent先用一行命令执行任务,然后把输出摘要再返回;或者做一个搜索工具,它只返回搜索结果的标题和链接,只有Agent明确要求时才抓取全文。
这些细节看起来不起眼,但在长跑型Agent里,一个高质量的工具结果处理逻辑,能帮你省下几万个Token,直接决定Agent能不能稳定跑完整个任务。
5.3 并行分支和重试机制会悄悄吃掉窗口
多Agent协作、并行子任务、自动重试,这几个功能看起来很美,但它们都是Context消耗的大杀器。
并行分支意味着每个子Agent都有自己的任务上下文,但它们的主线Agent需要汇总所有子Agent的结果。如果子Agent数量多,汇总结果本身就会占用大量Context。再加上多线并行时,主Agent的上下文可能同时记录每个分支的中间状态,Token消耗直接成倍增加。
重试机制更隐蔽。一个工具调用失败后,很多框架默认会把错误信息连同之前的工具输入一起再传给模型,让模型重新尝试。如果这个工具失败了好几次,每一次失败的错误信息和重试记录都累积在上下文里。我见过一个Agent递归修复代码,重试了15次,最后上下文里塞满了错误日志和未修改的旧代码,彻底把窗口撑爆了。
治理思路是:给重试次数设硬上限,一般3次以内足够。每次失败后,不要保留完整错误栈,只保留错误类型和关键信息。并行分支的结果做摘要后再合并到主上下文。这三点做到,上下文体积立刻能降一个量级。
5.4 偷懒但有效的工程技巧
这里分享几个实践下来比较实用的工程技巧,不算多高级,但是真的能救命。
技巧一:统一用小模型的API做摘要。上下文压缩、消息摘要这些操作不需要用你主力的大模型,直接用便宜的小模型处理就行。比如主力用Claude,压缩摘要可以让GPT-4o-mini或豆包等小模型来做,Token成本能降一个数量级。
技巧二:在系统提示词里就写明“最多保留最近N轮信息,更早的信息忽略”,这样模型自己也会形成对上下文范围的感知,减少它过度依赖早期信息的概率。有些模型会在回复时引用前几轮的错误信息,这种提示词可以明显改善。
技巧三:对输入做长度预算。在每次发起请求前,先估算一下各类内容的Token量级,如果超预算,立刻决定哪些内容该砍。别指望模型帮你处理,在请求构造阶段就把截断做好了最可靠。
技巧四:善用框架自带的调试工具。现在很多Agent框架都有上下文可视化界面,可以看到每一轮调用后Context的Token消耗图表。我每次搭完一个新的Agent任务,都会先跑一小段测试任务,看一眼哪个环节消耗了最多的Token,再针对性优化。
6. 常见报错与修复速查
平时踩坑太多,干脆整理一张表。
| 报错信息 | 本质原因 | 修复思路 |
|---|---|---|
| This model's maximum context length is 1048576 tokens... | 请求总Token数超过模型窗口上限 | 削减历史消息、工具结果体积,或换更大窗口模型 |
| context is too large and auto-compaction could not recover this | 框架自动压缩未能将上下文降到安全阈值 | 排查压缩失败原因,通常是大单条消息或压缩链过长;从源头限制单次消息大小 |
| Request too large | API层拒绝大体积请求 | 压缩单条消息,尤其是工具返回和文件内容 |
| Repeated tool failure导致上下文爆炸 | 重试累积导致Context翻倍 | 限制重试次数,失败只保留错误摘要 |
| 模型行为漂移、前后不一致 | 上下文过长导致注意力分散或关键信息被污染 | 裁剪无关历史,用RAG解决特定知识需求,关键约束放入System Prompt |
| Token耗尽但上下文未报错 | 上下文太大导致模型悄悄失去关键信息 | 主动做窗口裁剪和摘要,不要等到报错再来处理 |
我个人的习惯是:每次新Agent项目上线前,先做一个Context压力测试,故意把历史消息、工具返回都拉到预料中的最大值跑一遍,看整个链路会不会出问题。这个测试很花时间,但真的能省下后面大量排查问题的精力。Context的治理没有银弹,它本质上是一个工程权衡问题——你想让模型多聪明、多独立,就得给它更大的工作台;你想让它跑得更久、更省成本,就得控制这个工作台的体积。
这些年做Agent下来,我越来越认同一个观点:Context的容量决定了Agent能力的上限,Context的治理决定了Agent能力能否稳定发挥。与其到处找“更好的Agent框架”,不如先把自己手里的Context管好。框架可以换,模型可以换,但上下文治理的这些原理,是你在任何一个Agent项目里都用得上的底层能力。