前面三章——第4章的采集与预处理、第5章的智能检索、第6章的层次化记忆——解决的都是同一个问题:如何把信息高效地"放进"上下文。但一个被长期忽视的真相是:放进去只是第一步。更关键的问题是,放进去了之后呢?如果上下文不断膨胀、从不收缩,那么所有精心收集和组织的知识终将被淹没在噪声之中。本章直面上下文工程中最具挑战性的命题——不是越多越好,而是越精越好——系统阐述压缩、优化和缓存这三大使上下文保持"精瘦且有力"的技术支柱。
7.1 为什么压缩是必须的:成本、延迟与上下文腐烂
在讨论"怎么压缩"之前,必须先回答"为什么要费这个劲压缩"。答案是三重的,每一重都足以单独成立。
7.1.1 成本维度的不可持续性
2026年,一个典型的Agent交互场景可能是这样的:一个中等复杂度的编码Agent,在一个会话中可能消耗50K至200K的输入token——包括系统指令、工具定义、对话历史、检索结果、文件内容。以Claude Sonnet 4的定价(0.15至4,500至$18,000。而如果通过压缩将输入token减少50%,月成本直接减半。
但成本只是最浅层的理由。更深层的问题是延迟。
7.1.2 延迟维度的非线性惩罚
每增加1000个输入token,模型的prefill(预填充)阶段就多处理1000个token的注意力计算。而Transformer的注意力复杂度是O(n²)的——输入长度翻倍,注意力计算量翻四倍。这个"无声的延迟税"在长上下文场景下尤其显著:
- 1K token输入 → prefill延迟约0.1秒
- 100K token输入 → prefill延迟约2-5秒
- 1M token输入 → prefill延迟可达30-60秒
在一个ChatBot场景下,2秒的延迟可能是"有点慢但可以接受",而30秒的延迟意味着用户体验的彻底崩溃。上下文压缩在这里的意义不仅仅是"省钱",更是"让系统在人类耐心耗尽之前完成任务"。
7.1.3 上下文腐烂:ChromaDB 18个SOTA模型的定量判决
然而,成本可以承受,延迟可以优化——真正让压缩从"可选项"变成"必修课"的,是2025年7月ChromaDB研究团队发布的里程碑式报告《Context Rot: How Increasing Input Tokens Impacts LLM Performance》。
什么是上下文腐烂(Context Rot)? ChromaDB给出的定义是:LLM的性能并非随上下文长度均匀分布——模型对第10,000个token的处理并不可靠到与第100个token同等水平。 随着输入token的增加,模型性能以非均匀、常令人惊讶的方式退化。甚至在最极端的情况下,更多的上下文反而导致更差的回答——不是因为模型"忘记"了什么,而是因为它被无关信息"淹没"了。
实验设计与规模。 ChromaDB的研究共测试了18个SOTA模型——包括GPT-4.1系列、Claude 4系列(Sonnet和Opus)、Gemini 2.5系列(Pro和Flash)、Qwen3系列(8B、32B、72B)、Llama 4 70B、Llama 3.1 70B、Mistral Large 2、Yi 34B、Grok-2等——总共执行了194,480次LLM调用,拒绝了仅0.035%(69次)。每个模型在8个输入长度×11个needle位置上接受测试。这是迄今为止对长上下文性能退化最系统、最大规模的实证研究。
核心发现一:越长越差,且呈非线性加速。
所有18个模型的召回准确率都随输入token的增加而单调衰减——这已经不是"某些模型"的问题,而是Transformer架构的固有属性:
| 输入长度 | 高相似度Needle准确率 | 低相似度Needle准确率 |
|---|---|---|
| 1K tokens | 约95% | 约82% |
| 32K tokens | 约88% | 约65% |
| 64K tokens | 约82% | 约55% |
| 128K+ tokens | 约70-75% | 约40-50% |
这里最值得警惕的是低相似度场景——现实世界的查询极少与知识库中的原文一模一样,用户在Agent交互中的提问往往是"用自己的话说"的,而非精确的关键词匹配。这意味着现实场景中的上下文腐烂效应比Needle-in-a-Haystack基准(以精确词汇匹配为核心)所展示的更严重。
核心发现二:干扰项放大腐烂。
实验在Needle之外额外加入了1个或4个"干扰项"(语义相似但不包含答案的信息),结果令人不安:4个干扰项的存在,使模型在128K+输入下的准确率再降12-18%。而且不同模型的反应模式不同——Claude家族在不确定时倾向于弃权(保守策略),GPT家族在不确定时倾向于"自信地给出错误答案"(幻觉策略)。这意味着在真实的多文档检索场景中——几乎必然存在多个语义相似但非答案的文档——上下文腐烂效应会比实验室条件下严重得多。
核心发现三:反直觉的结构性发现。
实验对比了两种Haystack的组织方式——一种是原始文章(有逻辑连贯性),一种是随机打乱的句子(无逻辑连贯性)。结果出乎意料:打乱句子的Haystack反而让模型表现更好,准确率提升了5-8%。原因可能是:连贯的文本让Needle更容易"融入背景",模型对它的注意力权重降低;而打乱的文本让Needle作为"异常点"更突出。这揭示了一个深层问题:模型对"连贯文本中的关键信息"的检测能力,在长上下文下是系统性地不足的。
核心发现四:LongMemEval的最终判决。
LongMemEval是一个对话问答基准,测试模型在完整113K token对话历史中定位和提取关键信息的能力。所有模型在完整历史设置下的表现比"只保留相关部分的聚焦设置"下降至少25%。其中Claude模型下降幅度高达35%。这意味着:即使给你一个百万token的上下文窗口,直接塞进去"越多越好"的信息也在系统性损害模型的核心能力。
7.1.4 压缩的不可逃避性
ChromaDB的研究给出了一个清晰的结论:上下文腐烂不是一个可以通过"买更大的上下文窗口"来解决的问题——1M token窗口只是让你能塞更多信息进去,但那些信息中的很大一部分会互相干扰、彼此淹没。唯一的解方是:系统地移除不必要的信息,让剩下的每一个token都携带尽可能高的信号。这就是上下文压缩的根本使命。
7.2 Drew Breunig五大上下文失效模式
在ChromaDB从定量角度揭示了"上下文越长越差"的同时,Overture Maps Foundation策略师、O'Reilly《Context Engineering Handbook》作者Drew Breunig从定性角度完成了一项同样重要的工作——他在2025年6月发表的两篇博文《How Long Contexts Fail》和《How to Fix Your Context》中,系统归纳了五大上下文失效模式,为上下文工程提供了一个诊断框架。
7.2.1 上下文中毒(Context Poisoning)
定义: 幻觉或错误信息进入上下文后,被模型反复引用和放大,最终围绕一个错误的前提进行推理——像个"谣言"一样在上下文中传播。
经典案例: Google DeepMind团队在Gemini 2.5的Pokemon实验中发现了一个令人警醒的现象——Agent如果在游戏过程中产生了一个幻觉(例如错误地认为某个位置有稀有道具),这个幻觉会被写入Agent的"目标摘要"中。此后,Agent会发展出荒谬的策略并重复行为去追逐这个不可能实现的目标。即使后来Agent发现了矛盾的信息,最初的"中毒"信息已经在上下文中根深蒂固,需要大量的交互才能"解毒"。
DeepMind技术报告的原文精准地描述了这一现象:"上下文中毒是一种特别恶劣的失败形式……上下文的多个部分(目标、摘要)被关于游戏状态的错误信息'污染',而纠正这种污染往往需要非常长的时间。"
现实影响: 在企业Agent场景中,上下文中毒的后果更加危险。一个被错误信息"中毒"的技术支持Agent可能围绕一个错误的原因("用户的数据库崩溃了")进行一长串的诊断和决策——而实际上用户的问题只是一个简单的权限配置错误。这不仅是效率问题,更是可信度问题。
诊断信号:
- Agent的输出中出现越来越多的矛盾信息
- Agent执着于一个在早期对话中出现的、但可能不正确的前提
- 用户开始质疑"你刚才说的和之前不一样"
7.2.2 上下文分心(Context Distraction)
定义: 过长的上下文导致模型过度依赖上下文中的零碎信息,而忽略了自身训练时获得的结构化知识和判断力。通俗地说,模型变得"只见树木,不见森林"。
数据证据:
- Google Gemini团队发现,上下文超过100K token时,模型开始出现显著的"模式锁定"行为——倾向于重复历史中的行为模式,而非综合全局信息制定新计划。
- Databricks的独立研究发现,Llama 3.1 405B在约32K token处正确率就开始下降,更小的模型下降点更早。
- Meta在Galactic基准测试中的发现:上下文长度达到128K后,模型开始系统性地优先选择上下文中出现过的答案——即使那个答案是错误的。这种"上下文偏见"在事实性问答任务上尤其明显。
现实影响: 上下文分心最阴险的地方在于:模型看起来"没问题"——它仍然在输出流畅、自信的文本——但它已经在系统性地忽视自己的推理能力,转而依赖上下文中的碎片。在代码审查Agent中,这可能表现为Agent因为看了太多行代码而忽略了明显的逻辑错误;在医疗Agent中,这可能表现为Agent因为读了太多病历而忽略了一个关键的诊断线索。
7.2.3 上下文混淆(Context Confusion)
定义: 上下文中的无关或冗余信息(尤其是过多的工具定义和文档片段)相互干扰,导致模型调用不该调用的工具、读取不该读取的文件、或者将不同文档之间的信息张冠李戴。
数据证据:
- DeepSeek-v3团队的内部测试发现:工具数量超过30个时,工具描述开始重叠混淆;超过100个时,模型在测试中几乎必然失败。
- Berkeley Function-Calling Leaderboard的系统性测试显示:所有模型在提供多于1个工具时性能都有不同程度的下降——没有哪个模型能免疫。
- 论文《Less is More》中的精辟案例:Llama 3.1 8B在46个工具时调用失败,当工具数量减少到19个时(通过删除与该任务无关的工具),同一模型在同一任务上成功了。
现实影响: 上下文混淆是一种"杀死于无形"的失效——它不像中毒那样有明显的错误传播,也不像分心那样有研究基准可以量化,但它让Agent的行为变得不可预测。用户问的是A,Agent调用了B工具,返回了C信息,然后基于C给出了一个似是而非的回答——整个过程看起来"合理",但实际上从一开始就走错了方向。
7.2.4 上下文冲突(Context Clash)
定义: 上下文中的新旧信息产生矛盾——旧的工具结果说数据库是正常的,新的错误日志显示数据库连接超时;旧的用户偏好说"我不喜欢长篇回答",但当前任务明显需要详细的技术说明——模型面对这些矛盾时,无法确定应该相信哪一方。
数据证据: Microsoft Research与Salesforce的联合研究发现,将一个benchmark中的关键信息分散到多轮对话中(模拟真实的多步Agent场景)后,模型性能平均下降39%。更令人震惊的是OpenAI o3的表现:从原始的98.1分直降至64.1分。研究者分析发现,核心原因是模型在信息不完整时就过早尝试回答,错误的中间答案留在上下文中,干扰了最终答案的推理。
现实影响: 上下文冲突在"长期运行"的Agent中尤其常见——一个跨越数百次交互的Agent可能积累了来自不同时间段的、相互冲突的信息。用户两个月前说"我们用Python 3.10",但最近的项目实际上已经升级到了Python 3.12;如果不主动管理这种冲突,Agent会基于过时的假设进行推理。
7.2.5 性能与成本的隐性衰减
定义: 这不是单一的"事件",而是一种系统性的退化过程——随着上下文的增长,Agent的每次响应都变得更慢、更贵、更不准确。这三个维度的退化同时发生且互相加剧。
量化视角:
上下文翻倍 →
延迟:prefill时间约增加 3-4x(O(n²)注意力)
成本:输入token费用翻倍
质量:根据ChromaDB数据,准确率下降 10-20%
这三条曲线不是平行的——它们在最坏的情况下是相互强化的。更慢的响应意味着更长的对话(用户需要更多轮次来纠正或澄清),更长的对话意味着更多的上下文,更多的上下文意味着更慢的响应……这是一个螺旋下降循环。
Breunig的六大解决方案:
针对这五种失效模式,Breunig在后续文章中给出了六条解决方案:
- RAG(选择性注入):正如第5章所述,不把整个知识库塞进上下文,而是按需检索相关片段。
- 工具选配(Tool Loadout):只将当前任务相关的工具定义加入上下文,而非全量加载。"RAG MCP"论文显示,工具选择准确率可因此提升3倍。
- 上下文隔离(Context Quarantine):在不同线程/子Agent中隔离不同任务的上下文,防止交叉污染——这是第9章将要深入讨论的多智能体上下文管理的前奏。
- 上下文修剪(Context Pruning):使用自动化工具(如Provence,1.75GB的工具包)去除上下文中95%以上的无关内容,只保留核心段落。
- 上下文摘要(Context Summarization):将累积的上下文提炼为精简的摘要——这正是7.3节要展开的内容。
- 上下文卸载(Context Offloading):通过文件系统和工具将信息存储到上下文窗口之外——这是7.4.2节的主题。
7.3 2026年两种主流压缩范式
面对上下文腐烂和五大失效模式,工程界形成了两种互补的压缩范式:紧凑化(可逆压缩)和摘要化(不可逆压缩)。它们是上下文的"减肥手术"——前者是抽脂(移除多余的,保留原样),后者是塑形(改变结构,提炼精华)。
7.3.1 紧凑化:可逆压缩——保留引用,零信息丢失
核心思想: 紧凑化不是"忘记"信息,而是将信息"折叠"起来——用更少的token表达同样的信息,同时保留必要时"展开"的能力。
为什么紧凑化必须是"可逆的"?
想象一个Agent在处理一个跨越多天的复杂任务。在第一天,Agent遇到了一个bug,花了30轮交互来诊断和修复。到了第三天,这个bug的完整推理过程可能已经不重要了——但为什么选择了这个修复方案(而非其他方案)、这个修复是否引入了已知的副作用、相关的代码审查意见——这些都可能在未来成为关键信息。
如果只是生成一句摘要"第一天修复了一个bug",第三天当Agent发现这个修复导致了新的问题时,它无法回溯到原始的推理过程来理解为什么当初做了这个决定。紧凑化的可逆性保证:任何时候,只要有需要,都可以通过"展开"引用恢复到原始信息的完整精度。
紧凑化的五种实用技术:
技术一:工具结果的结构化缩减。
当一个工具的执行结果长达数千token(例如一次数据库查询返回了2000行数据),Agent通常只需要其中的关键信息和"继续深入"的入口。紧凑化不是说"结果太长,我删掉",而是说"结果太长,我保留关键摘要和查询引用,需要时可以重新执行"。
# 原始工具结果 (8,000 tokens)
┌─────────────────────────────────────────┐
│ SELECT * FROM orders WHERE ... │
│ [2000 rows × 15 columns] │
│ ... │
└─────────────────────────────────────────┘
# 紧凑化后 (约200 tokens)
┌─────────────────────────────────────────┐
│ [查询结果摘要] │
│ 查询: SELECT * FROM orders WHERE date │
│ > '2025-01-01' │
│ 返回: 2,000行, 15列 │
│ 关键发现: │
│ - 总订单金额: ¥3,247,890 │
│ - 平均订单金额: ¥1,623 │
│ - TOP 5 客户占据 34% 的订单量 │
│ [引用: query_execution_id=QK-8873] │
│ → 需要展开时重新执行查询或查看完整结果 │
└─────────────────────────────────────────┘
当一个工具结果被紧凑化后,Agent仍然知道"这个查询是怎么做的、返回了什么关键结果"——足以支持继续推理。如果需要精确的某一行数据,Agent可以通过引用ID重新执行查询或展开完整结果。
技术二:文件系统引用的路径化。
这与第6章讨论的"上下文卸载"是同一策略的不同侧面。对于文档内容(代码文件、配置、日志),紧凑化的做法是在上下文中保留路径引用+简短摘要,而非完整文件内容。Agent知道"哪些文件是相关的"和"每个文件大概包含什么",需要时可随时读取。
# 紧凑化的文件上下文
def compact_file_context(file_paths: list[str]) -> str:
"""将文件列表紧凑化为路径引用 + 摘要"""
lines = []
for path in file_paths:
size = os.path.getsize(path)
# 仅保留元数据,不读取内容
summary = infer_summary_from_path_and_size(path, size)
lines.append(f"- `{path}` ({format_size(size)}): {summary}")
return "\n".join(lines)
# 紧凑化前的上下文 (~50,000 tokens):
# "src/auth/login.py 文件内容:\n{class LoginView: ...}\n...\n...\n"
#
# 紧凑化后的上下文 (~500 tokens):
# - `src/auth/login.py` (12 KB): 登录视图,处理用户名/密码认证
# - `src/auth/middleware.py` (8 KB): 会话验证中间件
# - `src/api/endpoints.py` (45 KB): REST API端点定义,较大
# - `tests/test_auth.py` (22 KB): 认证模块的集成测试
技术三:对话轮次的层级折叠。
对于多轮对话,紧凑化的做法是:保留最近N轮的完整内容(逐字逐句),对于更早的内容,用一层折叠摘要替代("在第3-5轮中,用户和Agent讨论了数据库连接池的配置策略")。如果需要精确还原早期对话,Agent可以从持久化存储中展开折叠的摘要。
技术四:中间推理步骤的外化存储。
Agent在多步推理中产生的中间结果(如"我先验证了A假设,发现不成立,然后转向B假设"),如果完整保留在上下文中会迅速膨胀。紧凑化的做法是:将完整的中间推理存储到文件(如REASONING_LOG.md),在上下文中只保留当前正在进行的推理步骤和已得出结论的引用。
技术五:代码块的结构化折叠。
对于代码生成Agent,经常需要在上下文中展示大段代码。紧凑化的做法是:保留关键代码段(当前正在修改的文件、被引用的函数签名),对于"仅供参考"的代码块,只保留文件路径和行号引用。
7.3.2 摘要化:不可逆压缩——用LLM提炼精华
核心思想: 当紧凑化的收益递减时——即上下文中的"可折叠"信息已经被折叠到极限,剩下的"不可折叠但也不完全必要"的信息需要被更激进地处理——此时摘要化登场。它不像紧凑化那样保留恢复能力,但可以更大幅度地减少token。
摘要化的两个黄金原则:
原则一:只在紧凑化之后使用。 如果一个信息可以通过紧凑化的方式保留引用,就不要生成摘要。摘要化是"最后的手段",而非"首选方案"。因为摘要一旦生成,原始信息的内容就永久丢失了(除非另存了完整记录,但那又回到了紧凑化的思路)。
原则二:摘要的粒度和焦点由"未来需求"决定。 生成摘要时不要问"这段对话的核心是什么?"而要问"未来Agent需要从这段对话中得到什么信息?"——这两个问题产生的摘要有本质区别。前者生成的是叙事性摘要("用户和Agent讨论了数据库优化"),后者生成的是行动导向的摘要("用户决定采用连接池方案,Pool Size=20,Timeout=30s,需要在下一次部署时配置")。
摘要化的三种实用策略:
策略一:滚动摘要(Rolling Summaries)。
这是Anthropic推荐并在Claude Code中验证过的方案。核心做法是:保留最近N轮交互的完整内容(精确的消息历史),对N轮之前的所有内容生成一个"滚动摘要"——一个不断更新、不断压缩的叙事性摘要,每当有新内容需要合入时重新生成。
# 滚动摘要的更新逻辑
async def update_rolling_summary(
existing_summary: str,
new_messages: list[dict],
max_summary_tokens: int = 1000
) -> str:
"""用新消息更新滚动摘要"""
if not existing_summary:
return await generate_summary(new_messages)
updated = await llm.generate(f"""
你正在维护一个Agent会话的滚动摘要。
下面是当前的摘要和新的消息。
当前摘要:
{existing_summary}
新消息:
{format_messages(new_messages)}
请生成一个更新后的摘要,重点保留:
1. 已做出的关键决策和决策理由
2. 已完成的任务和结果
3. 未解决的bug和待办事项
4. 需要跨会话保留的上下文(路径、配置、状态等)
5. 重要的技术发现或经验教训
摘要应控制在 {max_summary_tokens} tokens 以内。
如果现有摘要中的某些信息已经过时(被新信息替代),可以删除。
""")
return updated.strip()
滚动摘要的关键参数是"N"——保留多少轮完整历史。N越大,Agent能看到越详细的近期对话,但token消耗也越大。2026年的经验值是N=10至20,取决于任务的复杂度和每轮对话的平均长度。
策略二:聚焦摘要(Focused Summaries)。
与滚动摘要的"全量压缩"不同,聚焦摘要只关注特定信息类型。在Anthropic的实践中,这意味着生成不同类型的摘要来服务不同的检索需求:
- 决策摘要:只提取决策和决策理由
- 状态摘要:只记录任务的当前状态(进度、阻塞、依赖)
- 知识摘要:只提取新的技术发现和经验教训
- 错误摘要:只记录遇到的错误和修复方案
每种聚焦摘要都是轻量的(200-500 tokens),这让Agent在后续会话中能够精准检索——如果Agent想知道"上次那个数据库连接错误是怎么修的",它不需要在滚动摘要中大海捞针,直接检索错误摘要即可。
策略三:条件触发摘要(Conditional Summarization)。
不是每个对话都需要被摘要化。条件触发摘要的核心思想是:只在上下文接近容量阈值时才触发摘要,平时尽可能保留完整的对话历史。
class ConditionalSummarizer:
def __init__(self, trigger_threshold: int = 0.85):
"""
trigger_threshold: 上下文窗口使用率,超过此值触发压缩
例如 0.85 表示使用率达到 85% 时触发
"""
self.threshold = trigger_threshold
self.keep_recent = 20 # 保留最近20条消息不变
def should_summarize(self, current_tokens: int,
max_tokens: int) -> bool:
return current_tokens / max_tokens > self.threshold
async def summarize_if_needed(self, messages: list,
model_context_limit: int):
current_tokens = count_tokens(messages)
if not self.should_summarize(current_tokens, model_context_limit):
return messages # 不需要摘要,原样返回
# 保留最近N条消息不动
recent = messages[-self.keep_recent:]
older = messages[:-self.keep_recent]
# 将更早的消息压缩为摘要
summary = await self.generate_summary(older)
# 返回:摘要 + 最近消息
return [
{"role": "system", "content": f"[历史摘要]\n{summary}"}
] + recent
这是LangChain的SummarizationMiddleware采用的设计模式——以GPT-4o Mini作为摘要模型(而非完整版GPT-4o),在上下文超过4000 tokens时触发,保留最近20条消息不变。
7.3.3 紧凑化与摘要化的互补决策树
需要从上下文中移除信息?
│
├── 这些信息"可能"在未来需要精确还原?
│ └── 是 → 紧凑化:保留引用,可逆
│
├── 这些信息"几乎不可能"在未来需要精确还原?
│ └── 是 → 摘要化:生成摘要,不可逆
│
├── 不太确定?
│ └── 紧凑化优先 → 如果紧凑化后仍然超过容量 → 摘要化
│
└── 关键变量:
├── 信息的"可重建性":原始信息是否可以通过工具/文件重新获取?
│ 高可重建性 → 摘要化更合适(丢了也可以找回来)
│ 低可重建性 → 紧凑化更合适(必须保留引用)
│
└── 信息的"可能复用频率":
高频复用 → 紧凑化(经常需要展开)
低频复用 → 摘要化(很少需要,摘要就够了)
7.4 上下文压缩的最佳实践
理论清楚之后,实践就是关键。本节聚焦三个在2026年产线中被反复验证的上下文压缩技术:Anthropic的Tool Result Clearing、上下文卸载、和滚动摘要的工程实践。
7.4.1 Anthropic的Tool Result Clearing:最安全、最轻量的压缩
在Anthropic的上下文工程实践体系中,Tool Result Clearing被明确标注为"最安全、最轻量的Compaction形式"。它的核心逻辑非常直觉化:一旦工具在消息历史的深处被调用过,且Agent已经消费(读取并推理过)了该工具的结果,为什么还需要在上下文中保留原始的工具返回数据?
工作机制:
消息历史(压缩前,~150K tokens):
┌─────────────────────────────────────────┐
│ [System] 你是一个代码审查Agent │
│ [User] 审查我的项目 │
│ [Assistant] => 调用 tool: list_files() │
│ [Tool Result] src/auth/... │ ← 7K tokens
│ [Tool Result] src/api/... │ ← 12K tokens
│ [Assistant] 我发现项目包含... │
│ [Assistant] => 调用 tool: read_file() │
│ [Tool Result] 完整文件内容... │ ← 35K tokens ← 已被Agent消费
│ [Assistant] 这个文件存在以下问题... │
│ ... │
└─────────────────────────────────────────┘
消息历史(压缩后,~95K tokens):
┌─────────────────────────────────────────┐
│ [System] 你是一个代码审查Agent │
│ [User] 审查我的项目 │
│ [Assistant] => 调用 tool: list_files() │
│ [Tool Result] src/auth/... │ ← 保留:最近使用
│ [Tool Result] src/api/... │ ← 保留:最近使用
│ [Assistant] 我发现项目包含... │
│ [Assistant] => 调用 tool: read_file() │
│ [Tool Result] [已清除,Agent已消费] │ ← 移除
│ [Assistant] 这个文件存在以下问题... │
│ ... │
└─────────────────────────────────────────┘
为什么这个策略是"安全的"? 因为Agent已经将工具结果的"信息"内化到了自己的推理中——它的分析文本已经包含了它对工具结果的理解和提炼。保留原始的工具返回数据提供的增量价值非常有限,但消耗的token成本非常大。
适用范围:
- 适用:一次性查询的结果(如数据库查询、文件读取、API调用)
- 部分适用:需要反复参考的结果(如配置文件、参考文档)——这种情况下应该用紧凑化而非直接清除
- 不适用:用户明确要求"保留完整信息"的场景、需要在后续进行精确引用或验证的场景
实现要点:
class ToolResultManager:
"""管理工具结果的自动清除"""
def __init__(self, max_recent_results: int = 3):
self.max_recent = max_recent_results
def clean_tool_results(self, messages: list) -> list:
"""清除已经被Agent消费过的旧工具结果"""
cleaned = []
tool_result_count = 0
recent_boundary = self._find_recent_boundary(messages)
for i, msg in enumerate(messages):
if msg["role"] == "tool":
# 判断该工具结果是否在"最近N个"范围内
tool_result_count += 1
if i > recent_boundary and tool_result_count > self.max_recent:
continue # 清除旧的结果
cleaned.append(msg)
return cleaned
def _find_recent_boundary(self, messages: list) -> int:
"""找到'最近'的边界——最近一次用户消息之后"""
for i in range(len(messages) - 1, -1, -1):
if messages[i]["role"] == "user":
return i
return max(0, len(messages) - 20)
7.4.2 上下文卸载:文件系统是"廉价硬盘"
核心思想: 如果说上下文窗口是Agent的"RAM"(昂贵、快速、容量有限),那么文件系统就是Agent的"硬盘"(廉价、相对慢速、容量巨大)。上下文卸载的核心思想是将"不需要在RAM中的信息"——完整的文件内容、长对话历史、未消费的工具结果——写到文件系统中,在上下文中只保留路径引用和简短摘要。
Anthropic的"think工具"——卸载的典范。
Anthropic在2025年底推出的Claude Think Tool是上下文卸载的代表性实践。它为模型提供了一个独立的空间(名为think的工具),让模型在推理过程中将"草稿"、"笔记"、"中间推理"写入这个空间,而不是堆积在主上下文窗口中。
关键设计:
- think工具的写入不污染主对话流——它存储在侧通道中,Agent可以在后续随时读取
- 它模拟的是人类在解决问题时的"草稿纸"——写下思路、列清单、做计算,但这些草稿不会出现在最终的报告里
- Anthropic的研究表明,think工具配合领域提示词可以在专业Agent基准测试中实现高达54%的性能提升
文件系统卸载的工程模式:
# 上下文卸载的简化实现
class ContextOffloader:
"""将上下文信息卸载到文件系统"""
def __init__(self, workspace_dir: str):
self.workspace = workspace_dir
async def offload_conversation_history(self,
old_messages: list,
session_id: str) -> dict:
"""将旧对话历史卸载到文件,返回引用"""
# 生成摘要(不可逆压缩)
summary = await self._summarize(old_messages)
# 将完整历史保存到文件(可逆——需要时可以展开)
history_file = f"{self.workspace}/.context/{session_id}/history.json"
os.makedirs(os.path.dirname(history_file), exist_ok=True)
with open(history_file, 'w') as f:
json.dump(old_messages, f, ensure_ascii=False, indent=2)
# 返回仅包含摘要和文件引用的上下文片段(~300 tokens)
return {
"summary": summary,
"full_history_ref": history_file,
"message_count": len(old_messages),
"date_range": f"{old_messages[0]['timestamp']} - {old_messages[-1]['timestamp']}"
}
async def offload_large_file_content(self,
file_path: str) -> dict:
"""将大文件的完整内容卸载,只保留元数据和摘要"""
size = os.path.getsize(file_path)
if size < 50_000: # 50KB以下,不太大,保留在上下文
return {"content": read_file(file_path), "path": file_path}
# 50KB以上,卸载到文件系统
summary = await self._summarize_file(file_path)
return {
"path": file_path,
"size": format_size(size),
"summary": summary,
"note": "[文件较大,内容已卸载。可使用 read_file 工具展开完整内容]"
}
上下文卸载的设计原则:
- 引用必须精确可追踪:被卸载的信息必须有明确的文件路径或引用ID,Agent随时可以恢复
- 摘要必须足以为决策提供依据:Agent在只看摘要的情况下,应该能够判断是否需要展开完整信息
- 卸载 ≠ 丢弃:卸载的信息必须持久化存储,不能是临时的
- 卸载的触发应该是自动的:不应该由用户手动触发,"上下文接近溢出"就是触发信号
7.4.3 滚动摘要:保留最近N轮,更早的压缩为摘要
滚动摘要是7.3.2节中讨论的"摘要化"策略的具体实现形式,但它本身已经发展出了一套完整的工程最佳实践。
滚动摘要的黄金参数配置:
| 参数 | 推荐值 | 理由 |
|---|---|---|
| 保留完整历史的最近N轮 | 10-20轮 | 太少的轮数导致Agent"健忘",太多浪费token |
| 摘要的最大token数 | 500-1000 tokens | 摘要是"辅助记忆",不是"百科全书" |
| 摘要更新频率 | 每增加5-10轮新消息后 | 不需要每轮都更新摘要,批量更新更高效 |
| 摘要模型 | 使用更小/更便宜的模型 | GPT-4o Mini、Claude Sonnet、Qwen 7B等 |
滚动摘要的内容优先级:
不是所有信息都应该进入滚动摘要。以下是经过验证的优先级排序:
- 最高优先级:决策记录。任何"做了决定"的信息——选择了什么方案、为什么、有什么约束。
- 高优先级:未完成事项。任何"还没做完"、"需要follow up"的信息。
- 中优先级:关键发现。技术洞察、错误模式、被验证有效的解决方案。
- 低优先级:上下文背景。项目结构、当前状态、环境配置。
- 不保留:可重建信息。可以通过重新执行工具或查询获得的信息——这些信息的摘要价值极低。
7.5 KV缓存优化
前面的三节讨论的都是"上下文内"的优化——如何让上下文更小、更精、更有信号。但上下文的成本不仅取决于"多少token被发送给模型",还取决于"有多少token被重复计算"。KV缓存优化要解决的就是这个"重复计算"的问题——让模型在处理"基本相同的前缀"时,不需要每次从头计算注意力。
7.5.1 缓存命中率:优化Agent成本和延迟的头号指标
Manus团队的一位工程师在一次技术分享中直言:"Cache hit rate is the single most important metric for optimizing agent costs and latency."(缓存命中率是优化Agent成本和延迟的最重要指标,没有之一。)
这句话不夸张。来看一个真实的成本分析:
场景:一个Agent会话,System Prompt + 工具定义 = 10K tokens
无缓存:
每次推理都从第零个token开始计算注意力 → 每次推理都需要处理这10K + 新增的N个token
10K tokens × $3.00/MTok (Claude Sonnet 4 输入价格) = $0.03/次
1000次推理/天 × 30天 = $900/月(仅前缀部分)
有缓存(92%命中率,Claude Code的真实数据):
10K tokens × 92% 命中 × $0.30/MTok (缓存读取价格)
+ 10K tokens × 8% 未命中 × $3.00/MTok
+ 写入成本(首次建立缓存)
= 约 $170/月(仅前缀部分)
节省:$900 → $170 = 81% 成本降低
这是Claude Code的真实数据:92%缓存命中率 → 81%成本节省。这不仅仅是省钱——更低的延迟(跳过10K token的prefill计算)带来了更好的用户体验。
缓存命中率的工程定义:
缓存命中率 = cache_read_input_tokens / (cache_read_input_tokens + cache_creation_input_tokens)
如果这个值低于70%,说明缓存策略存在可修复的问题——可能是前缀不够稳定、可能是工具定义在频繁变化、可能是模型切换导致了缓存失效。
7.5.2 稳定前缀策略:不要在System Prompt开头放时间戳
KV缓存的匹配机制是逐字节的精确哈希匹配。这意味着:缓存只对"从第一个token开始,一直到某个断点为止,完全逐字节相同的输入"生效。任何一点差异——哪怕是一个空格——都会导致缓存失效。
最常见的缓存杀手:时间戳。
# ❌ 缓存杀手——每次推理的前缀都不同
system_prompt = f"""
当前时间:{datetime.now().isoformat()}
你是一个代码助手...
"""
# ✅ 缓存友好——时间戳放在结尾或使用相对时间
system_prompt = """
你是一个代码助手...
"""
# 动态时间信息追加在消息列表末尾,而不是前缀
messages = [
{"role": "system", "content": system_prompt}, # ← 永远不变 → 缓存命中
{"role": "user", "content": f"当前时间是 {datetime.now().isoformat()}。请帮我..."}
]
稳定前缀的工程原则:
- System Prompt固定不变。角色定义、行为规则、工具描述——这些是整个会话生命周期甚至跨会话都不变的内容,是缓存的"锚点"。
- 用户ID、会话ID、时间戳等动态信息不要出现在前缀中。如果必须包含这类信息,放在第一条用户消息(动态后缀)中。
- 工具定义(JSON schema)保持稳定。序列化时开启
sort_keys=True确保键顺序一致。 - 跨会话复用同一个System Prompt。如果每次新会话都修改System Prompt的开头几个词,缓存就会完全失效。
7.5.3 追加式修改策略:只追加不修改已有消息
KV缓存的另一个关键特性是:可以增量延续。如果前10K token已经缓存了,而你在末尾追加了1K token,那么缓存的10K可以继续命中,只有新增的1K需要重新计算。
这意味着:在Agent的多轮交互中,绝对不要修改已经发送过的消息。
# ❌ 破坏缓存——修改历史消息
messages[3]["content"] = "更新后的内容" # 缓存从第3条消息开始失效
# ✅ 保护缓存——只在末尾追加
messages.append({"role": "assistant", "content": response})
messages.append({"role": "user", "content": new_query})
Append-only不仅保护了缓存,也维护了对话历史的完整性——Agent可以回溯完整的推理链,不会因为历史被修改而产生上下文冲突(7.2.4节)。
7.5.4 确定性序列化:JSON key顺序固定,保护缓存命中
即使两条消息的语义完全相同,如果它们在序列化成文本后的字节表示不同,缓存仍然不会命中。这在工具定义的JSON序列化中尤其常见——Python的json.dumps()在不同调用中可能产生不同的键顺序。
修复方案:
import json
# ❌ 不确定的序列化——键顺序随机,缓存可能不命中
tool_schema_str = json.dumps(tool_schema)
# ✅ 确定性的序列化——键顺序固定,缓存稳定命中
tool_schema_str = json.dumps(tool_schema, sort_keys=True, ensure_ascii=False)
三条硬约束:
- JSON序列化必须开启
sort_keys=True - 不在会话中途修改工具定义(加一个参数→整个缓存前缀失效)
- 不切换模型版本(缓存是模型绑定的——Claude Sonnet 4和Claude Opus 4的KV缓存不能共享)
7.5.5 "Mask Don't Remove":工具定义保持稳定,通过API层限制调用
这是上下文混淆(7.2.3节)与缓存优化之间的交汇点。我们面临一对矛盾:
- 为了避免上下文混淆,应该只给模型提供当前任务相关的工具定义(减少噪音)
- 为了保护KV缓存,工具定义必须在整个会话中保持不变(稳定前缀)
"Mask Don't Remove"就是解决这个矛盾的方案:
# "Mask Don't Remove" 模式
ALL_TOOLS = [
tool_search, tool_read, tool_write, tool_execute,
tool_database, tool_api_call, tool_email, ...
] # 所有工具定义 — 永不改变,缓存锚点
# 每次推理时,通过 API 参数限制可用工具,但不修改工具列表本身
response = client.messages.create(
model="claude-sonnet-4-20250514",
system=SYSTEM_PROMPT, # 不变 → 缓存命中
tools=ALL_TOOLS, # 不变 → 缓存命中
tool_choice={"type": "auto"}, # 自动选择
# 通过 allowed_tools 做硬约束(API层面),不改变 tools 定义
# 注:Claude API 目前使用 tool_choice 的 "any" + "tool" 子类型限制
# 或通过 system prompt 软约束;OpenAI 有原生 allowed_tools 参数
messages=messages, # append-only → 增量缓存
)
在OpenAI的实现中,allowed_tools是一个原生参数——工具定义保持不变(缓存命中),但模型在推理时被API层面硬性禁止调用不在allowed_tools列表中的工具:
# OpenAI 的 "Mask Don't Remove" 实现
response = client.responses.create(
model="gpt-4.1",
tools=ALL_TOOLS, # 不变 → 缓存命中
allowed_tools=["search", "read"], # 硬约束:只能用这两个
input=messages,
)
三层约束强度对比:
| 约束方式 | 强度 | 缓存影响 | 适用场景 |
|---|---|---|---|
| Prompt软约束("请只使用A和B工具") | 弱 | 无 | 简单任务,容错率高 |
| allowed_tools参数 | 硬约束 | 无 | OpenAI等商业API |
| 物理移除工具定义 | 硬约束 | 高 | 不推荐——除非不考虑缓存 |
7.5.6 OpenAI Prompt Caching与Anthropic Prompt Caching对比
核心差异一览:
| 特性 | OpenAI | Anthropic (Claude) |
|---|---|---|
| 触发方式 | 全自动,零配置 | 自动 + 显式断点(最多4个) |
| 写入成本 | 免费 | 1.25x 基础价格(5min TTL) |
| 读取折扣 | 50%-90%(按模型) | 90% 折扣(0.1x基础价格) |
| 默认TTL | 5-10分钟 | 5分钟(每次命中刷新) |
| 最小缓存长度 | 1024 tokens | 1024 tokens |
| 缓存粒度 | 128 tokens/单元 | Block级别 |
| 断点控制 | 无(全自动) | 最多4个显式断点 |
OpenAI的优势:零配置、零写入成本。 开发者不需要做任何额外工作,也不需要关心缓存策略——API层面自动处理。对于小型Agent和快速原型开发,这种"无感知缓存"体验非常好。而且OpenAI还提供了prompt_cache_key参数——允许开发者手动指定缓存的"分组键",将同一端点的不同场景(如不同租户使用同一个System Prompt)导向同一个缓存空间,提升命中率。在OpenAI的公开案例中,prompt_cache_key可以将缓存命中率从60%提升到87%。
Anthropic的优势:细粒度控制、更高的读取折扣。 虽然Anthropic对缓存写入收费(1.25x基础价格),但读取折扣更高(90% vs OpenAI的50%-90%)。更重要的是,Anthropic允许开发者设置最多4个显式缓存断点——可以精确指定"缓存前N条消息""缓存System Prompt"等。对于高频使用和长对话场景,这种精确控制可以显著提升缓存命中率。
Anthropic的"system_and_3"黄金策略:
这是社区总结的最优断点配置:
messages = [
# 断点1:System Prompt + 全部工具定义 ← 跨会话稳定
{"role": "system", "content": SYSTEM_PROMPT},
# ... 对话历史 ...
# 断点2:倒数第3条非system消息
{"role": "user", "content": "..."},
# 断点3:倒数第2条
{"role": "assistant", "content": "..."},
# 断点4:最后一条
{"role": "user", "content": "..."},
]
# 设置 cache_control 断点
messages[-3]["cache_control"] = {"type": "ephemeral"}
messages[-2]["cache_control"] = {"type": "ephemeral"}
messages[-1]["cache_control"] = {"type": "ephemeral"}
关键陷阱:
使用Anthropic Cache时有一个非常容易中招的坑:当使用OpenAI兼容端点(/v1/chat/completions)访问Claude模型时,cache_control参数会被静默丢弃——不报错、不警告、缓存不生效。必须使用Anthropic原生端点(/v1/messages)才能激活显式缓存控制。
7.6 动态上下文窗口:根据任务复杂度自动调整上下文大小
前面五节讨论的都是"被动"优化——在上下文已经形成之后,通过压缩、卸载、缓存来降低成本和延迟。但最理想的策略是在上下文"形成之前"就做出判断:这个任务需要多少上下文? 动态上下文窗口回答的就是这个问题。
7.6.1 问题的重新定义
传统思维:上下文窗口 = 模型的最大支持长度。有多少就用多少。
现代思维:上下文窗口 = 当前任务恰好需要的长度。多一分浪费,少一分不够。
这是一个从"固定分配"到"按需调度"的范式转变。Karpathy的经典类比在这里再次适用:LLM像CPU,上下文窗口像RAM,上下文工程就是决定哪些信息放入RAM的"调度器"。动态上下文窗口就是调度器的"内存管理单元"——不是给每个进程分配相同的内存,而是根据进程的需求动态分配。
7.6.2 动态上下文窗口的三种实现模式
模式一:渐进披露(Progressive Disclosure)。
这是7.5.5节"Mask Don't Remove"策略的逻辑延伸——不仅是工具定义可以"按需披露",上下文中的所有信息都可以分层加载。
Layer 1: 发现层(Discovery)—— ~200 tokens
├── 轻量级元数据
├── 知识库索引(可用的文档/数据源列表)
└── 工具目录(名称 + 一句话描述)
Layer 2: 激活层(Activation)—— ~500 tokens
├── 被选中工具的完整指令和参数
└── 被选中数据源的上下文片段
Layer 3: 执行层(Execution)—— ~1,500 tokens
├── Few-shot 示例
├── 参考文档全文
└── 历史相关案例
Claude Code的Skills系统是渐进披露的典范:Skills以文件形式存在.claude/skills/目录中,Agent通过ls发现可用Skill列表(仅名称,~50 tokens),只有在任务需要时才通过cat读取完整指令(500-1500 tokens)。这意味着一个拥有50个Skills的Agent,它的上下文并不是50×1500=75000 tokens起步,而是从200 tokens起步,按需增长。
模式二:Manus的层次化行动空间。
Manus团队在设计Agent的工具系统时,提出了一个三层行动空间模型:
- Layer 1:核心原子工具(~20个)。始终在上下文中。这些是"任何Agent任务都需要的"基本工具——文件操作、搜索、代码执行。数量固定且精简——约20个——确保不会触发上下文混淆(7.2.3节中提到的30个工具阈值)。
- Layer 2:沙箱工具。 通过bash CLI暴露,不占用工具定义token。例如,数据库操作、网络请求、数据分析——这些通过一个统一的
execute_command工具来间接调用,而非每个都有独立的工具定义。 - Layer 3:复杂逻辑通过代码实现。 Agent不是调用一个"分析数据"的工具,而是自己编写Python脚本来分析数据,然后通过
execute_command执行。这大幅减少了工具定义的数量,同时保持了Agent的能力灵活性。
这种设计的结果是:无论Agent支持多少功能,上下文的Token开销保持固定——不会因功能的扩展而线性膨胀。
模式三:基于任务复杂度的上下文预算分配。
一个更激进的方案是:在任务开始时,让系统评估任务的复杂度,然后预算分配上下文token。简单任务(查询天气、翻译句子)分配少量上下文预算;复杂任务(代码重构、多文档分析)分配更多预算。
class DynamicContextAllocator:
"""基于任务复杂度的上下文预算分配"""
def __init__(self, model_limit: int = 200_000):
self.model_limit = model_limit
# 复杂度→预算映射
self.budgets = {
"simple": 20_000, # 简单问答、翻译
"moderate": 60_000, # 代码审查、单文档分析
"complex": 120_000, # 多文档分析、代码重构
"multi_step": 180_000 # 长周期多步Agent任务
}
async def allocate_budget(self, user_query: str) -> int:
"""根据用户查询评估任务复杂度并分配预算"""
complexity = await self._assess_complexity(user_query)
return self.budgets.get(complexity, self.model_limit)
async def _assess_complexity(self, query: str) -> str:
"""快速评估任务复杂度"""
result = await self.fast_llm.generate(f"""
评估以下任务的复杂度,只输出一个词:simple、moderate、complex 或 multi_step。
评估维度:
- simple: 单步骤、不需要外部知识、不需要工具调用
- moderate: 需要少量检索或单工具调用
- complex: 需要多步推理、多工具调用、或多文档分析
- multi_step: 长周期任务,可能需要多次迭代和中间状态保存
用户查询:{query}
""")
return result.strip().lower()
模式四:LangChain的SummarizationMiddleware(自适应压缩)。
LangChain在2025年推出的SummarizationMiddleware是一个"监测→触发→压缩"的自动化循环。它在每次LLM调用后检查当前token数,如果超过阈值,自动对历史消息进行摘要压缩:
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
agent = create_agent(
model="gpt-4.1",
tools=[...],
middleware=[
SummarizationMiddleware(
model="gpt-4.1-mini", # 小模型做摘要,省钱
trigger={"tokens": 4000}, # 超过4000 tokens触发
keep={"messages": 20}, # 保留最近20条消息
),
],
)
这种模式本质上是7.4.3节滚动摘要的自动化版本——不需要开发者手动管理压缩时机,中间件自动监测和处理。但代价是增加了一次小模型调用(摘要生成)的延迟和成本。
7.6.3 动态上下文窗口的设计原则
原则一:按需增长,而非预先填满。 上下文的初始状态应该是最精简的——只包含系统指令和最基本的信息。Agent在推理过程中"发现"自己需要更多信息时,主动检索和加载。这不是"懒加载",而是"智能加载"——由Agent的能力驱动,而非由开发者的预设驱动。
原则二:预算意识是Agent的核心能力。 一个好的上下文工程系统应该让Agent对自己的"注意力预算"有清晰的认识。就像人类知道"我现在脑子有点乱,需要整理一下思路",Agent应该能够感知到"上下文已经很长了,信号开始衰减,我需要压缩"。
原则三:动态窗口不等于"无限弹性"。 动态分配不意味着可以无限增长。对于多步Agent任务,动态窗口策略必须与滚动摘要或上下文卸载(7.4节)配合使用——当窗口增长到一定阈值后,不是继续"动态"增长,而是"压缩"、"卸载"、"重置"。
本章小结
本章从"为什么必须压缩"开始,系统性地构建了上下文压缩与优化的完整工程方法论。
7.1节 给出了压缩的三重动机:成本(输入token的线性费用)、延迟(Transformer O(n²)注意力带来的prefill非线性惩罚)和上下文腐烂(ChromaDB对18个SOTA模型的194,480次调用实验给出的定量判决——所有模型的准确率都随上下文增长而单调衰减,低相似度场景下衰减幅度可达40-50%)。
7.2节 详细阐述了Drew Breunig归纳的五大上下文失效模式——上下文中毒(错误信息像谣言一样传播)、上下文分心(过长的上下文让模型丧失全局判断力)、上下文混淆(无关信息导致工具调用错误)、上下文冲突(新旧矛盾导致推理脱轨)和性能与成本的隐性衰减——为上下文问题提供了诊断框架和六条解决方案。
7.3节 建立了2026年两种主流压缩范式——紧凑化(可逆压缩,保留引用,零信息丢失)和摘要化(不可逆压缩,用LLM提炼精华)——并给出了互补决策树:高可重建性信息优先摘要化,低可重建性信息优先紧凑化。
7.4节 聚焦三大工程实践——Anthropic的Tool Result Clearing(最安全最轻量的压缩)、上下文卸载(文件系统是Agent的"硬盘")和滚动摘要(保留最近N轮完整,更早的压缩为摘要)。
7.5节 深入KV缓存优化的完整体系——以缓存命中率为"头号指标"(Claude Code 92%命中率→81%成本节省),围绕稳定前缀、追加式修改、确定性序列化和"Mask Don't Remove"四条原则构建缓存友好型上下文架构,并详细对比了OpenAI(零配置、零写入成本)和Anthropic(90%读取折扣、显式断点控制)的Prompt Caching方案。
7.6节 探讨了动态上下文窗口——从"固定分配"到"按需调度"的范式转变,介绍了渐进披露、Manus层次化行动空间、基于任务复杂度的预算分配和LangChain SummarizationMiddleware四种实现模式。
上下文压缩与优化是上下文工程中与信噪比最直接相关的技术领域——如果说采集(第4章)决定了上下文的"原料"质量、检索(第5章)决定了上下文的"广度"、记忆(第6章)决定了上下文的"深度",那么压缩与优化决定了上下文的"纯度"。在下一章,我们将转向另一个维度——当上下文需要与外部世界交互时,工具调用如何扩展上下文的边界,又能避免让边界本身变成漏洞。