首页
/ learn-claude-code s06 Context Compact:用三层压缩管道让 Agent 突破上下文窗口限制

learn-claude-code s06 Context Compact:用三层压缩管道让 Agent 突破上下文窗口限制

2026-09-06 14:52:58作者:贡沫苏Truman

本文基于 learn-claude-code 仓库(一个从零构建的 nano Claude-Code 风格 agent harness)中的 s06 章节文档,完整剖析"三层上下文压缩"策略的设计动机与真实实现:如何在每一轮静默地把陈旧工具结果替换为占位符(micro_compact)、在 token 估算超过阈值时自动落盘摘要(auto_compact),以及如何注册一个 compact 工具让模型自己决定何时压缩。读完本文,你能理解长会话 Agent 的上下文管理核心机制,并掌握其全部关键参数、代码位置与可运行的验证方法。

micro_compact 前后对比:保留最新 3 个工具结果,更早的结果替换为占位符

1. 问题:上下文窗口有限,工具结果无限增长

s06 文档开篇给出的量化背景:上下文窗口是固定的,一次读取 1000 行文件的 read_file 就要消耗约 4000 个 token;当 Agent 读取了 30 个文件、执行了 20 条 bash 命令之后,会话就能轻松突破 100,000+ token。文档的原话是:

"Context will fill up; you need a way to make room" —— 为无限会话设计的三层压缩策略。

对 harness 层而言,这一课的定位是 Compression: clean memory for infinite sessions(为无限会话清理内存)。核心洞察在 agents/s06_context_compact.py 的模块注释中被明确写出:

Key insight: "The agent can forget strategically and keep working forever." (Agent 可以有策略地遗忘,从而永远继续工作。)

2. 总体架构:三层逐级加强的压缩

s06 方案包含三个层次,激进度递增,文档中的整体流程图如下:

Every turn:
+------------------+
| Tool call result |
+------------------+
        |
        v
[Layer 1: micro_compact]        (silent, every turn)
  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 summarizes conversation.
              Replace all messages with [summary].
                    |
                    v
            [Layer 3: compact tool]
              Model calls compact explicitly.
              Same summarization as auto_compact.

三层的分工非常清晰:

名称 触发方式 是否调用模型 作用
Layer 1 micro_compact 每一轮模型调用前,无条件静默执行 把陈旧 tool_result 替换为占位符
Layer 2 auto_compact token 估算超过 THRESHOLD = 50000 是(一次摘要调用) 全量转录落盘 + 整段历史替换为摘要
Layer 3 compact 工具 模型在响应中显式调用 compact 与 auto_compact 相同的摘要流程,由模型主动发起

文档给出的 Agent Loop 集成方式(简化版)如下:

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(...)
        # ... tool execution ...
        if manual_compact:
            messages[:] = auto_compact(messages)       # Layer 3

关键保证是:转录文件把完整历史保存在磁盘上——"Nothing is truly lost, just moved out of active context"(没有任何东西真正丢失,只是被移出了活动上下文)。

3. Layer 1:micro_compact——把陈旧工具结果换成占位符

文档给出的简化伪代码如下:

def micro_compact(messages: list) -> list:
    tool_results = []
    for i, msg in enumerate(messages):
        if msg["role"] == "user" and isinstance(msg.get("content"), list):
            for j, part in enumerate(msg["content"]):
                if isinstance(part, dict) and part.get("type") == "tool_result":
                    tool_results.append((i, j, part))
    if len(tool_results) <= KEEP_RECENT:
        return messages
    for _, _, part in tool_results[:-KEEP_RECENT]:
        if len(part.get("content", "")) > 100:
            part["content"] = f"[Previous: used {tool_name}]"
    return messages

真实实现在 agents/s06_context_compact.py 中,比文档版本多了几个关键细节:

  1. 保留最近 3 个结果:常量 KEEP_RECENT = 3(第 59 行)。当 tool_result 总数不超过 3 时直接返回,不做任何修改。

  2. 工具名反查:实现中先遍历所有 assistant 消息,建立 tool_use_id → tool_name 的映射 tool_name_map(第 80-87 行),这样占位符才能写出有信息量的 [Previous: used read_file] 而非泛泛的"已使用某工具"。模型看到占位符后仍知道"这里发生过一次文件读取"。

  3. read_file 结果受保护:这是文档没有展开、但源码中非常关键的一点——

    PRESERVE_RESULT_TOOLS = {"read_file"}
    

    (第 60 行)。源码注释解释原因:文件内容是参考资料(reference material),如果把读过的文件内容压缩掉,Agent 就必须重新读一遍文件,反而浪费更多上下文与工具调用。micro_compact 会跳过这些结果。

  4. 100 字符阈值:只有 len(content) > 100 的旧结果才会被替换;短结果(比如报错信息、(no output))本身就没什么可压缩的。

