add claude code analysis 20260404

This commit is contained in:
Chauncygu
2026-04-04 09:43:49 -07:00
parent 23ae8ec64b
commit 7f33e40f22
14 changed files with 5033 additions and 1 deletions

View File

@@ -0,0 +1,871 @@
# Claude Code 上下文压缩算法深度分析
> 基于 Claude Code 源码2026-03-31 快照512K 行 TypeScript逆向分析
> 核心文件:`src/services/compact/` 目录下 11 个文件
---
## 目录
1. [架构总览](#1-架构总览)
2. [第1层微压缩Microcompact](#2-第1层微压缩microcompact)
3. [第2层自动压缩Auto-Compact](#3-第2层自动压缩auto-compact)
4. [第3层传统压缩Full Compact](#4-第3层传统压缩full-compact)
5. [第4层Session Memory 压缩](#5-第4层session-memory-压缩)
6. [消息分组算法](#6-消息分组算法)
7. [Token 估算算法](#7-token-估算算法)
8. [压缩提示词工程](#8-压缩提示词工程)
9. [5层错误恢复中的压缩角色](#9-5层错误恢复中的压缩角色)
10. [各层对比与设计哲学](#10-各层对比与设计哲学)
---
## 1. 架构总览
Claude Code 的上下文压缩不是单一算法,而是一个 **4层递进的压缩体系**,每层解决不同层面的问题:
```
用户消息 → [第1层微压缩] → [第2层自动压缩] → API 调用
↓ ↓
细粒度清理旧工具输出 上下文即将超限时触发
(不丢语义,<1ms) (调用 LLM 或 Session Memory)
┌──────────────┴──────────────┐
[第4层SM压缩] [第3层传统压缩]
(用已有摘要,<10ms) (Fork Agent 生成摘要5-30s)
```
**核心原则:** 尽可能用廉价的规则操作延迟昂贵的 LLM 调用,只在不得已时丢弃信息。
### 涉及的源文件
| 文件 | 行数 | 职责 |
|------|------|------|
| `microCompact.ts` | ~400 | 微压缩:规则清理旧工具结果 |
| `apiMicrocompact.ts` | — | API 层缓存编辑集成 |
| `timeBasedMCConfig.ts` | — | 时间触发微压缩配置 |
| `autoCompact.ts` | ~350 | 自动压缩:阈值判断 + 断路器 |
| `compact.ts` | ~600+ | 传统压缩Fork Agent 摘要 |
| `prompt.ts` | ~375 | 压缩提示词模板 |
| `sessionMemoryCompact.ts` | ~630 | Session Memory 压缩路径 |
| `grouping.ts` | ~63 | 消息按 API 轮次分组 |
| `postCompactCleanup.ts` | — | 压缩后清理 |
| `compactWarningHook.ts` | — | 压缩警告钩子 |
| `compactWarningState.ts` | — | 压缩警告状态 |
---
## 2. 第1层微压缩Microcompact
**源文件:** `microCompact.ts`
### 核心思想
不调用 LLM纯规则操作——清理旧的、大块的工具输出结果保留语义信息。这是每轮查询前都会执行的最轻量操作。
### 可压缩工具白名单
```typescript
const COMPACTABLE_TOOLS = new Set([
'Read', // 文件读取结果可能很大
'Bash', // Shell 输出可能很长
'Grep', // 搜索结果
'Glob', // 文件列表
'WebSearch', // 网页搜索结果
'WebFetch', // 网页抓取结果
'Edit', // 文件编辑的 diff
'Write', // 文件写入确认
])
```
不在白名单中的工具(如 Agent、Skill、MCP 等)的结果不会被微压缩。
### 两个子路径
#### 子路径 A时间触发微压缩
```
触发条件距上次助手消息的时间间隔超过阈值API 缓存已过期)
执行逻辑:
1. 收集所有可压缩工具的 tool_use ID
2. 保留最近 N 个工具结果
3. 将更早的 tool_result 内容替换为:
"[Old tool result content cleared]"
4. 不修改 tool_use 块(保持 API 配对完整性)
特点:
- 缓存已过期,所以无需保护缓存
- 直接修改本地消息内容
- 减少重传时的 token 消耗
```
#### 子路径 B缓存编辑微压缩Cached MC
这是更精巧的路径——在缓存仍然有效时工作:
```
触发条件:
- 特性开关 CACHED_MICROCOMPACT 开启
- 模型支持缓存编辑 API
- 当前是主线程查询(非 fork agent
执行逻辑:
1. collectCompactableToolIds(): 收集所有可压缩的 tool_use ID
2. registerToolResult(): 注册每个工具结果(按用户消息分组)
3. registerToolMessage(): 记录工具消息组
4. getToolResultsToDelete(): 根据 count/keep 阈值决定删除哪些
5. createCacheEditsBlock(): 生成 cache_edits API 块
关键区别:
- 不修改本地消息内容!
- 通过 API 的 cache_edits 字段告诉服务端删除特定工具结果的缓存
- 保持 prompt cache 命中率
- 状态通过 pendingCacheEdits / pinnedCacheEdits 管理
```
**缓存编辑的状态管理:**
```typescript
// 全局状态
let cachedMCState: CachedMCState | null = null
let pendingCacheEdits: CacheEditsBlock | null = null
// consumePendingCacheEdits():
// 返回待插入的缓存编辑块,然后清空 pending 状态
// 调用者在 API 请求后必须调用 pinCacheEdits() 固定它们
// getPinnedCacheEdits():
// 返回之前已固定的缓存编辑,需要在后续请求中重新发送
// markToolsSentToAPIState():
// 标记工具已发送给 API成功响应后调用
```
### Token 估算辅助
```typescript
function calculateToolResultTokens(block: ToolResultBlockParam): number {
if (typeof block.content === 'string') {
return roughTokenCountEstimation(block.content) // 字符数 / 4
}
// 数组:逐项计算
return block.content.reduce((sum, item) => {
if (item.type === 'text') return sum + roughTokenCountEstimation(item.text)
if (item.type === 'image' || item.type === 'document') return sum + 2000
return sum
}, 0)
}
function estimateMessageTokens(messages: Message[]): number {
let totalTokens = 0
for (const message of messages) {
for (const block of message.message.content) {
switch (block.type) {
case 'text': totalTokens += roughTokenCountEstimation(block.text); break
case 'tool_result': totalTokens += calculateToolResultTokens(block); break
case 'image':
case 'document': totalTokens += 2000; break // 固定估算
case 'thinking': totalTokens += roughTokenCountEstimation(block.thinking); break
case 'redacted_thinking': totalTokens += roughTokenCountEstimation(block.data); break
case 'tool_use': totalTokens += roughTokenCountEstimation(block.name + JSON.stringify(block.input ?? {})); break
default: totalTokens += roughTokenCountEstimation(JSON.stringify(block)); break
}
}
}
return Math.ceil(totalTokens * (4 / 3)) // × 4/3 保守填充
}
```
---
## 3. 第2层自动压缩Auto-Compact
**源文件:** `autoCompact.ts`
### 阈值计算
```typescript
// 有效上下文窗口 = 模型上下文窗口 - 摘要输出预留
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 // p99.99 的摘要输出是 17,387 tokens
function getEffectiveContextWindowSize(model: string): number {
const reservedTokensForSummary = Math.min(
getMaxOutputTokensForModel(model),
MAX_OUTPUT_TOKENS_FOR_SUMMARY,
)
let contextWindow = getContextWindowForModel(model)
// 支持环境变量覆盖(用于测试)
if (process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW) {
contextWindow = Math.min(contextWindow, parsed)
}
return contextWindow - reservedTokensForSummary
}
// 自动压缩阈值 = 有效上下文窗口 - 13K 缓冲
const AUTOCOMPACT_BUFFER_TOKENS = 13_000
function getAutoCompactThreshold(model: string): number {
return getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS
}
// 举例Opus 200K 上下文):
// 有效窗口 = 200,000 - 20,000 = 180,000
// 自动压缩阈值 = 180,000 - 13,000 = 167,000 tokens
```
### 其他阈值
```typescript
const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 // 警告阈值
const ERROR_THRESHOLD_BUFFER_TOKENS = 20_000 // 错误阈值
const MANUAL_COMPACT_BUFFER_TOKENS = 3_000 // 手动 /compact 的阻塞限制
```
### 断路器机制
```typescript
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
// BQ 数据分析发现1,279 个会话连续失败 50+ 次(最多 3,272 次),
// 每天浪费约 250K 次 API 调用。所以加了断路器。
if (tracking.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
return { wasCompacted: false } // 直接跳过,不再尝试
}
```
### shouldAutoCompact 决策树
```
shouldAutoCompact(messages, model, querySource):
1. querySource 是 'session_memory' 或 'compact'
→ 返回 false防止递归死锁——压缩代理自己不能触发压缩
2. querySource 是 'marble_origami'(上下文折叠代理)?
→ 返回 false防止破坏主线程的已提交日志
3. isAutoCompactEnabled() 返回 false
→ 返回 false
(检查 DISABLE_COMPACT、DISABLE_AUTO_COMPACT 环境变量和用户配置)
4. 响应式压缩模式开启tengu_cobalt_raccoon gate
→ 返回 false让 API 的 prompt-too-long 错误触发响应式压缩)
5. 上下文折叠模式开启?
→ 返回 false上下文折叠是 90%/95% 流程,自动压缩在 93% 会干扰它)
6. tokenCountWithEstimation(messages) - snipTokensFreed >= threshold
→ 返回 true
```
### autoCompactIfNeeded 执行流程
```
autoCompactIfNeeded(messages, context, ...):
1. DISABLE_COMPACT 环境变量?→ 跳过
2. 断路器检查consecutiveFailures >= 3→ 跳过
3. shouldAutoCompact() 返回 true
↓ 是
4. 优先尝试 Session Memory 压缩
↓ 成功 → 返回结果
↓ 失败
5. 回退到传统压缩compactConversation
↓ 成功 → 重置 consecutiveFailures = 0返回结果
↓ 失败
6. consecutiveFailures++
↓ 达到 3 次
7. 日志:断路器触发,本次会话不再尝试自动压缩
```
---
## 4. 第3层传统压缩Full Compact
**源文件:** `compact.ts` + `prompt.ts`
### 核心机制Fork Agent
传统压缩使用一个 **Fork Agent**——创建当前会话的一个分支,让它生成摘要。关键优势是**共享主会话的 prompt cache**。
```
主会话消息: [user1, assistant1, user2, assistant2, ...]
↓ 全部传入
Fork Agent共享 prompt cache
↓ 单轮回复
生成结构化摘要(<analysis> + <summary>
后处理 → 替换原消息
```
### 预处理管线
```
原始消息
stripImagesFromMessages() ← 图片 → "[image]",文档 → "[document]"
↓ (防止压缩请求自身超过上下文限制)
stripReinjectedAttachments() ← 删除技能发现/列表附件
↓ (压缩后会自动重新注入)
normalizeMessagesForAPI() ← 规范化消息格式
发送给 Fork Agent
```
### 摘要输出格式
Fork Agent 被要求生成两个 XML 块:
```xml
<analysis>
[思考草稿——用于提高摘要质量的中间推理过程]
[这部分最终会被删除,不会进入压缩后的上下文]
</analysis>
<summary>
1. Primary Request and Intent:
[详细描述用户的所有请求和意图]
2. Key Technical Concepts:
- [概念1]
- [概念2]
3. Files and Code Sections:
- [文件名1]
- [为什么这个文件重要]
- [代码片段]
- [文件名2]
- [代码片段]
4. Errors and fixes:
- [错误描述]:
- [修复方式]
- [用户反馈]
5. Problem Solving:
[问题解决过程]
6. All user messages:
- [逐条列出所有非工具结果的用户消息]
7. Pending Tasks:
- [待办事项1]
- [待办事项2]
8. Current Work:
[精确描述当前工作内容,包含文件名和代码片段]
9. Optional Next Step:
[下一步计划,包含最近对话的直接引用]
</summary>
```
### 防止工具调用的强力前导词
```typescript
const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- You already have all the context you need in the conversation above.
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
- Your entire response must be plain text: an <analysis> block followed by a <summary> block.
`
```
为什么需要这么强力?注释解释了:
```
// Sonnet 4.6+ 自适应思考模型有时会忽略较弱的尾部指令并尝试调用工具。
// 在 maxTurns: 1 的情况下,被拒绝的工具调用意味着没有文本输出
// → 回退到流式备用路径4.6 上 2.79% vs 4.5 上 0.01%)。
// 把这个放在最前面并明确说明拒绝后果,可以防止浪费轮次。
```
### 后处理formatCompactSummary
```typescript
function formatCompactSummary(summary: string): string {
// 1. 删除 <analysis> 块(草稿,已无价值)
summary = summary.replace(/<analysis>[\s\S]*?<\/analysis>/, '')
// 2. 提取 <summary> 内容,替换为可读标题
const match = summary.match(/<summary>([\s\S]*?)<\/summary>/)
if (match) {
summary = summary.replace(/<summary>[\s\S]*?<\/summary>/,
`Summary:\n${match[1].trim()}`)
}
// 3. 清理多余空行
summary = summary.replace(/\n\n+/g, '\n\n')
return summary.trim()
}
```
### 压缩后消息序列
```
[CompactBoundaryMessage] ← 标记压缩边界(含 token 统计、trigger 类型)
[SummaryUserMessage] ← 格式化后的摘要
[messagesToKeep] ← 保留的最近消息(如果有)
[Attachments] ← 重新注入的附件
- 最近读取的文件(前 5 个,每个 ≤ 5K tokens总预算 50K
- Plan 文件(如果有活跃计划)
- MCP 指令增量
- 技能发现增量
- 代理列表增量
[HookResults] ← PreCompact/PostCompact 钩子结果
```
### Prompt-Too-Long 重试机制
当压缩请求本身超过上下文限制时:
```typescript
const MAX_PTL_RETRIES = 3
function truncateHeadForPTLRetry(messages, ptlResponse): Message[] | null {
// 1. 移除上一次重试的标记消息(防止重试停滞)
// 2. 按 API 轮次分组
const groups = groupMessagesByApiRound(messages)
if (groups.length < 2) return null // 无法再裁剪
// 3. 决定删除多少组
const tokenGap = getPromptTooLongTokenGap(ptlResponse)
if (tokenGap !== undefined) {
// 能解析出 token 缺口 → 精确删除
let acc = 0, dropCount = 0
for (const g of groups) {
acc += roughTokenCountEstimationForMessages(g)
dropCount++
if (acc >= tokenGap) break
}
} else {
// 不能解析 → 删除 20% 最旧的组
dropCount = Math.max(1, Math.floor(groups.length * 0.2))
}
// 4. 保证至少保留 1 组
dropCount = Math.min(dropCount, groups.length - 1)
// 5. 如果裁剪后第一条是 assistant 消息,补一个 user marker
// API 要求第一条消息必须是 user
const sliced = groups.slice(dropCount).flat()
if (sliced[0]?.type === 'assistant') {
return [createUserMessage({ content: PTL_RETRY_MARKER, isMeta: true }), ...sliced]
}
return sliced
}
```
### 部分压缩Partial Compact
支持两个方向:
| 方向 | 提示词 | 用途 |
|------|--------|------|
| `from`(默认) | `PARTIAL_COMPACT_PROMPT` | 保留旧消息,仅摘要"最近的消息" |
| `up_to` | `PARTIAL_COMPACT_UP_TO_PROMPT` | 摘要旧消息,保留新消息。摘要会放在开头,后续消息跟在后面 |
`up_to` 模式的摘要包含一个特殊的第 9 章节 "Context for Continuing Work",专门为后续消息提供上下文。
---
## 5. 第4层Session Memory 压缩
**源文件:** `sessionMemoryCompact.ts`
### 核心思想
不调用 LLM 生成新摘要,而是直接使用已经通过后台记忆提取(`extractMemories`)积累的 Session Memory 作为"摘要"。
### 优势
- **速度快**:不需要 API 调用,<10ms
- **质量可预测**Session Memory 是在每轮查询后渐进更新的不是一次性压缩
- **保留近期消息**不像传统压缩那样替换所有消息
### 配置参数
```typescript
const DEFAULT_SM_COMPACT_CONFIG = {
minTokens: 10_000, // 至少保留 10K tokens 的最近消息
minTextBlockMessages: 5, // 至少保留 5 条含文本的消息
maxTokens: 40_000, // 最多保留 40K tokens硬上限
}
// 这些值通过 GrowthBook 远程配置,可动态调整
```
### calculateMessagesToKeepIndex 算法
这是 Session Memory 压缩的核心算法——决定保留哪些最近的消息
```
输入messages[], lastSummarizedIndexSession Memory 已覆盖到哪条消息)
算法:
1. startIndex = lastSummarizedIndex + 1
(即:从 Session Memory 尚未覆盖的消息开始)
2. 计算当前 [startIndex, end] 范围的 token 总量和含文本消息数
3. 如果已经超过 maxTokens (40K) → 直接返回(不再扩展)
4. 如果同时满足 ≥ minTokens (10K) AND ≥ minTextBlockMessages (5)
→ 直接返回(已足够)
5. 否则,从 startIndex 往前逐条扩展:
- 每加入一条消息,更新 token 和消息计数
- 停止条件:
a. 达到 maxTokens (40K)
b. 同时满足 minTokens 和 minTextBlockMessages
c. 到达上一个 CompactBoundary不跨越旧的压缩边界
6. adjustIndexToPreserveAPIInvariants(messages, startIndex)
→ 确保不切断 tool_use/tool_result 配对
→ 确保不分离共享 message.id 的 thinking 块
```
### adjustIndexToPreserveAPIInvariants 算法
这个算法解决一个棘手的问题——流式传输时一个 API 响应会产生多条消息thinkingtool_use 它们共享同一个 `message.id`如果在中间切断`normalizeMessagesForAPI` 合并时会丢失 thinking
```
输入messages[], startIndex
步骤 1修复 tool_use/tool_result 配对
1a. 收集 [startIndex, end] 范围内所有 tool_result 的 tool_use_id
1b. 收集范围内已有的 tool_use_id
1c. 找出缺失的 tool_use_id在范围外
1d. 向前搜索,把包含缺失 tool_use 的 assistant 消息纳入范围
步骤 2修复 thinking 块分离
2a. 收集范围内所有 assistant 消息的 message.id
2b. 向前搜索,把共享同一 message.id 的 assistant 消息纳入范围
返回:调整后的 startIndex
```
**源码注释中的真实 bug 场景:**
```
Session 存储(压缩前):
Index N: assistant, message.id: X, content: [thinking]
Index N+1: assistant, message.id: X, content: [tool_use: ORPHAN_ID]
Index N+2: assistant, message.id: X, content: [tool_use: VALID_ID]
Index N+3: user, content: [tool_result: ORPHAN_ID, tool_result: VALID_ID]
如果 startIndex = N+2
旧代码:只检查 N+2 的 tool_results找不到返回 N+2
normalizeMessagesForAPI 合并后:
msg[1]: assistant with [tool_use: VALID_ID] ← ORPHAN tool_use 被排除!
msg[2]: user with [tool_result: ORPHAN_ID, tool_result: VALID_ID]
API 报错:孤立的 tool_result 引用了不存在的 tool_use
```
### trySessionMemoryCompaction 完整流程
```
trySessionMemoryCompaction(messages, agentId, autoCompactThreshold):
1. shouldUseSessionMemoryCompaction()
→ 检查 tengu_session_memory AND tengu_sm_compact 特性开关
→ 支持 ENABLE_CLAUDE_CODE_SM_COMPACT / DISABLE_CLAUDE_CODE_SM_COMPACT 环境变量
2. 初始化远程配置(仅首次)
3. 等待正在进行的 Session Memory 提取完成(带超时)
4. 获取 lastSummarizedMessageId 和 sessionMemory 内容
5. Session Memory 文件不存在?→ 返回 null
6. Session Memory 是空模板?→ 返回 null
7. 确定 lastSummarizedIndex
a. 正常情况:在 messages 中查找 lastSummarizedMessageId 的索引
b. 恢复的会话:设为 messages.length - 1从末尾开始
8. calculateMessagesToKeepIndex() → startIndex
9. 过滤掉 messagesToKeep 中的旧 CompactBoundary
10. 执行 SessionStart 钩子(恢复 CLAUDE.md 等上下文)
11. 创建压缩结果:
- 截断过大的 Session Memory 章节
- 生成摘要用户消息
- 附加 Plan 文件(如果有)
12. 检查压缩后 token 是否仍超过阈值
→ 是:返回 null回退到传统压缩
→ 否:返回 CompactionResult
```
---
## 6. 消息分组算法
**源文件:** `grouping.ts`
```typescript
function groupMessagesByApiRound(messages: Message[]): Message[][] {
const groups: Message[][] = []
let current: Message[] = []
let lastAssistantId: string | undefined
for (const msg of messages) {
// 当出现新的 assistant message.id 时,开始新的一组
if (
msg.type === 'assistant' &&
msg.message.id !== lastAssistantId &&
current.length > 0
) {
groups.push(current)
current = [msg]
} else {
current.push(msg)
}
if (msg.type === 'assistant') {
lastAssistantId = msg.message.id
}
}
if (current.length > 0) {
groups.push(current)
}
return groups
}
```
**设计细节:**
- 同一 API 请求的流式块共享同一个 `message.id`
- `StreamingToolExecutor` 在流式输出期间交错插入 `tool_result`但它们属于同一轮次
- 只要 `message.id` 不变所有消息都在同一组内
- 不跟踪未解决的 `tool_use` ID——让分组边界自然形成 `ensureToolResultPairing` API 层修复残留的配对问题
---
## 7. Token 估算算法
贯穿所有压缩路径的核心辅助
### 粗略估算
```typescript
function roughTokenCountEstimation(text: string): number {
return text.length / 4 // 经验法则4 字符 ≈ 1 token
}
```
### 消息级估算
```typescript
function estimateMessageTokens(messages: Message[]): number {
let totalTokens = 0
for (const message of messages) {
if (message.type !== 'user' && message.type !== 'assistant') continue
for (const block of message.message.content) {
switch (block.type) {
case 'text':
totalTokens += roughTokenCountEstimation(block.text)
break
case 'tool_result':
totalTokens += calculateToolResultTokens(block)
break
case 'image':
case 'document':
totalTokens += 2000 // 固定值,不论实际大小
break
case 'thinking':
totalTokens += roughTokenCountEstimation(block.thinking)
// 注意不计算签名signature 是元数据,不被模型 tokenize
break
case 'redacted_thinking':
totalTokens += roughTokenCountEstimation(block.data)
break
case 'tool_use':
totalTokens += roughTokenCountEstimation(
block.name + JSON.stringify(block.input ?? {})
)
// 不计算 JSON wrapper 和 id 字段
break
default:
totalTokens += roughTokenCountEstimation(JSON.stringify(block))
break
}
}
}
return Math.ceil(totalTokens * (4 / 3)) // 保守填充 33%
}
```
### 精确计算
```typescript
// 当有 API 响应的 usage 数据时,使用精确值
function tokenCountFromLastAPIResponse(messages: Message[]): number | undefined {
// 从最后一条 assistant 消息的 usage 字段读取
// usage.input_tokens 包含了 API 看到的实际 token 数
}
// 混合策略
function tokenCountWithEstimation(messages: Message[]): number {
// 优先使用 API 返回的精确值
// 不可用时回退到估算
}
```
---
## 8. 压缩提示词工程
**源文件:** `prompt.ts`
### 三种提示词模板
| 模板 | 变量名 | 用途 |
|------|--------|------|
| 完整压缩 | `BASE_COMPACT_PROMPT` | 摘要整个对话 |
| 部分压缩from | `PARTIAL_COMPACT_PROMPT` | 只摘要最近的消息 |
| 部分压缩up_to | `PARTIAL_COMPACT_UP_TO_PROMPT` | 摘要旧消息作为后续消息的前导 |
### 提示词结构
```
[NO_TOOLS_PREAMBLE] ← 强力禁止工具调用
[DETAILED_ANALYSIS_INSTRUCTION] ← 要求 <analysis> 思考草稿
[MAIN_PROMPT] ← 9 章节结构化摘要要求 + 示例
[Custom Instructions] ← 用户自定义指令(如果有)
[NO_TOOLS_TRAILER] ← 再次强调不要调用工具
```
### <analysis> 块的作用
```
提示词要求模型先在 <analysis> 中"打草稿"
1. 按时间顺序分析每条消息
2. 识别用户意图、技术决策、代码模式
3. 特别关注用户反馈("用户让你做不同的事情"
4. 双重检查技术准确性和完整性
这类似于 Chain-of-Thought但最终会被 formatCompactSummary() 删除,
只保留 <summary> 部分进入压缩后的上下文。
```
### 压缩后的摘要消息
```typescript
function getCompactUserSummaryMessage(summary, suppressFollowUp, transcriptPath, recentPreserved): string {
let msg = `This session is being continued from a previous conversation that ran out of context.
The summary below covers the earlier portion of the conversation.
${formatCompactSummary(summary)}`
// 如果有 transcript 路径,提供回溯指引
if (transcriptPath) {
msg += `\n\nIf you need specific details from before compaction
(like exact code snippets, error messages, or content you generated),
read the full transcript at: ${transcriptPath}`
}
// 如果保留了最近消息
if (recentMessagesPreserved) {
msg += `\n\nRecent messages are preserved verbatim.`
}
// 如果是自动压缩suppressFollowUp = true
if (suppressFollowUpQuestions) {
msg += `\nContinue the conversation from where it left off
without asking the user any further questions.
Resume directly — do not acknowledge the summary,
do not recap what was happening,
do not preface with "I'll continue" or similar.
Pick up the last task as if the break never happened.`
}
// 如果是 Proactive/KAIROS 模式
if (proactiveModule?.isProactiveActive()) {
msg += `\n\nYou are running in autonomous/proactive mode.
This is NOT a first wake-up — you were already working autonomously before compaction.
Continue your work loop: pick up where you left off based on the summary above.
Do not greet the user or ask what to work on.`
}
return msg
}
```
---
## 9. 5层错误恢复中的压缩角色
`query.ts` 5 层错误恢复机制中压缩系统承担了前 2
```
API 调用失败
第1层上下文折叠排水Context Collapse Drain
↓ 失败
第2层响应式压缩Reactive Compact ← 使用 compact.ts
↓ 失败
第3层最大输出升级8K → 64K tokens
↓ 失败
第4层多轮恢复注入"请继续"消息,最多 3 次)
↓ 失败
第5层模型回退切换到备用模型剥离 thinking 签名块)
↓ 失败
暴露错误给用户
```
**响应式压缩** vs **主动压缩**
| 属性 | 主动压缩Auto-Compact | 响应式压缩Reactive Compact |
|------|--------------------------|-------------------------------|
| 触发 | token 数超过阈值 | API 返回 prompt-too-long 错误 |
| 时机 | API 调用前 | API 调用失败后 |
| 路径 | `autoCompact.ts` `compact.ts` | `query.ts` 错误恢复层 |
| 回退 | SM 传统压缩 放弃 | 传统压缩 裁剪最旧消息 |
---
## 10. 各层对比与设计哲学
### 横向对比
| 属性 | 微压缩 | Session Memory | 传统压缩 | PTL Recovery |
|------|--------|---------------|----------|-------------|
| **调用 LLM** | | | Fork Agent | |
| **信息损失** | 最小仅删工具输出 | 中等靠已有摘要 | 中等9章摘要 | 丢弃最旧消息 |
| **延迟** | <1ms | <10ms | 5-30s | ~0 |
| **触发** | 每轮自动 | 自动压缩优先路径 | 自动/手动 | 压缩自身超限 |
| **prompt cache** | 保留缓存编辑 | 破坏 | 共享Fork | 破坏 |
| **最大输出** | | | 20K tokens | |
| **断路器** | | | 3次 | 3次 |
| **token 预算** | | 40K 最大保留 | 50K 文件重注入 | 按组裁剪 |
### 设计哲学总结
1. **渐进式降级**从无损操作微压缩到有损操作传统压缩到丢弃操作PTL Recovery每一层都比上一层代价更高
2. **缓存优先**微压缩的缓存编辑路径专门设计为不破坏 prompt cache传统压缩通过 Fork Agent 共享 cache
3. **安全性保证**
- 永远不切断 tool_use/tool_result 配对
- 永远不分离共享 message.id 的消息块
- 断路器防止失败时无限重试
- 递归保护防止压缩代理触发自身压缩
4. **可观测性**每个压缩操作都记录 `logEvent` `tengu_compact`, `tengu_cached_microcompact`包含压缩前后的 token 触发原因重试次数等
5. **可配置性**几乎所有阈值都支持环境变量覆盖用于测试 GrowthBook 远程配置用于动态调整
---
*来自AI超元域 | B站频道https://space.bilibili.com/3493277319825652*
*基于 Claude Code 源码逆向分析2026-03-31*