职责概述

解决的问题:大模型有上下文窗口限制(200K token),但一个复杂的编程任务很容易超过这个限制——几十轮对话、几十个文件的内容、各种工具返回结果。需要一个智能的"内存管理器",在窗口快满时决定保留什么、丢弃什么,确保对话不会因为超限而崩溃。

应用场景:① 对话进行到第 20 轮,上下文快满了 → 自动压缩早期对话,保留关键决策 ② 用户打开一个大文件进行分析 → 文件内容占满窗口 → 智能截断只保留相关部分 ③ 长期记忆:把重要信息写入 ~/.claude/ 文件,下次对话还能用 ④ 工具返回的冗长输出自动精简。

一句话理解:就像你的工作记忆——你不能同时记住所有事,所以你会做笔记(Memory)、丢掉不重要的细节(Microcompact)、定期总结(Auto Compact),确保脑子不被撑爆。

架构设计

上下文构建层
context.ts
getUserContext()
getSystemContext()
getGitStatus()
压缩策略层
microCompact.ts
timeBasedMCConfig.ts
apiMicrocompact.ts
compact.ts
压缩编排层
autoCompact.ts
sessionMemoryCompact.ts
grouping.ts
记忆持久化层
SessionMemory/
extractMemories/
utils/memory/
基础设施层
prompt.ts
postCompactCleanup.ts
compactWarningState.ts

核心数据流

上下文生命周期

1. 上下文构建
getUserContext() 加载 CLAUDE.md + 日期; getSystemContext() 加载 git 状态 + 缓存破坏标记
2. 对话膨胀
每轮 API 调用后 token 增长,tokenCountWithEstimation() 估算当前用量
3. Microcompact
microcompactMessages() 在 API 调用前清除旧工具结果,保留缓存前缀
4. Session Memory
后台 forked agent 提取会话笔记到 markdown 文件,作为压缩时的摘要替代
5. Auto Compact
token 超过阈值时触发全量摘要或 session memory 替换,重建上下文
6. Post-Compact 恢复
重新注入文件内容、工具 schema、agent 状态、技能附件等关键上下文

压缩决策流程

shouldAutoCompact() 判断
检查: DISABLE_COMPACT 环境变量? 非 compact/session_memory 源? 非 CONTEXT_COLLAPSE 模式? token 超过阈值?
trySessionMemoryCompaction()
优先尝试 Session Memory 压缩 — 无需 API 调用,零延迟,保留最近消息
compactConversation() 回退
Session Memory 不可用时,forked agent 生成摘要,支持缓存共享和 PTL 重试
runPostCompactCleanup()
清理 microcompact 状态、重置 memory 文件缓存、通知缓存破坏检测

内存层级

对话历史
实时消息流,最高优先级
Session Memory
后台提取的会话笔记 Markdown
Compact Summary
紧急压缩时生成的摘要文本
Auto Memory
extractMemories 持久化到磁盘

关键类型与接口

CompactionResult — 压缩结果

// services/compact/compact.ts:299-310
export interface CompactionResult {
  boundaryMarker: SystemMessage          // 压缩边界标记
  summaryMessages: UserMessage[]         // 摘要消息
  attachments: AttachmentMessage[]       // 附件(文件、计划、技能)
  hookResults: HookResultMessage[]       // Hook 产生的消息
  messagesToKeep?: Message[]             // 保留的最近消息
  userDisplayMessage?: string            // 用户可见的提示
  preCompactTokenCount?: number          // 压缩前 token 数
  postCompactTokenCount?: number         // 压缩后 token 数
  truePostCompactTokenCount?: number     // 真实压缩后 token 估算
  compactionUsage?: ReturnType<typeof getTokenUsage>
}

SessionMemoryConfig — Session Memory 配置

// services/SessionMemory/sessionMemoryUtils.ts:18-29
export type SessionMemoryConfig = {
  /** 初始化最低 token 数(默认 10000) */
  minimumMessageTokensToInit: number
  /** 两次更新间最小 token 增长(默认 5000) */
  minimumTokensBetweenUpdate: number
  /** 两次更新间工具调用数(默认 3) */
  toolCallsBetweenUpdates: number
}

MicrocompactResult — 微压缩结果

// services/compact/microCompact.ts:207-220
export type MicrocompactResult = {
  messages: Message[]
  compactionInfo?: {
    pendingCacheEdits?: PendingCacheEdits  // 待执行的缓存编辑
  }
}

AutoCompactThreshold — 自动压缩阈值计算

// services/compact/autoCompact.ts:72-91
export function getAutoCompactThreshold(model: string): number {
  const effectiveContextWindow = getEffectiveContextWindowSize(model)
  const autocompactThreshold =
    effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS  // 13000 buffer
  // 支持 CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 百分比覆盖
  return autocompactThreshold
}

消息分组 — API 轮次分组

// services/compact/grouping.ts:22-63
export function groupMessagesByApiRound(messages: Message[]): Message[][] {
  // 按 assistant message.id 边界分组
  // 每个 API 调用产生一个 group,保证 tool_use/tool_result 配对完整
  let lastAssistantId: string | undefined
  for (const msg of messages) {
    if (msg.type === 'assistant' && msg.message.id !== lastAssistantId
        && current.length > 0) {
      groups.push(current)
      current = [msg]
    }
    // ...
  }
}

