learn-claude-code 三层上下文压缩:从 micro_compact 到 compact 工具的完整实现解析 📅 发布时间:2026/9/6 15:29:49 👁 浏览次数: learn-claude-code 三层上下文压缩从 micro_compact 到 compact 工具的完整实现解析【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code上下文窗口是有限的而 Agent 会话却会不断增长——本篇基于 learn-claude-code 仓库第 6 章文档docs/ja/s06-context-compact.md完整讲解该项目如何通过微压缩、自动压缩、手动压缩三层策略让 Agent 在有限窗口内实现近乎无限的会话。读完本文你将理解每一层的触发条件、阈值参数与取舍逻辑并能直接参考 agents/s06_context_compact.py 在自研 Agent 中落地同样的压缩管道。问题上下文窗口如何被耗尽文档开篇给出的账目非常直观对 1000 行文件执行一次read_file大约消耗 4000 token读 30 个文件再执行 20 次 bash 命令token 数轻松突破 100,000。在没有任何压缩机制的情况下Agent 根本无法在大型代码库上持续工作——请求会直接撞上模型的上下文上限。这正是该仓库 Harness 工程思路的核心场景之一。项目 READMEREADME.md将 Harness 定义为Model Harness中的车辆工具、知识、上下文管理、权限边界都由 Harness 承担。上下文压缩就是其中Manage context一项的具体实现属于该课程s01 s02 s03 s04 s05 [s06] s07 ...学习路径的第 6 步。解决方案三层渐进式压缩管道文档给出的整体架构是一个按积极性递增组织的三层管道每一轮turn都经历以下流程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.三层的设计哲学是先做无损且廉价的再做有损但昂贵的第 1 层只是文本替换不调用模型第 2 层在阈值触发时才动用一次额外的 LLM 调用第 3 层把决策权交还给模型本身。第 1 层micro_compact 把过期工具结果替换为占位符文档给出的核心逻辑是每次 LLM 调用之前收集所有tool_result块若总数超过保留窗口KEEP_RECENT就把最旧的、长度超过 100 字符的结果替换成[Previous: used {tool_name}]占位符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#L69-L99比文档示例多了两处关键细节值得单独说明tool_name_map反查机制。占位符中的{tool_name}并不是随手可得的——tool_result块本身不携带工具名。源码会先遍历历史 assistant 消息把每个tool_use块的id与name建立映射agents/s06_context_compact.py#L79-L87再通过result[tool_use_id]反查出真实工具名。这样占位符[Previous: used bash]依然保留了这里曾经执行过 bash这一语义线索模型看到它仍知道可以重新执行一次。PRESERVE_RESULT_TOOLS {read_file}白名单。源码中有一行显式跳过工具名在{read_file}中的结果永不压缩agents/s06_context_compact.py#L60。注释给出的理由很关键read_file的输出是参考资料而非一次性观测把它压缩掉只会迫使 Agent 重新读一遍文件反而浪费 token。这与该课程后续版本s08 章节进一步演化的先持久化大结果到磁盘、再替换的思路一脉相承可对照 s08_context_compact/README.md 查看演进后的四步管道。另外源码的触发条件是工具结果总数 KEEP_RECENT值为 3agents/s06_context_compact.py#L59时才开始替换最旧的部分且只有内容长度超过 100 字符的结果才会被替换——短结果如Wrote 42 bytes本身几乎没有 token 成本替换它们没有意义。第 2 层auto_compact 阈值触发 转录落盘 LLM 摘要当估算 token 数超过阈值时文档与源码一致取THRESHOLD 50000agents/s06_context_compact.py#L57进入第 2 层。文档给出的核心流程是先把完整转录写入磁盘再请求 LLM 生成摘要最后用摘要替换全部消息def auto_compact(messages: list) - list: # Save transcript for recovery transcript_path TRANSCRIPT_DIR / ftranscript_{int(time.time())}.jsonl with open(transcript_path, w) as f: for msg in messages: f.write(json.dumps(msg, defaultstr) \n) # LLM summarizes response client.messages.create( modelMODEL, messages[{role: user, content: Summarize this conversation for continuity... json.dumps(messages, defaultstr)[:80000]}], max_tokens2000, ) return [ {role: user, content: f[Compressed]\n\n{response.content[0].text}}, ]源码实现agents/s06_context_compact.py#L103-L131在文档骨架之上补充了几个可落地的细节摘要提示词有固定三要素Summarize this conversation for continuity. Include: 1) What was accomplished, 2) Current state, 3) Key decisions made. Be concise but preserve critical details.——即已完成什么、当前状态、关键决策。这是保证摘要具备续接性的提示词模板压缩后的第一条消息会带上[Conversation compressed. Transcript: 路径]前缀让模型知道完整历史在哪里。转录文件命名与位置TRANSCRIPT_DIR WORKDIR / .transcriptsagents/s06_context_compact.py#L58文件名为transcript_{unix秒级时间戳}.jsonlJSONL 格式一行一条消息方便用标准工具回放。摘要输入做了截断json.dumps(messages, defaultstr)[-80000:]取末尾 80000 字符送进摘要调用源码取尾部而非文档示例的头部截断因为当前状态集中在会话后半段摘要输出限制max_tokens2000并做了空摘要兜底No summary generated.。token 估算方式estimate_tokens采用约 4 字符 1 token的粗估agents/s06_context_compact.py#L63-L65即len(str(messages)) // 4。从源码结构看这是一种零成本的启发式精度足以触发50000 token 量级的阈值判断但不应被理解为精确计数。第 3 层compact 工具让模型自主触发压缩第 3 层把一个名为compact的工具暴露给模型。该工具的 schema 只有一个可选参数focusagents/s06_context_compact.py#L200-L201{name: compact, description: Trigger manual conversation compression., input_schema: {type: object, properties: {focus: { type: string, description: What to preserve in the summary}}}},当模型判断接下来要转入新阶段旧历史可以丢时可以主动调用它。源码中focus参数会被注入摘要提示词 Pay special attention to preserving details about: {focus}.agents/s06_context_compact.py#L113-L115——模型可以指定压缩时务必保留某某细节例如切换任务前提醒摘要保留当前文件的行号上下文。一个容易忽略的实现细节compact的处理函数只是返回一句Manual compression requested.agents/s06_context_compact.py#L188。真正的压缩发生在 agent loop 里——工具执行阶段检测到block.name compact就置位manual_compact标志并记录focus等所有tool_result都回填进消息之后再调用auto_compact(messages, focuscompact_focus)并直接返回本轮循环agents/s06_context_compact.py#L223-L243。也就是说手动压缩与自动压缩共用同一套落盘 摘要 替换逻辑区别只在触发者和时机。Agent 主循环如何整合三层文档给出的整合示意与源码agent_loopagents/s06_context_compact.py#L205-L243完全对应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执行顺序上值得注意三点第 1 层在每次 LLM 调用前运行先微压缩再估算避免把即将被替换的旧结果计入 token第 2 层在 LLM 调用之前做阈值检查并原地替换messages[:] ...保证外层history引用不变第 3 层在工具结果回填之后执行并结束本轮。循环退出条件是response.stop_reason ! tool_use。文档对最终不变量的总结值得原样引用转录让磁盘上的完整历史被保留。没有任何东西真正丢失只是被移出了活跃上下文。s05 到 s06 的变化ComponentBefore (s05)After (s06)Tools55 (base compact)Context mgmtNoneThree-layer compressionMicro-compactNoneOld results - placeholdersAuto-compactNoneToken threshold triggerTranscriptsNoneSaved to.transcripts/工具集本身没有扩容s05 已有的 4 个基础工具bash、read_file、write_file、edit_file加上新增的compact共 5 个agents/s06_context_compact.py#L191-L202。s05 是 skill 按需加载系统提示只放技能名正文在load_skill时才进入上下文s06 则从少往上下文里放转向放进去的东西会主动被清理——两者是同一上下文预算问题的攻防两面。动手试一遍运行环境要求Python 依赖见 requirements.txtanthropic0.25.0、python-dotenv1.0.0、pyyaml6.0并通过.env提供MODEL_ID等环境变量源码要求MODEL_ID存在可选ANTHROPIC_BASE_URL。在仓库根目录执行cd learn-claude-code python agents/s06_context_compact.py文档给出的三步验证指令对应三层机制的观察点Read every Python file in the agents/ directory one by one—— 逐个读取目录下的 Python 文件观察微压缩把较旧的tool_result替换为[Previous: used read_file]注意read_file结果在白名单内会被保留可换成多轮 bash 命令来观察替换效果Keep reading files until compression triggers automatically—— 持续读文件直到累计超过 50000 token 阈值观察[auto_compact triggered]打印、.transcripts/目录生成transcript_*.jsonl以及消息列表被折叠成单条摘要Use the compact tool to manually compress the conversation—— 要求模型调用compact工具可附加focus指定要保留的内容观察[manual compact]打印后本轮立即结束并重置历史。压缩正确性还有一条工程红线值得借鉴任何裁剪历史的操作都不能把assistant(tool_use)与对应user(tool_result)拆散否则下一次 API 请求会因孤儿 tool_result而非法。本仓库的 tests/test_compaction_tool_pairs.py 专门测试压缩后 tool_use/tool_result 配对的完整性可作为自研压缩管道时的测试参考。小结s06 这一章用不到 260 行代码回答了有限窗口如何支撑无限会话第一层用纯文本替换回收过期工具结果保留read_file参考资料第二层用 50000 token 阈值触发转录落盘 LLM 三要素摘要第三层把压缩决策权交给模型自身的compact工具。三层共用一条不变量——完整历史始终存在于.transcripts/活跃上下文中只保留模型继续工作所必需的部分。这套按信息损失与调用成本递增排序的压缩策略是构建长任务 Agent 时可以直接移植到 s08_context_compact/code.py 后续版本乃至自研项目中的实用模式。【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考