首页 / 资讯中心 / 文章详情

Context才是Agent的边界:从窗口原理到auto-compaction与Harness实战

Context才是Agent的边界:从窗口原理到auto-compaction与Harness实战 ★ FEATURED ARTICLE
如果你用命令行Agent跑过稍微有点规模的重构任务大概率被这样一条报错迎面捶过api error: 400 this models maximum context length is 1048576 tokens. however...后面接的token数一次比一次离谱。我第一次撞见的时候正在让Agent把一个多模块项目从旧接口迁到新接口前二十多轮对话都很顺利到第二十五轮左右它突然停摆抛给我一条400。我第一反应是检查网络和API key都正常。后来才意识到问题出在我一直没当回事的Context上——Agent工作台上的东西堆满了。这个报错几乎成了Agent开发圈的“集体记忆”。你搜“context is too large and auto-compaction could not recover this”能翻出一堆帖子。它不只是一个错误提示它直接把Agent开发的核心命题甩到你脸上Context到底是什么它怎么运作为什么百万级tokens的窗口还是不够用这篇东西就是我基于自己踩坑和拆源码的经历把Agent与Context之间的关系从头到尾捋一遍顺便把排查、修复、预防的方法都摊开讲。1. 一次400报错引发的思考Context才是Agent真正的边界1.1 那个让Agent开发者集体血压升高的报错先把这个报错掰开来说。1048576这个数字眼熟吗它是2的20次方也就是常说的“1M上下文窗口”。不少前沿模型把上下文窗口做到了这个量级听起来像天文数字很多人的第一反应是“这辈子的对话都装不满吧”。但实际跑Agent的人都知道这个窗口消耗起来比想象中快得多。一次代码重构对话前20轮的上下文累积可能只有几万token到第25轮工具一次性返回几百个匹配结果、某次读取了整份几千行的配置文件、再加上几轮大段diff回显窗口数字瞬间就逼近六位数甚至七位数。然后你发起新请求API直接回400意味着请求在HTTP层就被拒了压根走不到模型推理那一步。这里有个容易忽略的细节你看到的messages resulted in 786450 tokens这类数字未必真的超过了1048576但请求里除了messages之外还有系统提示词、工具schema定义、max_tokens输出预留空间这些全部计入上下文预算。也就是说名义上1M的窗口真正留给“临时工作区”的余额可能只有八九成。很多人在几十万token的时候就爆窗实际是被输出预留和虚拟token占用挤爆的。要验证也简单把同一段messages单独拎出来把max_tokens调小重新发一次请求会发现原本400的请求突然就能过了。这说明窗口的约束是“输入输出整包计价”不是只看历史多长。理解了这一层你才会明白为什么那些长期跑Agent的人都在强调“窗口管理”而不是“窗口越大越好”。1.2 为什么说Context就是Agent的工作台模型本身是没有记忆的。你每发一轮请求它看到的都是“系统提示词全部历史对话新输入”拼接在一起的完整文本序列。所谓Agent的“记忆”本质上全部由Context携带——这一轮不把上轮的内容带回来模型就什么都不记得。所以我把Context理解成Agent的“工作台”桌上摆着它正在看的方案、刚拿到的工具输出、还没归档的历史记录、以及你交代任务的便签。桌面大小固定桌上的东西越多它能腾出来动手的空间就越少桌上摆得越乱它翻找关键信息的速度就越慢。这个工作台模型能解释很多现象为什么Agent会在长任务后期突然“变笨”不是模型退化了是桌面上堆了太多无关杂物注意力被稀释。为什么同样的需求新开一个会话跑得比老会话顺畅因为新桌面上只有必要材料。为什么有些人觉得“1M窗口也没用”因为把1M窗口当仓库用塞满了历史糨糊模型在1M的垃圾里找几KB的关键信息效果自然差。所以Context管理本质上就是工作台管理。接下来拆原理的时候我会把这套模型延伸到具体机制上。2. Context原理拆解从注意力机制到“工作台”模型2.1 上下文窗口是怎么算出容量上限的要理解Context绕不开Transformer的注意力机制。模型处理文本时每个token的表示都要和序列中其他所有token做注意力计算判断彼此的相关性。序列越长需要计算的配对数量就按平方级增长——这就是常说的O(n²)复杂度。1M tokens的序列注意力矩阵的规模是1M×1M这个计算量和内存消耗都是恐怖的。模型推理时还有一个关键缓存叫KV Cache。已经处理过的历史token它们的Key和Value向量要被缓存下来避免每生成一个新token就从头再算一遍。这个缓存体积跟序列长度正相关1M上下文的KV Cache会占据非常大的显存空间。这也是为什么“百万级上下文”通常意味着更高的显存成本、更慢的推理速度以及厂商在服务端更严格的控制。窗口上限本质上是工程约束不是模型能力的天然边界。除开这个硬上限还有个更隐蔽的“软上限”有效上下文。学界有个著名的“Lost in the Middle”现象——模型对长文本开头和结尾的信息利用率明显高于中间部分。你给Agent塞一个100万字的历史记录它能精准回忆的大概率只有开头那几条指令和结尾那几条结果中间那90万字基本是“读过了但没记住”。所以Context管理的第一原则是不要把所有信息平均地塞进窗口要把关键信息放在开头和结尾让中间只放次要材料其余全部外置。2.2 auto-compaction系统的自救机制主流命令行Agent比如Claude Code这一类都内置了auto-compaction机制翻译过来叫“自动压缩”或“自动精简”。它的工作原理是在上下文占用接近窗口上限时框架会调用模型对前面的完整历史做一次回顾和总结把几十轮冗长对话压缩成一段“此前进展摘要”然后用这段摘要替换掉原始对话腾出窗口空间让任务能继续往下跑。这个设计思路是对的但它在两种情况下会失灵也就是那句经典报错context is too large and auto-compaction could not recover this出现的时候第一种情况当前输入本身已经巨大。比如你刚从工具那里拿到一份几十万token的完整网页内容正打算让Agent分析这个请求的输入量直接就逼近甚至超过了窗口上限。auto-compaction再及时也只能压缩“历史部分”压缩不了“当前这一轮新塞进来的东西”。第二种情况压缩产物太厚。当历史对话已经积累到极其庞大的程度时模型生成的摘要本身可能就有几万甚至十几万token。压缩完一算总账还是超限框架只能宣告自救失败。我把auto-compaction理解成一个“定期清理桌面的保洁阿姨”。她能把桌面上的杂物捆成压缩包但如果你刚把一个冰箱搬上桌她再怎么捆也腾不出地方。2.3 长上下文给模型带来的隐性降智很多人有个误区觉得“给模型的上下文越多它就越聪明”。实际体验恰恰相反上下文越长模型的有效推理能力下降得越明显。原因倒不复杂——注意力资源是有限的序列里塞的东西越多每个token分到的注意力就越稀薄。模型要在一堆历史噪音里定位“当前任务真正需要的那句话”难度直线上升。我做过一个很直观的测试让Agent在全新会话里读一个200行的模块并改一个bug一轮搞定质量很高让它在累积了8万token历史的会话里继续同一个任务它经常翻错文件、复用已经废弃的方案甚至开始“参考”历史里那些被否决的思路。这不是玄学这就是上下文污染。所以现在我看到有人在讨论“Agent记忆方案”时核心思路永远是“怎么让Agent在需要时精准取回信息而不是把记忆全都塞进窗口”。窗口是短期工作区不是长期仓库。3. Agent架构里的Context运转单Agent与多Agent的差异3.1 单Agent的Context生命周期一个典型单Agent的执行循环是这样的系统提示词常驻身份与规则→ 用户下达任务 → Agent调用工具 → 工具返回输出 → Agent基于新信息继续推理 → 再调工具……如此循环往复。每一轮循环的产物都会沉淀到Context里。换句话说工具调用记录、工具返回的原始输出、Agent的每一步思考过程全部是Context的一部分。这也解释了为什么单Agent跑长任务会越跑越慢不是模型抽风是每次请求携带的序列越来越长KV Cache越来越大推理时间自然水涨船高。这个环节里普通开发者最容易犯的错是“工具输出不设防”。比如你让Agent用ls -R列出整个仓库的文件结构几百上千条路径直接就进了上下文再比如你让它curl抓一个网页整个HTML源码全被当作工具输出带回对话。这些东西动辄吃掉几万token而且大部分对最终任务毫无价值。正确做法是引导Agent“先裁剪、再返回”。你可以明确要求它工具输出超过一定行数时先输出前面50行末尾20行中间用省略号替代需要全文分析时让它把内容写入临时文件再用分段读取的方式按需查看。这些约束写在系统提示词里能省下大量窗口空间。3.2 多Agent协作时的Context隔离与传递多Agent系统或者叫多智能体协作的Context问题跟单Agent完全不是一回事。很多人刚上手时容易有个直觉让多个Agent共享一个Context彼此都能看到全部信息协作起来岂不是更默契实测下来这是灾难。多个Agent共享同一份Context意味着每轮对话、每个Agent的工具输出都在互相污染视野。AAgent在写代码时BAgent在跑测试的中间结果不停地往窗口里挤BAgent读不到需要的上下文因为被AgentA几千行代码输出淹没了。最后所有Agent都在同一个满是噪音的窗口里挣扎效率还不如单Agent。成熟的多Agent架构里Context是隔离的每个Agent拥有自己独立的上下文窗口只保留自身任务的必要信息。Agent之间通过消息传递或共享存储交换“结果摘要”而不是交换原始数据。举个例子两个Agent协作一个负责改代码一个负责跑测试。改代码Agent完整持有diff和需求上下文跑测试Agent只需要知道“改动了哪些模块、关键风险点在哪、预期的测试命令是什么”。把AgentA的整个Diff丢给AgentB只会让B的窗口瞬间爆炸而且它真正需要的几个断言点反而挤在中间读不到。多Agent的Context设计本质上是在“信息共享”和“上下文隔离”之间找平衡。共享的是结论和坐标隔离的是过程和噪音。3.3 Agent Harness谁在替我们管Context讲到这里必须提一个热搜词里反复出现的概念Agent Harness。很多人问“harness和agent到底什么区别”一句话解释Agent是那个会思考、会调用工具、会执行任务的智能体Harness是承载Agent运行的外壳是那个“执行环境控制循环”负责管理Agent的每一步行为——其中很重要的一项工作就是管理Context。你用的Claude Code、Codex这类命令行Agent本质上是一整套封装好的Harness。它替你做了这些事按固定顺序拼装系统提示词、工具定义、历史对话和当前输入监控上下文占用设定压缩阈值在窗口紧张时触发auto-compaction控制工具输出的截断与展示格式管理多轮会话的存档与恢复。这也是为什么Harness的质量直接决定Agent的实际表现同一个模型放在一个Context管理粗糙的Harness里跑一会儿就报“context too large”放在一个Context管理精细的Harness里能稳定工作很久不爆窗。如果你是自己写Agent框架的人Harness里最值得投入的就是Context管理层——不要在用户提示词里随机应变而是把上下文策略写进框架逻辑让它在不同阶段自动决定“保留什么、压缩什么、丢弃什么、去哪里找回什么”。4. 实战复盘我是怎么定位并修复上下文超限的4.1 完整排查链路从报错倒查膨胀源头拿我自己那次400报错做一次完整的复盘。当时任务背景一个多模块Node.js项目要从旧的回调式API迁移到async/await风格仓库不算大但涉及十几个文件。我用命令行Agent跑了一下午报错出现在第25轮左右。第一步拿到完整报错信息。注意别只截取第一行报错尾部通常会给出your messages resulted in XXXX tokens这个数字直接告诉你超了多少。我当时看到的是大概四十多万token还没到一百万但请求里预留的输出空间和系统提示词已经把余额压垮了。第二步开启Agent的详细日志模式。命令行Agent一般都带调试模式会记录每一轮请求的上下文token数。如果没有你可以用API代理层自己统计或者直接把历史会话导出成JSON逐个计算。第三步按轮次统计token消耗。我当时的日志数据大概是大部分轮次每轮只有几千到一万左右的增量但有三轮特别异常——一次是全仓库grep匹配结果直接返回了几百行代码片段一次是Agent读取了某个几千行的配置文件全文还有一次是它连续三次调用工具都返回了大段报错堆栈每轮都累积了好几万token。第四步确认auto-compaction失效原因。我翻到报错时间点附近的日志发现框架确实尝试过自动压缩但压缩前的历史已经堆到三十多万token生成的摘要也有好几万token加上当轮新输入压缩后依然超过窗口最终宣告失败。这四步走完根因就很清楚了。4.2 根因分析为什么auto-compaction救不回来我那次的爆窗严格来说是三个问题叠加第一个问题是“输入失焦”。全仓库grep的结果、整文件读取、反复报错堆栈这些大块内容跟迁移任务相关性并不高却占据了大半窗口。它们本可以被裁剪、摘要或直接外置。第二个问题是“压缩策略触发太晚”。框架默认会在上下文接近上限时才压缩但等到那时候历史已经庞大到压缩本身也放不下。理论上有更早的压缩阈值可以设置但我没调默认策略扛不住这种大输入场景。第三个问题是“压缩内容无差别”。auto-compaction会把所有历史一视同仁地浓缩包括那些本就不该留在窗口里的杂质。它压缩的是“总长度”不是“有效信息”所以压缩完可能保住了噪音的摘要却丢掉了关键的接口变更细节。把这三个原因写出来修复方向就很明确了控制输入粒度、提前压缩、把关键信息外置而不是依赖窗口携带。4.3 修复方案与验证让Agent重新跑起来我当时的修复分了三步走第一步先让Agent重新跑起来。新开一个会话手动带上这次任务真正的必要上下文需要修改的文件清单、目标接口的签名、上一次会话产生的关键过渡性决策摘要。没有把旧的几十轮对话打包带过去只带“结论”。第二步修改Agent的执行策略在系统提示词里增加约束工具输出超过50行必须先摘要再返回不要直接展示全文任何文件读取必须按需、按行号分段读取严禁一次性读取大文件全文每个任务完成一个阶段后主动输出一份“阶段性进展摘要”包括已完成、进行中、下一步路径。第三步调整上下文压缩的触发阈值提前到窗口的50%-60%就触发而不是临近上限才行动。同时把“压缩摘要必须保留关键文件路径和常量名”写进压缩指令里避免压完只剩废话。验证结果很直观同一个迁移任务新方案下跑到第五十轮都没再报过400Agent的反应速度还比原来快了不少。因为窗口里堆的是“高密度信息”而不是“高体积历史”。这个对比让我彻底确定了Context管理的价值。5. 上下文系统的工程化设计让Agent记得住、忘得掉、找得到5.1 分层上下文结构常驻区、任务区、检索区在实战里我把Agent的窗口内容结构化地分成了三层这套设计现在一直沿用第一层是常驻区也就是系统提示词和全局规则。它是每次请求都要随行的那部分必须精简到一个“小而稳”的程度。原则是只放身份、行为边界、通用的输出规范不放任何具体项目信息。因为具体信息会变而常驻区变了会导致整个会话行为漂移。第二层是任务区当前任务的目标、输入材料、关联文件路径、约束条件。这一层要跟着任务走任务切换就换一套。它的设计重点是“精确到路径和文件”而不是“复制文件内容”。能写“读取src/utils/format.ts并按新规则重写”就不要把整个文件贴进提示词。第三层是检索区历史摘要、关键决策、待办事项。这一层是动态更新的每个阶段结束时刷新一次把窗口里已经完成的东西压缩成一句话把决定性的技术选择单独记录。这样窗口里的历史不是线性堆积而是不断翻新。这套分层结构用在一段提示词模板里大致长这样【系统规则】 你是本项目的高级工程师只负责完成本次任务。 工具输出超过50行先摘要再返回禁止直接展示全文。 每次完成任务的一个阶段输出阶段性进展摘要。 【任务目标】 将旧的回调式API迁移到async/await风格。 涉及文件src/db/connection.ts, src/services/user.ts, src/api/routes.ts 约束保持对外接口签名不变测试全部通过。 【历史快照】 已完成connection.ts迁移user.ts函数签名已确认。 进行中routes.ts重写中。 关键决策db连接改为顶层await不再使用回调包装。 待办运行全量测试、更新类型声明。这套结构下窗口里的内容密度很高Agent几乎每轮都在处理有效信息历史部分也不会无限膨胀。5.2 压缩与摘要压缩到底损失了什么很多人对auto-compaction又爱又恨其实就是没搞清“压缩损失的是什么”。压缩是拿历史细节换窗口空间损失的东西主要有三类具体数值和常量名、报错堆栈的细节、以及之前走过但没走通的“死路”。所以做压缩摘要时不能简单要求Agent“总结之前的对话”而是给一个结构化模板把该留的都留进去请基于以上对话生成结构化摘要保留以下内容 1. 已完成列出已完成任务和涉及的准确文件路径 2. 进行中当前正在处理的任务点 3. 阻塞当前遇到的报错信息、错误码、可疑代码位置 4. 关键决策已经确定的技术选型和常量/命名必须保留原始值 5. 下一步按顺序排列的后续步骤。 要求压缩后总长度控制在2500 token以内。这个模板的用意是让压缩过程“有选择地丢”。没有它模型很容易给你一段四平八稳的泛泛总结——“我们正在迁移API遇到了错误下一步继续排查”——完全没法用。有它在摘要才能作为下一阶段的可行上下文。顺带说一个经验摘要虽然比原历史小得多但它还是会消耗token而且摘要质量直接决定后面Agent能不能接上。所以我会控制压缩频率宁可“阶段性人工总结一次”也不要让auto-compaction在压力下仓促压一堆。人工总结跟自动压缩的差别就像归档文件和碎纸机的差别。5.3 外部记忆与检索增强把Context从窗口搬到仓库里窗口空间再大也不是用来当仓库的。真正的长期记忆应该放在窗口外面——文件系统、向量库、知识库这才是Agent记忆方案的正解。我的做法比较朴素给项目建一个AGENT_NOTES.md每个阶段跑完把关键结论、文件变更、踩坑记录追加进去。下次新开会话让Agent先读这个文件再决定下一步要查哪些代码。这样会话与会话之间是连续的但Context不会无限增长。检索增强的做法可以升级一些把项目的技术文档、接口定义、历史决策向量化塞进向量数据库。Agent需要某类信息时通过检索召回最相关的几段再放进窗口。这就把“背下整本参考书”变成了“按需查字典”。我做过一次对比实验同样一个功能迭代任务A方案是“把5万字历史记录全部塞进上下文让Agent处理”B方案是“把结论写进note文件开启分词检索召回再处理”。B方案的结果质量明显更高上下文占用只有A方案的十分之一速度也快了不止一倍。Context的定位应该是“临时工作台”外部记忆才是“仓库”。把仓库的东西全搬上工作台不是勤快是灾难。6. 那些不写在文档里的实战经验6.1 Agent Skill与Context配合的实战案例网页存Markdown最近Agent Skill这个概念火起来热搜里能看到“把网页保存成markdown的skill”“skill教程”之类。有人把它理解为“让Agent学会新技能”表面看是这样往深了看很多Skill的真正价值恰恰是帮Agent省Context。拿网页保存成markdown这个Skill举例。常规做法是你让Agent分析或保存一个网页它会直接抓取整个页面内容一次性塞进上下文一个页面少说几万token多则十几万。而一个设计合理的“网页转markdown”Skill会把完整流程封装成脚本Agent拿到链接调用这个Skill脚本去抓页面、转成markdown、存到本地文件最后只给Agent返回一句“已保存到notes/tool-roundup.md共12000字提取了正文部分”。你把十页的网页全文塞进窗口去分析不如让它把网页存成文件再按需读几个关键段落。这里面的核心思路就是“用工具代替堆上下文”。高频任务的操作细节、参数、脚本全部封装进Skill窗口里只留任务目标和结果摘要。我后来把“跑测试并提取失败概览”“构建产物差异报告”这类重复动作都做成了Skill窗口压力直线下降。6.2 框架选型的Context管理差异LangChain、Dify与CrewAI聊到Agent开发绕不开框架选型。最近好多人问LangChain、Dify、CrewAI哪个好我的看法是这几个东西在Context管理上的设计思路差异很大选型等于选Context策略。LangChain的Memory组件最灵活有ConversationBufferWindowMemory滑动窗口记忆、SummarizerMemory摘要记忆、EntityMemory实体记忆等你可以精确控制保留哪些历史。代价是组装成本高每块记忆组件都要自己调默认配置跑起来Context管理效果未必理想。适合愿意花时间打磨的开发者。Dify走的是可视化工作流路线Context由节点之间的变量传递天然管理。你可以把一个Agent的输入明确限定为“上一节点的输出字段”而不是整段历史。这种结构对业务场景很友好不容易爆窗但自定义程度相对低深度定制场景会受限。CrewAI主打多Agent协作每个Task自带ContextAgent之间通过任务队列传递结果。它的优势是Context隔离做得比较清晰不会出现多Agent共用一个大窗口互相污染的问题。但如果你没设计好任务粒度每个Task都可能重复搬运重复信息照样会膨胀。我的直接建议一个人写原型或研究选LangChain系列Context策略自主性最高要快速上线业务系统选Dify做明确的多Agent流水线选CrewAI。没有绝对好坏只有哪个匹配你的“Context管理模式”。另外最近Rust生态也冒出一些Agent运行时主打低内存占用和高并发适合想从底层自己实现Harness的人研究这也能看出Context管理正在成为框架竞争的核心战场。6.3 新手最容易踩的五个Context大坑最后把新手最容易踩的坑集中排一遍每一条都是我见过或者自己踩过的第一把整个仓库一次性拖进对话。几百个文件、几十万行代码全塞进去Agent还没开始干活窗口先爆了。解法是让Agent先用工具按需查看结构再锁定目标文件。第二粘贴几千行代码到提示词里。超过200行的内容就应该按路径读取、按函数读取而不是复制粘贴。你是想让Agent改代码不是让它背代码。第三完全不管工具输出的体积。一条find命令、一个curl返回、一次失败的堆栈都是动辄上万token的“沉默杀手”。必须在系统提示词里约束工具输出长度。第四一个会话从早跑到晚。会话越长Context里积累的杂质越多Agent越容易在后面阶段“犯蠢”。阶段目标完成就新开会话带上摘要继续。第五把动态数据写死在系统提示词里。很多人喜欢把当前任务状态、临时变量写进系统提示词但这部分每轮都会重复计费久了就占据了大量空间。动态信息应该放在每轮的用户消息里或放在外部note文件中轮询读取。这五条如果都能避掉你的Agent的Context寿命至少翻两倍。跑了小半年Agent我现在养成的习惯是大任务启动前先想清楚“这个任务至少需要哪些信息留在窗口里”任务每推进一个阶段就更新一次AGENT_NOTES.md看到工具要返回大输出就先问自己一句——这个东西Agent真的需要全文看到吗大部分时候它只需要一个结论。Context管理说到底就是一句话让Agent的手边只放它此刻需要的东西其余的放到它能够到的地方。能做到这一点1M窗口也好、128K窗口也好都不会成为瓶颈。
阅读完成 · 觉得有帮助?
咨询建站