mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
claude-code with OpenAI mode fix
This commit is contained in:
239
docs/context/compaction.mdx
Normal file
239
docs/context/compaction.mdx
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: "上下文压缩 - Compaction 三层策略与边界机制"
|
||||
description: "深度解析 Claude Code 上下文压缩的完整实现:Session Memory 压缩、传统 API 摘要压缩、MicroCompact 局部压缩三层策略,以及 CompactBoundary 消息、工具对保持、PTL 紧急降级等关键机制。"
|
||||
keywords: ["上下文压缩", "Compaction", "token 管理", "对话压缩", "上下文窗口", "MicroCompact"]
|
||||
---
|
||||
|
||||
{/* 本章目标:从源码层面剖析压缩的三层策略、边界机制和关键常量 */}
|
||||
|
||||
## 压缩的触发时机
|
||||
|
||||
上下文压缩不是单一操作,而是**三层递进**的策略系统,对应不同的触发条件和严重程度:
|
||||
|
||||
| 层级 | 触发条件 | 实现位置 | 是否需要 API 调用 |
|
||||
|------|---------|---------|:---:|
|
||||
| **MicroCompact** | 单个工具输出过长 | `microCompact.ts` | 否 |
|
||||
| **Session Memory Compact** | 自动压缩触发(需 feature flag) | `sessionMemoryCompact.ts` | 否 |
|
||||
| **传统 API 摘要** | 手动 `/compact` 或 SM 不可用时的自动回退 | `compact.ts` | 是 |
|
||||
|
||||
### 压缩入口的优先级链
|
||||
|
||||
源码路径:`src/commands/compact/compact.ts`
|
||||
|
||||
当用户执行 `/compact` 或系统触发自动压缩时,压缩命令按以下优先级尝试:
|
||||
|
||||
```typescript
|
||||
// compact.ts:55-99 — 简化后的优先级链
|
||||
if (!customInstructions) {
|
||||
const sessionMemoryResult = await trySessionMemoryCompaction(messages, ...)
|
||||
if (sessionMemoryResult) return sessionMemoryResult // 优先:SM 压缩
|
||||
}
|
||||
|
||||
if (reactiveCompact?.isReactiveOnlyMode()) {
|
||||
return await compactViaReactive(messages, ...) // 次选:Reactive 压缩
|
||||
}
|
||||
|
||||
// 兜底:传统 API 摘要
|
||||
const microcompactResult = await microcompactMessages(messages, context)
|
||||
const messagesForCompact = microcompactResult.messages
|
||||
// → 调用 AI 模型生成摘要
|
||||
```
|
||||
|
||||
注意:SM 压缩不支持自定义指令(`/compact 聚焦在认证模块`),有自定义指令时直接走传统路径。
|
||||
|
||||
## 第一层:MicroCompact — 局部压缩
|
||||
|
||||
源码路径:`src/services/compact/microCompact.ts`
|
||||
|
||||
MicroCompact 不压缩整个对话,而是**清除旧工具输出的内容**。它维护一个白名单:
|
||||
|
||||
```typescript
|
||||
const COMPACTABLE_TOOLS = new Set([
|
||||
'Read', // 文件读取
|
||||
'Bash', // 命令输出
|
||||
'Grep', // 搜索结果
|
||||
'Glob', // 文件列表
|
||||
'WebSearch', // 搜索结果
|
||||
'WebFetch', // 网页内容
|
||||
'Edit', // 编辑输出
|
||||
'Write', // 写入输出
|
||||
])
|
||||
```
|
||||
|
||||
替换策略:将超过时间窗口的工具输出内容替换为 `[Old tool result content cleared]`。这不是简单的截断——原始内容仍保留在 JSONL transcript 中,只是不再发送给 API。
|
||||
|
||||
MicroCompact 还有一个**时间衰减配置**(`timeBasedMCConfig.ts`):越旧的工具输出越容易被清除,最近的优先保留。
|
||||
|
||||
### 图片和文档的特殊处理
|
||||
|
||||
```typescript
|
||||
const IMAGE_MAX_TOKEN_SIZE = 2000
|
||||
```
|
||||
|
||||
图片 block 如果超过 2000 token 估算值,也会被 MicroCompact 清除。PDF document block 同理。
|
||||
|
||||
## 第二层:Session Memory Compact — 无 API 调用的压缩
|
||||
|
||||
源码路径:`src/services/compact/sessionMemoryCompact.ts`
|
||||
|
||||
当 `tengu_session_memory` + `tengu_sm_compact` 两个 feature flag 启用时,系统优先使用 Session Memory 进行压缩——**不需要调用摘要模型**,直接使用已经提取好的 Session Memory 作为对话摘要。
|
||||
|
||||
### 保留窗口的计算
|
||||
|
||||
```typescript
|
||||
// sessionMemoryCompact.ts:324-397
|
||||
export function calculateMessagesToKeepIndex(messages, lastSummarizedIndex) {
|
||||
const config = getSessionMemoryCompactConfig()
|
||||
// 默认: minTokens=10K, minTextBlockMessages=5, maxTokens=40K
|
||||
|
||||
let startIndex = lastSummarizedIndex + 1
|
||||
// 从 lastSummarizedIndex 向前扩展,直到满足两个下限或命中上限
|
||||
for (let i = startIndex - 1; i >= floor; i--) {
|
||||
totalTokens += estimateMessageTokens([msg])
|
||||
if (hasTextBlocks(msg)) textBlockMessageCount++
|
||||
startIndex = i
|
||||
if (totalTokens >= config.maxTokens) break
|
||||
if (totalTokens >= config.minTokens && textBlockMessageCount >= config.minTextBlockMessages) break
|
||||
}
|
||||
return adjustIndexToPreserveAPIInvariants(messages, startIndex)
|
||||
}
|
||||
```
|
||||
|
||||
这个算法确保压缩后保留的消息窗口满足:
|
||||
- 至少 10,000 token(有上下文深度)
|
||||
- 至少 5 条包含文本的消息(有对话连续性)
|
||||
- 最多 40,000 token(不会太大又触发下一次压缩)
|
||||
|
||||
### 工具对完整性保护
|
||||
|
||||
`adjustIndexToPreserveAPIInvariants()` 是压缩中一个**关键的正确性保证**:
|
||||
|
||||
API 要求每个 `tool_result` 都有对应的 `tool_use`,反之亦然。如果压缩恰好切在一条 `tool_result` 消息处,会导致 API 报错。
|
||||
|
||||
```typescript
|
||||
// sessionMemoryCompact.ts:232-314
|
||||
// Step 1: 向前扫描,找到所有被保留消息中 tool_result 引用的 tool_use
|
||||
// Step 2: 向前扫描,找到与被保留 assistant 消息共享 message.id 的 thinking block
|
||||
// 两种情况都需要将 startIndex 向前移动
|
||||
```
|
||||
|
||||
流式传输会将一个 assistant 消息拆分为多条存储记录(thinking、tool_use 等各有独立 uuid 但共享 `message.id`),这增加了边界情况的复杂度。
|
||||
|
||||
## 第三层:传统 API 摘要压缩
|
||||
|
||||
源码路径:`src/services/compact/compact.ts`
|
||||
|
||||
当 SM 压缩不可用时,系统回退到传统方式:调用 AI 模型生成对话摘要。
|
||||
|
||||
### 压缩前处理
|
||||
|
||||
发送给摘要模型之前,消息会经过多层预处理:
|
||||
|
||||
```typescript
|
||||
// compact.ts:147-202
|
||||
const stripped = stripImagesFromMessages(messages) // 图片→[image] 文字标记
|
||||
const stripped2 = stripReinjectedAttachments(stripped) // 移除会被重新注入的附件
|
||||
```
|
||||
|
||||
图片被替换为 `[image]` 标记,防止摘要 API 调用本身也触发 prompt-too-long 错误。
|
||||
|
||||
### 压缩后的重新注入
|
||||
|
||||
压缩后,系统会从摘要中**重新注入关键上下文**:
|
||||
|
||||
```typescript
|
||||
// compact.ts:124-132
|
||||
export const POST_COMPACT_TOKEN_BUDGET = 50_000 // 总预算
|
||||
export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5 // 最多恢复 5 个文件
|
||||
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 // 每文件 5K token
|
||||
export const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000 // 每技能 5K token
|
||||
export const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000 // 技能总预算 25K
|
||||
```
|
||||
|
||||
这 50K token 的重新注入预算用于:
|
||||
1. 恢复最近读取的文件内容(最多 5 个文件,每个截断到 5K token)
|
||||
2. 恢复已激活的技能指令(每个技能截断到 5K token,总计 25K)
|
||||
3. 重新注入 CLAUDE.md 内容
|
||||
4. 恢复 MCP 工具发现结果
|
||||
|
||||
## CompactBoundary:压缩的边界标记
|
||||
|
||||
源码路径:`src/utils/messages.ts`(`createCompactBoundaryMessage`)
|
||||
|
||||
每次压缩后,系统在消息流中插入一条 `SystemCompactBoundaryMessage`:
|
||||
|
||||
```typescript
|
||||
type SystemCompactBoundaryMessage = {
|
||||
type: 'system'
|
||||
message: {
|
||||
type: 'compact_boundary'
|
||||
compactMetadata: {
|
||||
compactType: 'auto' | 'manual' | 'micro'
|
||||
preCompactTokenCount: number
|
||||
lastUserMessageUuid: string
|
||||
preCompactDiscoveredTools?: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后续所有操作只处理**最后一条 boundary 之后**的消息:
|
||||
|
||||
```typescript
|
||||
// messages.ts
|
||||
export function getMessagesAfterCompactBoundary(messages: Message[]): Message[] {
|
||||
const lastBoundary = messages.findLastIndex(m => isCompactBoundaryMessage(m))
|
||||
return lastBoundary >= 0 ? messages.slice(lastBoundary + 1) : messages
|
||||
}
|
||||
```
|
||||
|
||||
### Preserved Segment 注解
|
||||
|
||||
boundary 消息上还附加了 `preservedSegment` 注解,记录哪些消息被保留而非压缩:
|
||||
|
||||
```typescript
|
||||
// compact.ts — annotateBoundaryWithPreservedSegment
|
||||
boundaryMarker.compactMetadata.preservedSegment = {
|
||||
summaryMessageUuid: string
|
||||
preservedMessageUuids: string[]
|
||||
}
|
||||
```
|
||||
|
||||
这在会话恢复时帮助加载器正确重建消息链,避免重复压缩已保留的消息。
|
||||
|
||||
## PTL 紧急降级:Prompt Too Long
|
||||
|
||||
当压缩后仍然超出 token 限制(`PROMPT_TOO_LONG` 错误),系统会进入紧急降级路径:
|
||||
|
||||
1. **Reactive Compact**:`reactiveCompactOnPromptTooLong()` 尝试更激进的压缩
|
||||
2. **截断重试**:如果 reactive 也失败,`truncateHeadForPTLRetry()` 直接截断最早的消息
|
||||
3. 放弃并报错
|
||||
|
||||
Reactive Compact 目前在反编译版本中是 stub(`isReactiveOnlyMode() → false`),表明这是 Anthropic 内部的实验性功能。
|
||||
|
||||
## 压缩的 Hook 机制
|
||||
|
||||
压缩前后可以执行自定义 Hook:
|
||||
|
||||
- **Pre-compact Hook**(`executePreCompactHooks`):在压缩前执行,可以注入"必须保留"的标记
|
||||
- **Post-compact Hook**(`executePostCompactHooks`):在压缩后执行,可以验证关键信息是否保留
|
||||
- **Session Start Hook**(`processSessionStartHooks('compact')`):SM 压缩使用此 Hook 恢复 CLAUDE.md 等上下文
|
||||
|
||||
Hook 结果以 `HookResultMessage` 的形式附加到压缩结果中,确保用户的自定义逻辑在压缩过程中被尊重。
|
||||
|
||||
## Snip Compact(实验性)
|
||||
|
||||
源码路径:`src/services/compact/snipCompact.ts`(stub)
|
||||
|
||||
Snip Compact 是另一种实验性压缩策略,在反编译版本中为空壳实现。从 stub 的类型签名推断:
|
||||
|
||||
```typescript
|
||||
snipCompactIfNeeded(messages, options?: { force?: boolean }) → {
|
||||
messages: Message[]
|
||||
executed: boolean
|
||||
tokensFreed: number
|
||||
boundaryMessage?: Message
|
||||
}
|
||||
```
|
||||
|
||||
它似乎是一种**更细粒度的消息级裁剪**(snip = 剪切),可能是对单条消息的进一步压缩,而非整个对话。`shouldNudgeForSnips()` 和 `SNIP_NUDGE_TEXT` 暗示它可能会提示用户触发。
|
||||
226
docs/context/project-memory.mdx
Normal file
226
docs/context/project-memory.mdx
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: "项目记忆系统 - 文件级跨对话记忆架构"
|
||||
description: "深度解析 Claude Code 记忆系统:基于文件的持久化存储、MEMORY.md 索引结构、四类型分类法、Sonnet 智能召回、Session Memory 压缩集成。"
|
||||
keywords: ["项目记忆", "MEMORY.md", "AI 记忆", "跨对话", "自动记忆", "memdir"]
|
||||
---
|
||||
|
||||
{/* 本章目标:从源码层面剖析记忆系统的存储架构、召回机制和注入链路 */}
|
||||
|
||||
## 记忆系统的存储架构
|
||||
|
||||
源码路径:`src/memdir/paths.ts`、`src/memdir/memdir.ts`
|
||||
|
||||
Claude Code 的记忆系统是**纯文件**的——没有数据库、没有向量存储,只有 Markdown 文件和目录结构。
|
||||
|
||||
### 目录布局
|
||||
|
||||
```
|
||||
~/.claude/projects/<sanitized-git-root>/memory/
|
||||
├── MEMORY.md ← 入口索引(每次对话加载)
|
||||
├── user_role.md ← 用户记忆
|
||||
├── feedback_testing.md ← 反馈记忆
|
||||
├── project_mobile_release.md ← 项目记忆
|
||||
├── reference_linear_ingest.md ← 参考记忆
|
||||
└── logs/ ← KAIROS 模式:每日日志
|
||||
└── 2026/
|
||||
└── 04/
|
||||
└── 2026-04-01.md
|
||||
```
|
||||
|
||||
路径解析链路(`getAutoMemPath()`):
|
||||
1. `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE` 环境变量(Cowork SDK 全路径覆盖)
|
||||
2. `autoMemoryDirectory` 设置(仅限 `policySettings`/`localSettings`/`userSettings`——**故意排除** `projectSettings`,防止恶意仓库将记忆路径指向 `~/.ssh`)
|
||||
3. 默认:`<memoryBase>/projects/<sanitized-git-root>/memory/`
|
||||
|
||||
同一个 Git 仓库的所有 worktree 共享一个记忆目录(通过 `findCanonicalGitRoot()` 找到真正的 `.git` 根)。
|
||||
|
||||
### MEMORY.md 索引
|
||||
|
||||
`MEMORY.md` 是记忆的入口索引,每次对话都完整加载到上下文中:
|
||||
|
||||
```typescript
|
||||
// memdir.ts:35-38
|
||||
export const ENTRYPOINT_NAME = 'MEMORY.md'
|
||||
export const MAX_ENTRYPOINT_LINES = 200
|
||||
export const MAX_ENTRYPOINT_BYTES = 25_000
|
||||
```
|
||||
|
||||
索引有**双重上限**:200 行 AND 25KB。超过任何一条都会被 `truncateEntrypointContent()` 截断并追加警告。设计原因:p97 的索引文件用 200 行就能覆盖,但有些索引条目特别长(p100 观测到 197KB/200 行),字节上限捕捉这种长行异常。
|
||||
|
||||
索引条目格式:
|
||||
```markdown
|
||||
- [Title](file.md) — one-line hook
|
||||
```
|
||||
|
||||
每条一行,~150 字符以内。`MEMORY.md` 本身没有 frontmatter——它只是一个链接列表,不是记忆内容。
|
||||
|
||||
## 四类型分类法
|
||||
|
||||
源码路径:`src/memdir/memoryTypes.ts`
|
||||
|
||||
记忆被约束为一个**封闭的四类型系统**,每种类型有明确的 `<when_to_save>`、`<how_to_use>` 和 `<body_structure>` 规范:
|
||||
|
||||
| 类型 | 存储内容 | 典型触发 |
|
||||
|------|---------|---------|
|
||||
| **user** | 用户角色、偏好、技术背景 | "我是数据科学家"、"我写了十年 Go" |
|
||||
| **feedback** | 用户对 AI 行为的纠正和确认 | "别 mock 数据库"、"单 PR 更好" |
|
||||
| **project** | 非代码可推导的项目上下文 | "合并冻结从周四开始"、"auth 重写是合规要求" |
|
||||
| **reference** | 外部系统指针 | "pipeline bugs 在 Linear INGEST 项目" |
|
||||
|
||||
关键设计约束:**只存储无法从当前项目状态推导的信息**。代码架构、文件路径、git 历史都可以实时获取,不需要记忆。
|
||||
|
||||
### 反馈类型的双通道捕获
|
||||
|
||||
`feedback` 类型的 `when_to_save` 指令特别强调:
|
||||
|
||||
> Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious.
|
||||
|
||||
这意味着 AI 不仅在用户说"不要这样做"时保存,也在用户说"对,就是这样"时保存。后一种更难捕捉,但同等重要——它防止 AI 的行为随时间漂移。
|
||||
|
||||
### 每条记忆的 Frontmatter 格式
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: {{memory name}}
|
||||
description: {{one-line description — 用于未来判断相关性}}
|
||||
type: {{user, feedback, project, reference}}
|
||||
---
|
||||
|
||||
{{memory content — feedback/project 类型建议包含 **Why:** 和 **How to apply:** 行}}
|
||||
```
|
||||
|
||||
`description` 字段是关键:它不是给人读的摘要,而是给 AI 召回系统做相关性判断的搜索关键词。
|
||||
|
||||
## 智能召回机制
|
||||
|
||||
源码路径:`src/memdir/findRelevantMemories.ts`、`src/memdir/memoryScan.ts`
|
||||
|
||||
不是所有记忆都适合每次对话。系统使用一个**轻量级 Sonnet 侧查询**来筛选最相关的记忆。
|
||||
|
||||
### 召回流程
|
||||
|
||||
```
|
||||
用户消息 → findRelevantMemories(query, memoryDir)
|
||||
├── scanMemoryFiles() — 扫描所有记忆文件的 frontmatter
|
||||
├── selectRelevantMemories() — Sonnet 侧查询,从清单中选出 ≤5 条
|
||||
└── 返回 [{path, mtimeMs}, ...]
|
||||
```
|
||||
|
||||
核心是 `selectRelevantMemories()` 函数,它调用 `sideQuery()`(一个独立的轻量 API 调用):
|
||||
|
||||
```typescript
|
||||
// findRelevantMemories.ts:98-121
|
||||
const result = await sideQuery({
|
||||
model: getDefaultSonnetModel(), // 用 Sonnet 做筛选(非主模型)
|
||||
system: SELECT_MEMORIES_SYSTEM_PROMPT,
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: `Query: ${query}\n\nAvailable memories:\n${manifest}${toolsSection}`
|
||||
}],
|
||||
max_tokens: 256,
|
||||
output_format: { type: 'json_schema', schema: { ... } },
|
||||
})
|
||||
```
|
||||
|
||||
### 近期工具去噪
|
||||
|
||||
当 AI 正在使用某个工具时,召回该工具的使用文档是噪音(对话中已有工作上下文)。`recentTools` 参数让召回系统跳过这些记忆:
|
||||
|
||||
```typescript
|
||||
// findRelevantMemories.ts:92-95
|
||||
const toolsSection = recentTools.length > 0
|
||||
? `\n\nRecently used tools: ${recentTools.join(', ')}`
|
||||
: ''
|
||||
```
|
||||
|
||||
System Prompt 明确指示:"如果已提供最近使用的工具列表,不要选择该工具的使用参考或 API 文档。**仍然要选择**关于这些工具的警告、陷阱或已知问题——这正是使用时最关键的信息。"
|
||||
|
||||
### 已展示去重
|
||||
|
||||
`alreadySurfaced` 参数过滤之前轮次已展示过的文件路径,让 Sonnet 的 5 槽预算花在新的候选上,而不是重复召回同一文件。
|
||||
|
||||
## 记忆注入 System Prompt 的链路
|
||||
|
||||
源码路径:`src/memdir/memdir.ts` → `src/context.ts`
|
||||
|
||||
`loadMemoryPrompt()` 是记忆注入的入口,每会话调用一次(通过 `systemPromptSection('memory', ...)` 缓存):
|
||||
|
||||
```typescript
|
||||
// memdir.ts:419-507
|
||||
export async function loadMemoryPrompt(): Promise<string | null> {
|
||||
// 优先级:KAIROS 日志模式 → TEAMMEM 组合模式 → 纯自动记忆
|
||||
if (feature('KAIROS') && autoEnabled && getKairosActive()) {
|
||||
return buildAssistantDailyLogPrompt(skipIndex)
|
||||
}
|
||||
if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) {
|
||||
return teamMemPrompts!.buildCombinedMemoryPrompt(...)
|
||||
}
|
||||
if (autoEnabled) {
|
||||
return buildMemoryLines('auto memory', autoDir, ...).join('\n')
|
||||
}
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
注入时机:`context.ts` 中 `getSystemContext()` 调用时,记忆 Prompt 作为 system prompt 的一个 section 被组装。`MEMORY.md` 的内容作为 **user context message** 注入(而非 system prompt),这样可以利用 Prompt Cache 的 prefix 共享。
|
||||
|
||||
## KAIROS 模式:每日日志
|
||||
|
||||
源码路径:`src/memdir/memdir.ts`(`buildAssistantDailyLogPrompt`)
|
||||
|
||||
长期运行的 assistant 会话使用不同的记忆策略:
|
||||
|
||||
- **标准模式**:AI 维护 `MEMORY.md` 作为实时索引 + 独立记忆文件
|
||||
- **KAIROS 模式**:AI 只往日期文件追加日志(`logs/YYYY/MM/YYYY-MM-DD.md`),不做重组
|
||||
|
||||
```typescript
|
||||
// 日志路径模式(非字面路径——因为 Prompt 被缓存)
|
||||
const logPathPattern = join(memoryDir, 'logs', 'YYYY', 'MM', 'YYYY-MM-DD.md')
|
||||
```
|
||||
|
||||
一个独立的夜间 `/dream` 技能负责将日志蒸馏为主题文件 + `MEMORY.md` 索引。
|
||||
|
||||
## 记忆漂移防御
|
||||
|
||||
源码路径:`src/memdir/memoryTypes.ts`(`TRUSTING_RECALL_SECTION`)
|
||||
|
||||
记忆可能过期。系统在 Prompt 中设置了一个专门的 section "Before recommending from memory":
|
||||
|
||||
```
|
||||
A memory that names a specific function, file, or flag is a claim
|
||||
that it existed *when the memory was written*. It may have been
|
||||
renamed, removed, or never merged. Before recommending it:
|
||||
|
||||
- If the memory names a file path: check the file exists.
|
||||
- If the memory names a function or flag: grep for it.
|
||||
```
|
||||
|
||||
这个 section 的标题经过 A/B 测试验证:"Before recommending from memory"(行动导向)比 "Trusting what you recall"(抽象描述)效果好(3/3 vs 0/3)。
|
||||
|
||||
### 忽略记忆的严格语义
|
||||
|
||||
```
|
||||
If the user says to *ignore* or *not use* memory:
|
||||
proceed as if MEMORY.md were empty.
|
||||
Do not apply remembered facts, cite, compare against,
|
||||
or mention memory content.
|
||||
```
|
||||
|
||||
这解决了 AI 的一个常见反模式:用户说"忽略关于 X 的记忆",AI 虽然正确识别了代码但仍然加上"不像记忆中说的 Y"——这不是"忽略",而是"承认然后覆盖"。
|
||||
|
||||
## Session Memory 与压缩的联动
|
||||
|
||||
源码路径:`src/services/compact/sessionMemoryCompact.ts`
|
||||
|
||||
记忆系统与上下文压缩有深度集成。当 `tengu_session_memory` 和 `tengu_sm_compact` 两个 feature flag 同时开启时,压缩优先使用 Session Memory 而非传统摘要:
|
||||
|
||||
```typescript
|
||||
// sessionMemoryCompact.ts:57-61
|
||||
const DEFAULT_SM_COMPACT_CONFIG = {
|
||||
minTokens: 10_000, // 压缩后至少保留 10K token
|
||||
minTextBlockMessages: 5, // 至少保留 5 条文本消息
|
||||
maxTokens: 40_000, // 最多保留 40K token
|
||||
}
|
||||
```
|
||||
|
||||
SM-compact 不调用压缩 API(没有摘要模型),而是直接使用已有的 Session Memory 作为摘要——更快、更便宜、且不会丢失信息。
|
||||
252
docs/context/system-prompt.mdx
Normal file
252
docs/context/system-prompt.mdx
Normal file
@@ -0,0 +1,252 @@
|
||||
---
|
||||
title: "System Prompt 动态组装 - AI 工作记忆构建"
|
||||
description: "深入解析 Claude Code 的 System Prompt 动态组装过程:缓存策略、分界标记、Section 注册表、CLAUDE.md 多级合并,以及如何将零散上下文拼装为 API 可消费的缓存友好结构。"
|
||||
keywords: ["System Prompt", "系统提示词", "动态组装", "CLAUDE.md", "Prompt Cache", "缓存策略"]
|
||||
---
|
||||
|
||||
## 从数组到 API 调用:System Prompt 的完整链路
|
||||
|
||||
System Prompt 在 Claude Code 中不是一段写死的文本,而是一个 **`string[]` 数组**(品牌类型 `SystemPrompt`,定义于 `src/utils/systemPromptType.ts:8`),经过组装、分块、缓存标记后发送给 API。
|
||||
|
||||
### 三阶段管道
|
||||
|
||||
```
|
||||
getSystemPrompt() → string[] (组装内容)
|
||||
↓
|
||||
buildEffectiveSystemPrompt() → SystemPrompt (选择优先级路径)
|
||||
↓
|
||||
buildSystemPromptBlocks() → TextBlockParam[] (分块 + cache_control 标记)
|
||||
```
|
||||
|
||||
1. **`getSystemPrompt()`**(`src/constants/prompts.ts:444`)—— 收集静态段 + 动态段,插入 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分界标记
|
||||
2. **`buildEffectiveSystemPrompt()`**(`src/utils/systemPrompt.ts:41`)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择
|
||||
3. **`buildSystemPromptBlocks()`**(`src/services/api/claude.ts:3214`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
|
||||
|
||||
## SystemPrompt 品牌类型
|
||||
|
||||
```typescript
|
||||
// src/utils/systemPromptType.ts:8
|
||||
export type SystemPrompt = readonly string[] & {
|
||||
readonly __brand: 'SystemPrompt'
|
||||
}
|
||||
export function asSystemPrompt(value: readonly string[]): SystemPrompt {
|
||||
return value as SystemPrompt // 零开销类型断言
|
||||
}
|
||||
```
|
||||
|
||||
品牌类型(branded type)防止普通 `string[]` 被意外传入 API 调用——只有通过 `asSystemPrompt()` 显式转换才能获得 `SystemPrompt` 类型。
|
||||
|
||||
## getSystemPrompt():内容组装的全景
|
||||
|
||||
`src/constants/prompts.ts:444` 是 System Prompt 的核心工厂函数,返回一个有序数组:
|
||||
|
||||
| 阶段 | 内容 | 缓存策略 |
|
||||
|------|------|----------|
|
||||
| **静态区** | Intro Section、System Rules、Doing Tasks、Actions、Using Tools、Tone & Style、Output Efficiency | 可跨组织缓存(`scope: 'global'`) |
|
||||
| **BOUNDARY** | `SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'` | 分界标记(不发送给 API) |
|
||||
| **动态区** | Session Guidance、Memory、Model Override、Env Info、Language、Output Style、MCP Instructions、Scratchpad、FRC、Summarize Tool Results、Token Budget、Brief | 每次会话不同(`scope: 'org'` 或无缓存) |
|
||||
|
||||
### 动态区的 Section 注册表
|
||||
|
||||
动态区通过 `systemPromptSection()` / `DANGEROUS_uncachedSystemPromptSection()` 注册,这两个工厂函数定义于 `src/constants/systemPromptSections.ts`:
|
||||
|
||||
```typescript
|
||||
// 缓存式 Section:计算一次,/clear 或 /compact 后才重新计算
|
||||
systemPromptSection('memory', () => loadMemoryPrompt())
|
||||
|
||||
// 危险:每轮重新计算,会破坏 Prompt Cache
|
||||
DANGEROUS_uncachedSystemPromptSection(
|
||||
'mcp_instructions',
|
||||
() => isMcpInstructionsDeltaEnabled() ? null : getMcpInstructionsSection(mcpClients),
|
||||
'MCP servers connect/disconnect between turns' // 必须给出破坏缓存的理由
|
||||
)
|
||||
```
|
||||
|
||||
`resolveSystemPromptSections()` 在每轮查询时解析所有 Section,对于 `cacheBreak: false` 的 Section,优先使用 `getSystemPromptSectionCache()` 中的缓存值。只有 MCP 指令等真正动态的内容使用 `DANGEROUS_uncachedSystemPromptSection`。
|
||||
|
||||
### `CLAUDE_CODE_SIMPLE` 快速路径
|
||||
|
||||
当环境变量 `CLAUDE_CODE_SIMPLE` 为真时,整个 System Prompt 缩减为一行:
|
||||
|
||||
```typescript
|
||||
`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`
|
||||
```
|
||||
|
||||
跳过所有 Section 注册、缓存分块、动态组装——用于最小化 token 消耗的测试场景。
|
||||
|
||||
## buildEffectiveSystemPrompt():五级优先级
|
||||
|
||||
`src/utils/systemPrompt.ts:41` 决定最终使用哪个 System Prompt:
|
||||
|
||||
| 优先级 | 条件 | 行为 |
|
||||
|--------|------|------|
|
||||
| **0. Override** | `overrideSystemPrompt` 非空 | 完全替换,返回 `[override]` |
|
||||
| **1. Coordinator** | `COORDINATOR_MODE` feature + 环境变量 | 使用协调者专用提示词 |
|
||||
| **2. Agent** | `mainThreadAgentDefinition` 存在 | Proactive 模式:追加到默认提示词尾部;否则:替换默认提示词 |
|
||||
| **3. Custom** | `--system-prompt` 参数指定 | 替换默认提示词 |
|
||||
| **4. Default** | 无特殊条件 | 使用 `getSystemPrompt()` 完整输出 |
|
||||
|
||||
`appendSystemPrompt` 始终追加到末尾(Override 除外)。
|
||||
|
||||
## 缓存策略:分块、标记、命中
|
||||
|
||||
这是 System Prompt 设计中最精密的部分。
|
||||
|
||||
### Anthropic Prompt Cache 基础
|
||||
|
||||
Anthropic API 的 Prompt Cache 允许跨请求复用相同的 System Prompt 前缀,按缓存命中量计费(远低于完整输入价格)。缓存键由内容的 Blake2b 哈希决定——任何字符变化都会导致缓存失效。
|
||||
|
||||
### `splitSysPromptPrefix()`:三种分块模式
|
||||
|
||||
`src/utils/api.ts:321` 是缓存策略的核心,根据条件选择三种分块模式:
|
||||
|
||||
#### 模式 1:MCP 工具存在时(`skipGlobalCacheForSystemPrompt=true`)
|
||||
|
||||
```
|
||||
[attribution header] → cacheScope: null (不缓存)
|
||||
[system prompt prefix] → cacheScope: 'org' (组织级缓存)
|
||||
[everything else] → cacheScope: 'org' (组织级缓存)
|
||||
```
|
||||
|
||||
MCP 工具列表在会话中可能变化(连接/断开),破坏了跨组织缓存的基础,因此降级为组织级。
|
||||
|
||||
#### 模式 2:Global Cache + Boundary 存在(1P 专用)
|
||||
|
||||
```
|
||||
[attribution header] → cacheScope: null (不缓存)
|
||||
[system prompt prefix] → cacheScope: null (不缓存)
|
||||
[static content] → cacheScope: 'global' (全局缓存!跨组织共享)
|
||||
[dynamic content] → cacheScope: null (不缓存)
|
||||
```
|
||||
|
||||
这是缓存效率最高的模式。`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 之前的静态内容(Intro、Rules、Tone & Style 等)对所有用户相同,可跨组织缓存。
|
||||
|
||||
#### 模式 3:默认(3P 提供商 或 Boundary 缺失)
|
||||
|
||||
```
|
||||
[attribution header] → cacheScope: null (不缓存)
|
||||
[system prompt prefix] → cacheScope: 'org' (组织级缓存)
|
||||
[everything else] → cacheScope: 'org' (组织级缓存)
|
||||
```
|
||||
|
||||
### `getCacheControl()`:TTL 决策
|
||||
|
||||
`src/services/api/claude.ts:359` 生成的 `cache_control` 对象:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: 'ephemeral',
|
||||
ttl?: '1h', // 仅特定 querySource 符合条件时
|
||||
scope?: 'global', // 仅静态区
|
||||
}
|
||||
```
|
||||
|
||||
1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 394 行):
|
||||
- **Bedrock 用户**:通过环境变量 `ENABLE_PROMPT_CACHING_1H_BEDROCK` 启用
|
||||
- **1P 用户**:通过 GrowthBook 配置的 `allowlist` 数组匹配 `querySource`,支持前缀通配符(如 `"repl_main_thread*"`)
|
||||
- **会话级锁定**:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致
|
||||
|
||||
### 缓存破坏:Session-Specific Guidance 的放置
|
||||
|
||||
`getSessionSpecificGuidanceSection()`(`src/constants/prompts.ts:352`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
|
||||
- 当前会话的 enabledTools 集合
|
||||
- `isForkSubagentEnabled()` 的运行时判定
|
||||
- `getIsNonInteractiveSession()` 的结果
|
||||
|
||||
这些运行时 bit 如果放在静态区,会产生 2^N 种 Blake2b 哈希变体(N = 运行时条件数),完全破坏缓存命中率。源码注释明确警告:
|
||||
|
||||
> Each conditional here is a runtime bit that would otherwise multiply the Blake2b prefix hash variants (2^N). See PR #24490, #24171 for the same bug class.
|
||||
|
||||
### `CLAUDE_CODE_SIMPLE` 模式
|
||||
|
||||
当设置了 `CLAUDE_CODE_SIMPLE` 环境变量时,整个系统提示词会大幅缩减:
|
||||
|
||||
```typescript
|
||||
return [`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`]
|
||||
```
|
||||
|
||||
## 上下文注入:System Context 与 User Context
|
||||
|
||||
System Prompt 数组本身不包含运行时上下文(git 状态、CLAUDE.md 内容)。上下文通过两个独立的管道注入:
|
||||
|
||||
### System Context(`src/context.ts:116`)
|
||||
|
||||
```typescript
|
||||
export const getSystemContext = memoize(async () => {
|
||||
return {
|
||||
gitStatus, // git 分支、状态、最近提交(截断至 MAX_STATUS_CHARS=2000)
|
||||
cacheBreaker, // 仅 ant 用户的缓存破坏器
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- 使用 `lodash.memoize` 缓存——**整个会话期间只计算一次**
|
||||
- Git 状态快照包含 5 个并行 `git` 命令(branch、defaultBranch、status、log、userName)
|
||||
- `status` 超过 2000 字符时截断并附加提示使用 BashTool 获取更多信息
|
||||
- `systemPromptInjection` 变更时,通过 `getUserContext.cache.clear?.()` 清除所有上下文缓存
|
||||
|
||||
### User Context(`src/context.ts:155`)
|
||||
|
||||
```typescript
|
||||
export const getUserContext = memoize(async () => {
|
||||
return {
|
||||
claudeMd, // 合并后的 CLAUDE.md 内容
|
||||
currentDate, // "Today's date is YYYY-MM-DD."
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- **CLAUDE.md 禁用条件**:`CLAUDE_CODE_DISABLE_CLAUDE_MDS` 环境变量,或 `--bare` 模式(除非通过 `--add-dir` 显式指定目录)
|
||||
- `--bare` 模式的语义是"跳过我没要求的东西"而非"忽略所有"
|
||||
|
||||
### 注入位置
|
||||
|
||||
在 `src/query.ts:449`:
|
||||
|
||||
```typescript
|
||||
// System Context 追加到 System Prompt 尾部
|
||||
const fullSystemPrompt = asSystemPrompt(
|
||||
appendSystemContext(systemPrompt, systemContext) // 简单拼接
|
||||
)
|
||||
```
|
||||
|
||||
User Context 通过 `prependUserContext()`(`src/utils/api.ts:449`)注入为 `<system-reminder>` 标签包裹的首条用户消息,放在所有对话消息之前。
|
||||
|
||||
## Attribution Header:计费与安全
|
||||
|
||||
每个 API 请求的 System Prompt 首块是 Attribution Header(`src/constants/system.ts:30`),包含:
|
||||
- **`cc_version`**:Claude Code 版本 + 指纹
|
||||
- **`cc_entrypoint`**:入口点标识(REPL / SDK / pipe 等)
|
||||
- **`cch=00000`**(NATIVE_CLIENT_ATTESTATION 启用时):Bun 原生 HTTP 层在发送前将零替换为计算出的哈希值,服务器验证此 token 确认请求来自真实 Claude Code 客户端
|
||||
|
||||
Header 始终 `cacheScope: null`——它因版本和指纹不同而变化,不适合缓存。
|
||||
|
||||
## CLAUDE.md:项目级知识注入
|
||||
|
||||
这是 Claude Code 最巧妙的设计之一。在项目根目录放一个 `CLAUDE.md` 文件,就能让 AI "理解" 你的项目:
|
||||
|
||||
- **项目概述**:这个项目做什么、用了什么技术栈
|
||||
- **开发约定**:代码风格、命名规范、分支策略
|
||||
- **常用命令**:怎么构建、怎么测试、怎么部署
|
||||
- **注意事项**:已知的坑、特殊的配置
|
||||
|
||||
系统会自动发现并合并多级 CLAUDE.md:
|
||||
|
||||
```
|
||||
~/.claude/CLAUDE.md ← 用户全局(个人偏好)
|
||||
└── /project/CLAUDE.md ← 项目根目录(团队共享)
|
||||
└── /project/src/CLAUDE.md ← 子目录(模块特定)
|
||||
```
|
||||
|
||||
加载逻辑在 `src/utils/claudemd.ts` 中的 `getClaudeMds()` 和 `getMemoryFiles()` 实现——从 CWD 向上遍历目录树,合并所有匹配的 CLAUDE.md 文件内容。
|
||||
|
||||
## 设计洞察:为什么是 `string[]` 而非单个 `string`
|
||||
|
||||
将 System Prompt 设计为数组而非单段文本,是为了 **缓存分块**:
|
||||
|
||||
1. Anthropic Prompt Cache 以 **内容块**(TextBlock)为缓存单位
|
||||
2. 将 System Prompt 拆为多个块,可以让不变的部分(Intro、Rules)获得独立的缓存命中
|
||||
3. 如果是单个 `string`,任何一个字符变化(如日期更新)都会导致整个 System Prompt 的缓存失效
|
||||
4. `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 标记允许 `splitSysPromptPrefix()` 精确地将静态区标记为 `scope: 'global'`,动态区不标记或标记为 `scope: 'org'`
|
||||
|
||||
这是 Claude Code 在 token 成本优化上的核心设计——一次典型的 System Prompt 约 20K+ tokens,通过缓存分块可以节省 30-50% 的输入 token 费用。
|
||||
168
docs/context/token-budget.mdx
Normal file
168
docs/context/token-budget.mdx
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: "Token 预算管理 - 上下文窗口动态计算"
|
||||
description: "从源码角度揭示 Claude Code token 预算管理:200K 上下文窗口的动态计算、截断机制、缓存优化和自动压缩的完整链路。"
|
||||
keywords: ["Token 预算", "上下文窗口", "token 计算", "截断机制", "缓存优化"]
|
||||
---
|
||||
|
||||
{/* 本章目标:从源码角度揭示 token 预算的动态计算、截断机制、缓存优化和自动压缩的完整链路 */}
|
||||
|
||||
## 上下文窗口:200K 不是全部
|
||||
|
||||
Claude Code 的默认上下文窗口为 200K tokens(`MODEL_CONTEXT_WINDOW_DEFAULT = 200_000`),但实际可用于对话的空间远小于此:
|
||||
|
||||
```
|
||||
上下文窗口(200K)
|
||||
├── 系统提示词(~15-25K,缓存后成本低)
|
||||
├── 工具定义(~10-20K,含 MCP 工具)
|
||||
├── 用户上下文(CLAUDE.md、git status 等)
|
||||
├── 输出预留(maxOutputTokens)
|
||||
│ ├── 默认上限:64K
|
||||
│ ├── 实际默认:8K(slot-reservation 优化)
|
||||
│ └── 触顶自动升级:一次 64K 重试
|
||||
└── 剩余:对话历史空间(随对话增长)
|
||||
```
|
||||
|
||||
`getContextWindowForModel()`(`src/utils/context.ts:51`)按 5 级优先级解析窗口大小:
|
||||
|
||||
1. `CLAUDE_CODE_MAX_CONTEXT_TOKENS` 环境变量覆盖
|
||||
2. 模型名含 `[1m]` 后缀 → 1M tokens
|
||||
3. `getModelCapability(model).max_input_tokens`
|
||||
4. 1M beta header + 支持的模型(claude-sonnet-4, opus-4-6)
|
||||
5. 兜底:200K
|
||||
|
||||
**有效上下文** = 窗口大小 - min(maxOutputTokens, 20K),因为压缩摘要需要预留输出空间。
|
||||
|
||||
## Token 计数:近似 vs 精确
|
||||
|
||||
系统使用两级 token 计数策略:
|
||||
|
||||
### 近似估算(毫秒级)
|
||||
|
||||
```typescript
|
||||
// src/services/tokenEstimation.ts
|
||||
function roughTokenCountEstimation(content: string, bytesPerToken = 4): number {
|
||||
return Math.round(content.length / bytesPerToken)
|
||||
}
|
||||
```
|
||||
|
||||
对不同内容类型有特殊处理:
|
||||
- **JSON/JSONL**:`bytesPerToken = 2`(密集的 `{`, `:`, `,` 符号,每个仅 1-2 token)
|
||||
- **图片/文档**:固定 2000 tokens(基于 2000×2000px 上限的保守估计)
|
||||
- **thinking block**:按实际文本长度 / 4
|
||||
- **tool_use**:序列化 `name + JSON.stringify(input)` 后 / 4
|
||||
|
||||
### 精确计数(API 调用)
|
||||
|
||||
使用 Anthropic 的 `beta.messages.countTokens` 端点。在不同 provider 上有不同路径:
|
||||
|
||||
| Provider | 方法 |
|
||||
|----------|------|
|
||||
| Anthropic 直连 | `anthropic.beta.messages.countTokens()` |
|
||||
| AWS Bedrock | `@aws-sdk/client-bedrock-runtime` 的 `CountTokensCommand` |
|
||||
| Google Vertex | Anthropic SDK + beta 过滤 |
|
||||
| 兜底(Bedrock 不支持) | 用 Haiku 发送 `max_tokens=1` 的请求,读取 `usage.input_tokens` |
|
||||
|
||||
精确计数在关键决策点使用(压缩前后对比、warning 判断),近似估算在热路径使用(每轮循环的 shouldAutoCompact 检查)。
|
||||
|
||||
## 自动压缩的触发阈值
|
||||
|
||||
```
|
||||
src/services/compact/autoCompact.ts — 核心阈值
|
||||
```
|
||||
|
||||
| 常量 | 值 | 含义 |
|
||||
|------|----|------|
|
||||
| `AUTOCOMPACT_BUFFER_TOKENS` | 13,000 | 窗口减去此值 = 自动压缩触发点 |
|
||||
| `WARNING_THRESHOLD_BUFFER_TOKENS` | 20,000 | 在触发点 + 20K 处显示警告 |
|
||||
| `ERROR_THRESHOLD_BUFFER_TOKENS` | 20,000 | 在触发点 + 20K 处显示错误 |
|
||||
| `MANUAL_COMPACT_BUFFER_TOKENS` | 3,000 | 手动 /compact 的阻塞上限 |
|
||||
| `MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES` | 3 | 连续失败 3 次后停止尝试 |
|
||||
|
||||
以 200K 窗口为例:
|
||||
- **~167K**:warning 闪烁,用户看到建议压缩的提示
|
||||
- **~180K**:自动压缩触发(200K - 20K 输出预留 = 180K 有效,再 - 13K buffer)
|
||||
- **~197K**:达到 blocking limit,新消息被阻止
|
||||
|
||||
`shouldAutoCompact()` 有多个逃逸条件:
|
||||
- `compact` / `session_memory` 来源的查询永不触发(防递归死锁)
|
||||
- `DISABLE_COMPACT` / `DISABLE_AUTO_COMPACT` 环境变量
|
||||
- 用户配置 `autoCompactEnabled = false`
|
||||
- Context Collapse 模式激活时抑制(collapse 自己管理上下文)
|
||||
- Reactive Compact 实验模式下抑制主动压缩
|
||||
- 超过连续失败上限(circuit breaker)
|
||||
|
||||
## Micro-Compact:工具结果的渐进式压缩
|
||||
|
||||
在触发全量压缩之前,系统先尝试 **micro-compact**——只压缩旧的工具调用结果:
|
||||
|
||||
```
|
||||
可压缩工具列表(COMPACTABLE_TOOLS):
|
||||
FileRead, Bash, Grep, Glob, WebSearch, WebFetch, FileEdit, FileWrite
|
||||
```
|
||||
|
||||
策略基于时间:
|
||||
- 超过一定时间(由 `timeBasedMCConfig` 控制)的工具结果被替换为简短占位符
|
||||
- 图片/文档结果替换为 `[image]` / `[document]` 文本
|
||||
- 每次替换释放 tokens,可能推迟全量压缩
|
||||
|
||||
工具本身也有 `maxResultSizeChars`(通常 100K)硬限制,超长结果在写入消息前就被截断。
|
||||
|
||||
## 全量压缩的完整流程
|
||||
|
||||
```
|
||||
autoCompactIfNeeded() / compactConversation()
|
||||
↓
|
||||
1. 执行 PreCompact hooks(外部可注入自定义指令)
|
||||
↓
|
||||
2. 尝试 Session Memory 压缩(更轻量,优先尝试)
|
||||
↓
|
||||
3. Session Memory 失败 → 全量压缩
|
||||
a. 图片/文档从消息中剥离(替换为 [image]/[document])
|
||||
b. skill_discovery/skill_listing 附件剥离(压缩后会重新注入)
|
||||
c. 通过 forked agent 发送摘要请求(复用主线程的 prompt cache)
|
||||
d. 如果摘要请求本身触发 prompt-too-long → truncateHeadForPTLRetry()
|
||||
从最老的 API 轮次开始删除,重试最多 3 次
|
||||
↓
|
||||
4. 压缩成功后重建上下文:
|
||||
- compactBoundaryMarker(记录压缩类型、前 token 数等)
|
||||
- 摘要消息(不可见的 user 消息)
|
||||
- 最近 5 个文件的重新读取(POST_COMPACT_TOKEN_BUDGET = 50K)
|
||||
- plan 文件附件(如果有)
|
||||
- plan mode 指令(如果在计划模式中)
|
||||
- 已调用的 skill 内容(每 skill ≤5K,总计 ≤25K)
|
||||
- deferred tools / agent listing / MCP 指令的增量重新注入
|
||||
- SessionStart hooks 重新执行
|
||||
- PostCompact hooks 执行
|
||||
↓
|
||||
5. 更新缓存基线,防止被误判为 cache break
|
||||
```
|
||||
|
||||
### Prompt Cache Sharing
|
||||
|
||||
压缩 API 调用是整个会话中最昂贵的操作之一。系统通过 `runForkedAgent` 复用主线程的缓存前缀(system prompt + tools + context messages),将缓存命中率从 2% 提升到接近 100%。这个优化单独节省了舰队级约 0.76% 的 `cache_creation` tokens。
|
||||
|
||||
## 输出 Token 的 Slot 优化
|
||||
|
||||
一个经常被忽视的优化:**maxOutputTokens 的动态调整**。
|
||||
|
||||
```typescript
|
||||
// src/services/api/claude.ts — getMaxOutputTokensForModel()
|
||||
const defaultTokens = isMaxTokensCapEnabled()
|
||||
? Math.min(maxOutputTokens.default, 8_000) // 默认降到 8K
|
||||
: maxOutputTokens.default // 原始默认 32K/64K
|
||||
```
|
||||
|
||||
为什么?因为 API 的 slot 机制按 `max_tokens` 预留推理容量。BQ p99 输出仅 4,911 tokens,32K 默认值浪费了 8-16 倍的 slot 容量。降到 8K 后,不到 1% 的请求被截断——这些请求会自动获得一次 64K 的 clean retry。
|
||||
|
||||
这个优化对 token 预算的影响是间接的:更多的 slot 容量意味着更少的排队延迟,间接减少了超时和重试。
|
||||
|
||||
## Partial Compact:选择性地压缩
|
||||
|
||||
除了全量压缩,用户还可以在消息历史中选择某个位置,只压缩该位置之前或之后的内容:
|
||||
|
||||
- **`up_to` 方向**:压缩选中消息之前的内容,保留最近的对话
|
||||
- **`from` 方向**:压缩选中消息之后的内容,保留早期的对话
|
||||
|
||||
`from` 方向保留 prompt cache(前缀不变),`up_to` 方向则破坏 cache(摘要插在保留内容之前)。
|
||||
|
||||
两种方向的 PTL(prompt-too-long)重试策略相同:从最老的 API 轮次开始删除,确保至少保留一组消息供摘要。
|
||||
Reference in New Issue
Block a user