设计模式与亮点

1. 多级渐进压缩策略

系统实现了从轻到重的四级压缩:时间驱动的 Microcompact(清除旧工具结果) → 缓存感知的 Microcompact(cache_edits API) → Session Memory 替换(零 API 调用) → 全量 Compact(forked agent 摘要)。每一级都是上一级的降级方案,确保在最极端情况下也能回收上下文空间。

2. Forked Agent 缓存共享

压缩和 Session Memory 提取都通过 runForkedAgent() 创建一个"完美分叉"——相同的 system prompt、tools、消息前缀。这意味着分叉的 agent 可以直接命中主线程的 prompt cache,避免重复发送大量已有上下文。这是 cacheSafeParams 的核心价值。

3. PTL(Prompt-Too-Long)自适应重试

压缩请求本身也可能超限。truncateHeadForPTLRetry() 在 API 轮次分组后丢弃最旧的分组,逐步缩小待摘要的消息集,最多重试 3 次。这是一种优雅的自愈机制。

// services/compact/compact.ts:243-291
export function truncateHeadForPTLRetry(
  messages: Message[],
  ptlResponse: AssistantMessage,
): Message[] | null {
  const groups = groupMessagesByApiRound(input)
  if (groups.length < 2) return null
  const tokenGap = getPromptTooLongTokenGap(ptlResponse)
  // 按 token 缺口计算要丢弃的分组数
  // 保底:丢弃 20%
  // 确保至少保留一个分组可摘要
}

4. 熔断器模式(Circuit Breaker)

MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3。连续压缩失败 3 次后停止尝试,防止在不可恢复的上下文溢出场景下无限重试。BQ 数据显示修复前有 1,279 个 session 产生了 50+ 次连续失败,每天浪费约 250K 次 API 调用。

5. Sequential + 尾随提取(Trailing Extraction)

Session Memory 和 extractMemories 都使用 sequential() 包装确保同一时刻只有一个提取在运行。当新请求到达时,它被暂存为 pendingContext,等当前提取完成后自动运行一次尾随提取,确保不丢消息。

6. 闭包作用域状态(Closure-scoped State)

initExtractMemories() 将所有可变状态(cursor、inProgress、pendingContext)封装在闭包内,而非模块级变量。测试在 beforeEach 中调用 init 获得干净状态,避免了跨测试污染。

开发者实践指南

扩展压缩策略时,优先在 microcompact 层添加轻量清除逻辑;只有在需要语义理解时才走 compactConversation。

如何添加新的可压缩工具

microCompact.tsCOMPACTABLE_TOOLS 集合中添加工具名称即可:

// services/compact/microCompact.ts:41-50
const COMPACTABLE_TOOLS = new Set<string>([
  FILE_READ_TOOL_NAME,
  ...SHELL_TOOL_NAMES,
  GREP_TOOL_NAME,
  GLOB_TOOL_NAME,
  // 在此添加新工具名
])

如何调整压缩阈值

自动压缩阈值由模型上下文窗口减去 13,000 buffer 决定。可通过环境变量覆盖:

# 百分比覆盖(1-100)
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=80

# 绝对覆盖(token 数)
CLAUDE_CODE_AUTO_COMPACT_WINDOW=100000

# 完全禁用自动压缩
DISABLE_AUTO_COMPACT=1

Post-Compact 附件注入

压缩后会丢失文件内容和工具 schema。createPostCompactFileAttachments() 重新注入最近读取的文件(最多 5 个,50K token 预算),getDeferredToolsDeltaAttachment() 重新宣告工具 schema。自定义附件可通过 AttachmentMessage 类型在 compactConversation() 中注册。

架构师决策指南

设计权衡:Session Memory vs Legacy Compact

Session Memory 压缩
  • 零 API 调用,即时完成
  • 保留最近消息不变
  • 依赖后台提取质量
  • 需要 GrowthBook 双 flag(session_memory + sm_compact)
Legacy 全量压缩
  • 需要 forked agent API 调用
  • 替换全部消息为摘要
  • 摘要质量由模型保证
  • 支持缓存共享减少开销

上下文窗口利用率的考量

系统使用 effectiveContextWindow = contextWindow - reservedTokensForSummary(最大 20,000) 作为可用空间。autocompact 在使用率达到约 97% 时触发(扣除 13,000 buffer)。这个 buffer 需要平衡:太小会导致频繁压缩影响体验,太大会浪费上下文空间。当前 13K 的设定基于 p99.99 的 compact summary 输出为 17,387 tokens 的统计。

缓存一致性挑战

压缩操作会破坏 prompt cache。系统通过三种方式缓解:(1) 缓存共享路径让 forked agent 复用主线程 cache;(2) notifyCompaction() 通知缓存破坏检测系统忽略预期的 cache read 下降;(3) cache_edits API 在不修改本地消息的情况下通过服务端编辑清除工具结果。这三者协同确保压缩后的首次请求不会因为 cache miss 而被误判为缓存断裂。

压缩后重新注入附件(文件内容、工具 schema、技能)会增加 25-50K tokens 的 cache_creation 开销。在成本敏感场景中,应评估附件数量和大小。

可视化处理拓扑图