值得强调的是,micro_compact纯文本/结构操作,不产生任何 API 调用,所以它可以每一轮无条件运行而不增加成本与延迟——这正是"从低成本操作开始"的分层思想。

4. Layer 2:auto_compact——转录落盘,然后 LLM 摘要

文档中的简化版本:

def auto_compact(messages: list) -> list:
    # Save transcript for recovery
    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 summarizes
    response = client.messages.create(
        model=MODEL,
        messages=[{"role": "user", "content":
            "Summarize this conversation for continuity..."
            + json.dumps(messages, default=str)[:80000]}],
        max_tokens=2000,
    )
    return [
        {"role": "user", "content": f"[Compressed]\n\n{response.content[0].text}"},
    ]

真实实现见 agents/s06_context_compact.py,与文档的几处差异(以源码为准):

  • 转录目录与命名:TRANSCRIPT_DIR = WORKDIR / ".transcripts"(第 58 行),文件名为 transcript_<unix时间戳>.jsonl,逐条消息以 JSONL 格式写入,落盘后终端会打印 [transcript saved: <path>]
  • 送摘要的片段:conversation_text = json.dumps(messages, default=str)[-80000:]——取序列化后的最后 80000 字符(即对话的最近部分,因为越靠近当前的内容对续接任务越重要),文档伪代码中写作 [:80000],属于简化表述。
  • 摘要提示词要求三个要素:"1) What was accomplished(已完成什么) 2) Current state(当前状态) 3) Key decisions made(关键决策)",并强调"concise but preserve critical details"。
  • focus 参数:auto_compact(messages, focus="") 支持第二个参数,非空时会追加指令 Pay special attention to preserving details about: {focus},让本次摘要重点保留某个主题的细节。
  • 替换结果的格式:所有消息被替换为单条 user 消息,内容是 [Conversation compressed. Transcript: <path>]\n\n<summary>——摘要中内嵌了转录文件路径,Agent 后续需要细节时可以自行回读磁盘上的完整历史。

触发条件中的 token 估算同样值得注意(第 63-65 行):

def estimate_tokens(messages: list) -> int:
    """Rough token count: ~4 chars per token."""
    return len(str(messages)) // 4

这是一个"约 4 字符 = 1 token"的粗略启发式,配合 THRESHOLD = 50000(第 57 行)使用。它不是精确的 tokenizer 计数,而是一个足够便宜、足够准的代理指标——估算偏差由后续的容错机制(见第 8 节的演进部分)兜底。

5. Layer 3:compact 工具——模型自主决定压缩时机

自动阈值只知道"上下文有多大",但模型最知道"当前阶段结束了、下一阶段只需要摘要"。因此 s06 注册了第五个工具 compact(工具定义):

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

实现上有两个值得学习的设计:

  1. 批次完整性:compact 的处理器本身只返回字符串 "Manual compression requested."(第 188 行),真正的压缩发生在整批 tool_use 全部执行完之后。这样即使模型在同一次响应里既写了文件又调用 compact,文件写入的副作用记录(tool_result)已经先于压缩进入历史,压缩后模型不会重复执行已有副作用的操作。
  2. 压缩即结束当前回合:agent_loop 中,检测到 manual_compact 后会执行 messages[:] = auto_compact(messages, focus=compact_focus)return——把控制权交还 CLI 输入循环。focus 参数从 block.input.get("focus", "") 取出并传给摘要,实现"定向压缩"。

6. 三层在 Agent Loop 中的完整整合

整合逻辑在 agents/s06_context_compact.pyagent_loop 中,完整调用链为:

while True:
    micro_compact(messages)                    # Layer 1: 每轮静默执行
    if estimate_tokens(messages) > THRESHOLD:  # ~4 chars/token
        messages[:] = auto_compact(messages)   # Layer 2: 超过 50000 自动摘要
    response = client.messages.create(model=MODEL, system=SYSTEM,
                                      messages=messages, tools=TOOLS,
                                      max_tokens=8000)
    messages.append({"role": "assistant", "content": response.content})
    if response.stop_reason != "tool_use":
        return
    # 顺序执行本批次所有 tool_use(包括 compact 标记),
    # 追加一条 role=user 的 tool_result 批次消息
    if manual_compact:
        messages[:] = auto_compact(messages, focus=compact_focus)  # Layer 3
        return

三个要点:

  • micro_compact每次 messages.create 之前运行,保证发给模型的一定是当前最精简的版本;
  • 自动压缩就地替换(messages[:] = ...),不改外层引用,CLI 主循环持有的 history 列表始终有效;
  • max_tokens=8000 限制单次模型输出,避免长输出进一步挤占下一轮的上下文余量。

