learn-claude-code s06 Context Compact:用三层压缩管道让 Agent 突破上下文窗口限制
本文基于 learn-claude-code 仓库(一个从零构建的 nano Claude-Code 风格 agent harness)中的 s06 章节文档,完整剖析"三层上下文压缩"策略的设计动机与真实实现:如何在每一轮静默地把陈旧工具结果替换为占位符(micro_compact)、在 token 估算超过阈值时自动落盘摘要(auto_compact),以及如何注册一个 compact 工具让模型自己决定何时压缩。读完本文,你能理解长会话 Agent 的上下文管理核心机制,并掌握其全部关键参数、代码位置与可运行的验证方法。
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 中,比文档版本多了几个关键细节:
-
保留最近 3 个结果:常量
KEEP_RECENT = 3(第 59 行)。当tool_result总数不超过 3 时直接返回,不做任何修改。 -
工具名反查:实现中先遍历所有 assistant 消息,建立
tool_use_id → tool_name的映射tool_name_map(第 80-87 行),这样占位符才能写出有信息量的[Previous: used read_file]而非泛泛的"已使用某工具"。模型看到占位符后仍知道"这里发生过一次文件读取"。 -
read_file结果受保护:这是文档没有展开、但源码中非常关键的一点——PRESERVE_RESULT_TOOLS = {"read_file"}(第 60 行)。源码注释解释原因:文件内容是参考资料(reference material),如果把读过的文件内容压缩掉,Agent 就必须重新读一遍文件,反而浪费更多上下文与工具调用。
micro_compact会跳过这些结果。 -
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"}}}},
实现上有两个值得学习的设计:
- 批次完整性:
compact的处理器本身只返回字符串"Manual compression requested."(第 188 行),真正的压缩发生在整批tool_use全部执行完之后。这样即使模型在同一次响应里既写了文件又调用compact,文件写入的副作用记录(tool_result)已经先于压缩进入历史,压缩后模型不会重复执行已有副作用的操作。 - 压缩即结束当前回合: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.py 的 agent_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.py 的 TOOLS 列表可以核实:s06 共注册 5 个工具,即 bash、read_file、write_file、edit_file 四个基础工具加上新增的 compact。上下文管理从"无"变为三层压缩,是本课唯一但核心的一步能力跃迁。
8. 运行与验证
运行前提(依据 agents/s06_context_compact.py 的环境读取逻辑):
- 依赖
anthropic与python-dotenv(见 requirements.txt); - 环境变量
MODEL_ID为必填(源码直接os.environ["MODEL_ID"]读取,缺失会抛KeyError); - 通过
.env或环境变量提供 API Key; - 可选
ANTHROPIC_BASE_URL指向兼容端点,设置后代码会主动pop掉ANTHROPIC_AUTH_TOKEN以避免认证头冲突。
运行方式(文档 Try It 章节原样继承):
cd learn-claude-code
python agents/s06_context_compact.py
进入交互循环(提示符 s06 >>),按文档建议依次执行三个观察实验:
Read every Python file in the agents/ directory one by one—— 观察 micro-compact 把 3 个窗口之外的旧工具结果替换成[Previous: used <tool>](注意read_file结果按保护规则被保留);Keep reading files until compression triggers automatically—— 当估算 token 超过 50000 时,终端打印[auto_compact triggered],并生成.transcripts/transcript_<ts>.jsonl;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 的头部、尾部裁剪用例都经过该断言验证。
阅读路径建议:先吃透本文的 s06 三层骨架,再对照 s08_context_compact/README.md 与 s08_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 字符。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00