上下文管理是一个 4 层渐进式压缩管线:从最廉价的消息裁剪到最昂贵的 AI 摘要生成。每一层在前一层无法释放足够空间时才触发,90%+ 的场景在前两层即完成。压缩完成后执行附件重建和进程级状态清理,确保对话连贯性。

上下文构建入口每次 API 调用:getUserContext() + getSystemContext() — context.ts:116-189
shouldAutoCompact() 触发检查autoCompact.ts:160 — tokenUsage / effectiveWindow >= 97%?含熔断器
触发 → 进入 4 层压缩管线(廉价 → 昂贵) ↓

Layer 1-2 — 轻量压缩:Snip / Microcompact

1
Snip — 消息裁剪 · 0ms · 无 AI 调用
直接移除最早的历史消息对(user+assistant)。不修改消息内容,只缩短消息数组长度。autoCompact.ts:160-239 中 snipTokensFreed 参数追踪释放量。
释放足够 → 返回
仍超阈值 → 继续 ↓
2
Microcompact — Cache Editing · 0ms · 无 AI 调用
microcompactMessages() (microCompact.ts:253)。两条子路径:
(a) cachedMicrocompactPath() — 通过 cache_edits API 服务端清除工具结果,本地消息不变
(b) maybeTimeBasedMicrocompact() — 长时间间隔时清除旧工具结果
释放足够 → 返回
仍超阈值 → 进入重量级压缩 ↓

Layer 3 — 重量级压缩:Compact (AI 摘要)

3
compactConversation() — AI 摘要生成 · ~5s · Sonnet 调用
compact.ts:387。将完整消息历史发送给模型生成结构化摘要。
PreCompact Hooks 执行compact.ts:387 — 用户自定义压缩逻辑
streamCompactSummary()compact.ts:1136 — 构建 compact prompt + prompt cache sharing
摘要生成成功替换消息历史为
SystemCompactBoundaryMessage
Prompt-Too-LongtruncateHeadForPTLRetry()
丢弃最老轮次组重试 ×3

Layer 4 — 后压缩:附件重建 + 状态清理

压缩完成 — 摘要替换旧消息buildPostCompactMessages() — compact.ts:330
附件重建(4 类)createPostCompactFileAttachments() — 最近读取文件恢复createPlanAttachmentIfNeeded() — 计划文件恢复createSkillAttachmentIfNeeded() — 已激活技能恢复createAsyncAgentAttachmentsIfNeeded() — 后台 agent 状态恢复
runPostCompactCleanup()postCompactCleanup.ts:31 — 重置缓存 + 状态
主线程压缩clear getUserContext.cache
resetGetMemoryFilesCache
clearSystemPromptSections
子 agent 压缩仅 resetMicrocompactState
+ clearSystemPromptSections
不触碰主线程缓存
SystemCompactBoundaryMessage 创建compactBoundary + PostCompact hooks + SessionStart hooks
上下文窗口恢复到健康水位通知 notifyCompaction() — 缓存断裂检测忽略预期下降
渐进设计要点:4 层压缩从廉价到昂贵逐层升级。Layer 1-2 不调用 AI、不修改本地消息(Snip 截断 / cache_edits 服务端编辑),几乎零成本。Layer 3 的 compactConversation 是唯一需要 AI 调用的层,通过 prompt cache sharing 复用主对话前缀最大化缓存命中。Layer 4 的附件重建通过 delta 增量机制(而非全量重发)减少 cache_creation 开销。子 agent 与主线程的压缩清理通过 querySource 前缀隔离,防止子 agent 清空共享进程的模块级缓存。

核心处理流程详解

上下文管理系统的核心职责是在有限的上下文窗口中保持对话连贯性。它由三个层次组成:顶层上下文构建(context.ts)、自动压缩引擎(autoCompact.ts + compact.ts)和轻量微压缩(microCompact.ts)。以下是从上下文构建到压缩完成的完整处理链路:

