1. 长会话为什么会突然“失忆”:从 token 膨胀说起
如果你用 Claude Code 写过稍大一点的项目,大概率遇到过这种场景:前面聊得好好的,突然某次提问后它开始答非所问,或者直接甩出一句prompt_too_long,之前辛苦对齐的上下文像被橡皮擦抹掉了一样。这不是模型变笨了,而是对话的 token 总量撞上了模型上下文窗口的天花板。
Claude Code 里负责兜住这个天花板的机制,就是autocompact(自动压缩)。它的定位很明确:当对话 token 使用量接近模型上下文窗口上限时,自动触发上下文压缩,把历史消息折叠成摘要,从而避免prompt_too_long报错,让长会话能继续跑下去。适合谁看?正在做 Agent 长会话、需要理解 token 膨胀治理、或者想自己搭一套类似压缩链路的开发者。
这篇不空谈概念,我会沿着源码里shouldAutoCompact的判定链路,一层层拆到 token 阈值、来源排除、会话内存优先折叠、常规模型压缩回退,最后给你一份可复制的settings.json与config.toml配置骨架,并给出验证压缩是否真的生效的操作步骤。理解这套机制后,你调参时就不会再靠猜。
2. 前置准备:用 TaoToken 打通模型调用链路
要复现和验证压缩行为,你得先有一个能稳定调用 Claude 系列模型的入口。我这边用的是 TaoToken,它把模型对话、API Key 管理、编码计划这些能力放在一个控制台里,配置起来比较省心。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api (这个不加 UTM)。
你需要提前准备三样东西:一个可用的 API Key、确认目标模型名、以及本地 Claude Code 的配置文件路径。API Key 在控制台的 API Keys 页面创建,建议单独建一个用于压缩实验的 Key,方便后面看调用量。模型对话入口可以用来做单轮验证,确认 Key 和模型名对得上。
注意:压缩实验会产生多次模型调用,建议先用小项目或短会话跑通链路,再上真实长会话,避免 Key 额度被快速消耗。
拿到 Key 之后,把它写进环境变量,别硬编码进配置文件里。下面这一步是后面所有验证的基础。
export TAOTOKEN_API_KEY="sk-你的key" export ANTHROPIC_BASE_URL="https://taotoken.net/api"3. 压缩决策链路:shouldAutoCompact 到底在判断什么
shouldAutoCompact是整个自动压缩的入口闸门,它不是一个简单的“token 超了就压”,而是做了多层检查。理解这几层,你才能知道为什么有时候明明 token 很高却没触发压缩。
第一层是递归防护。源码里排除了session_memory、compact、marble_origami这几个来源。原因很直白:压缩过程本身也会发起查询,如果不排除,压缩触发的查询又触发压缩,就会递归死锁。compact是压缩过程自身发起的查询,session_memory是会话记忆压缩发起的查询,marble_origami是上下文折叠代理,启用CONTEXT_COLLAPSE时它的查询也会跳过自动压缩。
第二层是功能开关。它会检查DISABLE_COMPACT、DISABLE_AUTO_COMPACT这两个环境变量,以及用户设置。也就是说,你可以通过环境变量一键关掉压缩,这在调试压缩逻辑时非常有用。
第三层是实验功能协调。它和REACTIVE_COMPACT、CONTEXT_COLLAPSE等实验功能互斥,避免多套压缩机制同时生效打架。
第四层才是Token 阈值检查,通过getAutoCompactThreshold计算触发阈值。这里的关键点是:不同模型的阈值不一样,不能用一个固定数字套所有模型。
来源枚举这块值得单独列一下,因为它直接决定了哪些查询会被排除在压缩之外:
| 来源枚举 | 含义 | 是否参与压缩 |
|---|---|---|
repl_main_thread | 主 REPL 线程,用户交互主入口 | 是 |
repl_main_thread:outputStyle:custom | 主线程下自定义输出样式 | 是 |
sdk | SDK 直接调用的查询 | 是 |
agent:custom | 用户通过 /agent 启动的自定义代理 | 是 |
agent:builtin:fork | 内置分叉代理,递归防护会检查 | 视情况 |
compact | 压缩过程自身发起的查询 | 否,防递归 |
session_memory | 会话记忆压缩发起的查询 | 否,防递归 |
marble_origami | 上下文折叠代理 | 否 |
side_question | 侧边问题子查询 | 否 |
bash_classifier | Bash 命令分类器 | 否 |
这张表的核心信息是:只有主交互链路和代理链路的查询才会被纳入压缩判定,辅助性、工具性、压缩自身的查询全部排除。这样设计既避免了递归,也避免了把一次性辅助查询的 token 算进主会话预算。
4. 优先策略:会话内存折叠,零延迟的轻量压缩
判定需要压缩之后,Claude Code 并不是立刻调大模型去总结历史。它有一个优先策略:先尝试会话内存压缩(session memory compaction),对部分会话内存进行折叠,并保留完整内容的链接。
源码里这段逻辑在autoCompact.ts的 L316-L339 附近,核心调用是trySessionMemoryCompaction:
// EXPERIMENT: Try session memory compaction first const sessionMemoryResult = await trySessionMemoryCompaction( messages, toolUseContext.agentId, recompactionInfo.autoCompactThreshold, ) if (sessionMemoryResult) { // ... 清理和通知 return { wasCompacted: true, compactionResult: sessionMemoryResult, } }这个策略的设计优势有三个:轻量级,基于预计算的会话内存摘要,避免调用大模型;零延迟,本地执行,没有 API 调用成本;保真度高,保留原始消息结构和工具调用链。
它的输入是一个消息数组加上agentId和阈值。假设前 10 条消息已经被总结,lastSummarizedMessageId指向第 10 条消息的 uuid,那么第 11 条开始的消息会被保留:
const inputMessages: Message[] = [ { type: 'user', uuid: 'msg-1', message: { content: '你好,请帮我写一个Python函数...' } }, { type: 'assistant', uuid: 'msg-2', message: { content: '当然,以下是示例代码...' } }, // ... 更多历史消息(msg-3 到 msg-10) { type: 'user', uuid: 'msg-11', message: { content: '这个函数能再优化一下吗?' } }, { type: 'assistant', uuid: 'msg-12', message: { content: '可以,我们可以添加缓存机制...' } }, // ... 直到最新消息 msg-20 ]; const agentId = 'agent-123'; const autoCompactThreshold = 80000; // 自动压缩阈值:80k tokens const result = await trySessionMemoryCompaction( inputMessages, agentId, autoCompactThreshold );成功时的输出会带一个边界标记和摘要消息,关键字段是压缩前后的 token 数:
const outputResult: CompactionResult = { boundaryMarker: { type: 'system', uuid: 'compact-boundary-xyz', message: { id: 'system_compact', content: '对话已压缩', compactMetadata: { type: 'auto', preCompactTokenCount: 95000, lastMessageUuid: 'msg-20', preservedSegmentEndUuid: 'msg-20', preCompactDiscoveredTools: [] } } }, summaryMessages: [ { type: 'user', uuid: 'summary-abc', message: { content: `以下是我们对话的摘要:\n\n${truncatedSessionMemory}\n\n完整会话记录可查看:/path/to/transcript.json`, role: 'user', isCompactSummary: true, isVisibleInTranscriptOnly: true } } ], attachments: [], hookResults: [/* CLAUDE.md 恢复结果等 */], messagesToKeep: inputMessages.slice(10), preCompactTokenCount: 95000, postCompactTokenCount: 15000, truePostCompactTokenCount: 15000 };注意preCompactTokenCount: 95000到postCompactTokenCount: 15000这个落差,压缩比相当可观。摘要内容本身也有截断逻辑,flushSessionSection会逐行保留,直到累计字符数接近maxCharsPerSection,然后追加一行截断标记:
{ "truncatedContent": "# Session Title\n_A short and distinctive 5-10 word descriptive title for the session._\n长时间调试会话\n\n# Worklog\n_Step by step, what was attempted, done?_\nStep 1: 初始化项目...\nStep 2: 安装依赖...\n(保留前约 8000 字符的行)\n[... section truncated for length ...]", "wasTruncated": true }这个截断标记很重要,它告诉模型“这里被裁过”,避免模型误以为摘要就是全部内容。
5. 回退机制:常规模型压缩怎么兜底
如果会话内存压缩不适用——比如没有预计算的会话内存摘要,或者当前场景不满足折叠条件——就会走常规模型压缩回退:对历史消息进行总结,降低总 token 数。
这条路径和会话内存压缩的区别在于:它需要真正调用大模型来做总结,因此有 API 成本和延迟,但适用面更广。压缩完成后同样会生成包含摘要信息的系统边界标记,并保留关键消息,确保对话连贯性的同时控制 token 总量。
两条路径的取舍逻辑可以这样理解:会话内存压缩是“本地折叠 + 保留链接”,快且免费,但依赖预计算摘要;常规模型压缩是“调模型总结”,慢且有成本,但几乎总能兜住。实际运行中,优先走前者,前者不行才走后者。
6. 可复制配置骨架:settings.json 与 config.toml
理解了链路,接下来是能直接抄的配置。Claude Code 的压缩行为受环境变量和用户设置双重影响,下面这份骨架把关键开关都列出来了。
settings.json骨架:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的key", "DISABLE_AUTO_COMPACT": "0", "DISABLE_COMPACT": "0" }, "autoCompact": { "enabled": true, "thresholdRatio": 0.85, "preserveRecentMessages": 10, "maxCharsPerSection": 8000 } }config.toml骨架:
[model] name = "claude-sonnet" context_window = 200000 [compact] auto = true threshold_ratio = 0.85 preserve_recent = 10 max_chars_per_section = 8000 session_memory_first = true [compact.experimental] reactive_compact = false context_collapse = false几个参数的含义:thresholdRatio控制触发阈值占上下文窗口的比例,0.85 表示用到 85% 时触发;preserveRecentMessages对应源码里的messagesToKeep,保留最近 N 条不压缩;maxCharsPerSection对应flushSessionSection的单节字符上限;session_memory_first决定是否优先走会话内存折叠。
注意:
DISABLE_AUTO_COMPACT和DISABLE_COMPACT设为1会关闭压缩,调试时可以用它对比压缩前后的行为差异,但生产环境别关,否则长会话必炸。
7. 验证压缩是否生效:三步操作
配置写完,怎么确认压缩真的触发了?给你三步可跟做的验证。
第一步,开一个长会话,持续对话直到 token 接近阈值。你可以用一段循环脚本灌入大量文本,观察是否出现prompt_too_long。如果压缩生效,会话应该能继续,而不是报错中断。
第二步,检查输出里是否出现压缩边界标记。压缩成功后会生成system_compact类型的边界消息,compactMetadata.type为auto,并带有preCompactTokenCount和postCompactTokenCount。你可以把响应日志打到文件里,用 grep 过滤:
grep -n "system_compact\|preCompactTokenCount\|postCompactTokenCount" claude_session.log第三步,对比压缩前后的 token 数。如果postCompactTokenCount明显小于preCompactTokenCount,说明压缩链路走通了。如果两者接近,可能是阈值没到,或者压缩被环境变量关掉了。
# 统计压缩事件次数 grep -c "compactMetadata" claude_session.log # 查看最近一次压缩的 token 落差 grep "postCompactTokenCount" claude_session.log | tail -18. 常见报错排查
报错一:prompt_too_long依然出现。先确认DISABLE_AUTO_COMPACT和DISABLE_COMPACT没被设成1。再检查thresholdRatio是不是设得太高,比如 0.98,导致压缩还没来得及触发就已经超窗。调到 0.8 到 0.85 之间比较稳。
报错二:压缩后模型答非所问。大概率是preserveRecentMessages设得太小,最近的关键上下文被压掉了。把它从 10 调到 15 或 20 试试。另外检查maxCharsPerSection是不是太小,导致摘要被截断得太狠。
报错三:压缩反复触发,会话卡顿。这通常是递归防护没生效,或者实验功能互斥没配好。确认reactive_compact和context_collapse没有和auto同时开启。源码里shouldAutoCompact会排除compact、session_memory、marble_origami来源,如果你的自定义代理来源没被正确标记,就可能绕过防护。
报错四:会话内存压缩一直不生效,总是走模型压缩。检查是否有预计算的会话内存摘要。trySessionMemoryCompaction依赖lastSummarizedMessageId指向的已总结消息,如果这个指针缺失或失效,就会回退到模型压缩。确认session_memory_first为true。
9. 继续深入与接入入口
压缩机制调通之后,下一步通常是把它接进真实的编码工作流。如果你要长期跑编码任务或 Agent,可以看 Coding Plan 把压缩策略和编码计划结合起来:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。想先单轮验证模型对压缩摘要的理解是否准确,用模型对话入口最快:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。需要管理多个实验用 Key、看调用量,去控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。创建和管理 Key 在 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。接入细节和参数说明查文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你在用 Claude Code 的 Anthropic 兼容模式,这个页面有专门说明:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite 。
我自己的习惯是:先用模型对话确认摘要质量,再把thresholdRatio和preserveRecentMessages固定下来,最后才上 Coding Plan 跑长任务。压缩参数没有万能值,得按你的会话长度和任务类型微调,跑几轮日志对比,比看任何文档都管用。