上下文撑破之前,Claude Code 如何“清理记忆“——源码精读(二)

张开发
2026/4/3 23:58:35 15 分钟阅读
上下文撑破之前,Claude Code 如何“清理记忆“——源码精读(二)
上下文撑破之前Claude Code 如何清理记忆——源码精读二核心摘要一个 Agent 能处理的信息量根本上受制于上下文窗口。当你要求它分析整个代码仓库——读 50 个文件、跑 30 条命令——上下文轻松突破 100k token然后就 OOM 了。Claude Code 用三个机制来破这道墙Subagent把脏活甩给子进程父节点保持干净、Skill 系统按需加载知识不预先塞满 prompt、三层上下文压缩micro → auto → reactive激进程度递增。本文是源码精读第二篇深入这三个机制的实现细节。 为什么上下文会爆先量化一下这个问题。假设一次代码分析任务 - 读 10 个 Python 文件每个 100 行 ≈ 40,000 token - 运行 20 条 bash 命令每条输出 50 行 ≈ 20,000 token - 工具调用结构体本身 ≈ 5,000 token ────────────── 合计 ≈ 65,000 token这还是单次任务。一个长期运行的 Agentcontext 只会越来越大。更麻烦的是上下文增大不只是花钱的问题而是质量问题——模型的注意力是有限的早期读的文件内容会被后来的工具结果覆盖系统提示的影响力逐渐被稀释。下面三个机制分别从不同角度解决这个问题。 Subagent用独立上下文隔离脏活问题的本质父 Agent 问“这个项目用什么测试框架”要回答这个问题可能需要读requirements.txt、setup.py、pytest.ini、tox.ini……读完之后这些文件的内容全部永久留在父 Agent 的 messages 里——但父 Agent 只需要一个词的答案“pytest”。这就是上下文污染子任务的中间过程消耗了父 Agent 宝贵的上下文空间。Subagent 的解法图4父 Agent 通过 task 工具派发子任务子 Agent 以空 messages[] 启动可能执行 30 次工具调用读取大量文件但整个 sub_messages 执行完毕后直接丢弃父 Agent 只收到最终摘要文本上下文保持干净。核心代码defrun_subagent(prompt:str)-str:# 子 Agent 用全新的空消息列表启动sub_messages[{role:user,content:prompt}]for_inrange(30):# 安全限制最多 30 轮responseclient.messages.create(modelMODEL,systemSUBAGENT_SYSTEM,messagessub_messages,toolsCHILD_TOOLS,max_tokens8000,)sub_messages.append({role:assistant,content:response.content})ifresponse.stop_reason!tool_use:break# 执行工具追加结果...# 子 Agent 可能跑了 30 次工具调用# 整个 sub_messages 直接丢弃# 父 Agent 只收到这段摘要文本return.join(b.textforbinresponse.contentifhasattr(b,text))or(no summary)一个关键设计决策子 Agent 不能使用task工具禁止递归生成子 Agent避免指数级的上下文爆炸。PARENT_TOOLSCHILD_TOOLS[task_tool]# 父 Agent 有 task 工具# 子 Agent 只有 CHILD_TOOLS不包含 task这和人类分工一模一样你委托同事做调研最后他给你一份一页纸的报告而不是把所有调研材料都扔给你。 Skill 系统按需加载的领域知识把知识全塞进 System Prompt 的代价假设你想让 Agent 遵循特定的工作流git 约定、测试模式、代码审查清单、PDF 处理流程……如果把所有知识全塞进 system prompt10 个 Skill × 每个 2000 token 20,000 token 固定开销而且大部分跟当前任务毫无关系。你在处理 git 操作PDF 处理知识就是在白白占位。两层注入廉价索引 按需内容Skill 系统把知识分成两层第一层System Prompt始终存在You are a coding agent. Skills available: - git: Git workflow helpers ← 每个 Skill ~100 token - test: Testing best practices - pdf: Process PDF files第二层模型主动调用load_skill(git)时按需注入skillnamegit完整 git 工作流说明... ← 2000 token用时才加载 Step 1: .../skill每个 Skill 是一个目录包含一个带 YAML frontmatter 的 Markdown 文件skills/ git/ SKILL.md # ---\nname: git\ndescription: Git workflow\n---\n... pdf/ SKILL.md code-review/ SKILL.mdSkillLoader 扫描这些文件自动构建索引classSkillLoader:def__init__(self,skills_dir:Path):self.skills{}forfinsorted(skills_dir.rglob(SKILL.md)):textf.read_text()meta,bodyself._parse_frontmatter(text)namemeta.get(name,f.parent.name)self.skills[name]{meta:meta,body:body}defget_content(self,name:str)-str:skillself.skills.get(name)returnfskill name{name}\n{skill[body]}\n/skill注册为普通工具加载逻辑不过是一次字典查找TOOL_HANDLERS{# ...其他工具...load_skill:lambda**kw:SKILL_LOADER.get_content(kw[name]),}模型知道有哪些 Skill廉价需要时再加载完整内容贵。这是延迟加载思想在 Agent 设计中的直接应用。 三层上下文压缩从微手术到全身麻醉这是 Claude Code 中最精妙的工程设计之一。图5三层压缩的触发条件和激进程度依次递增。第一层静默替换旧工具结果无信息损失第二层在 token 超阈值时用 LLM 做摘要有信息损失但磁盘有完整备份第三层是 API 报错时的应急响应。三层配合实现无限会话。第一层micro_compact——悄无声息地清理旧垃圾最轻量的一层。工具结果用完之后它的历史价值其实已经不大了——10 分钟前读的文件内容大概率已经被处理完。defmicro_compact(messages:list)-list:# 找到所有 tool_resulttool_results[]fori,msginenumerate(messages):ifmsg[role]userandisinstance(msg.get(content),list):forj,partinenumerate(msg[content]):ifisinstance(part,dict)andpart.get(type)tool_result:tool_results.append((i,j,part))# 只保留最近 N 个旧的替换为占位符iflen(tool_results)KEEP_RECENT:returnmessagesfor_,_,partintool_results[:-KEEP_RECENT]:iflen(part.get(content,))100:part[content][Old tool result content cleared]returnmessages模型不知道发生了什么。它只是发现一些旧的工具结果变成了占位符——这是可接受的因为那些内容它已经处理过了。Anthropic 版 Claude Code 还有一个更精妙的变体直接用 cache editing API 在 API 层删除不修改本地消息连模型看到的历史都不用动。第二层auto_compact——LLM 给自己写会议纪要当 token 数超过阈值context_window - 13,000 buffer就要更激进的手段defauto_compact(messages:list)-list:# 1. 先把完整历史存磁盘安全网transcript_pathTRANSCRIPT_DIR/ftranscript_{int(time.time())}.jsonlwithopen(transcript_path,w)asf:formsginmessages:f.write(json.dumps(msg,defaultstr)\n)# 2. 用 LLM 做摘要responseclient.messages.create(modelMODEL,messages[{role:user,content:Summarize this conversation for continuity...json.dumps(messages,defaultstr)[:80000]}],max_tokens2000,)# 3. 用一条摘要消息替换所有历史return[{role:user,content:f[Compressed]\n\n{response.content[0].text}},]关键保护Circuit Breaker连续失败 3 次就停止重试防止无限死循环磁盘备份完整历史保存到.transcripts/信息没有真正丢失只是移出了活跃上下文递归保护压缩操作本身产生的调用不会再触发压缩第三层reactive compact——应急处理API 返回prompt_too_long错误时触发的紧急压缩。不是用户主动调用而是系统自动响应——这是前两层防线都没拦住时的兜底。为什么需要三层而不是一层层级激进程度触发频率信息损失micro_compact极低每轮极小auto_compact中等偶尔有但有备份reactive compact最高极少最大单一策略做不到这个权衡。三层组合实现了无限会话轻量操作稳定消耗重量操作在必要时托底。 三个机制的共同哲学仔细看这三个机制会发现一个共同主题在框架层解决问题不依赖模型自身的长期记忆。模型不会自己管理上下文不会自己清理垃圾不会自己加载知识。这些全都是框架层替它做的Subagent 隔离了子任务的垃圾Skill 系统控制了知识加载的时机三层压缩维持了上下文的可用性模型只需要做好推理记忆和上下文管理是基础设施的职责。 下一篇预告这一篇解决了单 Agent 的规模化问题。但真正的大任务——比如同时开发前端、后端、测试——需要多个 Agent 协作。下一篇进入系列的高潮从持久化任务图、后台并发执行到多 Agent 团队通信、自治认领一直讲到 git worktree 的并行隔离——看 Claude Code 是怎么从一个 Agent 跑循环变成一支 Agent 团队协作的。觉得有启发的话欢迎点赞、在看、转发。跟进最新 AI 前沿关注公众号机器懂语言

更多文章