1. 顶层上下文构建
每次 API 调用需要两类上下文。getUserContext()(context.ts:155)通过 getMemoryFiles() 发现 CLAUDE.md 文件并解析为 claudeMd 字符串,还包含当前日期。getSystemContext()(context.ts:116)包含 git 状态快照(分支、状态、最近提交)和可选的缓存破坏注入。两者都通过 memoize 缓存,确保同一会话中只计算一次 — context.ts:116-189
2. 自动压缩触发检查
shouldAutoCompact()(autoCompact.ts:160)在每个 API 轮次后被调用。它计算当前 token 使用量与阈值(getAutoCompactThreshold(),autoCompact.ts:72)的比值。阈值由 getEffectiveContextWindowSize()(autoCompact.ts:33)计算(上下文窗口大小减去最大输出 token),再扣除约 13K buffer。当使用率达到约 97% 时触发自动压缩。还包含熔断器逻辑——连续压缩失败时暂停自动压缩 — autoCompact.ts:160-239
3. 微压缩预处理
在完整压缩之前,microcompactMessages()(microCompact.ts:253)尝试轻量级清理。两种策略:(1) cachedMicrocompactPath()(microCompact.ts:305)通过 cache_edits API 在不修改本地消息的情况下清除服务端的大体量工具结果;(2) maybeTimeBasedMicrocompact()(microCompact.ts:446)当对话出现长时间间隔时,清除间隔前的旧工具结果。时间策略通过 evaluateTimeBasedTrigger()(microCompact.ts:422)评估 — microCompact.ts:253-530
4. 压缩引擎执行(compactConversation)
这是核心压缩路径。compactConversation()(compact.ts:387)首先执行 PreCompact hooks(允许用户自定义压缩逻辑),然后调用 streamCompactSummary()(compact.ts:1136)将完整消息历史发送给模型生成摘要。如果摘要请求本身触发 prompt-too-long,truncateHeadForPTLRetry()(compact.ts:243)逐步丢弃最老的 API 轮次组并重试(最多 3 次)— compact.ts:387-763
5. 摘要流式生成
streamCompactSummary()(compact.ts:1136)构建压缩专用 prompt(通过 getCompactPrompt() 获取模板),利用 prompt cache sharing(tengu_compact_cache_prefix 特性开关,默认启用)复用主对话的缓存前缀。在 Fork agent 路径中,压缩请求的 API 前缀与主对话完全一致,最大化 cache 命中率 — compact.ts:1136-1396
6. 附件重建与状态恢复
压缩后需要重建被丢弃的上下文。createPostCompactFileAttachments()(compact.ts:1415)将最近读取过的文件(最多 POST_COMPACT_MAX_FILES_TO_RESTORE 个)作为附件重新注入。createPlanAttachmentIfNeeded()createSkillAttachmentIfNeeded()createAsyncAgentAttachmentsIfNeeded() 分别恢复计划、技能和异步 agent 状态。还通过 getDeferredToolsDeltaAttachment() 重新通告延迟加载工具的 schema — compact.ts:1415-1600
7. 后压缩清理
runPostCompactCleanup()(postCompactCleanup.ts:31)执行全面的缓存和状态清理:重置微压缩状态、清除上下文折叠缓存、刷新 CLAUDE.md 文件缓存(getUserContext.cache.clear())、清除权限分类器缓存、推测性检查缓存。关键设计:子 agent 的压缩不重置主线程的模块级状态(通过 querySource 前缀判断)——子 agent 共享同一进程的模块变量 — postCompactCleanup.ts:31-77
8. 边界标记与 SessionStart hooks
压缩完成后创建 SystemCompactBoundaryMessage(compact.ts 中的 createCompactBoundaryMessage())作为消息历史中的分界点。边界消息携带压缩前的 token 数量和 pre-compact 发现的工具名列表。随后执行 PostCompact hooks 和 SessionStart hooks(重新加载 hook 指令),使 hook 系统在压缩后保持一致 — compact.ts:600-760
关键处理逻辑:上下文管理的核心矛盾是"压缩会破坏 prompt cache"。系统通过三层机制缓解:第一层,压缩请求复用主对话的 system prompt 前缀(通过 cacheSafeParams.forkContextMessages),最大化 cache 命中;第二层,notifyCompaction() 通知缓存断裂检测系统忽略预期的 cache read 下降,避免误报;第三层,压缩后重新注入的附件通过 delta 增量机制(而非全量重发)减少 cache_creation 开销。三层协同使压缩操作的成本从"接近全量重建"降低到"仅摘要输出 + 增量附件"。

设计精华

1. 微压缩的 Cache Editing API

传统压缩需要一次完整的 API 调用来生成摘要,而微压缩通过 cache_edits API 实现零模型调用的上下文缩减。具体机制:不修改本地消息历史,而是通过服务端 API 编辑(pinCacheEdits(),microCompact.ts:111)清除大体量工具结果块。下次 API 请求时,服务端应用编辑,工具结果变为空——本地消息不变,但发送到 API 的内容减少了。这样 prompt cache 前缀完全不变(因为消息结构未修改),只丢了工具结果的 body。

Cache Edits 生命周期(microCompact.ts:88-128)
pending → pin → re-send,零 API 调用清除工具结果
// 收集可压缩的工具调用 ID
const compactableIds = collectCompactableToolIds(messages)
// 生成 cache_edits block(不修改本地消息)
// 下次 API 请求时通过 cache_edits 参数发送
// 服务端在读取时应用编辑,工具结果被替换为空
设计洞察:Cache Editing 是"数据不动、视图变"的典范。它利用了 Anthropic API 的 cache_edits 扩展能力,在服务端修改 prompt cache 的内容而不影响客户端状态。这意味着压缩后如果需要回滚(比如自动压缩后用户撤销),本地消息完整无损。

2. 进程级状态隔离的精细控制

子 agent 与主线程共享同一个 Node.js 进程和模块级变量。runPostCompactCleanup()(postCompactCleanup.ts:31)通过 isMainThreadCompact 标志精细控制哪些状态可以被重置。主线程压缩可以清空 getUserContext 的 memoize 缓存和 CLAUDE.md 文件缓存;子 agent 压缩跳过这些——因为清空主线程的上下文缓存会导致主线程的下一个 API 调用使用陈旧或不一致的数据。

主线程 vs 子 agent 状态清理(postCompactCleanup.ts:36-61)
子 agent 压缩只清理共享状态,不触碰主线程专属缓存
const isMainThreadCompact =
  querySource === undefined ||
  querySource.startsWith('repl_main_thread') ||
  querySource === 'sdk'

