1. 为什么你的 Agent 跑着跑着就“失忆”了
如果你正在跟着 learn-claude-code 这个项目从零手写 ClaudeCode,大概率会在 s05 之后遇到一个很现实的问题:智能体刚开始还挺聪明,读几个文件、跑几条命令之后,回答开始变慢、变糊,甚至把前面确认过的结论忘得一干二净。这不是模型变笨了,而是上下文窗口被塞满了。
Context Compact(上下文压缩)就是 learn-claude-code 第六章要解决的核心问题。简单说,它是一套让 AI Agent 在长会话里“腾地方”的机制:把旧的工具调用结果替换成占位符、把超长的对话历史摘要成一段话、再让模型自己决定什么时候主动压缩。适合谁?适合所有想让自己的 Agent 连续工作几十分钟甚至几小时、而不是聊三轮就崩的开发者。
这一篇我会把 s06 的三层压缩策略拆开讲清楚,同时把 ClaudeCode 的 settings.json 配置骨架搭起来,用 TaoToken 作为统一的 Key/API 通道,让你在本地真正跑通压缩流程,并且能观察到 token 消耗的变化。整套流程我实测下来是可以直接复制的,配置片段和验证命令都会给全。
2. 先搞懂 Context Compact 到底在压什么
2.1 上下文膨胀的真实来源
很多人以为 token 是被“对话”吃掉的,其实真正的大头是工具调用结果。你让 Agent 读一个 1000 行的 Python 文件,差不多就是 4000 token;读 30 个文件、跑 20 条 bash 命令,轻松突破 100k token。而 Claude 这类模型的上下文窗口是有限的,一旦接近上限,性能会急剧下降,关键信息开始丢失,最后直接报错或者胡言乱语。
在 s05 及之前的版本里,Agent 用的是最简单的消息累积模式:每次工具调用的结果都完整塞进 messages 列表,历史只增不减。这种模式在短任务里没问题,但一旦进入大项目,基本没法干活。
2.2 三层压缩策略的分工
learn-claude-code 的 s06 给出了三层压缩,激进程度递增:
| 层级 | 名称 | 触发时机 | 压缩动作 | 特点 |
|---|---|---|---|---|
| Layer 1 | micro_compact | 每轮 LLM 调用前 | 旧 tool_result 替换为[Previous: used {tool_name}] | 轻量、无感知 |
| Layer 2 | auto_compact | token 估算 > 50000 | 存 transcript 到磁盘,LLM 摘要,替换全部消息 | 自动、保命 |
| Layer 3 | compact tool | 模型主动调用 | 同 auto_compact 的摘要机制 | 模型自主控制 |
第一层是“静默清理”,保留最近 3 次工具调用的完整结果,更早的替换成占位符。第二层是“紧急刹车”,当估算 token 超过阈值时,把完整对话持久化到.transcripts/目录,然后让 LLM 生成结构化摘要,用两条消息替换掉整个历史。第三层是“主动瘦身”,模型自己意识到需要重置上下文时,调用 compact 工具触发同样的摘要流程。
关键点在于:完整历史并没有真正丢失,它被保存在磁盘上的 transcript 文件里,只是移出了活跃上下文。这样既保证了任务连续性,又极大降低了 token 消耗。
3. 用 TaoToken 搭好 ClaudeCode 的配置骨架
3.1 为什么需要统一 Key/API 通道
learn-claude-code 的代码里用的是 Anthropic SDK,通过ANTHROPIC_BASE_URL和MODEL_ID来指定模型。如果你直接对接官方,需要处理 Key 管理、额度、多模型切换这些琐事。用 TaoToken 的好处是:一个 Key 走通所有模型调用,base_url 统一,配置一次就能在 s06 到 s12 之间无缝切换。
TaoToken 的 API 地址是https://taotoken.net/api,官网是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。你需要在控制台创建一个 API Key,然后把它写进环境变量或 settings.json。
3.2 settings.json 配置骨架
ClaudeCode 的配置通常放在项目根目录或用户目录下的settings.json。下面是我实测可用的骨架,把 base_url、model、token 阈值、transcript 目录都集中管理:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "sk-your-taotoken-key", "MODEL_ID": "claude-sonnet-4-20250514" }, "context_compact": { "threshold": 50000, "keep_recent": 3, "transcript_dir": ".transcripts", "summary_max_tokens": 2000 }, "tools": { "compact": { "enabled": true, "description": "Trigger manual conversation compression." } } }这里有几个参数需要解释。threshold是 auto_compact 的触发阈值,默认 50000,你可以根据模型窗口大小调整。keep_recent是 micro_compact 保留的最近工具结果数量,默认 3。transcript_dir是完整历史落盘的位置,建议放在项目根目录下并加入.gitignore。summary_max_tokens控制摘要的长度,2000 足够保留关键信息。
如果你不想用 settings.json,也可以直接在.env里写:
ANTHROPIC_BASE_URL=https://taotoken.net/api ANTHROPIC_AUTH_TOKEN=sk-your-taotoken-key MODEL_ID=claude-sonnet-4-20250514代码里用load_dotenv(override=True)加载,然后client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))就能走通。注意 s06 的代码里有一行os.environ.pop("ANTHROPIC_AUTH_TOKEN", None),这是为了避免 SDK 自动读取环境变量导致冲突,实际使用时保留即可。
3.3 把配置接进 s06 的 agent_loop
s06 的agent_loop已经把三层压缩串起来了,你只需要确保配置能读到:
import json from pathlib import Path CONFIG = json.loads(Path("settings.json").read_text()) THRESHOLD = CONFIG["context_compact"]["threshold"] KEEP_RECENT = CONFIG["context_compact"]["keep_recent"] TRANSCRIPT_DIR = Path(CONFIG["context_compact"]["transcript_dir"])这样阈值和保留数量就不用硬编码在代码里,改配置就能调行为。对于长期编码和 Agent 场景,如果你打算把 s06 到 s12 都跑一遍,建议直接用 TaoToken 的 Coding Plan,额度和模型切换会更省心,入口在https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。
4. 可复制的压缩模块实现与触发验证
4.1 micro_compact 的四阶段实现
micro_compact 的核心逻辑是扫描所有 tool_result,保留最近 KEEP_RECENT 个,更早的替换成占位符。代码分四个阶段:
def micro_compact(messages: list) -> list: # 第一阶段:收集所有工具结果 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_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 # 第四阶段:执行替换 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这里有个细节:只有内容长度超过 100 字符的结果才会被替换,短结果保留原样,避免把有用的短输出也清掉。工具名称映射是为了让占位符里能显示具体用了哪个工具,保持语义连贯。
4.2 auto_compact 的落盘与摘要
auto_compact 做三件事:存 transcript、调 LLM 摘要、替换消息列表。
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") print(f"[transcript saved: {transcript_path}]") 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."}, ]摘要 prompt 明确要求包含三个维度:已完成工作、当前状态、关键决策。这样压缩后的上下文虽然短,但任务连续性不会断。transcript 文件路径也保留在摘要消息里,方便追溯。
4.3 触发验证:观察 token 消耗变化
跑起来之后,你可以用下面这组 prompt 验证三层压缩是否生效:
cd learn-claude-code python agents/s06_context_compact.py然后在交互界面里输入:
Read every Python file in the agents/ directory one by one你会看到 micro_compact 开始工作,旧的 tool_result 被替换成[Previous: used read_file]。继续输入:
Keep reading files until compression triggers automatically当估算 token 超过 50000 时,控制台会打印[auto_compact triggered]和[transcript saved: .transcripts/transcript_xxx.jsonl]。这时候你去.transcripts/目录下能看到完整的 JSONL 历史文件。
最后输入:
Use the compact tool to manually compress the conversation模型会主动调用 compact 工具,控制台打印[manual compact],然后走一遍 auto_compact 的摘要流程。
如果你想更直观地看 token 变化,可以在estimate_tokens里加一行打印:
def estimate_tokens(messages: list) -> int: tokens = len(str(messages)) // 4 print(f"[token estimate: {tokens}]") return tokens这样每轮调用前都能看到当前估算值,压缩前后对比非常明显。我试过连续读 20 个文件,压缩前估算值冲到 6 万多,auto_compact 触发后直接降到 2000 以内。
5. 本篇常见错排查
5.1 transcript 目录写入失败
如果你在容器或只读文件系统里跑,TRANSCRIPT_DIR.mkdir(exist_ok=True)可能报权限错误。解决办法是把 transcript_dir 改到有写权限的路径,比如/tmp/.transcripts,或者提前手动创建目录并赋权。
5.2 auto_compact 不触发
最常见的原因是estimate_tokens的估算方式太粗糙。s06 用的是len(str(messages)) // 4,对于中文和代码混合的内容,这个比例可能偏小。如果你发现 token 已经很多但没触发,可以把阈值调低到 30000,或者改用更精确的 tokenizer 估算。
另一个原因是 messages 结构不对。micro_compact 只处理role == "user"且content是 list 的消息,如果你的工具结果是以字符串形式塞进 user 消息的,就扫不到。确保工具结果按 Anthropic 的 tool_result 格式组织。
5.3 摘要后模型“失忆”
如果摘要 prompt 太简略,模型可能丢掉关键决策。建议在 prompt 里明确要求保留文件路径、函数名、变量名这些具体信息。另外summary_max_tokens不要设太小,2000 是底线,复杂任务可以调到 4000。
5.4 API 调用报 401 或 base_url 错误
检查ANTHROPIC_BASE_URL是否写成了https://taotoken.net/api,注意不要多加斜杠或路径。Key 要放在ANTHROPIC_AUTH_TOKEN里,不是ANTHROPIC_API_KEY。如果你在 s06 代码里看到os.environ.pop("ANTHROPIC_AUTH_TOKEN", None),那是为了清理环境变量冲突,实际运行时确保.env里的值能被load_dotenv正确加载。
5.5 compact 工具调用后没有压缩
检查TOOL_HANDLERS里是否注册了 compact,以及agent_loop里manual_compact标志是否在工具执行后被正确检查。s06 的逻辑是:先遍历 response.content 找到 compact 工具调用,标记manual_compact = True,然后在工具结果追加到 messages 之后,再执行messages[:] = auto_compact(messages)。顺序错了就不会触发。
6. 把压缩流程接进你的日常开发
跑通 s06 之后,你手里就有了一套可复用的上下文压缩骨架。接下来可以做的事:把 settings.json 里的阈值和保留数量做成可调参数,针对不同任务类型切换;把 transcript 文件按日期归档,方便回溯;在 compact 工具的 description 里加上 focus 参数,让模型摘要时能指定保留重点。
如果你要长期跑编码 Agent,建议把 API Key 和模型配置统一走 TaoToken,接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,API Keys 管理在https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。想先验证模型对话效果,可以直接用模型对话入口https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=试几条压缩相关的 prompt。
最后一句话经验:压缩不是目的,让 Agent 在长会话里保持“记得住关键、忘得掉冗余”才是。三层策略里,micro_compact 负责日常清理,auto_compact 负责保命,compact tool 负责自主控制,三者配合起来,你的 ClaudeCode 才算真正能在大项目里干活。