Skip to content

⭐ 06 上下文压缩

"上下文总会满,要有办法腾地方" — 三层压缩策略,换来无限会话。

Harness 层:压缩 — 干净的记忆,无限的会话。

[[00-课程概览/综述|← 返回综述]] | [[00-课程概览/教学大纲|📋 教学大纲]]


问题:上下文窗口是有限资源

[[04-子智能体|s04 子智能体]] 解决了同一层级的上下文隔离:子任务在独立 messages[] 中运行,用完即弃。但父智能体自己的上下文仍然在持续增长。

考虑一次真实的编码会话:

  • 读一个 1000 行的 Python 文件 ≈ 4000 token
  • lsgrepgit log 的输出每次几百到几千 token
  • 10 个文件读取 + 20 个 shell 命令 = 轻松突破 100k token
  • 模型推理速度随上下文长度平方级下降
  • API 成本线性增长(Claude 100k 输入 ≈ $3/次)

不压缩,智能体根本没法在大项目里持续工作。

这呼应了 [[01-智能体循环|s01 提出的循环代价]]:循环跑得越多,上下文越脏,下一条推理越慢越贵。s04 解决了跨层级污染,s06 解决的是时间维度上的上下文膨胀


解决方案:三层压缩

Harness 工程采用三层压缩策略,激进程度递增,在信息保留和上下文管理之间寻找平衡:

Every turn:
+------------------+
| Tool call result |
+------------------+
        |
        v
[Layer 1: micro_compact]        (静默执行,每轮)
  Replace tool_result > 3 turns old
  with "[Previous: used {tool_name}]"
        |
        v
[Check: tokens > 50000?]
   |               |
   no              yes
   |               |
   v               v
continue    [Layer 2: auto_compact]
              Save transcript to .transcripts/
              LLM 摘要对话
              Replace all messages with [summary]
                    |
                    v
            [Layer 3: compact tool]
              模型主动调用 compact
              同上摘要机制

每一层都在不同的激进程度上工作,它们不是互斥的——而是协同配合:

层级触发方式激进程度信息丢失
第一层每轮自动温和仅丢弃旧 tool_result 的详细内容
第二层Token 阈值中等整个对话压缩为摘要
第三层模型按需同第二层同上,但由模型决定时机

第一层:micro_compact(静默压缩)

每次 LLM 调用前,将超过 N 轮(默认 3 轮)的旧 tool_result 替换为占位符。这层压缩是无感知的——模型不会察觉,因为消息结构和时间线完整,只丢失了过期的详细输出。

python
KEEP_RECENT = 3

def micro_compact(messages: list) -> list:
    # 收集所有 tool_result 条目
    tool_results = []
    for msg_idx, msg in enumerate(messages):
        if msg["role"] == "user" and isinstance(msg.get("content"), list):
            for part_idx, part in enumerate(msg["content"]):
                if isinstance(part, dict) and part.get("type") == "tool_result":
                    tool_results.append((msg_idx, part_idx, part))
    if len(tool_results) <= KEEP_RECENT:
        return messages
    # 向前查找 tool_use_id 对应的 tool_name
    tool_name_map = {}
    for msg in messages:
        if msg["role"] == "assistant":
            content = msg.get("content", [])
            if isinstance(content, list):
                for block in content:
                    if hasattr(block, "type") and block.type == "tool_use":
                        tool_name_map[block.id] = block.name
    # 清除非最近的 N 条,替换为占位符
    to_clear = tool_results[:-KEEP_RECENT]
    for _, _, result in to_clear:
        if isinstance(result.get("content"), str) and len(result["content"]) > 100:
            tool_id = result.get("tool_use_id", "")
            tool_name = tool_name_map.get(tool_id, "unknown")
            result["content"] = f"[Previous: used {tool_name}]"
    return messages

关键设计决策

  • 保留最近 3 条完整 tool_result,确保模型能看到最近的工具输出用于决策
  • 占位符包含 tool_name,让模型知道曾经执行过什么操作,只是丢失了具体输出
  • 只替换长度 > 100 字符的结果——短结果直接保留,进一步减少信息损失

第二层:auto_compact(自动摘要)

estimate_tokens() > THRESHOLD(默认 50k)时,触发自动压缩:

python
def auto_compact(messages: list) -> list:
    # 保存完整对话到磁盘,用于恢复
    TRANSCRIPT_DIR.mkdir(exist_ok=True)
    transcript_path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
    with open(transcript_path, "w") as f:
        for msg in messages:
            f.write(json.dumps(msg, default=str) + "\n")
    # LLM 生成摘要
    conversation_text = json.dumps(messages, default=str)[:80000]
    response = client.messages.create(
        model=MODEL,
        messages=[{"role": "user", "content":
            "Summarize this conversation for continuity. Include: "
            "1) What was accomplished, 2) Current state, 3) Key decisions made. "
            "Be concise but preserve critical details.\n\n" + conversation_text}],
        max_tokens=2000,
    )
    summary = response.content[0].text
    # 将所有消息替换为压缩后的两条消息
    return [
        {"role": "user", "content": f"[Conversation compressed. Transcript: {transcript_path}]\n\n{summary}"},
        {"role": "assistant", "content": "Understood. I have the context from the summary. Continuing."},
    ]

为什么分层还不够? auto_compact 在 token 超标时触发,但它是一个昂贵的操作(需要一次 LLM 调用生成摘要)。micro_compact 的低成本让它可以在每一轮执行——两者互补,不是替代。

第三层:compact tool(手动触发)

当模型自己觉得上下文太臃肿时,可以调用 compact 工具主动触发压缩。这给了模型元认知能力——它知道自己何时需要"忘掉过去"才能继续前进。