resetMicrocompactState()  // 所有路径都执行
if (isMainThreadCompact) {
  getUserContext.cache.clear?.()   // 仅主线程
  resetGetMemoryFilesCache('compact')  // 仅主线程
}
clearSystemPromptSections()  // 所有路径
设计洞察:这种"按 querySource 分级清理"的设计避免了为子 agent 引入独立进程的开销。一个进程内通过命名约定(repl_main_threadagent:builtin:*)区分不同执行上下文,用最小的运行时成本实现了逻辑隔离。querySource 成为了整个系统的"上下文身份标识"。

3. PTL 重试的渐进式截断

压缩请求本身也可能触发 prompt-too-long(PTL)错误——当消息历史太长时,连同压缩 prompt 一起超过了上下文窗口。truncateHeadForPTLRetry()(compact.ts:243)不是简单地截断消息,而是通过 groupMessagesByApiRound()(grouping.ts:22)按 API 轮次边界分组,逐组丢弃最老的轮次,直到 token 差额被覆盖。最多重试 3 次(MAX_PTL_RETRIES)。这确保了即使在极端情况下(用户从未手动压缩),系统也能完成压缩操作。

设计洞察:按 API 轮次(而非按消息)截断保证了截断后的消息序列仍然符合 API 规范——每个 assistant 消息后要么是 tool_result + assistant,要么是 user。如果按消息截断,可能在一个 API 调用的中间断开,导致 API 拒绝请求。

4. 压缩后附件的增量重建

压缩后不是全量重发所有上下文附件,而是通过 delta 机制增量注入。getDeferredToolsDeltaAttachment() 计算压缩前后工具 schema 的差异,只发送新增或变更的工具定义。同样,getAgentListingDeltaAttachment()getMcpInstructionsDeltaAttachment() 也走 delta 路径。唯一需要全量重发的是文件附件(因为压缩丢失了"模型已看过哪些文件"的信息),但限制在 POST_COMPACT_MAX_FILES_TO_RESTORE 个以内。

设计洞察:Delta 附件机制的核心假设是"压缩只改变消息历史,不改变工具和 MCP 配置"。这个假设在绝大多数情况下成立——压缩发生在对话中间,工具和 MCP 服务器配置不会因此改变。利用这个不变量,delta 机制将附件重建的 cache_creation 开销从约 50K tokens 降低到约 5-10K tokens。

Agent 实践借鉴 — 客服 Agent 上下文管理设计

场景映射:客服 agent 在上下文管理上的真实问题

客服场景的上下文管理和代码助手完全不同——不是"文件很多",而是"对话很长、数据很大、客户会反复来"。典型痛点包括:

借鉴 CC + 客服改造

1. Token 预算监控 + 阈值触发(借鉴 CC shouldAutoCompact)

CC 在每个 API 轮次后检查 token 使用率,达到 ~97% 就启动压缩。客服场景按对话轮数或 token 数触发:

// 借鉴 CC 的 shouldAutoCompact → 客服场景的主动式上下文管理
const SERVICE_CONTEXT_LIMITS = {
  maxTokens: 128_000,          // 模型上下文窗口
  bufferTokens: 8_000,         // 预留给回复的 buffer
  compactThreshold: 0.85,      // 85% 时触发压缩(比 CC 的 97% 更保守)
  maxConversationRounds: 25,   // 超过 25 轮主动压缩
}

// 每轮对话后检查——不等溢出
function shouldCompactServiceContext(
  tokenUsage: number,
  roundCount: number,
): { needed: boolean; reason: string } {
  const threshold = SERVICE_CONTEXT_LIMITS.maxTokens
    * SERVICE_CONTEXT_LIMITS.compactThreshold

  if (tokenUsage >= threshold) {
    return { needed: true, reason: `token 使用率 ${Math.round(tokenUsage/threshold*100)}% 超过阈值` }
  }
  if (roundCount >= SERVICE_CONTEXT_LIMITS.maxConversationRounds) {
    return { needed: true, reason: `对话轮数 ${roundCount} 超过上限` }
  }
  return { needed: false, reason: '' }
}

// 熔断器——借鉴 CC 的连续失败保护
let compactFailures = 0
const MAX_COMPACT_FAILURES = 3

async function tryCompactServiceContext(messages: Message[]): Promise<void> {
  if (compactFailures >= MAX_COMPACT_FAILURES) {
    logWarning('上下文压缩连续失败3次,跳过压缩,降低回复质量')
    return
  }
  try {
    await compactServiceMessages(messages)
    compactFailures = 0
  } catch (e) {
    compactFailures++
    logError('上下文压缩失败', e)
  }
}

2. 微压缩:清除工具返回的大块原始数据(借鉴 CC Microcompact)

CC 的 Microcompact 在不调用 AI 的情况下清除旧工具结果。客服场景的微压缩重点是"数据瘦身"——物流 50 节点变 3 节点、订单详情只保留关键字段:

// 借鉴 CC 的 microcompactMessages → 客服场景的数据瘦身
// 零 AI 调用,直接压缩工具返回的原始数据

interface ServiceMicrocompactRules {
  toolName: string
  maxDataPoints: number        // 最多保留多少条数据
  keepStrategy: 'latest' | 'important' | 'summary'
}

