标签归档:Manus

从 LangChain 和 Manus 两巨头上下文工程实战中学到的 5 点经验

最近仔细看了 LangChain 的 Lance 和 Manus 的 Pete 的视频,主题是构建 AI 智能体中的上下文工程。这次分享没有那些空泛的理论,全是在生产环境中验证过的实战经验。

我结合自己的思考,整理了我认为 5 个最有价值的技术要点,每一个都直接影响着智能体的性能和可用性。

1. 上下文爆炸是智能体最大的瓶颈

Manus 统计显示,一个典型的智能体任务需要约 50 次工具调用。Anthropic 也提到,生产环境中的智能体对话可能持续数百轮。每次工具调用都会返回结果,这些结果会不断累积在消息历史中。

问题在于,随着上下文长度增加,模型性能会明显下降。Anthropic 把这个现象称为”上下文腐烂”(context rot)。具体表现是模型开始重复输出、推理速度变慢、回答质量下降。大部分模型在 200k Token 左右就开始出现这些问题,远低于它们宣称的上限。

这就形成了一个矛盾:智能体需要大量上下文来保持工作状态的连续性,但上下文太长又会导致性能下降。这是所有开发智能体的团队都会遇到的核心挑战。

Lance 在分享中展示了他们 Open Deep Research 项目的数据。这个开源项目在 Deep Research Bench 测试中排名前十,但即使是这样优秀的实现,也需要在三个阶段中不断地卸载和压缩上下文才能保持高效运行。

Pete 则分享了 Manus 的经验。他们发现,如果不做任何优化,上下文会在几十轮对话后就达到模型的处理极限。更糟糕的是,用户往往在这时才开始进入任务的核心部分。

解决这个问题没有捷径,必须从架构设计开始就考虑上下文管理。这也是为什么”上下文工程”这个概念在今年 5 月开始迅速流行的原因。它不是可选的优化项,而是构建生产级智能体的必要条件。

2. 上下文工程的五大组成部分

Lance 和 Pete 在分享中系统地总结了上下文工程的五个核心组成部分,这些部分相互关联,形成了一个完整的技术体系。

上下文卸载 是把部分信息移出主消息历史,存储到外部系统中。最常见的是使用文件系统。当搜索工具返回大量结果时,不是把完整内容放在消息队列里,而是写入文件,只在上下文中保留文件路径。Claude Code、Manus、Open Deep Research 项目都大量使用这种方法。

上下文缩减 包括压缩和摘要两种策略。压缩是去掉可以从外部重建的信息,摘要是对信息进行不可逆的精简。Claude 3.5 Sonnet 已经内置了修剪旧工具调用的功能。Cognition 在智能体交接时也会触发摘要。

上下文检索 解决如何按需获取被卸载的信息。有两派做法:Cursor 使用索引加语义搜索,Claude Code 和 Manus 只用文件系统加 grep、glob 这样的简单工具。两种方法各有优劣,选择哪种取决于具体场景。

上下文隔离 通过多智能体架构实现关注点分离。每个子智能体有自己的上下文窗口,避免信息混杂。Manus 的 Wide Agents、LangChain 的 Deep Agents、Claude 的多智能体研究员都采用了这种设计。

上下文缓存 利用 KV 缓存等机制减少重复计算。Anthropic 等服务商提供的缓存功能对长上下文智能体至关重要,特别是在输入远长于输出的场景下。

这五个部分不是独立的,而是相互影响的系统。卸载和检索让缩减更高效,稳定的检索让隔离变得安全。但隔离会减慢上下文传递,降低缩减频率。更多的隔离和缩减又会影响缓存效率。

Pete 特别强调,这种相互依赖意味着你不能只优化某一个方面,必须从整体考虑。比如,如果你的检索机制不够稳定,就不能激进地进行上下文隔离,否则子智能体可能无法获取必要的信息。

3. 上下文管理和数据库系统的对比

在分享中我个人的感觉是上下文工程的设计和数据库系统设计有比较多的相似性。

上下文卸载和数据库索引有很多相似之处,但它们的设计目标和实现机制存在本质差异。

展开来讲:相似性是都是为了解决容量与性能的矛盾

数据库索引解决的是「如何在海量数据中快速定位」的问题,而上下文卸载解决的是「如何在有限窗口中访问无限信息」的问题。两者都是通过建立某种间接引用机制,让系统能够处理超出直接处理能力的数据量。