python
{"name": "compact", "description": "Trigger manual conversation compression.",
 "input_schema": {"type": "object", "properties": {
    "focus": {"type": "string", "description": "What to preserve in the summary"}
 }}}

三层整合

python
def agent_loop(messages: list):
    while True:
        micro_compact(messages)                         # Layer 1: 每轮压缩
        if estimate_tokens(messages) > THRESHOLD:
            messages[:] = auto_compact(messages)        # Layer 2: 自动压缩
        response = client.messages.create(...)
        # ... 工具执行 ...
        if manual_compact:
            messages[:] = auto_compact(messages)        # Layer 3: 手动压缩

注意 messages[:] = 的语法——它在原地替换列表内容,确保所有引用同步更新。


⭐ 核心设计原则

Transcript 即真相

auto_compact 的关键原则:压缩不是删除,是归档。 完整的历史通过 transcript 保存在磁盘上的 .transcripts/ 目录中。信息没有真正丢失,只是移出了活跃上下文。

这在调试时尤其重要——如果压缩丢失了关键信息,可以重放 transcript 进行恢复。

Micro 是 MVP

三层压缩中最重要的是第一层 micro_compact。它 几乎零成本(纯数组操作,无 LLM 调用),无感知(模型不察觉),无副作用(不改变消息结构)。

一个常见的误区是认为"等上下文满了再压缩也不迟"。但实际上,模型在上下文饱满时的推理质量已经下降了。micro_compact 让上下文始终保持苗条,而不是满了再做减肥。

与 s04 子智能体的协同

[[04-子智能体|s04]] 和 s06 解决的是同一个问题的两个维度:

s04 子智能体s06 上下文压缩
解决的问题跨任务上下文污染时间线上的上下文膨胀
方式丢弃子任务的完整历史压缩父任务的过时信息
粒度整体丢弃逐层渐进式压缩
信息保留仅摘要文本返回父智能体Transcript 磁盘归档

两者结合使用:子智能体保证执行过程不污染主脑,上下文压缩保证历史对话不拖慢当前推理。


相对 s05 的变更

组件之前 (s05)之后 (s06)
Tools5(基础 + task)5(基础 + task + compact)
上下文管理三层压缩
Micro-compact旧结果 → 占位符
Auto-compactToken 阈值触发
Transcripts保存到 .transcripts/

从 Harness 工程演进来看:

  • [[01-智能体循环|s01]] — 循环骨架
  • [[02-工具使用|s02]] — 工具分发
  • [[03-待办写入|s03]] — 任务规划
  • [[04-子智能体|s04]] — 上下文隔离
  • [[05-技能加载|s05]] — 知识注入
  • s06 — 上下文压缩,无限会话

试一试

sh
cd learn-claude-code
python agents/s06_context_compact.py

试试这些 prompt(英文 prompt 对 LLM 效果更好,也可以用中文):

  1. Read every Python file in the agents/ directory one by one(观察 micro-compact 替换旧结果)
  2. Keep reading files until compression triggers automatically
  3. Use the compact tool to manually compress the conversation

观察要点

  • 第一组 Read agents/*.py 时,注意后面的 tool_result 内容是否被替换为 [Previous: used read_file] 占位符
  • 持续读文件,观察 token 计数何时触发 Threshold(50k)
  • auto_compact 后,检查 .transcripts/ 目录中是否生成了 transcript 文件
  • 使用 compact 工具后,模型是否还能从摘要中准确回忆之前的信息

挑战

  1. 如果把 KEEP_RECENT 从 3 改为 0,micro_compact 会做什么?模型还能正常工作吗?
  2. 为什么 auto_compact 的 summary prompt 要包含 "What was accomplished, Current state, Key decisions made" 三个维度?
  3. 如果要支持增量压缩(在旧摘要基础上追加新信息而不是重新摘要全部对话),代码需要怎么改?

本课小结

  • 上下文窗口是有限的——不压缩,Agent 无法在大项目中持续工作
  • micro_compact 是 MVP——零成本替换旧 tool_result,每轮无感知执行
  • auto_compact 在 token 超标时触发——保存 transcript 后用 LLM 摘要代替全部历史
  • compact tool 让模型拥有元认知——自己决定何时需要压缩
  • Transcript 归档确保信息没有真正丢失——压缩 = 存档,不是删除
  • 三层压缩是时间维度上对 [[04-子智能体|s04 空间维度]] 上下文管理的补充

下节预告

上下文压缩让 Agent 无限会话成为可能,但单体 Agent 终究有天花板。[[07-任务系统|s07 任务系统]] 将引入任务抽象——把大任务分解为可追踪、可恢复、可并行的单元,走向真正的工程化 Agent。

⭐ 核心路径

s01 [智能体循环] → s02 [工具使用] → s03 [待办写入] → s04 [子智能体]
→ s05 [技能加载] → s06 [上下文压缩] → s07 [任务系统] → s09 [智能体团队]

[[05-技能加载|← Lesson 05:技能加载]] ⭐ | [[07-任务系统|Lesson 07:任务系统 →]] ⭐


延伸阅读

  • Learn Claude Code 项目 — 练习代码和完整文档
  • [[00-课程概览/教学大纲|教学大纲]] — 查看完整课程地图
  • [[00-课程概览/综述|课程综述]] — Agent Harness 工程导论
  • [[术语表#Token估计|Token 估计]] — 快速计算上下文使用量
  • [[讨论课/压缩策略比较|讨论课:压缩策略比较]] — 不同压缩方式的优劣分析

「让模型学会遗忘,它才能记住真正重要的东西」

基于 Learn Claude Code 项目改编