应对折叠问题:一种支持长时间运行的代理会话与数据压缩的解决方案。
代理会话在何种时长内仍能保持成本可控且逻辑连贯:以缓存尾部作为“租金”,在压缩前保留五层数据,以委托机制作为内存边界,以及恢复会话的成本。
登山者并不会将整个行装一起带上山,而是将其分段存放。漫长的攀登过程会被划分为多个营地,每个营地都会准备下一阶段所需的物品。那些不再需要的东西则留在山下,以便队伍需要返回时使用,而不会被继续带上更高的地方。最终能到达山顶的,只有那些在每个营地筛选后仍被保留下来的物品。
上下文窗口的运作方式也是如此。其容量是固定的,但路径并非如此,迟早会有东西不得不被舍弃。
提示缓存已经为智能体工具箱定下了一条规则:绝不能使缓存失效。要保持稳定部分的内容不变,将变更推迟到最后再应用。不过这条规则将会话视为静态照片,而实际上会话具有时间维度——每个会话都会随时间推移而变化。对话会不断进行,直到窗口被填满而不得不进行折叠处理。
折叠功能会缩小窗口,这是其机械层面的作用。其代价则不那么明显,因为折叠操作会导致缓存失效。系统只需支付一次代价,之后剩余内容会被重新写入,会话便可继续进行。不过缓存失效只是附带问题,真正重要的是决定哪些内容能够保留下来。良好的压缩并不仅仅是删除操作:它是将历史记录浓缩为一种让程序能够继续运行的形式。而且一个漫长的会话并非只折叠一次,它会不断折叠、运行、再次折叠,迟早还会出现多层折叠的情况。
这重新定义了规划问题。与其询问何时进行压缩,不如关注经过多轮折叠后还剩下什么以及到那时已经丢失了什么。时间因素属于成本问题,因为每次无效化操作都会产生费用;而压缩质量则关乎身份识别,因为它决定了后续的会话是否仍是最初那个会话。基于这两个原因,压缩应被置于优先级的高位,而非低位。
费用累积的环节
每个请求都会分为两个计费区域。一个是前缀部分,它会从一轮到另一轮保持不变;另一个是尾部部分,它会持续增长。
前缀部分包含系统提示、工具定义、稳定存储库的上下文以及规则文件。这些内容只需编写一次并缓存起来,之后以远低于原始价格的费用被读取。其大小始终不变。
后续内容仅包括对话、工具调用、其结果、思考过程以及差异对比。这些内容只会不断增多。
不过,真正造成问题的并非内容增多,而是反复收费。新消息并非单独传输——系统会重新发送对应的前缀,为读取该内容支付缓存读取费用,同时为新增内容支付写入附加费,而整个后续内容会在每次读取时都被一同传输。因此,150,000个标记的后续内容并非只是静静存在于窗口中,无论模型在下一个问题中是否需要这些内容,都会再次被计费。旧的历史记录实际上会在会话持续期间一直被占用。
在200,000标记的窗口容量中,大部分空间早已被占用。根据所使用的系统及其他若干因素,真正可用于处理的免费空间可能仅剩下7,500到15,000个标记左右。
缓存也会过期。系统会刷新其有效时间,这意味着一旦暂停,下一次操作就会变成冷写入。人类思考所花费的时间其实是一种隐形的缓存成本,却无人提及。
从这个角度看,压缩缓存尾部同样是对缓存的操作。通过一次性支付失效费用,就可以免除后续的持续费用。这就是整个交易的核心,而在采取这种措施之前还有更便宜的方案。
五步解决方案
有五种方法可以应对缓存尾部不断增长的问题。这些方法与其说是多种选择,不如说是逐级上升的步骤:层级越高,现有历史数据受到的干扰就越大。
第一步:永远不要生成它。将输出限制在工具允许范围内。Anthropic的文本编辑工具支持max_characters参数,其网络搜索功能则支持max_uses参数。Codex默认将每个工具的输出限制在大约10,000到16,000个标记之间,然后再进行压缩计算。另一种更为严格的处理方式是截取法:保留开头和结尾部分,删去中间内容,因为理论上命令的起始和结束比中间的内容更重要。这种方法的缺点是在还不知道哪些内容重要的情况下就进行了截断,而且如果堆栈跟踪缺少相关帧信息,那比没有堆栈跟踪还要糟糕。
第二步:提取所需内容。如果不需要完整的观测数据,就无需保留它。当某个工具的输出超过特定大小阈值时,该工具会将其写入磁盘并留下引用。完整的输出文件会保留下来,而实时的上下文信息则不会。这与截断处理不同,因为截断会丢弃部分数据。一个包含50万行的构建日志可以存储在磁盘上,而上下文仅保存其路径、元数据或可搜索的索引,之后通过内置搜索或读取工具即可获取相关部分内容。保留输出文件,删除不必要的观测数据。这样就能将存储与上下文清晰分开,确保信息长久保存,而不会在后续处理中带来额外负担。
问题在于,该工具必须具备重新找到这些内容的手段。只有当智能体能够搜索它所保存的内容、读取并理解它们时,数据溢出才有意义;否则,上下文溢出只会换来一个充满无法检索内容的文件系统。
各类工具在这方面的设计趋势颇为一致。Claude Code会将超过约100k字节的工具处理结果写入磁盘,并返回预览内容及路径,而Hermes也对每个结果设置了同样的100k字节限制。两者都对读取工具作出了相同的例外处理,因为一旦发生读取数据溢出,智能体只会再次读取该路径上的内容,如此无限循环。这类循环很容易被陷入。唯一旨在恢复内容的工具,绝不能出现数据溢出现象。
第三步:隔离数据。将任务交给子代理处理。该子代理配备了相应工具,能够读取文件、筛选日志并扫描存储库,从而积累大量私有上下文信息,而这些内容都不会进入父进程的跟踪范围。任务完成后,其临时窗口会被丢弃,只有返回值会被传递出去。今年,各类工具供应商都将此功能列为最广泛部署的功能之一。
这是有原因的,因为探索比改动更安全。读取错误文件的工人只会导致任务需要重新执行,而编辑错误文件的工人则会在其工作上下文消失后留下损害,因此由哪个模型来执行改动至关重要。古代军队便体现了这一点:侦察兵是那些被派往前线的、成本低且可牺牲的人,因为观察的成本很低且报告可以核实;而真正的决定性行动则交由最优秀的指挥官来执行,因为这些行动无法逆转。这里也适用同样的分工——将侦察任务交给成本低、可替代的上下文,把最强大的模型留用于改变世界的步骤,并让上级参与核查。上级应为结果买单,而非为调查过程付费。
第四步:用指针替代它。如果内容已经以文件、成果物、生成的报告或输出结果等形式被持久存储,对话只需一个对该内容的引用即可。再次查看时只需前往内容存储的位置,无需随身携带。Anthropic的记忆工具正是实现了这一点,而OpenAI也通过函数调用支持类似功能。原本成本高昂的对话状态 thus变成了对持久存储状态的廉价引用。不过有一个注意事项:这种节省仅能在底层内容仍在窗口之外时持续有效。一旦重新读取,它又会变成临时数据,并立即开始产生“成本”。
第五步:之后再移除它。清除、压缩或折叠。只有这一步才会使已缓存的资料失效。这一轮的操作是用累积的历史数据换取更小的数据量,期望下一轮能以此为基础继续推进。完整的记录可以保留在磁盘上,但实时上下文却已不同。
从这里开始,成本问题就变成了一个赌注:在剩余会话中节省下来的成本是否高于现在重写历史数据所需的代价?过早压缩会丢弃那些本可支撑数百次高效操作的优质缓存;而过晚压缩则意味着要在大量历史数据上付出代价,而在此期间模型可能已经变得不那么擅长利用这些数据了。大多数关于长会话的讨论都是从这个问题开始的,但实际上不该如此。五个步骤的顺序如下:
- 永远不要生成它:从源头阻止数据量的持续增长。
- 直接透露:将冗余的观察结果移除,保留核心信息。
- 避免提及:将调查任务转移到其他情境中。
- 间接暗示:在对话中留下简单的参考线索。
- 日后删除:压缩或折叠信息,接受信息丢失的事实。
最廉价的情境并非压缩后的情境,而是从一开始就不需要存在的情境。
工具集即对成本承担方的选择
工具定义的位置很重要。定义位于开头部分,属于一次性写入并缓存固定的费用;而结果则位于结尾部分,是需要持续支付的变动费用。因此选择工具集实际上就是在决定由哪个区域来承担信息的成本。工具越少,开头部分的费用就越低,但结尾部分的原始观察数据越多;工具越多,则需要消耗更多的开头部分资源,才能获得更少但结构更清晰的观察结果。
Terminus 2代表了极简主义极致:它是一个无头终端,基于Unix的理念,即一个通用工具可以无限组合。Codex常与它被归为一类,但这并不完全准确。它的原生工具集其实并不小,约有20个常用工具、一套多智能体组件以及v1/v2版本。它通过延迟加载策略避免臃肿,将MCP和连接器工具索引在搜索引擎中,需要时再加载。Codex真正将极简理念体现在尾部功能上。apply_patch便是例证——短短的差异对比就能替代数千行重写的代码,同时每个工具的输出上限也防止了原始数据的大量积累。两者最终通过不同路径达到类似的效果:Terminus 2保持工具集小巧,而Codex则让尾部功能保持精简。
Claude Code和Hermes在那些能返回结构化观测结果而非原始数据的专用工具上投入了更多前缀资源。若将LSP、计划模式、工作树以及cron计算在内,Claude Code的相关分类数量会超过40个,而Hermes则约为55个,这些工具被分为终端、文件、内存、技能、任务分配等不同的工具集。其模式为使用Grep返回匹配内容而非整个文件,这样可以减少数千个标记的冗余输出,从而提升效率。
因此,比较的重点并非工具的数量,而在于额外的前缀开销是否足以抑制后续内容的增长,从而延缓折叠发生。这其实与“提前加载”与“按需加载”之间的权衡如出一辙,只是应用层级更高。Anthropic的延迟加载机制明确指出:工具定义可以在需要时才被加载,而不必随每个请求一同包含。Claude Code则通过ToolSearch机制实现这一点——它会让延迟加载的工具名称保持可见,直到模型主动询问才会提供完整的结构信息;当MCP工具定义占上下文窗口比例达到约10%时,该机制会自动启动。甚至排序方式也被精心设计以保护前缀,因为按字母顺序排序能使各轮次之间的定义块在字节级上完全一致,从而便于缓存。无论前缀的大小如何,其状态都被设计为始终保持活跃状态。
该轴依然具有实用价值,但其作用发生了变化。它不再决定是否进行压缩,而是决定先积累多少历史数据。而这些历史数据并非同质化的——搜索结果往往可以重新推导,但决策却无法如此。文件路径可以被重建,而某个方法被拒绝的原因则未必能被知晓。会话运行时间越长,混淆这两种类型记忆的代价就越高。
作为记忆边界的委托机制
委托机制的独特之处在于它能够在不改动父节点缓存的情况下清除历史数据,这比单纯降低任务执行成本更有价值。子代理拥有自己的上下文窗口:它可能会生成庞大的数据尾部、填满整个窗口、在内部进行压缩,然后消失,而整个过程都不会修改父节点的任何历史记录。
有一个常见的假设需要调整。人们倾向于想象一种同模型分叉的场景,即子节点会继承父节点积累的完整上下文以及其中的前缀部分。但实际上可验证的实现大多并不采用这种方式。Claude Code、OpenCode、Codex和Hermes都是各自启动独立的工作者上下文,而非复制父节点的整个上下文。有些系统可以让工作者使用不同的模型,例如Codex在启动时会指定模型,而Claude Code则允许每个子代理选择sonnet、opus或haiku模型。但这并非普遍现象,Hermes的代理也没有直接切换模型的方式。在各种系统中真正被传递的是独立的上下文,而非模型选择。
这表明问题远不止是分支操作那么简单。子进程的“尾巴”可以被丢弃:它可能会读取四十个文件、扫描日志并确定命名规则,而整个上下文会消失,只留下一段文本。委托机制起到了内存边界的作用——父进程分配任务并明确所需结果,这实际上相当于提前进行了压缩,因为执行者清楚谁会读取其输出以及该输出需要支持何种决策。当子代理返回时,父进程通常会检查结果。这样做虽然需要多几轮交互,但却是明智之举。
压缩是在事后反向进行的。它必须为尚不存在的读者进行总结,以规避后续可能出现的任何问题。而预先压缩的好处在于已有明确的读者群体。这便引出了一个预算原则:应为返回的内容预算,而非为处理过程本身预算。一个模型可以消耗数十万个标记,却不会让其中任何一个成为父级内存。而返回的文本确实会成为内存。400个标记的答案会存入父级内存的尾部,并在后续的每一次处理中都被计费,因此在一个漫长的会话中,原始差异文本的成本可能高于那些仅返回一句有用信息的昂贵模型。调查是暂时的,而答案则成为历史,而这些答案又必须经受折叠处理的考验。
平台所提供的功能
模型平台决定了该工具需要自行构建多少相关组件。
Anthropic提供了两种针对不同问题的辅助功能。第一种是clear_tool_uses_20250919,具有精确性。当提示词的长度超过阈值(默认为10万输入标记,且会保留最近三次工具使用记录)时,它会清除服务器上的旧工具处理结果,并在每个位置放置占位符。其clear_at_least选项通过设定最低清除数量来分散清除成本。第二种是compact_20260112,属于功能较强的工具:会在服务器端进行完整总结,生成对话的新版本,而非对现有版本进行截取处理。
Claude Code是一个很好的案例研究,因为就连负责提供这些基础组件的公司所开发的工具框架,似乎也会自行处理更重的/compact格式数据。对此功能的支持源自社区的逆向工程,其源头是一个公开的GitHub代码库,而非Anthropic的官方文档。文本的简化处理在服务器端完成,而决定哪些对话状态会被保留的步骤则由客户端控制。
OpenAI专注于内容压缩功能。其/v1/responses/compact接口会返回一个加密后的不可见数据块,客户端会原封不动地将其传回,而不进行任何查看,这部分是为了防止提示注入攻击,避免被篡改的摘要影响后续处理流程。该功能并不具备精确清理的能力,因此Codex必须自行控制工具输出的大小;在这两种路径之下还有一种简单的备用方案:如果窗口仍然溢出,Codex就会丢弃最旧的历史记录并重新尝试。
DeepSeek在上下文管理方面的能力要弱得多。其服务器端前缀缓存虽然降低了成本,但并未解决固定窗口的问题,因此任何相关工具都需要自行实现完整的压缩流程。官方的DeepSeek工具会构建这样的压缩流程,且不会破坏原有数据:被屏蔽的事件仍会保留在日志中,可随时获取。经测试的某款第三方终端界面则跳过了这一步骤,从不主动进行数据压缩。
重要的并非某个 API 的压缩端点的质量,而是由谁来决定哪些内容应当保留。Hermes 是一个具有代表性的极限案例,因为它涉及多个服务提供商,且无法假设其中任何一方都提供合适的压缩机制。因此,它以自己的客户端压缩器作为基准,设定两个阈值:根据其自身的令牌统计设定为 50%,而通过更粗略的网关级检查设定为 85%,后者用于检测在无人监控时会不断膨胀的会话。只要当前使用的服务提供商有更优的方案,就会采用该方案。
这确实是一种合理的设计思路。由于客户端拥有服务器可能永远无法获取的信息,因此应默认由其负责压缩操作,只有当平台具备相应能力时才将其任务移交给平台。无论由谁来执行压缩,核心问题依然存在:哪些内容是可以被舍弃的?
返回营地
以上所有情况都假设会话永远不会中断。但实际上会话会停止——笔记本电脑关闭、进程终止、CI任务结束,还有人会输入/resume,期望能从上次中断的地方继续。
这是最简单的命令,却会导致会话成本最高的一次切换。它包括斜杠在内共有七个字符,形状类似/help或/clear,无需任何参数、无需确认,也没有任何阻碍。它没有任何迹象表明其高昂的成本,界面设计也让人感觉重新开始是免费的。
对话内容可以恢复,但缓存却无法恢复。重新启动的会话并非被暂停的进程。系统会从磁盘重新读取对话记录并重建消息历史,因此对话内容可以从存储中恢复,而临时缓存则不行。这就产生了两种不同的恢复成本。
首先是成本问题。Anthropic的前缀缓存默认保存时间为5分钟,若购买了更高容量的版本则为1小时,且只有有活动发生时才会刷新它。空闲状态则被推至极致,即完全没有进程运行的状态。原本成本较低的前缀缓存会立刻变得无法使用,下一轮需要支付全额费用才能重新构建它。最昂贵的情况则是恢复已运行数天的会话,这正是需要谨慎管理会话的一个重要原因。
第二点是信息问题。当转录内容超出可用窗口范围时,系统可能需要在执行任何新操作之前对其进行折叠处理。这种折叠方式风险极高,因为在任何人重新确定哪些信息重要之前就发生了压缩。在转录版本中可能存在解释某次决策理由的约束条件,但在压缩版本中却消失了。智能体会以更小的窗口和看似合理的历史记录继续运行,但这些历史记录未必就是让早期决策显得合理的那些记录。
无论什么内容能够经受住折叠操作,也同样能够经受住恢复操作,这正是持久化状态的深层依据。项目根级别的规则可以从磁盘重新加载,文件可以重新读取,待办列表可以重新生成,而生成的构建日志则可以存储并被引用。仅存在于对话中的决策没有外部来源;如果它只是因为对话记录才存在,那么在恢复时就必须再次创建;而如果折叠操作将其移除,它可能就永远消失了。
因此,排序标准既考虑持久性也考虑成本。从最坚固到最脆弱依次为:可重建的状态、持久化的外部状态、实时的对话状态,以及短暂的观察结果。排序越靠下,该内容就越容易受到折叠操作的影响。
折叠操作失败的两种方式
折叠操作会以两种方式失败。那种明显的失败很容易被发现:压缩功能正常工作,但大量工具输出立即填满窗口,束带再次压缩,同样的输出再次出现,如此循环往复,就像在船身上钻孔的同时还试图往外舀水。Claude Code会停止重试,避免在循环中不断消耗调用次数,它具备内置逻辑,能够检测到系统陷入僵局且毫无进展。这确实是正确的处理方式,但这种循环本身就表明当前使用了错误的处理步骤。理想情况下,智能体应该退一步尝试其他方法,而非重复使用同一个原生工具。如果某个过大的文件持续迫使系统进行压缩,降低阈值并不能解决问题。正确的做法应是只读取有限行数的内容、在限定范围内进行搜索、指向稳定的输出结果,或者将文件交给那些窗口容量可牺牲的子智能体来处理。 每当开始进行压缩优化时,都要思考解决方案是在更高层级上实现,还是需要采用完全不同的方法。
缓慢的故障更为隐蔽。压缩过程会丢失信息,而且长时间的运行过程绝不会只进行一次压缩。它会不断压缩、处理数据、再次压缩、继续处理,如此循环,总结层叠在总结之上。
层层叠加
第一次折叠可能效果很好,但第三次折叠的结果却可能很差。第二个总结器从未见过原始历史记录,只看到第一个总结器选择的表示形式。第三个总结器则两者都看不到,只看到第二个总结器对第一个总结器结果的判断。看看这会对事实与理由造成什么影响。第一次折叠可能会保留“选择了方法B”这一信息;第二次折叠可能会保留“B是当前的实现方式”这一信息;第三次折叠可能会保留“该实现使用了B”这一信息。这些说法都是正确的,但为何拒绝方法A的原因却消失了。系统并没有丢失决策本身,而是失去了让决策易于理解的关键信息,而这正是多层折叠所带来的特定损害。
哪些内容应当保留
在不同的框架中,结构化的总结大致会归为相同的几类:
- 目的:要实现什么目标?
- 状态:已了解或发生的变化是什么?
- 工作与待办事项:已完成的内容以及剩余的任务是什么?
- 未解决事项:哪些问题仍然悬而未决、存在限制或正在考虑中?
不同工具的标签有所不同,但结构却相似,这表明它接近于会话必须保留的最小实用状态。一些工具还增加了文件恢复步骤,以便在会话折叠后重新加载工作集。