数据库有内存中的索引页、磁盘上的索引文件、以及实际的数据页。上下文工程也有类似的层次:活跃上下文(相当于内存中的热数据)、文件路径引用(相当于索引)、文件系统中的完整内容(相当于磁盘数据)。

数据库索引针对不同的查询模式有不同类型:B 树索引适合范围查询,哈希索引适合等值查询,全文索引适合文本搜索。上下文卸载也有类似的分类:代码文件用路径索引,搜索结果用语义向量索引,对话历史用时间戳索引。

数据库的 buffer pool 会缓存频繁访问的索引页和数据页。上下文工程中的 KV 缓存也会保留频繁引用的上下文片段。两者都使用 LRU 或类似算法决定淘汰策略。

关键差异:索引是查询优化,卸载是容量管理

数据库索引的主要目标是加速查询,即使没有索引,全表扫描也能得到结果,只是慢一些。而上下文卸载是为了突破硬性限制,没有卸载机制,超出窗口的信息就完全无法访问。这是「优化」与「必需」的区别。

数据库索引是冗余信息,删除索引不会丢失数据,只会影响查询性能。但上下文卸载中,如果文件系统中的内容丢失,而上下文中只有路径引用,信息就永久丢失了。这就是为什么 Pete 强调卸载必须配合可靠的检索机制。

数据库索引需要随数据更新而维护,这是 ACID 事务的一部分。而上下文卸载通常是单向的:信息从上下文移到外部存储,很少需要反向更新。即使文件内容被修改,上下文中的引用通常保持不变。

数据库可以选择性地为某些列建立索引,基于查询模式和数据分布做决策。而上下文卸载往往是强制的:当接近 Token 限制时,必须卸载某些内容,没有”不建索引”的选项。

就像数据库有内存、磁盘、缓存的层次结构,上下文工程也有类似的分层:活跃上下文(内存)、文件系统(磁盘)、KV 缓存(缓存层)。数据库的查询优化对应着上下文检索,事务隔离对应着智能体隔离,数据压缩对应着上下文压缩。

Pete 提到的”通过通信”和”通过共享内存”两种模式,实际上对应着数据库中的消息传递和共享存储两种架构。当子智能体需要完整历史记录时,使用共享上下文模式,这就像数据库中的共享存储架构。当任务相对独立时,使用通信模式,类似于分布式数据库的消息传递。

智能体间的状态同步问题,本质上就是分布式系统的一致性问题。

Manus 的解决方案也借鉴了数据库的设计思想。他们使用结构化的模式(schema)来定义智能体间的通信接口,确保数据的一致性。他们的分层行为空间设计(函数调用、沙盒工具、软件包与API)类似于数据库的存储引擎分层。

这种类比不仅仅是理论上的相似。在实践中,你可以直接应用数据库系统的很多优化技术。比如,使用 LRU 策略决定哪些上下文应该被压缩或摘要;使用预写日志(WAL)的思想,在摘要前先把完整上下文写入日志文件;使用索引来加速文件系统中的信息检索。

我们可以借鉴数据库领域几十年的研究成果和工程实践,而不是从零开始摸索。

3. 上下文工程的核心是做减法,而不是加法

Pete 在分享最后强调的这一点可能是最重要的:避免上下文过度工程(context over-engineering)。

他提到,Manus 上线六七个月以来,最大的性能提升不是来自增加更复杂的上下文管理机制,而是来自简化架构、移除不必要的功能、对模型给予更多信任。他们已经重构了 Manus 五次,每次都是在做减法。

比如,早期的 Manus 使用 todo.md 文件来管理任务列表,结果发现约三分之一的操作都在更新这个列表,浪费了大量 Token。后来他们改用更简单的结构化规划器,效果反而更好。

另一个例子是工具选择。他们没有使用复杂的语义搜索来动态加载工具,而是固定使用 10-20 个原子功能,其他所有功能都通过沙盒中的命令行工具或 Python 脚本来实现。这种分层的行为空间设计,让系统既灵活又简单。

Pete 还提到了一个评估架构的方法:在强弱模型之间切换测试。如果你的架构从弱模型切换到强模型后能获得明显提升,说明架构有较好的未来适应性。因为明年的弱模型可能就和今天的强模型一样好。

