上下文管理
对话上下文窗口的全生命周期管理:从构建、膨胀、压缩到重建的完整策略
职责概述
解决的问题:大模型有上下文窗口限制(200K token),但一个复杂的编程任务很容易超过这个限制——几十轮对话、几十个文件的内容、各种工具返回结果。需要一个智能的"内存管理器",在窗口快满时决定保留什么、丢弃什么,确保对话不会因为超限而崩溃。
应用场景:① 对话进行到第 20 轮,上下文快满了 → 自动压缩早期对话,保留关键决策 ② 用户打开一个大文件进行分析 → 文件内容占满窗口 → 智能截断只保留相关部分 ③ 长期记忆:把重要信息写入 ~/.claude/ 文件,下次对话还能用 ④ 工具返回的冗长输出自动精简。
一句话理解:就像你的工作记忆——你不能同时记住所有事,所以你会做笔记(Memory)、丢掉不重要的细节(Microcompact)、定期总结(Auto Compact),确保脑子不被撑爆。
架构设计
核心数据流
上下文生命周期
压缩决策流程
内存层级
实时消息流,最高优先级
后台提取的会话笔记 Markdown
紧急压缩时生成的摘要文本
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.ts 的 COMPACTABLE_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
- 零 API 调用,即时完成
- 保留最近消息不变
- 依赖后台提取质量
- 需要 GrowthBook 双 flag(session_memory + sm_compact)
- 需要 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 而被误判为缓存断裂。
◈ 可视化处理拓扑图
上下文管理是一个 4 层渐进式压缩管线:从最廉价的消息裁剪到最昂贵的 AI 摘要生成。每一层在前一层无法释放足够空间时才触发,90%+ 的场景在前两层即完成。压缩完成后执行附件重建和进程级状态清理,确保对话连贯性。
Layer 1-2 — 轻量压缩:Snip / Microcompact
microcompactMessages() (microCompact.ts:253)。两条子路径:(a)
cachedMicrocompactPath() — 通过 cache_edits API 服务端清除工具结果,本地消息不变(b)
maybeTimeBasedMicrocompact() — 长时间间隔时清除旧工具结果Layer 3 — 重量级压缩:Compact (AI 摘要)
SystemCompactBoundaryMessage
丢弃最老轮次组重试 ×3
Layer 4 — 后压缩:附件重建 + 状态清理
resetGetMemoryFilesCache
clearSystemPromptSections
+ clearSystemPromptSections
不触碰主线程缓存
querySource 前缀隔离,防止子 agent 清空共享进程的模块级缓存。⇉ 核心处理流程详解
上下文管理系统的核心职责是在有限的上下文窗口中保持对话连贯性。它由三个层次组成:顶层上下文构建(context.ts)、自动压缩引擎(autoCompact.ts + compact.ts)和轻量微压缩(microCompact.ts)。以下是从上下文构建到压缩完成的完整处理链路:
getUserContext()(context.ts:155)通过 getMemoryFiles() 发现 CLAUDE.md 文件并解析为 claudeMd 字符串,还包含当前日期。getSystemContext()(context.ts:116)包含 git 状态快照(分支、状态、最近提交)和可选的缓存破坏注入。两者都通过 memoize 缓存,确保同一会话中只计算一次 — context.ts:116-189shouldAutoCompact()(autoCompact.ts:160)在每个 API 轮次后被调用。它计算当前 token 使用量与阈值(getAutoCompactThreshold(),autoCompact.ts:72)的比值。阈值由 getEffectiveContextWindowSize()(autoCompact.ts:33)计算(上下文窗口大小减去最大输出 token),再扣除约 13K buffer。当使用率达到约 97% 时触发自动压缩。还包含熔断器逻辑——连续压缩失败时暂停自动压缩 — autoCompact.ts:160-239microcompactMessages()(microCompact.ts:253)尝试轻量级清理。两种策略:(1) cachedMicrocompactPath()(microCompact.ts:305)通过 cache_edits API 在不修改本地消息的情况下清除服务端的大体量工具结果;(2) maybeTimeBasedMicrocompact()(microCompact.ts:446)当对话出现长时间间隔时,清除间隔前的旧工具结果。时间策略通过 evaluateTimeBasedTrigger()(microCompact.ts:422)评估 — microCompact.ts:253-530compactConversation()(compact.ts:387)首先执行 PreCompact hooks(允许用户自定义压缩逻辑),然后调用 streamCompactSummary()(compact.ts:1136)将完整消息历史发送给模型生成摘要。如果摘要请求本身触发 prompt-too-long,truncateHeadForPTLRetry()(compact.ts:243)逐步丢弃最老的 API 轮次组并重试(最多 3 次)— compact.ts:387-763streamCompactSummary()(compact.ts:1136)构建压缩专用 prompt(通过 getCompactPrompt() 获取模板),利用 prompt cache sharing(tengu_compact_cache_prefix 特性开关,默认启用)复用主对话的缓存前缀。在 Fork agent 路径中,压缩请求的 API 前缀与主对话完全一致,最大化 cache 命中率 — compact.ts:1136-1396createPostCompactFileAttachments()(compact.ts:1415)将最近读取过的文件(最多 POST_COMPACT_MAX_FILES_TO_RESTORE 个)作为附件重新注入。createPlanAttachmentIfNeeded()、createSkillAttachmentIfNeeded()、createAsyncAgentAttachmentsIfNeeded() 分别恢复计划、技能和异步 agent 状态。还通过 getDeferredToolsDeltaAttachment() 重新通告延迟加载工具的 schema — compact.ts:1415-1600runPostCompactCleanup()(postCompactCleanup.ts:31)执行全面的缓存和状态清理:重置微压缩状态、清除上下文折叠缓存、刷新 CLAUDE.md 文件缓存(getUserContext.cache.clear())、清除权限分类器缓存、推测性检查缓存。关键设计:子 agent 的压缩不重置主线程的模块级状态(通过 querySource 前缀判断)——子 agent 共享同一进程的模块变量 — postCompactCleanup.ts:31-77SystemCompactBoundaryMessage(compact.ts 中的 createCompactBoundaryMessage())作为消息历史中的分界点。边界消息携带压缩前的 token 数量和 pre-compact 发现的工具名列表。随后执行 PostCompact hooks 和 SessionStart hooks(重新加载 hook 指令),使 hook 系统在压缩后保持一致 — compact.ts:600-760cacheSafeParams.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。
// 收集可压缩的工具调用 ID
const compactableIds = collectCompactableToolIds(messages)
// 生成 cache_edits block(不修改本地消息)
// 下次 API 请求时通过 cache_edits 参数发送
// 服务端在读取时应用编辑,工具结果被替换为空
2. 进程级状态隔离的精细控制
子 agent 与主线程共享同一个 Node.js 进程和模块级变量。runPostCompactCleanup()(postCompactCleanup.ts:31)通过 isMainThreadCompact 标志精细控制哪些状态可以被重置。主线程压缩可以清空 getUserContext 的 memoize 缓存和 CLAUDE.md 文件缓存;子 agent 压缩跳过这些——因为清空主线程的上下文缓存会导致主线程的下一个 API 调用使用陈旧或不一致的数据。
const isMainThreadCompact =
querySource === undefined ||
querySource.startsWith('repl_main_thread') ||
querySource === 'sdk'
resetMicrocompactState() // 所有路径都执行
if (isMainThreadCompact) {
getUserContext.cache.clear?.() // 仅主线程
resetGetMemoryFilesCache('compact') // 仅主线程
}
clearSystemPromptSections() // 所有路径
repl_main_thread、agent:builtin:*)区分不同执行上下文,用最小的运行时成本实现了逻辑隔离。querySource 成为了整个系统的"上下文身份标识"。3. PTL 重试的渐进式截断
压缩请求本身也可能触发 prompt-too-long(PTL)错误——当消息历史太长时,连同压缩 prompt 一起超过了上下文窗口。truncateHeadForPTLRetry()(compact.ts:243)不是简单地截断消息,而是通过 groupMessagesByApiRound()(grouping.ts:22)按 API 轮次边界分组,逐组丢弃最老的轮次,直到 token 差额被覆盖。最多重试 3 次(MAX_PTL_RETRIES)。这确保了即使在极端情况下(用户从未手动压缩),系统也能完成压缩操作。
4. 压缩后附件的增量重建
压缩后不是全量重发所有上下文附件,而是通过 delta 机制增量注入。getDeferredToolsDeltaAttachment() 计算压缩前后工具 schema 的差异,只发送新增或变更的工具定义。同样,getAgentListingDeltaAttachment() 和 getMcpInstructionsDeltaAttachment() 也走 delta 路径。唯一需要全量重发的是文件附件(因为压缩丢失了"模型已看过哪些文件"的信息),但限制在 POST_COMPACT_MAX_FILES_TO_RESTORE 个以内。
◈ Agent 实践借鉴 — 客服 Agent 上下文管理设计
场景映射:客服 agent 在上下文管理上的真实问题
客服场景的上下文管理和代码助手完全不同——不是"文件很多",而是"对话很长、数据很大、客户会反复来"。典型痛点包括:
- 对话太长 token 不够:客户打了 20 分钟电话,来回 30 轮对话(咨询退货政策、比价、犹豫、改主意),agent 的对话历史已经很长,API token 上限快到了。
- 跨会话记忆断层:客户上周聊过退货问题,这次来问退货进度,agent 不记得上周聊了什么,客户被迫重新描述一遍。体验极差。
- 工具返回数据太大:查物流轨迹返回 50 个节点,但 agent 只需要最新状态和预计到达时间。大量原始数据占满 token 预算。
- 重复进线浪费 token:同一个客户今天问了 3 次物流(上午 10 点、下午 2 点、下午 5 点),每次都是相同的订单号和物流信息,重复的上下文信息白白消耗 token。
借鉴 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 自适应重试——客服场景对话不会像代码助手那样达到极端长度,不需要逐步丢弃最老分组的重试机制。
💡 Memory 文件创建机制详解
白话版:Memory 文件什么时候会被创建?
CC 的 Memory 文件有两条创建路径:
用户:"记住我喜欢用 TypeScript"
→ CC 判断这是值得持久化的用户偏好
→ 调用 Write 工具写入 ~/.claude/projects/.../memory/user_role.md
触发条件:无特殊条件,只要 autoMemoryEnabled 没关就行
常见场景:用户说"记住..."、CC 发现重要的项目上下文、用户偏好等
触发时机:每轮对话结束(模型回复完,没有要调工具了)
执行者:后台 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() 判断优先级
优先级从高到低:
1. CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 → 直接关闭
2. CLAUDE_CODE_SIMPLE (--bare 模式) → 直接关闭
3. CCR 远程模式且无 REMOTE_MEMORY_DIR → 直接关闭
4. settings.json 中 autoMemoryEnabled → 显式配置
5. 默认 → true(开启)
isExtractModeActive() 判断 — 服务端开关
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_quail | false | 后台自动提取的总开关(最关键) |
tengu_bramble_lintel | 1 | 每 N 轮对话触发一次提取 |
tengu_onyx_plover | 24h/5 sessions | Auto-Dream 后台整合:每小时数/会话数 |
tengu_slate_thimble | false | 允许非交互模式下的提取 |
tengu_moth_copse | false | 跳过 MEMORY.md 索引,用附件召回 |
① 检查 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 | ~631 | Session 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 | ~154 | API 侧微压缩:context management 策略配置 |
services/compact/timeBasedMCConfig.ts | ~44 | 时间驱动微压缩配置:gap 阈值、keepRecent 数量 |
services/SessionMemory/sessionMemory.ts | ~496 | Session Memory 主逻辑:提取触发、forked agent 执行 |
services/SessionMemory/prompts.ts | ~325 | Session Memory 提示词和模板:摘要更新指令、截断策略 |
services/SessionMemory/sessionMemoryUtils.ts | ~208 | Session 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 | ~90 | Memory 路径解析:isAutoMemoryEnabled、isExtractModeActive、目录结构 |
memdir/memdir.ts | ~510 | Memory 提示词构建: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 入口 |