BLOG / POST
上下文压缩与优化
三种生产就绪的上下文压缩方法(锚定迭代、不透明、再生)与四种优化策略(压缩、观察掩码、KV 缓存、上下文分区)。
- context-engineering
- llm
来源:Agent-Skills-for-Context-Engineering。
模块1:上下文压缩(Context Compression)
核心概念
问题:当代理会话产生数百万token的对话历史时,压缩变得必不可少。
关键洞察:
错误目标:最小化每次请求的token数(tokens-per-request)
正确目标:最小化完成任务的token总数(tokens-per-task)
为什么?
当压缩丢失关键细节(文件路径、错误消息)时,
代理必须重新获取信息、重新探索方法,
浪费更多token恢复上下文。
示例:
策略A:节省0.5%更多tokens,但导致20%更多重新获取
策略B:多使用0.7% tokens,但保留关键信息
→ 策略B的tokens-per-task更低,尽管tokens-per-request更高
三种生产就绪的压缩方法
方法1:锚定迭代摘要(Anchored Iterative Summarization)
原理: 维护结构化、持久的摘要,包含会话意图、文件修改、决策和下一步的明确部分。
工作流程:
1. 定义明确的摘要部分(匹配代理需求)
2. 首次压缩触发时,将截断的历史摘要到各部分
3. 后续压缩时,仅摘要新截断的内容
4. 将新摘要合并到现有部分而非重新生成
5. 跟踪信息来自哪个压缩周期以便调试
结构化摘要模板:
## 会话意图
[用户试图完成什么]
## 文件修改
- auth.controller.ts: 修复JWT token生成
- config/redis.ts: 更新连接池配置
- tests/auth.test.ts: 添加新配置的mock设置
## 决策
- 使用Redis连接池而非每请求连接
- 对瞬态失败使用指数退避重试逻辑
## 当前状态
- 14个测试通过,2个失败
- 剩余:会话服务测试的mock设置
## 下一步
1. 修复剩余测试失败
2. 运行完整测试套件
3. 更新文档
为什么有效:
结构强制保留信息!
专用部分作为检查清单,
摘要器必须填充,
防止静默信息漂移。
如果没有明确部分:
→ 摘要器可能遗漏文件路径
→ 摘要器可能忽略关键决策
→ 静默信息丢失难以检测
有了明确部分:
→ 每个部分必须明确处理
→ 丢失信息显而易见
→ 可验证保留了什么
性能指标:
- 压缩比:98.6%(保留1.4%原始tokens)
- 质量分数:3.70/5.0
- 最佳适用:长期会话、文件跟踪重要、需要验证保留内容
适用场景:
- ✅ 长期运行的会话(100+条消息)
- ✅ 文件跟踪很重要(编码、调试)
- ✅ 需要验证保留了什么
方法2:不透明压缩(Opaque Compression)
原理:产生针对重建保真度优化的压缩表示。
特点:
- 最高压缩比(99%+)
- 牺牲可解释性
- 无法验证保留了什么
- 人类不可读
性能指标:
- 压缩比:99.3%(仅保留0.7%原始tokens)
- 质量分数:3.35/5.0
适用场景:
- 需要最大token节省
- 会话相对较短
- 重新获取成本低
- 不需要可解释性
方法3:再生完整摘要(Regenerative Full Summary)
原理: 每次压缩时生成详细的结构化摘要。
特点:
- 产生可读输出
- 可能因完整重新生成而非增量合并,在重复压缩周期中丢失细节
性能指标:
- 压缩比:98.7%(保留1.3%原始tokens)
- 质量分数:3.44/5.0
适用场景:
- 摘要可解释性至关重要
- 会话有清晰的阶段边界
- 每次压缩都可接受完整上下文审查
三种方法对比
| 方法 | 压缩比 | 质量分数 | 优势 | 劣势 |
|---|---|---|---|---|
| 锚定迭代 | 98.6% | 3.70/5.0 | 最佳质量,可验证 | 略少压缩 |
| 再生 | 98.7% | 3.44/5.0 | 可读,良好质量 | 可能丢失细节 |
| 不透明 | 99.3% | 3.35/5.0 | 最佳压缩 | 质量损失,不可验证 |
关键发现:
结构化摘要保留的额外0.7% tokens
购买0.35质量点(3.70 vs 3.35)
对于任何重新获取成本重要的任务,
这种权衡倾向于结构化方法。
示例:
项目:5M token代码库迁移
不透明压缩:
- 保留35K tokens
- 但丢失关键文件路径
- 需要10次重新获取(每次5K tokens)
- 总计:35K + 50K = 85K tokens
锚定迭代:
- 保留70K tokens
- 保留所有关键路径
- 需要0次重新获取
- 总计:70K tokens
→ 锚定迭代节省15K tokens!
压缩触发策略
| 策略 | 触发点 | 优势 | 劣势 |
|---|---|---|---|
| 固定阈值 | 70-80%上下文利用率 | 简单,可预测 | 可能压缩太早或太晚 |
| 滑动窗口 | 保留最后N轮 + 摘要 | 可预测的上下文大小 | 可能丢失早期重要信息 |
| 基于重要性 | 先压缩低相关性部分 | 保留信号 | 复杂实现 |
| 任务边界 | 在逻辑任务完成时压缩 | 清晰摘要 | 时机不可预测 |
推荐:
对于大多数编码代理用例:
滑动窗口 + 结构化摘要
原因:
- 可预测的上下文大小
- 保留最近轮次的完整性
- 结构化摘要保留关键信息
- 实施相对简单
关键问题:工件轨迹完整性
最弱的维度:
所有压缩方法在工件轨迹完整性上
得分仅为 2.2-2.5/5.0(满分5.0)
即使具有明确文件部分的结构化摘要
也难以在长会话中维持完整文件跟踪。
编码代理需要知道:
1. 创建了哪些文件
2. 修改了哪些文件以及改变了什么
3. 读取了但未更改哪些文件
4. 函数名、变量名、错误消息
示例失败:
摘要:"我们修复了一些配置问题"
丢失:
- 哪些配置文件?
- 什么具体更改?
- 影响哪些函数?
为什么这么难?
- 文件操作在对话中分散
- 细节在多轮中累积
- 摘要可能关注高层决策
解决方案:
这个问题可能需要超出一般摘要的专门处理:
1. 单独的工件索引
- 维护独立的文件操作列表
- 每次文件操作时更新
- 压缩时包括完整索引
2. 代理脚手架中的显式文件状态跟踪
- 工具级别跟踪
- 自动记录文件操作
- 与摘要系统分离
探针评估法(Probe-Based Evaluation)
传统指标的问题:
ROUGE或嵌入相似性等传统指标
无法捕捉功能压缩质量。
示例:
原始:"修复auth.controller.ts中的JWT生成,
该函数在第45行有null指针异常"
压缩摘要:"修复了一些认证问题"
ROUGE分数:可能不错(重叠词汇)
功能质量:差(丢失文件和具体错误)
问题:摘要可能在词汇重叠上得分高,
但缺少代理需要的那个文件路径。
探针评估法: 通过压缩后提问直接测量功能质量。
| 探针类型 | 测试内容 | 示例问题 | 良好回答 | 差回答 |
|---|---|---|---|---|
| 召回 | 事实保留 | “原始错误消息是什么?” | “401 Unauthorized from /api/auth/login” | “登录失败” |
| 工件 | 文件跟踪 | “我们修改了哪些文件?” | “auth.controller.ts, config/redis.ts” | “一些配置文件” |
| 延续 | 任务规划 | “下一步我们应该做什么?” | “修复剩余2个测试失败” | “继续测试” |
| 决策 | 推理链 | “关于Redis问题我们决定什么?” | “使用连接池和重试逻辑” | “修复了配置” |
工作原理:
如果压缩保留了正确信息
→ 代理正确回答
如果没有
→ 代理猜测或产生幻觉
优势:
- 直接测试功能质量
- 模拟实际使用场景
- 捕捉传统指标遗漏的问题
评估维度
六个维度捕捉编码代理的压缩质量:
-
准确性(Accuracy)
- 技术细节正确吗?
- 文件路径、函数名、错误代码
-
上下文意识(Context Awareness)
- 响应反映当前对话状态吗?
- 还是过时信息?
-
工件轨迹(Artifact Trail)
- 代理知道读取或修改了哪些文件吗?
- 可以跟踪文件操作吗?
-
完整性(Completeness)
- 响应解决问题的所有部分吗?
- 还是遗漏细节?
-
连续性(Continuity)
- 工作能否在不重新获取信息的情况下继续?
- 或需要从头开始?
-
指令遵循(Instruction Following)
- 响应遵循陈述的约束吗?
- 还是忽略要求?
研究发现:
准确性:压缩方法间最大变化(0.6分差距)
→ 结构化方法显著优于不透明
工件轨迹:普遍薄弱(2.2-2.5范围)
→ 需要专门处理,一般摘要不够
完整性和连续性:结构化方法更好
实际案例:调试会话压缩
原始上下文(89,000 tokens,178条消息):
会话历史:
- /api/auth/login端点上的401错误
- 跟踪auth控制器、中间件、会话存储
- 发现陈旧Redis连接
- 修复连接池,添加重试逻辑
- 14个测试通过,2个失败
详细过程:
1. 用户报告登录失败
2. 检查auth.controller.ts
3. 验证JWT生成(正确)
4. 检查CORS中间件(无问题)
5. 检查会话存储(发现问题)
6. 诊断Redis连接
7. 实施连接池
8. 添加重试逻辑
9. 运行测试
... 178条消息
压缩后的结构化摘要(约1,200 tokens):
## 会话意图
调试/api/auth/login上的401 Unauthorized错误
尽管凭据有效。
## 根本原因
会话存储中的陈旧Redis连接。
JWT正确生成但会话无法持久化。
## 文件修改
- auth.controller.ts: 无更改(仅读取)
- middleware/cors.ts: 无更改(已检查)
- config/redis.ts: 修复连接池配置
- services/session.service.ts: 为瞬态失败添加重试逻辑
- tests/auth.test.ts: 更新mock设置
## 测试状态
14个通过,2个失败(mock设置问题)
## 下一步
1. 修复剩余测试失败(mock会话服务)
2. 运行完整测试套件
3. 部署到staging
压缩效果:
- Token减少:98.6%(89K → 1.2K)
- 关键信息保留:✓
- 可继续工作:✓
三阶段压缩工作流
对于大型代码库或超过上下文窗口的代理系统:
阶段1:研究阶段
输入:
- 架构图
- 文档
- 关键接口
输出:研究文档
过程:将探索压缩为组件和依赖关系的结构化分析
示例:
研究:5M token代码库
输出:
- 组件列表(服务、模块、接口)
- 依赖关系图
- 关键架构决策
- 数据流分析
→ 约5,000词研究文档
阶段2:规划阶段
输入:研究文档
输出:实现规范
过程:将研究转换为包含函数签名、类型定义和数据流的实现规范
压缩效果:
输入:5M token代码库 + 研究文档
输出:约2,000词规范
包含:
- 函数签名
- 类型定义
- 接口契约
- 数据流
- 边界条件
阶段3:实现阶段
输入:实现规范
过程:针对规范执行
优势:
上下文专注于规范(2,000词)
而非原始代码库探索(5M tokens)
好处:
- 上下文窗口内可容纳
- 更快推理
- 更低成本
- 保持焦点
使用示例工件作为种子
关键洞察:
当提供手动迁移示例或参考PR时,
将其用作理解目标模式的模板。
示例揭示静态分析无法发现的约束:
- 哪些不变量必须保持
- 哪些服务因更改而中断
- 什么构成干净的迁移
为什么重要:
代理无法区分:
- 必要复杂性(业务需求)
- 偶然复杂性(遗留变通方法)
示例工件编码了这种区别。
实际应用:
任务:迁移认证系统
提供:
- 手动迁移的示例PR
- 包含3个文件的更改
代理学习:
1. 哪些文件必须更改
2. 更新模式是什么
3. 哪些测试必须更新
4. 什么算"成功"
而不是盲目地
尝试所有可能的迁移策略。
模块2:上下文优化(Context Optimization)
目标:
不是神奇地增加上下文窗口,
而是更好地利用可用容量。
关键洞察:
有效的优化可以在不要求更大模型
或更长上下文的情况下,
使有效上下文容量翻倍或翻三倍。
四种主要策略:
- 压缩(Compaction)- 接近限制时摘要上下文
- 观察掩码(Observation Masking)- 用紧凑引用替换冗长输出
- KV缓存优化(KV-Cache Optimization)- 重用缓存计算
- 上下文分区(Context Partitioning)- 跨隔离上下文拆分工作
策略1:压缩(Compaction)
什么是压缩:接近限制时摘要上下文内容,然后用摘要重新初始化新的上下文窗口。
原理:
这以高保真度蒸馏上下文窗口的内容,
使代理能够以最小性能退化继续。
实现方式:
1. 识别可以压缩的部分
2. 生成捕捉要点的摘要
3. 用摘要替换完整内容
压缩优先级:
| 优先级 | 组件 | 处理方式 | 原因 |
|---|---|---|---|
| 1 | 工具输出 | 替换为摘要 | 通常最大消耗者 |
| 2 | 旧轮次 | 摘要早期对话 | 历史价值递减 |
| 3 | 检索文档 | 如有较新版本则摘要 | 可能过期 |
| 4 | 系统提示词 | 永不压缩 | 核心指导 |
摘要生成策略:
工具输出摘要:
保留:
- 关键发现
- 指标和测量
- 结论和结果
移除:
- 冗长的原始输出
- 中间步骤
- 冗余信息
示例:
原始:"读取了file.json(50,000 tokens)"
摘要:"file.json包含:用户列表(10K项),
最后更新:2025-01-15,
关键字段:id, name, email"
对话轮次摘要:
保留:
- 关键决策
- 承诺和协议
- 上下文转换
- 重要结论
移除:
- 填充词
- 来回对话
- 重复内容
示例:
原始:10轮讨论数据库选择
摘要:"决定:PostgreSQL而非MongoDB,
原因:ACID事务支持"
检索文档摘要:
保留:
- 关键事实
- 主要声明
- 重要数据点
移除:
- 支持证据
- 详细说明
- 例子
示例:
原始:"完整API文档(20页)"
摘要:"API端点:/api/users,
方法:GET, POST, PUT, DELETE,
认证:Bearer token"
性能目标:
- Token减少:50-70%
- 质量下降:< 5%
策略2:观察掩码(Observation Masking)
观察问题:
工具输出可占代理轨迹中token使用量的80%+。
其中大部分是已达到目的的冗长输出。
一旦代理使用工具输出做出决策,
保留完整输出提供递减价值
同时消耗大量上下文。
示例:
轮次5:读取config.json(5K tokens)
使用配置设置连接
轮次10:config.json仍在上下文中
但已经用于决策
→ 仅消耗空间,无额外价值
观察掩码原理:
用紧凑引用替换冗长工具输出。
信息保持需要时可访问,
但不持续消耗上下文。
掩码策略选择:
永不掩码:
✓ 对当前任务关键的观察
✓ 最近一轮的观察
✓ 在活跃推理中使用的观察
原因:
可能立即需要
不应丢失访问
考虑掩码:
~ 3轮以上前的观察
~ 可提取关键点的冗长输出
~ 目的已达到的观察
决策:
基于内容价值
和重新获取成本
始终掩码:
✗ 重复输出
✗ 样板页眉/页脚
✗ 对话中已摘要的输出
原因:
冗余信息
无需保留
实现示例:
def mask_observation(observation, max_length=500):
"""用紧凑引用替换冗长观察"""
if len(observation) <= max_length:
return observation # 短观察保持原样
# 存储完整观察以供需要时访问
ref_id = store_observation(observation)
# 提取关键点
key_point = extract_key(observation)
# 返回紧凑引用
return f"[观察:{ref_id} 已省略。关键:{key_point}]"
# 使用示例
原始 = """
读取large_dataset.json
{
"users": [
{"id": 1, "name": "Alice", ...},
{"id": 2, "name": "Bob", ...},
... 10,000 条记录
]
}
(50,000 tokens)
"""
掩码后 = "[观察:ref_123 已省略。关键:10,000用户数据集,最后更新:2025-01-15]"
(~50 tokens)
节省:99.9%
性能目标:
- 被掩码观察减少:60-80%
策略3:KV缓存优化(KV-Cache Optimization)
理解KV缓存:
KV缓存存储推理期间计算的键和值张量,
随序列长度线性增长。
示例:
序列长度 = 1000 tokens
KV缓存大小 ≈ 1000 单位
序列长度 = 10000 tokens
KV缓存大小 ≈ 10000 单位
跨具有相同前缀的请求缓存KV缓存
避免重新计算。
节省:
时间:不重新计算前缀
成本:较少计算资源
延迟:更快响应
前缀缓存:
使用基于哈希的块匹配,
跨具有相同前缀的请求重用KV块。
这对具有公共前缀的请求
显著降低成本和延迟:
- 系统提示词
- 工具定义
- 标准模板
示例:
请求A:[系统提示] + [工具定义] + [用户查询1]
请求B:[系统提示] + [工具定义] + [用户查询2]
前缀匹配!
→ 重用系统提示和工具定义的KV缓存
→ 仅计算用户查询2部分
缓存优化模式:
1. 最大化缓存命中:
重新排序上下文元素以最大化缓存命中
原则:
稳定内容 → 经常重用内容 → 独特内容
示例:
context = [
system_prompt, # 最稳定,所有请求共享
tool_definitions, # 非常稳定
reused_templates, # 经常重用
unique_content # 不稳定,每个请求独特
]
2. 设计缓存稳定提示词:
避免动态内容:
nop "当前时间:{timestamp}" # 每次请求不同
✓ "系统时间:见时间工具" # 稳定
使用一致格式:
nop 有时用JSON,有时用XML
✓ 始终使用相同格式
保持结构稳定:
nop 有时重新排序部分
✓ 始终相同顺序
缓存稳定性检查清单:
✓ 系统提示词稳定吗?
✓ 工具定义顺序一致吗?
✓ 模板格式标准化吗?
✓ 避免动态时间戳吗?
✓ 结构在会话间一致吗?
性能目标:
- 稳定工作负载命中率:70%+
实际影响:
无缓存优化:
- 每个请求完整计算
- 成本:1.0x
- 延迟:1.0x
有缓存优化(80%命中):
- 仅计算20%独特部分
- 成本:0.2-0.3x
- 延迟:0.3-0.4x
节省:70-80%!
策略4:上下文分区(Context Partitioning)
子代理分区:
最激进的上下文优化形式
是跨具有隔离上下文的子代理分区工作。
每个子代理在专注于其子任务的
干净上下文中操作,
而不携带来自其他子任务的累积上下文。
优势:
分离关注点:
协调器:
- 专注于综合和分析
- 不关心详细实现
- 清晰高层视图
子代理:
- 专注于具体任务
- 详细的搜索上下文
- 与其他子任务隔离
实际示例:
任务:分析10个文件的代码库
单代理方法:
代理1:
- 上下文:所有10个文件
- 结果:上下文过大,质量下降
分区方法:
协调器:
- 上下文:任务定义 + 结果摘要
- 作用:分发任务,聚合结果
子代理A:
- 上下文:文件1-3
- 作用:分析这些文件
子代理B:
- 上下文:文件4-6
- 作用:分析这些文件
子代理C:
- 上下文:文件7-10
- 作用:分析这些文件
结果:
- 每个子代理有清晰小上下文
- 高质量分析
- 协调器聚合结果
结果聚合:
1. 验证所有分区完成
- 确保所有子代理完成
- 检查失败或错误
2. 合并兼容结果
- 组合输出
- 解决冲突
3. 如果仍然太大则摘要
- 应用压缩
- 产生最终摘要
何时使用分区:
✓ 任务可分解为独立子任务
✓ 子任务不需要完整上下文
✓ 可以组合结果
✓ 单个上下文过大
✗ 任务高度依赖
✗ 需要全局视图
✗ 小型任务(开销不值得)
预算管理(Budget Management)
上下文预算分配:
设计明确的上下文预算。
分配tokens到类别:
系统提示词: 500-2000 tokens(稳定)
工具定义: 100-500/工具(稳定)
检索文档: 可变(可能很大)
消息历史: 可变(随时间增长)
保留缓冲: 10-20%总预算(安全边际)
总计:不超过上下文窗口的80-90%
预算跟踪:
def monitor_context_budget(context, limit):
"""监控上下文使用与预算"""
tokens = count_tokens(context)
utilization = tokens / limit
if utilization > 0.9:
return "CRITICAL: 需要立即优化"
elif utilization > 0.8:
return "WARNING: 触发优化"
elif utilization > 0.7:
return "INFO: 准备优化"
else:
return "OK: 容量充足"
基于触发的优化:
监控信号:
1. Token利用率 > 80%
→ 触发:压缩或掩码
2. 退化指示器
→ 触发:评估和分区
3. 性能下降
→ 触发:全面优化
应用技术决策树:
什么主导上下文?
工具输出主导 → 观察掩码
检索文档主导 → 摘要或分区
消息历史主导 → 压缩与摘要
多个组件 → 组合策略
示例:
工具输出:60%
历史:30%
文档:10%
→ 优先:观察掩码
→ 次要:压缩历史
→ 可选:摘要文档
优化决策框架
何时优化:
上下文利用率超过70%
→ 预防性优化
随对话延长响应质量下降
→ 响应性优化
长上下文导致成本增加
→ 成本优化
对话长度导致延迟增加
→ 性能优化
应用什么:
诊断 → 治疗
工具输出主导 → 观察掩码
检索文档主导 → 摘要或分区
消息历史主导 → 压缩与摘要
多个组件 → 组合策略
实际示例
示例1:压缩触发
# 简单但有效
if context_tokens / context_limit > 0.8:
context = compact_context(context)
示例2:观察掩码
def mask_observations(context, max_length=500):
"""掩码冗长观察"""
masked = []
for msg in context:
if msg['role'] == 'tool' and len(msg['content']) > max_length:
ref_id = store_observation(msg['content'])
key = extract_key(msg['content'])
masked.append({
'role': 'tool',
'content': f"[观察:{ref_id} 已省略。关键:{key}]"
})
else:
masked.append(msg)
return masked
示例3:缓存友好排序
# 稳定内容优先
context = [
system_prompt, # 最稳定
tool_definitions, # 非常稳定
reused_templates, # 经常重用
unique_content # 不稳定
]
示例4:分区实现
def partition_task(task, num_partitions):
"""分区任务到子代理"""
subtasks = split_task(task, num_partitions)
results = []
for subtask in subtasks:
# 每个子代理获得干净上下文
sub_agent = create_agent(context=subtask.context)
result = sub_agent.execute(subtask)
results.append(result)
# 聚合结果
return aggregate_results(results)
压缩原则
-
优化tokens-per-task,不是tokens-per-request
- 考虑重新获取成本
- 质量比数量重要
- 0.7%额外tokens可节省20%重新获取
-
使用结构化摘要
- 明确部分强制保留信息
- 结构作为检查清单
- 可验证性是关键
-
在70-80%利用率触发压缩
- 不要等到太晚
- 也不要太早
- 滑动窗口是好的默认
-
增量合并而非完整重新生成
- 保留信息来源跟踪
- 防止静默信息漂移
- 锚定迭代最佳
-
使用探针评估测试质量
- 功能性测试 > 词汇重叠
- 测试实际保留的信息
- 模拟使用场景
优化原则
-
测量前先优化
- 了解当前状态
- 建立基线
- 监控有效性
-
尽可能先压缩后掩码
- 压缩保留更多信息
- 掩码作为第二道防线
- 组合使用最有效
-
设计缓存稳定性
- 一致的提示词格式
- 稳定内容优先
- 避免动态内容
-
在上下文成为问题前分区
- 预防性隔离
- 不要等到退化
- 子代理架构
-
监控优化有效性
- 迭代策略
- 平衡节省与质量
- 测量重新获取率
(完)