7. 相对 s05 的变化(What Changed From s05)

文档用下表总结了 s06 在前一课(技能按需加载)基础上的增量,完整保留如下:

Component Before (s05) After (s06)
Tools 5 5 (base + compact)
Context mgmt None Three-layer compression
Micro-compact None Old results → placeholders
Auto-compact None Token threshold trigger
Transcripts None Saved to .transcripts/

对照 agents/s06_context_compact.pyTOOLS 列表可以核实:s06 共注册 5 个工具,即 bashread_filewrite_fileedit_file 四个基础工具加上新增的 compact。上下文管理从"无"变为三层压缩,是本课唯一但核心的一步能力跃迁。

8. 运行与验证

运行前提(依据 agents/s06_context_compact.py 的环境读取逻辑):

  • 依赖 anthropicpython-dotenv(见 requirements.txt);
  • 环境变量 MODEL_ID必填(源码直接 os.environ["MODEL_ID"] 读取,缺失会抛 KeyError);
  • 通过 .env 或环境变量提供 API Key;
  • 可选 ANTHROPIC_BASE_URL 指向兼容端点,设置后代码会主动 popANTHROPIC_AUTH_TOKEN 以避免认证头冲突。

运行方式(文档 Try It 章节原样继承):

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

进入交互循环(提示符 s06 >>),按文档建议依次执行三个观察实验:

  1. Read every Python file in the agents/ directory one by one —— 观察 micro-compact 把 3 个窗口之外的旧工具结果替换成 [Previous: used <tool>](注意 read_file 结果按保护规则被保留);
  2. Keep reading files until compression triggers automatically —— 当估算 token 超过 50000 时,终端打印 [auto_compact triggered],并生成 .transcripts/transcript_<ts>.jsonl;
  3. Use the compact tool to manually compress the conversation —— 模型调用 compact(可带 focus),终端打印 [manual compact],整段历史被替换为带转录路径的压缩摘要。

验证产物:检查当前目录下 .transcripts/ 中的 JSONL 文件,即完整对话历史归档——这正是"没有信息真正丢失"的落盘证据。

9. 仓库内演进:从三层到四步管道

s06 的三层方案是同一主题在仓库中的早期形态;从源码结构看,后续章节 s08_context_compact 把同一问题演进了更完整的四步管道,可对照阅读以理解设计取舍:

tool_result_budget   # 超大结果(>30000 字符)持久化到 .task_outputs/tool-results/
    → snip_compact   # 消息数超 50 时归档中段消息,保留首 3 + 尾 47
    → micro_compact  # 与 s06 同源:保留最近 3 个结果,其余换占位符
    → compact_history # 字符数超 CONTEXT_CHAR_LIMIT=50000 才调用 LLM 摘要

相比 s06 的演进要点:触发单位从"token 估算"改为"字符计数"(estimate_chars),新增 reactive_compact 作为 API 返回 prompt_too_long 时的一次性重试兜底(MAX_REACTIVE_RETRIES = 1),并且在所有裁剪点上显式保护 tool_use/tool_result 配对不产生孤儿结果。这些配对不变式由测试 tests/test_compaction_tool_pairs.py 固化——例如 assert_no_orphan_tool_results 断言任何含 tool_result 的 user 消息前一条必须是含对应 tool_use 的 assistant 消息,snip_compact/reactive_compact 的头部、尾部裁剪用例都经过该断言验证。

s08 压缩管道总览:每个模型调用前依次经过 budget、snip、micro 三步,超限才进入摘要

阅读路径建议:先吃透本文的 s06 三层骨架,再对照 s08_context_compact/README.mds08_context_compact/code.py,可以看到"低成本确定性操作优先、模型摘要永远放最后"这一原则如何在更大规模上成立。

10. 要点小结

  • 分层原则:按"信息可恢复性 × 调用成本"排序——先做零 API 调用的占位符替换,再做需要模型调用的全量摘要;每一轮先跑最便宜的一步;
  • 占位符带语义:[Previous: used {tool_name}] 保留"发生过什么工具调用"的线索,模型仍能据此决定是否需要重跑;
  • 受保护结果:参考性内容(read_file)不压缩,避免"压缩后再重读"的双重成本;
  • 磁盘是安全网:压缩前全量转录落盘 .transcripts/,摘要中内嵌路径,长会话信息只移动、不销毁;
  • 模型可自主压缩:compact 工具带 focus 参数,在整批工具执行完成后才触发,既保留副作用记录,又支持定向摘要;
  • 参数一览:THRESHOLD = 50000(token 估算,约 4 字符/token)、KEEP_RECENT = 3、占位符阈值 100 字符、摘要 max_tokens = 2000、送摘要片段取最近 80000 字符。
登录后查看全文
热门项目推荐
相关项目推荐