// 客服场景的压缩规则
const SERVICE_COMPACT_RULES: ServiceMicrocompactRules[] = [
  {
    toolName: 'query_logistics',    // 物流查询
    maxDataPoints: 3,               // 50 个节点 → 只保留最新 3 个
    keepStrategy: 'latest',
  },
  {
    toolName: 'query_order_list',   // 订单列表
    maxDataPoints: 5,               // 可能有 20+ 订单 → 只保留最近 5 个
    keepStrategy: 'latest',
  },
  {
    toolName: 'query_customer',     // 客户信息
    maxDataPoints: 999,             // 客户信息本身不大,保留完整
    keepStrategy: 'important',
  },
]

function microcompactServiceData(
  messages: Message[],
  rules: ServiceMicrocompactRules[],
): Message[] {
  return messages.map(msg => {
    if (msg.type !== 'tool_result') return msg

    const rule = rules.find(r => r.toolName === msg.toolName)
    if (!rule) return msg

    // 按规则压缩原始数据
    const data = JSON.parse(msg.content)
    const compressed = compressData(data, rule)

    return { ...msg, content: JSON.stringify(compressed) }
  })
}

// 具体压缩:物流轨迹 50 节点 → 最新 3 个 + 状态摘要
function compressLogisticsData(raw: LogisticsResponse): CompressedLogistics {
  const latestNodes = raw.trackingNodes.slice(-3)
  return {
    orderNo: raw.orderNo,
    currentStatus: raw.currentStatus,         // "运输中"
    estimatedArrival: raw.estimatedArrival,    // "2024-01-15"
    latestNodes,                               // 只保留最新 3 个节点
    totalNodes: raw.trackingNodes.length,      // 标注原始节点数
  }
  // 压缩前: ~3000 tokens → 压缩后: ~300 tokens,节省 90%
}

3. 会话摘要:20 轮对话压缩为关键信息(借鉴 CC AutoCompact)

CC 用 AI 生成结构化摘要替换旧消息。客服场景也用 AI 摘要,但模板不同——保留客户诉求、已处理事项、待办事项:

// 借鉴 CC 的 compactConversation → 客服场景的会话摘要
async function compactServiceMessages(messages: Message[]): Promise<CompactResult> {
  // 1. 分离:保留最近 5 轮不压缩(当前上下文最重要)
  const recentRounds = messages.slice(-10)  // 最近 5 轮(每轮 user+assistant)
  const oldRounds = messages.slice(0, -10)

  if (oldRounds.length === 0) return { messages, compressed: false }

  // 2. AI 生成客服场景的结构化摘要
  const summary = await generateServiceSummary(oldRounds)

  // 3. 合并:摘要 + 最近消息
  const compactedMessages = [
    createSummaryMessage(summary),
    ...recentRounds,
  ]

  return { messages: compactedMessages, compressed: true }
}

// 客服场景的摘要模板——保留关键业务信息
async function generateServiceSummary(oldMessages: Message[]): Promise<string> {
  const prompt = `请将以下客服对话历史压缩为结构化摘要,保留以下关键信息:
1. 客户诉求(一句话概括)
2. 已处理事项(列表)
3. 待办事项(如果有)
4. 客户情绪状态(正常/不满/愤怒)
5. 已承诺事项(如"24小时内回电")

对话历史:
${formatMessages(oldMessages)}`

  const summary = await callLLM(prompt)
  return summary
}

// 摘要示例输出:
// """
// 【客户诉求】退货 ORD-20240101,原因:收到商品破损
// 【已处理】已查订单(已签收)、已查物流(显示正常配送)、已提交退货申请
// 【待办】等待仓库确认收货后退款
// 【客户情绪】不满,因物流显示正常但实际破损,已安抚并承诺加急处理
// 【已承诺】48小时内退款到账
// """

4. 跨会话记忆:客户画像持久化(借鉴 CC Memory Extraction)

CC 的 extractMemories 把关键事实写入磁盘。客服场景需要提取客户偏好、历史问题、关键事实,持久化到 CRM 或独立记忆库:

// 借鉴 CC 的 initExtractMemories → 客服场景的跨会话记忆
interface CustomerMemory {
  customerId: string
  preferences: string[]       // 如 ["偏好上午回电", "不喜欢机器人回复"]
  frequentIssues: string[]    // 如 ["物流查询(最近3次进线都是查物流)"]
  importantFacts: string[]    // 如 ["对花生过敏(影响食品推荐)", "住址无电梯(影响大件配送)"]
  lastContactDate: string
  lastContactSummary: string  // 上次对话的一句话摘要
}

// 对话结束时提取记忆——借鉴 CC 的尾随提取
async function extractCustomerMemory(
  customerId: string,
  messages: Message[],
): Promise<CustomerMemory> {
  const existingMemory = await loadCustomerMemory(customerId)

  // AI 提取本次对话的新增信息
  const prompt = `从以下客服对话中提取需要跨会话记住的关键信息:

已有记忆:${JSON.stringify(existingMemory)}
本次对话:
${formatMessages(messages)}

请输出 JSON:
{
  "newPreferences": [],           // 新发现的客户偏好
  "newFrequentIssues": [],        // 本次咨询的问题类型
  "newImportantFacts": [],        // 新发现的重要事实
  "contactSummary": "",           // 本次对话的一句话摘要
}`

  const extracted = await callLLM(prompt)

  // 合并已有记忆 + 新增信息
  return mergeMemories(existingMemory, extracted)
}