关于是否要训练专门的模型,Pete 的观点也很明确:初创公司应该尽可能长时间地依赖通用模型和上下文工程。训练专门模型不仅成本高,而且在产品快速迭代的阶段,很可能在优化错误的指标。MCP(Model Context Protocol)的推出就是个例子,它彻底改变了 Manus 的设计,如果之前投入大量资源训练专门模型,这些投入就浪费了。

他们现在也不使用开源模型,不是因为质量问题,而是成本问题。对于输入远长于输出的智能体应用,KV 缓存至关重要。大型云服务商有更好的分布式缓存基础设施,算下来使用他们的 API 反而更便宜。

这些经验告诉我们:上下文工程的目标是让模型的工作变得更简单,而不是更复杂。不要为了优化而优化,每个设计决策都要基于实际的性能数据和用户反馈。

4. 压缩与摘要的本质区别决定了使用策略

Pete 详细解释了压缩和摘要这两种看似相似实则截然不同的策略,这个区分对实际应用至关重要。

压缩是可逆的。在 Manus 中,每个工具调用都有完整格式和紧凑格式。比如写文件操作,完整格式包含路径和内容,紧凑格式只保留路径。因为文件已经存在于文件系统中,通过路径可以随时恢复完整信息。这种可逆性至关重要,因为你永远不知道哪个历史操作会在后续变得重要。

摘要是不可逆的。一旦执行摘要,原始信息就永久丢失了。因此必须非常谨慎。Manus 的做法是在摘要前将完整上下文转储为日志文件,作为最后的保险。而且摘要时要保留最新几次工具调用的完整信息,让模型知道当前的工作状态。

关于阈值的确定,Pete 分享了具体数据:大多数模型在 200k Token 左右开始出现性能下降,他们将 128k-200k 设定为”腐烂前”阈值。这不是随意设定的,而是通过大量评估得出的。他们测试了不同长度下模型的重复率、推理速度、输出质量等指标。

触发策略是渐进式的:

  • 接近 128k 时,开始压缩最旧的 50% 工具调用
  • 压缩后检查实际释放的空间
  • 如果压缩效果不明显(因为即使紧凑格式也占用空间),才考虑摘要
  • 摘要时使用完整版本而非压缩版本的数据
  • 始终保留最新的几次完整工具调用

尽可能保留信息的完整性,只在必要时才接受信息损失。这就像数据压缩算法中的无损压缩和有损压缩,你总是先尝试无损方案。

不同类型的内容有不同的压缩潜力。搜索结果、API 响应这类内容压缩效果好,因为大部分信息可以通过查询重新获取。而用户的指令、模型的推理过程压缩空间有限,因为这些信息往往都是必要的。

5. 隔离策略的选择取决于任务特性而非直觉

多智能体隔离是个老话题,但 Pete 分享的经验推翻了很多常见做法。

首先是反模式的识别。很多团队喜欢按人类角色划分智能体:设计师智能体、程序员智能体、经理智能体等。Pete 明确指出这是个陷阱。这种划分源于人类组织的局限性(个人能力和注意力有限),而不是 AI 系统的最优设计。Manus 只有极少的几个功能性智能体:通用执行器、规划器、知识管理器。

关于隔离模式的选择,Pete 区分了两种场景:

通信模式适合结果导向的独立任务。主智能体发送明确指令,子智能体独立完成,返回结果。整个过程主智能体不关心细节,只要最终输出。比如”在代码库中搜索特定函数”、”将这段文本翻译成中文”。这种模式的优势是上下文干净、成本低、易于调试。

共享上下文模式适合需要完整历史信息的复杂任务。子智能体能看到所有历史对话和工具调用,但有自己的系统提示和工具集。比如深度研究任务,最终报告需要参考大量中间搜索和笔记。这种模式成本高(需要预填充大量上下文,无法复用 KV 缓存),但对某些任务是必要的。

判断使用哪种模式的关键是从结果推理过程的依赖性。如果最终输出强依赖于中间步骤的细节,就需要共享上下文。如果只需要最终结果,通信模式就足够了。

Pete 还提到了一个实用技巧:”智能体即工具”(agent as tool)。从主智能体视角,调用子智能体就像调用普通工具,有明确的输入输出接口。这简化了系统设计,也让调试更容易。Manus 的”高级搜索”功能就是这样实现的,表面上是个工具,实际是个完整的子智能体工作流。

