VINO/WANG返回博客 ←

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问题我们决定什么?” “使用连接池和重试逻辑” “修复了配置”

工作原理

如果压缩保留了正确信息
→ 代理正确回答

如果没有
→ 代理猜测或产生幻觉

优势:
- 直接测试功能质量
- 模拟实际使用场景
- 捕捉传统指标遗漏的问题

评估维度

六个维度捕捉编码代理的压缩质量:

  1. 准确性(Accuracy)

    • 技术细节正确吗?
    • 文件路径、函数名、错误代码
  2. 上下文意识(Context Awareness)

    • 响应反映当前对话状态吗?
    • 还是过时信息?
  3. 工件轨迹(Artifact Trail)

    • 代理知道读取或修改了哪些文件吗?
    • 可以跟踪文件操作吗?
  4. 完整性(Completeness)

    • 响应解决问题的所有部分吗?
    • 还是遗漏细节?
  5. 连续性(Continuity)

    • 工作能否在不重新获取信息的情况下继续?
    • 或需要从头开始?
  6. 指令遵循(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)

目标

不是神奇地增加上下文窗口,
而是更好地利用可用容量。

关键洞察:
有效的优化可以在不要求更大模型
或更长上下文的情况下,
使有效上下文容量翻倍或翻三倍。

四种主要策略

  1. 压缩(Compaction)- 接近限制时摘要上下文
  2. 观察掩码(Observation Masking)- 用紧凑引用替换冗长输出
  3. KV缓存优化(KV-Cache Optimization)- 重用缓存计算
  4. 上下文分区(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)

压缩原则

  1. 优化tokens-per-task,不是tokens-per-request

    • 考虑重新获取成本
    • 质量比数量重要
    • 0.7%额外tokens可节省20%重新获取
  2. 使用结构化摘要

    • 明确部分强制保留信息
    • 结构作为检查清单
    • 可验证性是关键
  3. 在70-80%利用率触发压缩

    • 不要等到太晚
    • 也不要太早
    • 滑动窗口是好的默认
  4. 增量合并而非完整重新生成

    • 保留信息来源跟踪
    • 防止静默信息漂移
    • 锚定迭代最佳
  5. 使用探针评估测试质量

    • 功能性测试 > 词汇重叠
    • 测试实际保留的信息
    • 模拟使用场景

优化原则

  1. 测量前先优化

    • 了解当前状态
    • 建立基线
    • 监控有效性
  2. 尽可能先压缩后掩码

    • 压缩保留更多信息
    • 掩码作为第二道防线
    • 组合使用最有效
  3. 设计缓存稳定性

    • 一致的提示词格式
    • 稳定内容优先
    • 避免动态内容
  4. 在上下文成为问题前分区

    • 预防性隔离
    • 不要等到退化
    • 子代理架构
  5. 监控优化有效性

    • 迭代策略
    • 平衡节省与质量
    • 测量重新获取率

(完)