上下文窗口是所有前沿模型的核心特性。用token来衡量的话,它通常由系统提示词以及不断累积的提示词、响应和工具调用历史构成,一直延伸到模型所允许的最大限度。
模型每生成一个token,都会先回顾整个上下文窗口。这是模型在聊天会话中保持记忆的唯一机制。模型并不会在轮次之间保留任何持久的内部状态。而且,我们将会看到,上下文窗口并非被动的存储空间。其中的每一个内容,都有潜力影响模型的输出结果,无论好坏。
这是一种常见的体验:一个会话开始时表现强劲,但逐渐就丢了思路。由上下文窗口内的内容所导致的模型输出质量的逐步下滑,通常被称为“上下文腐烂”。
我们可以将上下文腐烂的根源分为两类。第一类是内在腐烂:这是模型在生成输出时,如何在整个上下文窗口中应用注意力机制所固有的属性。我们稍后会详细探讨,但关键在于:无论上下文多么完美相关,模型回忆和区分信息的能力终究是不完美的。
第二类,我们称之为内容腐烂。这是指一个会话过程中,过时的、错误的、矛盾的信息不断累积。比如模型反复尝试却行不通的方法,或者十几个毫无结果的边缘工具调用。所有这些都被反复处理,慢慢塑造着会话的最终结果。
幸运的是,与内在腐烂不同,内容腐烂是我们可以掌控的——学会控制它,就能将像Claude Code这样的工具,从偶尔令人沮丧转变为始终如一的精准。
内在腐烂:模型的底线
内在腐烂指的是模型架构本身固有的性能局限。这不是我们通过提示词工程就能绕开或改进的;它是所有其他一切的基础。但理解其背后的机制仍有两点意义:它能解释本文后面提到的某些实践方法背后的动机,也能帮助我们破除一个幻觉:即认为模型背后真的有一个小工程师在默默积累对你项目的理解。(如果想全面了解注意力机制,3Blue1Brown的视频教程非常出色。)
每一轮对话,你整个会话(提示词、响应、文件读取、工具输出)都会被压平成一个长长的token序列,送入模型。LLM的架构很复杂,但注意力头是与上下文最相关的组件。每个注意力头只回答一个问题:每个token应该在多大程度上参考它之前的每一个token?一个相关的token对下一个预测贡献很大;一个不相关的token贡献很小,但绝不会是零。
这个“绝不为零”就是关键所在。每个注意力头的相关性分数都要经过softmax函数处理,这个函数强制所有分数之和为1。这意味着注意力预算是一个固定的总量。由于softmax是基于指数运算的,没有一个token的预算份额能真正达到零。你上下文窗口中的每一个token,无论多么无关紧要,都在竞争同一个固定的预算。这并非实现上的意外;softmax之所以是注意力的核心,恰恰因为它能将相互竞争的分数转化为一个平滑、可学习的分布。然而,这个有用的属性也反映了一个权衡:无关的上下文从来都不是免费的。需要明确的是,“稀释”只是长上下文性能退化现象的几种主流解释之一。模型也确实学会了一些局部变通方法,比如把多余的注意力分配到一些无关紧要的位置上,但这些都是缓解措施,而非根本解决。
这种影响其实很微妙。随着上下文增长,模型可能仍然能正确地将相关token排在最高位;排序本身得以保留。真正被侵蚀的是“余量”:即相关token与周围所有其他无效信息的“质量”之间的差距。即使模型“知道”该看哪里,它读取到的信息也已经变得模糊,混合了来自上下文窗口中数千个微弱贡献。可以把它想象成稀释:固定预算被摊得越来越薄,信噪比不断恶化。
所谓的上下文限制并不是一个性能悬崖;性能下降是渐进的,而且很早就开始了。
位置也很重要。词序是语言的基础,所以模型会编码token的位置(开源模型通常使用旋转位置编码;闭源模型则很少公开其方法)。具体机制不如实际测量结果重要:将一个事实放置在长上下文的各个不同位置,检索准确率会呈现出一个U形曲线,开头和结尾最高,中间最低(Liu et al., 2024)。确切原因尚存争议。但对我们来说,重要的是这个“低谷”:在长达数小时的会话中,你大部分真实内容都位于这个广阔的中部区域,而这里并不是一个安全的存储地带。
这些就是所谓的“大海捞针”基准测试。对于简单的情况,供应商的测试曲线相对平坦,问题基本解决。但只要稍微增加检索难度,比如去掉问题和答案之间明显的词汇重叠,让模型必须根据语义来匹配(NoLiMa, Modarressi et al., 2025),性能下降就远早于其宣传的上下文限制出现。当需要同时处理并推理多项信息时,即使完美检索并排除了所有干扰项,性能在远低于标称上下文窗口的范围内就已经开始退化(Du et al., 2025)。一个能在100k token长度下找到事实的模型,未必能在同样的长度下进行有效思考。
所以,对于任何试图用这些模型完成实际工作的人来说,一个实用的假设是:你真正可用的上下文预算,远低于任何标称的限制。
内容腐烂:会话如何退化
这一节要讨论的是会话过程中加载到上下文里的内容。前面我们说到内容腐烂是我们能够控制的领域。这没错,但在实际操作中,一个会话里有多方参与者:我们、模型,以及过程中产生的任何子袋里。
我们通常通过一套允许的命令来设定模型的自主程度:比如在项目工作区里搜索、从远程git仓库拉取代码、通过MCP访问公司知识库等等。给予一定程度的自主权正是这些工具的意义所在,也是生产力来源。当我们逐一分析导致内容腐烂的各种故障模式时,目的不是要收索这个自主权,而是要认识到,我们仍然是这个反馈回路中最好的纠错者:要能及时发现过于宽泛的文件读取,或是那个其实只是缺少配置却一直在被反复排查的错误,并在问题造成更大损害之前停止这个循环。
你的会话本质上是一个反馈循环。每一轮,模型上下文窗口中的所有内容——每一次文件读取、每一次工具输出、每一次失败的尝试——都会被反馈给模型,塑造它下一轮的输出,然后被折叠进下一个token里。输出变成了输入。一个清晰的步骤添加一条清晰的线索;一个混乱的步骤则添加一条混乱的线索,而模型会继续以其自身的混乱为条件。错误会持续存在并向前传递。
下面这四种失败模式,源于Drew Breunig对上下文失败的分类(混淆、冲突、分心、毒化),出自他的文章《长上下文是如何失败的》,这里我们将其落地到具体agent编程会话的日常场景中:
加载过多内容到作用域(混淆)。
现在越来越普遍的做法是,在每个会话开始时,就加载一个日益庞大的工具、技能和MCP服务器清单。单独来看这没问题:专用工具配上专门的指令集可以非常高效。但你有没有注意到,模型有时会为了执行最琐碎的任务而调用一个工具,甚至调用错误的工具?更糟糕的是,臃肿的工具集不仅会混淆模型对工具的选择(Kate et al., 2025),而且每次轮次都会增加输入量,而我们之前已经说明,在远低于标称限制的输入长度下,推理能力就会退化(Levy et al., 2024; Du et al., 2025)。
面对一个任务,拥有众多工具的模型倾向于调用它们,即使它自己的知识足以解决问题:有研究测量到,模型倾向于调用工具来掩盖推理上的不足(Zeng et al., 2026)。用更直白的话说,即使是一个闲置的工具也不是免费的:它的定义会随着每一轮对话被加载到上下文窗口中,无论你是否调用它,它都在消耗你的注意力预算。
让调试过程定义了错误方向(冲突)。
你有没有遇到过这种情况:你努力说服模型它的诊断是错的,而它却不断地将新的、矛盾的信息强行纳入一个已经说不通的假设里,甚至在问题已经解决后,它还会绕回到那个最初的错误点?这种现象已被测量证实:模型会过早地锁定一个解释,并且难以摆脱它。对于那些一次性获得所有信息就能干净利落解决的任务,一旦相同的信息是在多次轮次中混乱地提供,模型就会犯错(Laban et al., 2025)。问题的症结在于,探索路径本身变得具有粘性。
搜索范围过大,带回了干扰项(分心)。
当agent进行宽泛的搜索或转储整个目录来寻找它需要的东西时,它会拉入测试夹具、废弃的实现、模拟数据和来自不相关模块的相似函数名,所有这些看起来都挺像那么回事。问题在于,模型会严重依赖上下文窗口:它不会把上下文当作可选的参考材料,而是会可靠地将其纳入考虑,即使这些信息是无关的(Shi et al., 2023)。仅仅一个主题相关的干扰项就足以显著降低模型性能(Mirzadeh et al., GSM-Symbolic, 2024),而一个代码库中充满了这样的干扰项。模型会过度关注搜索拖进来的内容,损害了它本可以干净利落地进行的推理。这直接回到了我们之前说的“底线”:目标与邻近干扰项之间的“余量”消失了,一个本意是为了澄清的搜索,最终却用大量令人信服的替代品塞满了上下文窗口,而模型无法不关注它们。
让一个错误的记录固化为事实(毒化)。
这在长时间运行的任务中尤其常见。通常源于好意:随着项目成长和决策堆积,agent会把它发现的东西记录在NOTES.md文件里,或者一系列跟踪步骤和已完成工作的命名文件中,目的是为将来提供有用的上下文。后来你在实时聊天中发现其中一个假设是错的。你打断它,纠正它,然后继续。但那个笔记从未更新。它仍然存在于文件中,稍后被重新读取,可以比你在聊天中的纠正活得更久。一旦你的纠正滑出了上下文窗口,那个过时的文件就成了留在那里的版本。模型自己写的笔记不是证据;它是一个等待被确认的声明。失败的根源在于过时的状态,而不是状态本身。
在Breunig的四种模式之外,还可以补充一点:复合效应。每一种模式都会喂养下一种:一个错误的文件笔记导致agent去寻找一个本不需要的修复,搜索拖进来一堆看似相关的干扰项,这些干扰项又误导它产生一个新的错误理论,而为了解决这个新问题开启的调试分支又埋下了另一个隐患。每一轮对话都以累积的混乱为条件,错误不仅会叠加,还会让下一个错误更容易发生。
这引出了一个更微妙的观点:模型在退化时很少会表现出犹豫。 在走错方向后,它会基于错误继续构建,而不是标记出这个错误(Laban et al., 2025)。一个深度腐烂的会话,可能看起来和健康的会话一样,直到输出结果彻底崩溃。你不能指望模型自己来叫停,所以一个外部的观察者仍然是获得最佳结果所必需的。
如果真正的限制是内容质量,那么管理上下文就不再只是节省token,而是更好地识别模式并改变未来的行为。
管理上下文:日常实践
一个简单的实践原则:上下文不是存储空间,而是活跃的输入。以下内容将以Claude Code为例进行阐述,这是我最熟悉的工具,但每个实践方法首先是一个原则,这些指导可以应用于任何agentic工具,只是命令名称可能不同。
在第一个提示词之前:精心策划
如果你还不清楚,那么有一点值得强调:一个全新的开始,比任何后续的恢复路径都更有价值。这正是前期工作值得投入的原因:一个精心策划的项目(一个简洁的CLAUDE.md文件,正确的技能和钩子)使得启动一个新会话变得廉价。成本是一次性、有意识地付出的,这样之后每一次重置都几乎免费。
大多数人都熟悉CLAUDE.md文件。每个会话都是从读取这个文件开始的,并且它会在每一轮对话中被携带。这就是为什么在编辑它时要保持无情:每一行都要证明自己存在的价值,冗余必须被剔除。一个有用的思路是,把它当作一个新工程师入职第一天需要阅读的最低限度材料。具体来说,就是那些他们光看代码看不出来的东西:构建和测试命令、项目的结构、你真正坚持的约定、以及一些坑。去掉模型能自己推断出来的内容、经常变化的内容、以及只有偶尔才相关的内容。用/init命令会给你一个不错的起点,然后大刀阔斧地进行精简。
保持始终加载的上下文最小化,并且是高信噪比的。 通用知识模型自己就能轻易回忆起来,把它们作为上下文加载是一种浪费,只会增加我们之前提到的稀释效应。把上下文窗口留给那些具体、新颖、或训练后才有变化的信息,其余部分则依赖模型自己的知识。
你裁剪掉的内容不必浪费;可以把它移到只在需要时才加载的地方。流程和领域知识可以变成技能,其内容在调用前处于休眠状态。必须始终遵守的规则(比如格式规范、一个被禁止的命令)可以变成钩子,在主会话上下文之外运行。养成记录这些的习惯:在当下记录有用信息的成本,远低于事后重新构建它们的成本。
审计你放入作用域的内容。 我们已经看到拥挤的工具空间会如何降低性能。运行/mcp或/skills命令,禁用新任务不需要的服务器或技能。
用能缩小范围的方式来引导,而不是提供相关的内容。 如果你知道相关的文件,直接用@path/to/file来指定它们。
让它先规划再动手。 一个错误的规划成本只是一个段落,修正起来很便宜。对于任何涉及多于几个文件的操作,按Shift+Tab进入规划模式,或者使用/plan命令。
工作时:保持干净
保持目标的新鲜度。 我们知道长上下文窗口的中部是模型回忆最不可靠的地方。在较长的任务中,在每个里程碑节点刷新会话目标:重新陈述当前目标、实时约束,以及一个简短的、由agent重写的任务列表。
将繁琐的输出推给子袋里。 测试运行、日志排查、依赖审计:任何会产生大量你不需要保留的输出内容。我们只需要结论。
将持久状态外部化,并保持其真实。 由agent维护的笔记文件存在于上下文窗口之外,可以跨会话持久化;任何需要跨会话持久化的信息都可以放在这里。但必须是有意识的:过时的记忆比没有记忆更糟糕。它可能比你之前在聊天中的纠正活得更久,并在之后以“既定事实”的身份重新进入上下文。
回归真相,并使之成为习惯。 用文件、测试、实际的错误信息来验证,而不是依赖模型对它们的记忆。前缀!可以运行一个shell命令并将其真实输出直接放入上下文,所以!git status或!npm test能让会话重新基于真实情况,而不是模型记忆中的内容。如果你发现自己经常这样做,不妨把它做成自己的小工具:一个简单的技能,可以一次性注入你项目的真实状态(git状态、测试结果、类型检查)。
当情况变糟时:重置,而不是硬撑
这是最难遵循的指导;因为此刻你积累的对话轮次感觉像是已经取得的进展,扔掉似乎很可惜。但这是沉没成本谬误:无论你是否保留上下文窗口,那些token已经花掉了,唯一的问题是,下一轮对话是从一个腐坏的上下文开始,还是一个健康的上下文开始。
在同一问题上纠正两次,就是信号。 当你已经告诉过模型两次bug不在认证层,而它还在绕圈子时,停下来。重置。
如果可能,在它中途就抓住它。 如果你看到它朝着错误的方向前进,按下Ctrl+C并引导它,而不是让一个糟糕的改动落地并累积。如果已经发生了,Esc+Esc可以打开回退功能:将对话、代码或两者都回滚到之前的检查点。
重新生成,而不是携带前进。 对于自包含、可重新推导的工作,在一个干净会话中重新生成结果,通常比携带一个提炼过的历史更好。一旦尝试已经出错,效果尤为明显。在Letta的Recovery-Bench中,被赋予完整失败历史记录的agent,表现比从零开始的agent更差,腐坏的上下文反而拖累了恢复过程,而不是提供信息支持。
何时采取行动,以及携带什么前进,取决于会话的当前状态:
- 新的、无关的任务 →
/clear。你不需要任何旧的上下文。 - 相关工作的下一个阶段,会话健康 → 交接状态(见下文)。
- 任务中达到上下文限制,会话健康 →
/compact。你需要的是已发生事件的精简历史。 - 会话已经变差 → 手动快照状态并清除。你不能信任模型去总结一个它自己都读不好的上下文,所以你需要自己提取出重要的东西:来自文件、diff、测试和已知决策,然后再重置。
交接状态,而不是对话记录。 在健康的分界点,让agent写一个交接简报。
有指导意义的失败在这里很有用,保存它们的方式很重要:将每个失败提炼成一行教训(“尝试了X,因为Y而失败”)。
在新会话中,从简报和真实情况开始。 一个干净的窗口,如果它的第一个动作是定位而非猜测,那么它是最快的。让它指向交接简报,然后直接建立现实:读取相关文件,运行测试,检查git状态。简报告诉它事情截止到哪一步;实时检查告诉它事情现在实际如何。
对于长任务或并行任务
在可验证的接缝处分解任务。 在结果容易验证的地方拆分任务(一个通过的测试、一个编译成功的构建、一个对得上的数字),而不是分解到最小的可能单元。
只有当子任务相互独立时,才分派子袋里。 每个子任务使用一个干净的上下文,每个返回一个精简的摘要,这种方法适用于广度任务:并行研究、独立查找。对于编程任务,子任务通常是耦合的,隔离的agent会做出相互矛盾的假设,而你则要承担协调它们的工作。保持紧密耦合的工作在一个线程中,如果你确实需要隔离但又不想协调,一个git工作树(claude -w)可以为并行任务提供一个独立的分支来工作。
隔离调试过程。 一个bug就是那种分叉能带来回报的情况。将调查分叉到一个新分支或从头开始一个新会话,让它在你将丢弃的上下文中形成和抛弃错误理论,只带回原因和修复方案。
引入新鲜视角。 一个/code-review,或者一个从干净上下文开始阅读的专用审查者子袋里,可能捕捉到一个腐坏会话自身无法察觉的问题。
将它们联系起来:将会话视为git树
这一节描述的是我自己的工作流程,它基于上述发现而形成。目的是让这些原则自动生效,并减少同时运行多个会话时的决策疲劳。这不是经过基准测试的,所以请把它当作一个实践者的总结。
将会话视为一个git分支。你的主对话线程是任务的组织者,目标是让它的上下文保持干净并专注于任务。当出现一个分支任务(那种会消耗大量时间在调研、死胡同或数据挖掘上的任务)时,你就把它分叉到自己的会话中,带上当前的历史(claude --continue --fork-session),然后在那个分支里做那些脏活。这就是分叉与子袋里的区别。子袋里处理的是并行的、独立的工作:启动、探索、返回一个摘要。分叉则用于主线程上的顺序分支任务:需要完整会话历史才能理解的混乱调查,没有自动返回路径,并且你想选择带什么回来。
这使得上下文管理变得简单且熟悉:分支、探索、提炼、合并。这个分叉的上下文本来就是一次性的。它可以积累它需要的任何程度的腐烂,因为你会丢弃整个上下文窗口,只保留结果。分支里发生的一切都不应该污染主线程。
当分支有了答案,你就提炼它并合并回去。这个过程的两部分是我在原生分叉功能之上构建的技能。一个/conclude技能将分支内容写入一个专用文件夹(我使用./cc-branches)中的单个文件;一个类型参数可以从多个模板中选择,描述不同的分支总结方法,匹配所执行工作的类型。回到主会话后,一个/merge技能加载那个文件,将结论(以及产生结论的零噪音)追加到上下文中。
除了上下文卫生,git框架还给我的工作流程带来了清晰度,并减少了在多个任务之间切换的疲劳感。它提供了一个清晰的思维模型:每个命名的分支都成为一个离散的问题,带有一个清晰的答案,供将来参考。
到目前为止,所有假设都依赖于一个在回路中的纠错者,而大多数情况下,这个纠错者是一个人。不是因为人在工作上比模型更好,而是因为模型没有内部刹车。它看不见自己的腐烂,所以纠错必须来自外部。这就是管理上下文的本质:由上下文窗口之外的某人来决定什么进入窗口,什么留在外面,以及什么能存活下来。
被治理的上下文
花点时间使用Claude Code,看着它读取文件、运行命令、编写补丁、生成子袋里、维护笔记,你会很快觉得它就像一个小工程师在你背后:内化你的代码库,理解任务和错误之间的界限,然后继续修复问题。
其实不完全是,至少不是在这里关键的地方。
模型体验的不是你的项目作为一个项目。它体验的是当前的上下文:指令、可见的文件、工具输出、最近的对话。所有这一切,合在一起,作为工作输入。其中一些是信号。一些是噪音。一些曾经是真实的。一些从未真实过。模型无论如何都必须行动。当我们将会话视为累积的理解时,我们的行为会改变,输出会变差,有时我们还会责怪模型。
这就是为什么上下文管理不是一个次要问题。Claude Code是控制到达模型上下文的控制面板,它的核心命令(clear, compact, rewind, branch, subagents, skills, hooks)不仅仅是便利功能。它们是控制什么进入上下文、什么留在外面、以及什么在会话之间存活的开关。
治理上下文不是命令模型。它是在照料属于你的那一半问题——内容层——在一个由架构设定且你无法移动的底线之上。底线由实验室的研发进度来改善。内容层则随着你的进步而改善。
这就是成功使用这些工具的形态:不是最大的上下文,而是被治理的上下文。