他们解决通信问题的方法是强制使用结构化输出。主智能体调用子智能体前必须定义输出模式,子智能体通过专门的”提交结果”工具返回数据,系统用约束解码确保格式正确。这避免了自由格式通信带来的解析问题和信息丢失。

6. 一些额外的技术洞察

除了这五个核心经验,还有一些值得记录的技术细节。

信息的局部性。使用基于行的格式意味着每行都是独立的信息单元,读取第 100 行不需要解析前 99 行。这对于大文件的随机访问至关重要。

结构化不等于复杂格式。Pete 强调,他们优先使用基于行(line-based)的纯文本格式,而不是 JSON 或 XML。原因很简单:模型可以用 grep 搜索特定行,用 sed 修改特定内容,用 head/tail 读取部分数据。这些都是模型已经掌握的基础工具。

关于 Markdown 的使用要特别谨慎。虽然模型都接受过 Markdown 训练,但过度使用会带来副作用。某些模型会因此输出过多的项目符号和格式化标记,浪费 Token 还影响可读性。Manus 的经验是:在需要结构时用 Markdown,在存储数据时用纯文本。

关于模型切换的评估方法。Pete 提到,他们会在强弱模型间切换来评估架构的未来适应性。如果一个架构在弱模型上也能工作(虽然效果差一些),说明它不是过度依赖特定模型能力的脆弱设计。

关于工具数量的上限,他们的经验是不超过 30 个。这不是任意数字,而是基于大量测试。超过这个数量,模型开始频繁调用错误的工具,甚至调用不存在的工具。解决方案不是动态加载工具,而是设计分层的行为空间:少量原子工具 + 沙盒命令 + 代码执行。

关于评估体系,Manus 的三层评估值得借鉴:用户评分是北极星指标,自动化测试覆盖可验证的任务,真人评估处理主观性强的输出。学术基准测试只是参考,不能作为主要依据。

上下文工程不是一些零散的技巧,而是一个需要系统思考的工程领域。每个设计决策都会影响到其他部分,没有放之四海而皆准的最佳实践,只有基于具体场景的权衡取舍。

以上。

Agent 核心策略:Manus、Gemini CLI 和 Claude Code 的上下文压缩策略和细节

做 AI Agent 开发的都需要考虑上下文爆炸的问题,不仅仅是成本问题,还有性能问题。

许多团队选择了压缩策略。但过度激进的压缩不可避免地导致信息丢失。

这背后的根本矛盾在于:Agent 需要基于完整的历史状态来决策下一步行动,但我们无法预知当前的某个观察细节是否会在未来的某个关键时刻变得至关重要。

前面一篇文章讲了整个上下文的管理策略,今天着重聊一下上下文管理的压缩策略和细节。

下面根据 Manus、Gemini CLI 和 Claude Code 这三个项目的源码来聊一下上下文的压缩。

三个产品,有三个不同的选择,或者说设计哲学。

Manus 的选择是:永不丢失。 他们认为,从逻辑角度看,任何不可逆的压缩都带有风险。所以他们选择了一条看似”笨拙”但实际上很聪明的路:把文件系统当作”终极上下文”。

Claude Code 的选择是:极限压榨。 92% 的压缩触发阈值,这个数字相当激进。他们想要榨干上下文窗口的每一个 token,直到最后一刻才开始压缩。

Gemini CLI 的选择是:稳健保守。 70% 就开始压缩,宁可频繁一点,也要保证系统的稳定性。

这三种选择没有对错,只是适用场景不同。接下来我们逐个分析。

1. Manus

Manus 的压缩最让我印象深刻的是其在可恢复性上的策略。

Manus 团队认为:任何不可逆的压缩都带有风险。你永远不知道现在丢掉的信息是不是未来解决问题的关键。

他们选择了不做真正的删除,而是将其外部化存储。

传统做法是把所有观察结果都塞进上下文里。比如让 Agent 读一个网页,它会把整个 HTML 内容都存下来,可能就是上万个 token。Manus 不这么干。

当 Agent 访问网页时,Manus 只在上下文中保留 URL 和简短的描述,完整的网页内容并不保存。需要重新查看网页内容时,通过保留的 URL 重新获取即可。这就像是你在笔记本上记录”参见第 23 页的图表”,而不是把整个图表重新画一遍。