// 同一客户重复进线——直接加载记忆,不重复询问
async function loadContextForRepeatCustomer(customerId: string): Promise<string> {
  const memory = await loadCustomerMemory(customerId)
  if (!memory) return ''

  return `[客户记忆] ${memory.preferences.join(';')}
[常咨询问题] ${memory.frequentIssues.join(';')}
[重要事实] ${memory.importantFacts.join(';')}
[上次接触] ${memory.lastContactDate} - ${memory.lastContactSummary}`
}

// 使用:客户再次进线时,记忆自动注入 system prompt
// agent 直接知道"这个客户上周退过货,偏好上午回电"

落地清单

必须抄的:
1. 渐进式压缩策略——微压缩(数据瘦身)→ 会话摘要(AI 压缩)→ 跨会话记忆(持久化),从廉价到昂贵逐层升级。
2. 主动触发(不等溢出)——85% token 或 25 轮对话时主动压缩,不等 API 报错才处理。
3. 跨会话记忆持久化——客户偏好、历史问题、关键事实写入数据库,下次进线自动加载。
4. 数据瘦身规则——物流 50 节点变 3 节点,订单列表只保留最近 5 个,工具返回必须精简。
不需要抄的:
1. Cache Editing API——客服场景不需要服务端编辑本地消息,直接本地压缩即可。CC 用 Cache Editing 是为了不破坏 prompt cache,客服场景对话短,cache 命中率本身就不高。
2. 4 层压缩——客服 3 层够用(微压缩 → 会话摘要 → 跨会话记忆),不需要 CC 的 Memory Extraction 那么复杂的自动记忆提取。
3. PTL 自适应重试——客服场景对话不会像代码助手那样达到极端长度,不需要逐步丢弃最老分组的重试机制。
常见坑: 1. 等到 token 溢出才压缩——客服场景最常见的错误。客户已经打了 30 分钟电话,agent 突然"失忆"(API 返回 prompt-too-long,压缩后摘要丢失了客户 15 分钟前的退货要求)。必须在 85% 时主动压缩,留出 buffer。 2. 微压缩丢掉了关键信息——物流 50 节点变 3 节点时,只保留了最新的,但客户投诉的其实是第 10 个节点("显示已签收但没收到")。压缩规则必须根据客户诉求动态调整,不能一刀切。 3. 跨会话记忆泄露隐私——客户 A 的记忆不能被客户 B 看到。记忆存储必须按 customerId 隔离,且敏感信息(手机号、地址)要脱敏后再存入记忆。

💡 Memory 文件创建机制详解

白话版:Memory 文件什么时候会被创建?

CC 的 Memory 文件有两条创建路径

路径一:对话中主动写入(随时可用)
CC 在对话过程中根据系统提示指引,自行判断是否需要写 memory 文件
用户:"记住我喜欢用 TypeScript"
  → CC 判断这是值得持久化的用户偏好
  → 调用 Write 工具写入 ~/.claude/projects/.../memory/user_role.md

触发条件:无特殊条件,只要 autoMemoryEnabled 没关就行
常见场景:用户说"记住..."、CC 发现重要的项目上下文、用户偏好等
路径二:后台自动提取(有严格前置条件)
每轮对话结束后,后台 Agent 回顾对话内容,提取值得记忆的信息
触发时机:每轮对话结束(模型回复完,没有要调工具了)
执行者:后台 forked Agent(5 轮预算)
写入位置:~/.claude/projects/<项目路径>/memory/*.md

必须同时满足的 5 个条件:
┌─────────────────────────────────────────────────────┐
│ ① EXTRACT_MEMORIES 编译期 feature flag = true       │ ← 基本都有
│ ② tengu_passport_quail 服务端开关 = true            │ ← 关键门控!
│ ③ isAutoMemoryEnabled() = true                      │ ← 默认 true
│ ④ 当前是主 Agent(不是子 Agent)                     │ ← 主对话才触发
│ ⑤ 对话轮数达到阈值(默认每轮都检查)                  │ ← tengu_bramble_lintel
└─────────────────────────────────────────────────────┘

条件 ② 是服务端 GrowthBook 远程配置控制的灰度开关。
如果 Anthropic 没有对你的账户开启,后台提取永远不会执行。

源码级触发链路

用户发送消息 → Query 循环执行 → 模型回复完成(无工具调用)
                                    │
                                    ▼
                         query/stopHooks.ts:handleStopHooks()
                                    │
                    ┌───────────────┼───────────────┐
                    │  检查 5 个前置条件               │
                    │  ① feature('EXTRACT_MEMORIES')  │
                    │  ② !toolUseContext.agentId       │
                    │  ③ isExtractModeActive()         │ ← tengu_passport_quail
                    │  ④ !isBareMode                   │
                    │  ⑤ turnsSinceLastExtraction >= N │
                    └───────────────┼───────────────┘
                                    │ 全部通过
                                    ▼
              services/extractMemories/extractMemories.ts
              executeExtractMemories()
                                    │
                                    ▼
                    runForkedAgent(后台 Agent,5 轮预算)
                    工具权限:Read/Grep/Glob 不限
                              Edit/Write 仅限 memory 目录
                                    │
                                    ▼
                    后台 Agent 读取对话历史 + 现有 memory 文件
                    决定是否写入/更新 memory 文件
                                    │
                                    ▼
                    ~/.claude/projects/<path>/memory/*.md

isAutoMemoryEnabled() 判断优先级

memdir/paths.ts — 本地配置检查
优先级从高到低:
1. CLAUDE_CODE_DISABLE_AUTO_MEMORY=1    → 直接关闭
2. CLAUDE_CODE_SIMPLE (--bare 模式)     → 直接关闭
3. CCR 远程模式且无 REMOTE_MEMORY_DIR   → 直接关闭
4. settings.json 中 autoMemoryEnabled   → 显式配置
5. 默认                                → true(开启)

isExtractModeActive() 判断 — 服务端开关

memdir/paths.ts — 服务端配置检查
export function isExtractModeActive(): boolean {
  // 门控:GrowthBook 远程配置 tengu_passport_quail
  if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)) {
    return false  // ← 默认 false,需要服务端开启
  }
  // 非交互模式也需要额外开关
  return !getIsNonInteractiveSession() ||
    getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_thimble', false)
}

相关 GrowthBook Feature Flags

Flag默认值作用
tengu_passport_quailfalse后台自动提取的总开关(最关键)
tengu_bramble_lintel1每 N 轮对话触发一次提取
tengu_onyx_plover24h/5 sessionsAuto-Dream 后台整合:每小时数/会话数
tengu_slate_thimblefalse允许非交互模式下的提取
tengu_moth_copsefalse跳过 MEMORY.md 索引,用附件召回
诊断你的 Memory 是否正常工作:
① 检查 memory 目录:ls ~/.claude/projects/*/memory/
② 检查本地配置:cat ~/.claude/settings.json | grep autoMemory
③ 检查环境变量:env | grep CLAUDE_CODE_DISABLE
④ 如果本地配置正常但 memory 目录为空 → 服务端开关 tengu_passport_quail 未对你开启
⑤ Memory 文件分布不均是正常的——只有对话内容值得记忆时才会写入

