引子:一个被“遗忘”的架构决策
你在用Agent执行一个微服务重构任务。任务进行到第50轮时,发生了一件令人困惑的事——Agent在第3轮就确认了“所有服务间通信必须使用gRPC而非REST”这一架构决策,但到了第50轮,它突然开始用REST风格编写新接口。
你翻看上下文,发现那条关键决策被“淹没”在第5轮和第10轮之间大量的工具输出、代码片段和中间推理之中。Agent并非“故意忽略”它——而是上下文已经膨胀到18万token,那条决策恰好落在了注意力最薄弱的位置。这就是Context Rot(上下文腐烂):随着对话变长,关键信息在上下文中的位置逐渐“腐烂”,模型的注意力无法有效覆盖,导致性能持续衰减。Stanford/UC Berkeley的研究系统性地证实了这一现象:当关键信息被放在长上下文的中间位置时,任务失败率可提高30%以上,性能曲线呈现明显的U型——两头高、中间低。
但这个问题的解法,比“把上下文压短”要复杂得多。它涉及一个核心的工程约束:KV缓存命中率。
一、从Prompt Engineering到Context Engineering:一次范式升级
在讨论KV缓存之前,先理清一个概念上的演进。
过去几年,行业焦点是Prompt Engineering——如何写出好的系统提示词、如何组织少样本示例、如何设计输出格式。这解决的是“单次问答”的最优解问题。
但Agent的工作模式完全不同。Agent在一个循环中运行,每一轮迭代都会产生新的工具调用结果、新的观察数据、新的推理中间态。这些信息不断追加到上下文中,形成一个持续膨胀的状态。Anthropic对Context Engineering的定义精确地指出了这个变化:Context Engineering是“在LLM推理过程中,策划和维护最优token集合的策略”,包括所有可能进入上下文的信息,而不仅仅是提示词。
这意味着Agent开发者的工作重心发生了转移。在Prompt Engineering时代,你优化的是“怎么问”;在Context Engineering时代,你优化的是“每一轮迭代应该给模型看什么、藏什么、压缩什么、丢弃什么”。Anthropic将Context Engineering视为Prompt Engineering的自然演进——当Agent需要在多轮推理和更长的时间跨度上运行时,你需要管理的是整个上下文状态:系统指令、工具定义、MCP配置、外部数据、消息历史,所有这些都要被周期性地精炼和重组。
Java视角:Prompt Engineering相当于优化一个SQL查询的语句——让单次查询跑得更快。Context Engineering相当于设计一个数据库的缓存策略——决定哪些数据常驻内存、哪些按需加载、哪些写回磁盘、哪些过期淘汰。前者是查询级的优化,后者是系统级的架构设计。
二、KV缓存:为什么它是Agent的“生命线”
理解了Context Engineering是什么之后,下一个问题是:在这个领域,什么指标最重要?
Manus团队给出了一个非常明确的答案:“如果我必须选择仅一个指标,我认为KV缓存命中率是生产阶段AI Agent最重要的单一指标。它直接影响到延迟和成本。”
要理解为什么这个指标如此关键,需要先看清楚Agent的Token消耗结构。
Agent的工作循环是:模型根据当前上下文从预定义动作空间选择一个动作,在环境中执行产生观察结果,动作和观察结果被追加到上下文,形成下一轮输入。随着每一步执行,上下文持续增长,但模型的输出——通常是一个结构化的函数调用——保持相对简短。这就导致Agent的输入输出Token比高度倾斜。在Manus中,平均输入输出Token比约为100:1。
Chatbot的输入输出比大约是1:1——你问一段话,它回一段话。Agent的100:1意味着每次迭代都要把之前所有轮次的上下文重新读一遍,而实际产生的新内容极少。绝大部分计算成本花在了“读上下文”上。
幸好,具有相同前缀的上下文可以利用KV缓存,大幅降低首Token生成时间和推理成本。节省幅度有多大?以Claude Sonnet为例,缓存输入的Token成本是0.30美元/百万Token,未缓存则高达3美元/百万Token——相差10倍。对于一个需要执行几十步的Agent任务,如果每一步的上下文前缀都无法命中缓存,成本会迅速失控。
Manus团队从工程实践中总结了提升KV缓存命中率的三项核心操作:
第一,保持提示前缀稳定。由于LLM的自回归特性,即使一个Token的差异也会使该Token之后的所有缓存失效。一个常见的错误是在系统提示中写入精确到秒的时间戳——每一次请求时间戳都不同,前缀的第一段就无法命中缓存。Manus的做法是:将时间戳从系统提示中移除,或者将其精度降低到分钟级别。
第二,上下文只追加不修改。如果对上下文中的已有内容进行删改,缓存的前缀连续性就被打断。Manus的设计中,上下文修改只允许追加操作,工具的增删通过“遮蔽”(masking)而非“移除”来实现——被遮蔽的工具在上下文中仍然存在,只是被标记为不可用状态,这样前缀的字节序列保持不变。
第三,确保可序列化的确定性。任何包含随机性或不稳定因素的字段——比如自动生成的UUID、动态排序的JSON key、浮点数精度问题——都会破坏前缀的一致性。Manus在序列化时对JSON key进行确定性排序,并确保所有字段的序列化格式在不同请求间完全一致。
Java视角:Agent的KV缓存优化和Java中连接池+缓存预热的思路完全一致。连接池的核心价值是避免每次请求都重新建立TCP连接——把“建立连接”这个昂贵操作的结果缓存起来复用。KV缓存也是同理:把上下文前缀的注意力计算结果缓存起来,避免每次迭代都从头计算。Manus对“稳定前缀”的执着,相当于Java中做缓存预热时强调“热路径上的数据不要频繁变动”。而“只追加不修改”的策略,和Java中事件溯源(Event Sourcing)的设计原则一致——状态通过追加事件来演进,而不是原地修改已有记录,这样每一段历史都可以被独立缓存和回放。
三、当上下文装不下时:压缩与隔离的策略选择
即使KV缓存优化到位,Agent的上下文仍然会随着任务推进不断膨胀,最终突破窗口上限。这时候需要两个补充策略:压缩和隔离。
压缩:有损的事后补救
Claude Code的src/query.ts实现了一个自愈的状态机,核心循环不是简单的“发请求→收响应”,而是包含了一个五层压缩管线。在调用API之前,依次执行五个压缩步骤:
- 工具结果预算截断:按
maxResultSizeChars限制单个工具输出的长度 - 历史Snip压缩:对历史消息进行裁剪
- 微压缩:将工具结果摘要化
- 上下文折叠:对特定区块进行折叠
- 自动压缩:超出阈值时触发,将对话历史替换为摘要
每一步的输出是下一步的输入,形成串行管道。Snip和Microcompact释放的Token数会传递给AutoCompact的阈值计算,避免重复压缩。
但压缩是有代价的。生产级Agent系统在压缩时最容易丢失的,不是细节本身,而是早期的架构决策、约束背后的推理以及失败的路径。大语言模型做摘要时,通常优先删除那些“看起来可以重新获取的信息”——但一个架构决策背后的推理过程,一旦被删除,Agent可能就丧失了理解“为什么不能这样做”的依据。
因此在生产级系统中,需要明确定义压缩时的保留优先级:
- 架构决策和关键约束:不得总结
- 修改文件列表和关键变更记录:完整保留
- 验证状态(通过/失败):必须保留
- 未解决的待办事项和回滚说明:必须保留
- 工具输出:可以删除,仅保留通过/失败结论
此外,UUID、哈希、IP地址、端口号、URL和文件名等标识符必须完全按原样保留——即使PR编号或提交哈希中的一个数字改变,也会直接导致后续工具调用失败。
隔离:优于压缩的事前排除
压缩是在信息已经进入上下文之后进行的补救。更直接的方法是从一开始就将大量中间信息排除在主上下文之外。这就是子智能体上下文隔离:主智能体将生成大量中间内容的任务——比如“读取大量文件”或“在代码库中进行广泛搜索”——委托给独立的子智能体。子智能体在自身上下文中完成探索,仅向主智能体返回简洁的总结。
用一个具体任务来对比两种方法的差异。任务是“在代码库中找到处理支付回调的函数”。
如果主智能体自己搜索,它可能会将几十个文件和数万Token的原始代码带入主上下文。一旦找到目标,大部分这些材料会作为永久噪声留在窗口中,之后必须通过压缩移除。
如果委托给搜索子智能体,主上下文仅获得两条消息:一个任务描述和一个结论——比如“函数是src/payment/callbacks.py中的handle_callback,还有另外两个调用点”。中间过程的数万Token与子智能体的上下文一起被丢弃。
这本质上是用隔离代替压缩:压缩是一种有损的事后补救措施,需要额外的LLM调用,而隔离从一开始就将噪声排除在主上下文之外,且不影响主智能体的KV缓存前缀。代价是子智能体看不到主智能体的完整上下文,因此任务描述必须自含且目标必须明确。
Java视角:压缩相当于Java中的GC(垃圾回收)——运行到一定阶段后扫描上下文,回收不再需要的对象。但GC有代价:Stop-the-World暂停(对应LLM调用压缩模型时的额外延迟),以及可能的误回收(对应摘要丢失关键信息)。隔离相当于微服务拆分——把重量级的探索任务放到独立的服务(子智能体)中运行,主服务只接收结构化的结果摘要,中间过程的内存占用和计算开销都被隔离在子服务内部。这就像微服务架构中,一个聚合服务不需要把所有下游服务的原始数据都加载到自己的内存中,只需要接收它们返回的DTO即可。
四、没有万能的方案:不同框架的上下文策略差异
不同Agent框架在上下文管理上的设计取舍,恰恰反映了不同场景对“什么信息最重要”的不同判断。
Claude Code的策略是激进压缩。它的五层压缩管线在每次API调用前都运行,目的是让单个Agent在极端长的任务中持续运行而不崩溃。这种设计适合需要连续执行数十步甚至上百步的复杂任务,但代价是可能丢失早期决策的上下文。
OpenAI Agents SDK的策略是持久化会话+摘要。它的Session对象提供了持久化记忆层,支持跨多次Agent运行的对话历史保持。OpenAIConversationsSession用于将记忆同步到Conversations API,MemorySession用于本地开发。SDK内置了两种上下文管理技术:修剪(trimming)——丢弃较早的轮次,保留最近N轮;压缩(compression)——将较早的内容摘要化。这种设计适合需要跨会话保持用户偏好的客服Agent或助手类应用,但每次新会话的冷启动成本较高。
Manus的策略是KV缓存优先+文件系统卸载。它的核心优化目标是让上下文前缀的字节序列保持稳定,从而最大化KV缓存命中率。同时,它把大量中间信息卸载到文件系统,只在上下文中保留文件路径和操作指针。这种设计适合需要频繁调用工具、且工具输出体积不稳定的任务型Agent。
Java视角:这三种策略的选择,本质上和Java中缓存策略的选择是同一类问题。Claude Code选择了“LRU淘汰+压缩”——优先保证活跃窗口的紧凑性。OpenAI Agents SDK选择了“持久化存储+按需加载”——优先保证跨会话状态的一致性。Manus选择了“稳定前缀+冷热分离”——优先保证热路径上的缓存命中。没有哪个策略是绝对正确的,取决于你的场景对延迟、成本、状态一致性的优先级排序。
五、写在最后:Context Engineering是Agent开发者的第一工作
回到本文开头引用的Anthropic的判断——“上下文工程是构建AI智能体的工程师的首要工作”。这并非夸张。
Prompt Engineering优化的是单次交互的质量,而Context Engineering决定的是Agent能否在长时间跨度上保持有效。当Agent运行到第50轮时,它看到的上下文是前49轮所有信息经过压缩、筛选、隔离后的结果。这个“经过工程化处理的状态”的质量,直接决定了Agent是继续沿着正确的方向推进,还是像本文引子中的场景一样——突然“忘记”了第3轮就确认的架构决策。
理解KV缓存的经济学约束、掌握压缩与隔离的策略权衡、根据不同场景选择合适的上下文管理方案——这些能力,正在成为Agent开发者和传统后端开发者之间最核心的技能分野。
Java视角的总结:Agent的上下文管理,本质上是一个有状态服务的状态管理问题。上下文窗口是有限的内存,KV缓存是热数据的快速访问层,压缩是GC,隔离是微服务拆分,文件系统是持久化存储。你在Java中做分布式系统状态管理时积累的经验——缓存策略、状态机设计、容错恢复、读写分离——在Agent上下文工程中几乎可以一一映射。唯一的区别是,这里的“内存”是Transformer的注意力窗口,“GC”是LLM的摘要能力,“序列化”是Token的编码方式。
下一篇进入1.3 Agent的工具调用,从OpenClaw的“两只手”双通道设计出发,拆解MCP协议如何把工具调用从“每个Agent自己造轮子”变成“标准化服务接入”。
🍃 系列专栏导航
- 🔖 专栏导航 大模型学习
其他专栏衔接
- 🔖 《若依框架全攻略:从入门到项目实战》
- 🔖 《深入浅出Mybatis》
- 🔖 全面掌握MySQL工具
- 🔖 《深入浅出Maven》
- 🔖 《深入浅出Kafka》
- 🔖 《全面掌握Swagger:从入门到实战》
- 🔖 《Lombok:高效Java开发的秘密武器(完全解读)》
- 🍃 博客概览:《程序员技术成长导航,专栏汇总》
建议按系列顺序阅读,从基础到进阶逐步掌握核心能力,避免遗漏关键知识点~
全景导航博文系列
- 🔖《一口气学完fastJson》
- 🔖《一文搞懂PageHelper》
- 🔖《一文搞懂MyBatis》