文档处理也是同样的逻辑。一个 100 页的 PDF 文档,Manus 不会把全部内容放进上下文,而是只记录文档路径、页数、最后访问的位置等元信息。当 Agent 需要查看具体内容时,再通过文件路径读取相应的页面。

Manus 把文件系统视为”终极上下文”——一个容量几乎无限、天然持久化、Agent 可以直接操作的外部记忆系统。

文件系统的层级结构天然适合组织信息。Agent 可以创建不同的目录来分类存储不同类型的信息:项目背景放一个文件夹,技术细节放另一个文件夹,错误日志单独存放。需要时按图索骥,而不是在一个巨大的上下文中大海捞针。

这不是简单的存储。Manus 训练模型学会主动使用文件系统来管理自己的「记忆」。当发现重要信息时,Agent 会主动将其写入特定的文件中,而不是试图把所有东西都记在上下文里。就像一个经验丰富的研究员,知道什么该记在脑子里,什么该写在笔记本上,什么该归档保存。

在 Manus 团队对外的文章开头,指出了为什么必须要有压缩策略:

第一,观察结果可能非常庞大。与网页或 PDF 等非结构化数据交互时,一次观察就可能产生数万个 token。如果不压缩,可能一两次操作就把上下文占满了。

第二,模型性能会下降。这是个很多人忽视的问题。即使模型声称支持 200k 的上下文窗口,但实际使用中,超过一定长度后,模型的注意力机制效率会显著下降,响应质量也会变差。这就像人的工作记忆一样,信息太多反而会降低处理效率。

第三,成本考虑。长输入意味着高成本,即使使用了前缀缓存等优化技术,成本依然可观。特别是在需要大量交互的场景下,成本会快速累积。

在具体实现过程中,可恢复压缩有几个关键点:

保留最小必要信息。对于每个外部资源,只保留能够重新获取它的最小信息集。网页保留 URL,文档保留路径,API 响应保留请求参数。这些信息占用的空间极小,但足以在需要时恢复完整内容。

智能的重新加载时机。不是每次提到某个资源就重新加载,而是根据上下文判断是否真的需要详细内容。如果只是确认文件存在,就不需要读取内容;如果要分析具体细节,才触发加载。

缓存机制。虽然内容不在上下文中,但 Manus 会在本地维护一个缓存。最近访问过的资源会暂时保留,避免频繁的重复加载。这个缓存是独立于上下文的,不占用宝贵的 token 额度。

2. Claude Code

Claude Code 的策略完全是另一个极端——他们要把上下文用到极致。

2.1 92% 的阈值

这个数字有一些讲究。留 8% 的缓冲区既保证了压缩过程有足够的时间完成,又避免了频繁触发压缩带来的性能开销。更重要的是,这个缓冲区给了系统一个「反悔」的机会——如果压缩质量不达标,还有空间执行降级策略。

const COMPRESSION_CONFIG = {
  threshold0.92,           // 92%阈值触发
  triggerVariable"h11",    // h11 = 0.92
  compressionModel"J7()",  // 专用压缩模型
  preserveStructuretrue    // 保持8段结构
};

2.2 八段式结构化摘要

Claude Code 的压缩不是简单的截断或摘要,而是八段式结构。这个结构我们可以学习一下:

const COMPRESSION_SECTIONS = [
  "1. Primary Request and Intent",    // 主要请求和意图
  "2. Key Technical Concepts",        // 关键技术概念
  "3. Files and Code Sections",       // 文件和代码段
  "4. Errors and fixes",              // 错误和修复
  "5. Problem Solving",               // 问题解决
  "6. All user messages",             // 所有用户消息
  "7. Pending Tasks",                 // 待处理任务
  "8. Current Work"                   // 当前工作
];

每一段都有明确的目的和优先级。「主要请求和意图」确保 Agent 永远不会忘记用户最初想要什么;「关键技术概念」保留重要的技术决策和约束条件;「错误和修复」避免重复踩坑;「所有用户消息」则保证用户的原始表达不会丢失。

这种结构的好处是,即使经过多次压缩,Agent 仍然能保持工作的连贯性。关键信息都在,只是细节被逐步抽象化了。

2.3 专用压缩模型 J7

Claude Code 使用了一个专门的压缩模型 J7 来处理上下文压缩。这不是主模型,而是一个专门优化过的模型,它的任务就是理解长对话并生成高质量的结构化摘要。