Memory 目录结构

~/.claude/
  CLAUDE.md                              # 全局用户指令(手动创建)
  projects/-Users-apple-...-project/
    memory/                              # 自动记忆目录
      MEMORY.md                          # 索引入口(200 行上限)
      user_role.md                       # 用户画像
      feedback_agent.md                  # 反馈记录
      project_state.md                   # 项目状态
      team/                              # 团队共享记忆
        MEMORY.md
    settings.json                        # 项目级设置

项目根目录/
  CLAUDE.md                              # 项目指令(可 git 提交)
  CLAUDE.local.md                        # 本地私有指令(gitignore)
  .claude/
    CLAUDE.md
    rules/*.md                           # 条件/无条件规则
    settings.json                        # 项目设置(不可信 autoMemoryDirectory)

代码索引

文件行数说明
context.ts~190顶层上下文构建:getUserContext、getSystemContext、getGitStatus
services/compact/compact.ts~1706核心压缩引擎:compactConversation、partialCompact、streamCompactSummary、附件重建
services/compact/autoCompact.ts~352自动压缩编排:阈值计算、shouldAutoCompact、熔断器
services/compact/microCompact.ts~531轻量微压缩:工具结果清除、缓存编辑、时间驱动策略
services/compact/sessionMemoryCompact.ts~631Session Memory 压缩路径:零 API 调用的快速压缩
services/compact/prompt.ts~375压缩提示词模板:BASE/PARTIAL/UP_TO 三种变体
services/compact/grouping.ts~64消息分组:按 API 轮次边界切分
services/compact/postCompactCleanup.ts~78压缩后清理:重置缓存、状态、模块级变量
services/compact/apiMicrocompact.ts~154API 侧微压缩:context management 策略配置
services/compact/timeBasedMCConfig.ts~44时间驱动微压缩配置:gap 阈值、keepRecent 数量
services/SessionMemory/sessionMemory.ts~496Session Memory 主逻辑:提取触发、forked agent 执行
services/SessionMemory/prompts.ts~325Session Memory 提示词和模板:摘要更新指令、截断策略
services/SessionMemory/sessionMemoryUtils.ts~208Session Memory 工具函数:配置管理、游标跟踪、等待机制
services/extractMemories/extractMemories.ts~616持久化记忆提取:闭包状态、尾随提取、工具权限
services/extractMemories/prompts.ts~155记忆提取提示词:自动/团队记忆变体
utils/memory/types.ts~12记忆类型定义:User/Project/Local/Managed/AutoMem/TeamMem
memdir/paths.ts~90Memory 路径解析:isAutoMemoryEnabled、isExtractModeActive、目录结构
memdir/memdir.ts~510Memory 提示词构建:loadMemoryPrompt、ensureMemoryDirExists
memdir/memoryTypes.ts~60四种记忆类型:user/feedback/project/reference 及 frontmatter 规范
services/autoDream/autoDream.ts~250后台整合 Agent:24h/5 sessions 周期性重整记忆文件
utils/backgroundHousekeeping.ts~50启动时初始化:extractMemories + autoDream 后台服务
query/stopHooks.ts~160每轮结束触发:extractMemories + autoDream 入口