async function contextCompression(currentContext{
  // 检查压缩条件
  if (currentContext.tokenRatio < h11) {
    return currentContext;  // 无需压缩
  }
  
  // 调用专用压缩模型
  const compressionPrompt = await AU2.generatePrompt(currentContext);
  const compressedSummary = await J7(compressionPrompt);
  
  // 构建新的上下文
  const newContext = {
    summary: compressedSummary,
    recentMessages: currentContext.recent(5),  // 保留最近5条
    currentTask: currentContext.activeTask
  };
  
  return newContext;
}

AU2 负责生成压缩提示词,它会分析当前上下文,提取关键信息,然后构造一个结构化的提示词给 J7。J7 处理后返回符合八段式结构的压缩摘要。

2.4 上下文生命周期管理

Claude Code 把上下文当作有生命周期的实体来管理。这个设计理念很先进——上下文不是静态的数据,而是动态演化的有机体。

class ContextManager {
  constructor() {
    this.compressionThreshold = 0.92;  // h11 = 0.92
    this.compressionModel = "J7";      // 专用模型
  }
  
  async manageContext(currentContext, newInput) {
    // 1. 上下文更新
    const updatedContext = this.appendToContext(currentContext, newInput);
    
    // 2. 令牌使用量检查
    const tokenUsage = await this.calculateTokenUsage(updatedContext);
    
    // 3. 压缩触发判断
    if (tokenUsage.ratio >= this.compressionThreshold) {
      // 4. 八段式压缩执行
      const compressionPrompt = await AU2.generateCompressionPrompt(updatedContext);
      const compressedSummary = await this.compressionModel.generate(compressionPrompt);
      
      // 5. 新上下文构建
      return this.buildCompressedContext(compressedSummary, updatedContext);
    }
    
    return updatedContext;
  }
}

整个流程是自动化的。每次有新的输入,系统都会评估是否需要压缩。压缩不是一次性的动作,而是持续的过程。随着对话的进行,早期的详细内容会逐渐被抽象化,但关键信息始终保留。

2.5 优雅降级机制

当压缩失败时,系统不会死板地报错或者强行应用低质量的压缩结果,而是有一整套 Plan B、Plan C。这种”永不放弃”的设计理念,让系统在各种极端情况下都能稳定运行。

降级策略包括:

  • 自适应重压缩:如果首次压缩质量不佳,会调整参数重试
  • 混合模式保留:压缩旧内容,但完整保留最近的交互
  • 保守截断:最坏情况下,至少保证系统能继续运行

2.6 压缩后的信息恢复

虽然 Claude Code 的压缩是有损的,但它通过巧妙的设计最小化了信息损失的影响。压缩后的八段式摘要不是简单的文本,而是结构化的信息,包含了足够的上下文让 Agent 能够理解之前发生了什么,需要做什么。

特别值得一提的是第 6 段”All user messages”。即使其他内容被压缩了,用户的所有消息都会以某种形式保留。这确保了用户的意图和需求不会在压缩过程中丢失。

2.7 实践指南

Claude Code 在实践中还有一些最佳实践:

  • 定期使用 /compact 命令压缩长对话:用户可以主动触发压缩,不必等到自动触发
  • 在上下文警告出现时及时处理:系统会在接近阈值时发出警告,用户应该及时响应
  • 通过 Claude.md 文件保存重要信息:将关键信息外部化,减少上下文消耗

3. Gemini CLI

Gemini CLI 选择了一条中庸之道,或者说是实用之道。Gemini CLI 项目开源了,这部分的说明会多一些。

3.1 70/30?

Gemini CLI 选择了 70% 作为压缩触发点,30% 作为保留比例。这个比例我们也可以参考学习一下:

为什么是 70% 而不是 92%

  • 更早介入,避免紧急压缩导致的卡顿
  • 给压缩过程留出充足的缓冲空间
  • 适合轻量级应用场景,不追求极限性能

30% 保留的合理性:

  • 刚好覆盖最近 5-10 轮对话
  • 足够维持上下文连续性
  • 不会让用户感觉”突然失忆”

共背后的逻辑是:宁可频繁一点地压缩,也要保证每次压缩都是从容的、高质量的。

3.2 精选历史提取

Gemini CLI 有个独特的概念叫精选历史”。不是所有的历史都值得保留,系统会智能地筛选有效内容:

function extractCuratedHistory(comprehensiveHistory: Content[]): Content[] {
  if (comprehensiveHistory === undefined || comprehensiveHistory.length === 0) {
    return [];
  }
  const curatedHistory: Content[] = [];
  const length = comprehensiveHistory.length;
  let i = 0;
  while (i < length) {
    // 用户轮次直接保留
    if (comprehensiveHistory[i].role === 'user') {
      curatedHistory.push(comprehensiveHistory[i]);
      i++;
    } else {
      // 处理模型轮次
      const modelOutput: Content[] = [];
      let isValid = true;
      // 收集连续的模型轮次
      while (i < length && comprehensiveHistory[i].role === 'model') {
        modelOutput.push(comprehensiveHistory[i]);
        // 检查内容有效性
        if (isValid && !isValidContent(comprehensiveHistory[i])) {
          isValid = false;
        }
        i++;
      }
      // 只有当所有模型轮次都有效时才保留
      if (isValid) {
        curatedHistory.push(...modelOutput);
      }
    }
  }
  return curatedHistory;
}

这个策略的巧妙之处在于:

  • 用户输入全部保留:所有用户输入都被视为重要信息,无条件保留
  • 模型轮次有条件保留:连续的模型轮次被视为一个整体进行评估
  • 全有或全无的处理:要么全部保留,要么全部丢弃,避免了复杂的部分保留逻辑

3.3 内容有效性判断

什么样的内容会被认为是无效的?Gemini CLI 有明确的标准:

function isValidContent(content: Content): boolean {
  // 检查 parts 数组是否存在且非空
  if (content.parts === undefined || content.parts.length === 0) {
    return false;
  }
  for (const part of content.parts) {
    // 检查 part 是否为空
    if (part === undefined || Object.keys(part).length === 0) {
      return false;
    }
    // 检查非思考类型的 part 是否有空文本
    if (!part.thought && part.text !== undefined && part.text === '') {
      return false;
    }
  }
  return true;
}

无效内容包括:空响应、错误输出、中断的流式响应等。这种预过滤机制确保进入压缩流程的都是高质量的内容。

3.4 五段式结构化摘要

相比 Claude Code 的八段式,Gemini CLI 的五段式更简洁,但涵盖了所有关键信息:

1. overall_goal - 用户的主要目标
2. key_knowledge - 重要技术知识和决策
3. file_system_state - 文件系统当前状态
4. recent_actions - 最近执行的重要操作
5. current_plan - 当前执行计划

压缩时,系统会生成 XML 格式的结构化摘要。这种格式的好处是结构清晰,LLM 容易理解和生成,同时也便于后续的解析和处理。

3.5 基于 Token 的智能压缩

Gemini CLI 的压缩不是简单的定时触发,而是基于精确的 token 计算:

async tryCompressChat(
  prompt_id: string,
  force: boolean = false,
): Promise<ChatCompressionInfo | null> {
  const curatedHistory = this.getChat().getHistory(true);

  // 空历史不压缩
  if (curatedHistory.length === 0) {
    return null;
  }

  const model = this.config.getModel();

  // 计算当前历史的 token 数量
  const { totalTokens: originalTokenCount } =
    await this.getContentGenerator().countTokens({
      model,
      contents: curatedHistory,
    });

  // 获取压缩阈值配置
  const contextPercentageThreshold =
    this.config.getChatCompression()?.contextPercentageThreshold;

  // 如果未强制压缩且 token 数量低于阈值,则不压缩
  if (!force) {
    const threshold =
      contextPercentageThreshold ?? COMPRESSION_TOKEN_THRESHOLD; // 默认 0.7
    if (originalTokenCount < threshold * tokenLimit(model)) {
      return null;
    }
  }

  // 计算压缩点,保留最后 30% 的历史
  let compressBeforeIndex = findIndexAfterFraction(
    curatedHistory,
    1 - COMPRESSION_PRESERVE_THRESHOLD, // COMPRESSION_PRESERVE_THRESHOLD = 0.3
  );

  // 确保压缩点在用户轮次开始处
  while (
    compressBeforeIndex < curatedHistory.length &&
    (curatedHistory[compressBeforeIndex]?.role === 'model' ||
      isFunctionResponse(curatedHistory[compressBeforeIndex]))
  ) {
    compressBeforeIndex++;
  }

  // 分割历史为需要压缩和需要保留的部分
  const historyToCompress = curatedHistory.slice(0, compressBeforeIndex);
  const historyToKeep = curatedHistory.slice(compressBeforeIndex);

  // 使用 LLM 生成历史摘要
  this.getChat().setHistory(historyToCompress);
  const { text: summary } = await this.getChat().sendMessage(
    {
      message: {
        text: 'First, reason in your scratchpad. Then, generate the <state_snapshot>.',
      },
      config: {
        systemInstruction: { text: getCompressionPrompt() },
      },
    },
    prompt_id,
  );

  // 创建新的聊天历史,包含摘要和保留的部分
  this.chat = await this.startChat([
    {
      role: 'user',
      parts: [{ text: summary }],
    },
    {
      role: 'model',
      parts: [{ text: 'Got it. Thanks for the additional context!' }],
    },
    ...historyToKeep,
  ]);
}

这个实现有几个细节值得注意:

  • 支持强制压缩:通过 force 参数,用户可以主动触发压缩
  • 智能分割点选择:确保压缩点在用户轮次开始,避免打断对话逻辑
  • 两阶段压缩:先生成摘要,再重建对话历史

3.6 多层压缩机制

Gemini CLI 的压缩是分层进行的,每一层都有特定的目标:

第一层:内容过滤:过滤掉无效内容、thought 类型的部分,确保进入下一层的都是有价值的信息。

第二层:内容整合:合并相邻的同类内容,比如连续的纯文本 Part 会被合并成一个,减少结构冗余。

第三层:智能摘要:当 token 使用量超过阈值时,触发 LLM 生成结构化摘要。

第四层:保护机制:确保关键信息不被压缩丢失,比如用户的最新指令、正在进行的任务等。

3.7 模型适配的 Token 限制

不同的模型有不同的 token 限制,Gemini CLI 对此有精细的适配:

export function tokenLimit(model: Model): TokenCount {
  switch (model) {
    case 'gemini-1.5-pro':
      return 2_097_152;
    case 'gemini-1.5-flash':
    case 'gemini-2.5-pro':
    case 'gemini-2.5-flash':
    case 'gemini-2.0-flash':
      return 1_048_576;
    case 'gemini-2.0-flash-preview-image-generation':
      return 32_000;
    default:
      return DEFAULT_TOKEN_LIMIT; // 1_048_576
  }
}

系统会根据使用的模型自动调整压缩策略。对于支持超长上下文的模型(如 gemini-1.5-pro 的 200 万 token),可以更宽松;对于受限的模型,会更积极地压缩。

3.8 历史记录的精细处理

recordHistory 方法负责记录和处理历史,实施了多个优化策略:

  1. 避免重复:不会重复添加相同的用户输入
  2. 过滤思考过程:thought 类型的 Part 会被过滤掉,不进入最终历史
  3. 合并优化:相邻的模型轮次会被合并,相邻的纯文本也会合并
  4. 占位符策略:如果模型没有有效输出,会添加空的占位符保持结构完整

3.9 压缩的用户体验设计

Gemini CLI 特别注重压缩对用户体验的影响:

  • 无感压缩:70% 的阈值确保压缩发生在用户察觉之前
  • 连续性保持:保留 30% 的最新历史,确保当前话题的连贯性
  • 透明反馈:压缩前后的 token 数量变化会被记录和报告

4. 写在最后

研究完这三个项目的源码,我最大的感受是:压缩策略的选择,本质上是对「什么是重要的」这个问题的回答。 Manus 说「所有信息都可能重要,所以我不删除,只是暂时收起来」;Claude Code 说「结构化的摘要比原始细节更重要」;Gemini CLI 说「用户体验比技术指标更重要」。三种回答,三种哲学。

这让我想起一句话:在 AI 时代,真正稀缺的不是信息,而是注意力。 上下文压缩就是在教 AI 如何分配注意力——什么该记住,什么可以忘记,什么需要随时能找回来。

这是人类智慧的核心能力之一。我们每天都在做类似的决策:重要的事情记在心里,次要的写在本子上,琐碎的存在手机里。Manus、Claude Code 和 Gemini CLI 只是用不同的方式在教 AI 做同样的事。

没有完美的压缩策略,只有最适合你场景的策略。 选择哪种策略不重要,重要的是理解它们背后的设计智慧,然后根据自己的需求做出明智的选择。

以上。