← note 专题系列 · 2026-04-11
Context Catch
Claude Code 在上下文接近上限时通过 Context Catch 压缩与摘要保留关键信息,本文整理其三层记忆机制并给出 AI-Interview 面试系统的快照恢复应用设计。
Context Catch 是 Claude Code 中上下文管理的技术,用于在上下文空间快用完时,智能地保留重要信息。
问题背景
Claude Code 上下文窗口限制
graph TD
subgraph Window["Token 上限(比如 200K tokens)"]
Seg["系统提示 │ 历史消息 │ 代码上下文 │ 工具输出 │ 剩余空间"]
end
Seg -->|快满了!更多输入| Overflow["超出窗口 → 丢失早期上下文"]
如果继续添加 → 超出窗口 → 丢失早期上下文
Context Catch 的机制
graph TD
S1["Step 1: 检测上下文使用率<br/>当使用量 > 80% 时触发 catch 策略"]
S2["Step 2: 识别'可压缩'的内容<br/>❌ 工具执行结果(可重新执行)<br/>❌ 重复的日志输出<br/>❌ 次要的中间过程<br/>✅ 关键决策点<br/>✅ 用户明确的需求<br/>✅ 代码修改的核心内容"]
S3["Step 3: 执行压缩/摘要<br/>'之前的搜索结果已保存,现在执行下一步...'<br/>'已完成的代码变更:[文件A, 文件B]...'"]
S4["Step 4: 释放空间,继续工作"]
S1 --> S2 --> S3 --> S4
实际效果
压缩前(原始记录)
[11:30] 用户: 帮我创建一个用户注册功能
[11:31] Claude: 我来创建这个功能...
[11:31] 工具: 搜索现有用户相关代码
[11:31] 工具: 找到 5 个相关文件
[11:32] 工具: 读取 user.ts
[11:32] 工具: 读取 auth.ts
[11:32] 工具: 读取 database.ts
[11:33] Claude: 分析了现有代码结构
[11:33] Claude: 开始实现...
[11:34] 工具: 创建 user-service.ts
[11:35] 工具: 更新 user.ts 添加新字段
[11:36] Claude: 完成了用户注册功能
Context Catch 压缩后
[11:30-11:36] 用户注册功能已完成
- 创建: user-service.ts
- 修改: user.ts
- 关键需求: 用户名、邮箱、密码字段
- 状态: 可运行,待测试
Claude Code 中的记忆系统分层
graph TD
subgraph Memory["Claude Code 记忆系统"]
WM["Working Memory (当前上下文)<br/>当前会话的完整上下文<br/>所有工具调用和结果<br/>代码修改历史"]
CC["Compressed Context<br/>关键决策点摘要<br/>已完成/进行中的工作摘要<br/>重要文件状态"]
PM["Persistent Memory<br/>项目知识 (README, 架构)<br/>用户偏好设置<br/>长期项目上下文"]
WM -->|"Context Catch 压缩/摘要"| CC --> PM
end
Context Catch 的策略
| 策略 | 说明 |
|---|---|
| 工具输出压缩 | 工具执行结果压缩为”已执行”摘要 |
| 重复检测 | 连续相似的操作只保留一个 |
| 重要性评分 | 根据用户反馈、决策点等评分 |
| 层级保留 | 关键信息 > 中间过程 > 详细日志 |
使用示例
对话很长后,Claude 自动压缩
用户: 继续刚才的工作
Claude: (Context Catch 检测到上下文已压缩)
我记起来了:
- 正在实现用户注册功能
- 已完成 user-service.ts 和 user.ts 修改
- 需要完成: 验证逻辑 + 测试
让我继续…
总结:Context Catch 就是 Claude Code 在上下文快满时,自动压缩/摘要早期内容,保留关键信息,确保重要上下文不丢失的机制。
详细讲解:Context Catch 与 Claude Code 记忆系统
针对你在 AI-Interview 项目中的应用需求,以下是更完整的架构说明和代码实现。
整体架构:三层记忆系统
graph TD
subgraph ThreeLayer["Claude Code 记忆系统三层架构"]
L1["L1: Working Context (活动层)<br/>当前 session 的完整上下文<br/>实时消息、工具调用、代码修改<br/>Token 消耗大户 (会触发 catch)<br/>容量:~200K tokens<br/>淘汰:超出时触发 Context Catch"]
L2["L2: Compressed Context (压缩层)<br/>压缩后的会话摘要<br/>关键决策点 (用户确认的选择、设计决策)<br/>工作进度快照 (完成了什么、待完成什么)<br/>文件变更摘要<br/>容量:~20K tokens (L1 的 10%)<br/>淘汰:Session 结束或长期不活跃"]
L3["L3: Persistent Memory (持久层)<br/>项目知识 (项目 README、架构文档、代码规范)<br/>用户偏好 (编程语言偏好、注释风格)<br/>跨 session 的长期状态<br/>容量:无限制 (存储在磁盘)<br/>淘汰:用户主动删除或项目结束"]
L1 -->|"Context Catch 压缩"| L2 --> L3
end
L1 → L2 的 Context Catch 压缩过程(代码示例)
// 压缩前的原始数据 (假设 1500 tokens)
interface RawSession {
messages: [
{ role: "user", content: "帮我实现用户登录功能" },
{ role: "assistant", content: "我来帮你实现..." },
{ role: "system", content: "正在搜索现有代码..." },
{ role: "tool", content: "找到 5 个相关文件: user.ts, auth.ts..." },
{ role: "tool", content: "读取 user.ts 完成 (200行)" },
{ role: "tool", content: "读取 auth.ts 完成 (150行)" },
{ role: "assistant", content: "分析完成,开始实现..." },
{ role: "tool", content: "修改 user.ts: +50行" },
{ role: "tool", content: "创建 auth-service.ts: +80行" },
{ role: "assistant", content: "登录功能已完成" }
];
}
// Context Catch 压缩后
interface CompressedSnapshot {
sessionId: string;
startTime: Date;
endTime: Date;
duration: string; // "15分钟"
goal: "实现用户登录功能";
status: "已完成";
decisions: [
{ point: "使用 JWT 进行身份验证", timestamp: "11:35" },
{ point: "密码使用 bcrypt 加密", timestamp: "11:36" }
];
changes: {
created: ["auth-service.ts", "jwt-util.ts"],
modified: ["user.ts", "auth.ts"],
deleted: []
};
todo: [
"添加单元测试",
"更新 API 文档"
];
keySnippets: [
"function login(): Promise<{token, user}> { ... }"
];
compressedTokens: 300;
}
Context Catch 的具体策略(伪代码)
// 1. 工具输出压缩
const toolOutputCompression = {
before: "读取文件 /src/user.ts 完成\n共 200 行:\nline 1: import {...}\n...",
after: "[已读取] user.ts (200行)",
preserveIf: (output) => output.includes("ERROR") || output.includes("找到")
};
// 2. 重复消息折叠
const duplicateCollapse = {
before: ["正在搜索...", "正在搜索...", "正在搜索..."],
after: "[重复操作已折叠] 搜索 x3"
};
// 3. 中间过程摘要
const processSummarization = {
before: ["分析代码结构...", "分析依赖关系...", "确定实现方案..."],
after: "完成代码分析和方案设计"
};
// 4. 决策点保留
const decisionPreservation = {
preserve: [
"用户确认使用 TypeScript",
"用户选择 REST API 风格",
"用户要求添加日志中间件"
],
priority: "HIGH"
};
AI-Interview 项目应用设计(基于 Context Catch)
面试记忆系统架构
graph TD
subgraph InterviewMem["AI-Interview 记忆系统设计"]
UserSession["用户会话层 (User Session)<br/>面试进行中的实时上下文<br/>候选人的回答历史<br/>评分和笔记<br/>触发 Context Catch 条件: Token>80% / 用户主动暂停中断 / 长时间无交互(>30分钟)"]
Snapshot["面试快照层 (Interview Snapshot)<br/>当用户中断面试时保存:<br/>sessionId: 'interview_2024_001'<br/>candidateName: 张三 / position: 高级后端工程师<br/>currentQuestion: 5 / totalQuestions: 10<br/>keyInsights: 分布式理解深入 / 算法偏弱<br/>overallImpression: '80%推荐进入下一轮'"]
Profile["候选人档案层 (Candidate Profile)<br/>候选人基础信息 (简历、教育背景)<br/>历史面试记录 (如有)<br/>技能评估雷达图<br/>面试偏好设置<br/>应用: 下次面试快速预热 / 跟踪成长 / 面试官间共享"]
UserSession --> Snapshot --> Profile
end
核心 TypeScript 实现
// interview-context-catch.ts
interface InterviewSnapshot {
sessionId: string;
candidateInfo: {
name: string;
position: string;
interviewId: string;
};
progress: {
currentPhase: 'introduction' | 'coding' | 'system-design' | 'behavioral' | 'qna';
currentQuestionIndex: number;
totalQuestions: number;
answeredQuestions: number[];
};
evaluation: {
questionScores: Record<string, number>;
strengthAreas: string[];
improvementAreas: string[];
overallScore?: number;
};
keyInsights: {
candidateStrengths: string[];
candidateWeaknesses: string[];
technicalDepth: string[];
redFlags: string[];
};
currentContext: {
lastQuestion: Question | null;
partialAnswer?: string;
ongoingDiscussion?: string;
};
createdAt: Date;
updatedAt: Date;
expiresAt?: Date;
}
class InterviewContextCatch {
private threshold = 200000; // token 上限
private maxMessages = 100;
shouldCompress(context: InterviewContext): boolean {
const tokenUsage = calculateTokens(context);
return (
tokenUsage > this.threshold * 0.8 ||
context.messages.length > this.maxMessages ||
this.hasLongSilence(context)
);
}
compress(context: InterviewContext): InterviewSnapshot {
return {
sessionId: context.sessionId,
candidateInfo: context.candidateInfo,
progress: context.progress,
evaluation: this.compressEvaluation(context.evaluation),
keyInsights: this.extractKeyInsights(context),
currentContext: {
lastQuestion: context.currentQuestion,
partialAnswer: context.currentAnswer?.slice(-500),
},
summary: this.generateSummary(context),
updatedAt: new Date()
};
}
extractKeyInsights(context: InterviewContext): InterviewSnapshot['keyInsights'] {
return {
candidateStrengths: this.deduplicate([
...context.evaluation.technicalStrengths,
...context.evaluation.communicationStrengths,
]),
candidateWeaknesses: this.deduplicate([
...context.evaluation.technicalWeaknesses,
...context.evaluation.areasForImprovement,
]),
technicalDepth: context.questions
.filter(q => q.answer?.depth > 7)
.map(q => q.topic),
redFlags: context.evaluation.redFlags || [],
};
}
async restore(snapshot: InterviewSnapshot): Promise<InterviewContext> {
return {
sessionId: snapshot.sessionId,
candidateInfo: snapshot.candidateInfo,
progress: snapshot.progress,
evaluation: snapshot.evaluation,
keyInsights: snapshot.keyInsights,
currentQuestion: snapshot.currentContext.lastQuestion,
partialAnswer: snapshot.currentContext.partialAnswer,
recoveryMessage: this.generateRecoveryMessage(snapshot),
};
}
generateRecoveryMessage(snapshot: InterviewSnapshot): string {
return `
面试已暂停,现在恢复。
候选人: ${snapshot.candidateInfo.name}
应聘岗位: ${snapshot.candidateInfo.position}
当前进度: 第 ${snapshot.progress.currentQuestionIndex + 1} 题,共 ${snapshot.progress.totalQuestions} 题
已完成评估:
- 总体评分: ${snapshot.evaluation.overallScore || '待定'}/10
- 优势: ${snapshot.keyInsights.candidateStrengths.join('、')}
- 不足: ${snapshot.keyInsights.candidateWeaknesses.join('、')}
待完成任务:
${snapshot.progress.answeredQuestions.length}/1 部分回答待完成
下一步: ${snapshot.keyInsights}
请继续面试。
`.trim();
}
}
快照持久化存储
// interview-snapshot-storage.ts
class InterviewSnapshotStorage {
private snapshotDir = "./snapshots";
async save(snapshot: InterviewSnapshot): Promise<void> {
const path = this.getSnapshotPath(snapshot.sessionId);
await fs.writeFile(path, JSON.stringify({
...snapshot,
createdAt: snapshot.createdAt.toISOString(),
updatedAt: snapshot.updatedAt.toISOString(),
expiresAt: snapshot.expiresAt?.toISOString(),
}, null, 2));
await this.saveToUserLocalStorage(snapshot);
}
async load(sessionId: string): Promise<InterviewSnapshot | null> {
const path = this.getSnapshotPath(sessionId);
if (await fs.exists(path)) {
const data = await fs.readFile(path, 'utf-8');
return this.deserialize(JSON.parse(data));
}
return null;
}
async list(candidateName?: string): Promise<InterviewSnapshot[]> {
const files = await fs.readdir(this.snapshotDir);
const snapshots = await Promise.all(
files
.filter(f => f.endsWith('.json'))
.map(f => this.load(f.replace('.json', '')))
);
if (candidateName) {
return snapshots.filter(s => s?.candidateInfo.name.includes(candidateName));
}
return snapshots.filter((s): s is InterviewSnapshot => s !== null);
}
async cleanup(): Promise<void> {
const snapshots = await this.list();
const now = new Date();
for (const snapshot of snapshots) {
if (snapshot.expiresAt && new Date(snapshot.expiresAt) < now) {
await this.delete(snapshot.sessionId);
}
}
}
}
用户中断 → 恢复的完整流程
graph TD
S1["Step 1: 检测中断<br/>beforeunload 事件触发 → 自动保存快照 → 保存到 localStorage + 服务器"]
S2["Step 2: 生成压缩快照<br/>ContextCatch.compress(interviewContext) → 生成 InterviewSnapshot<br/>保留关键洞察、进度、评分,压缩中间过程"]
S3["Step 3: 持久化存储<br/>保存到 IndexedDB (浏览器本地) + 服务器数据库,设置7天过期"]
Note["用户重新打开应用"]
S4["Step 4: 检测未完成面试<br/>应用启动 → 检查 localStorage → 发现未完成快照 → 询问用户是否恢复"]
S5["Step 5: 恢复上下文<br/>snapshot = load(snapshotId); context = restore(snapshot)<br/>生成恢复消息,展示摘要给用户确认"]
S6["Step 6: 继续面试<br/>用户确认 → 继续面试流程"]
S1 --> S2 --> S3 --> Note --> S4 --> S5 --> S6
关键设计要点
| 要点 | 说明 |
|---|---|
| 优先级 | 关键洞察 > 评分数据 > 进度 > 中间过程 |
| 压缩比 | L1 → L2 通常压缩到 10-15% |
| 保留决策 | 面试官的评价、候选人表现判断必须保留 |
| 过期机制 | 快照设置过期时间,定期清理 |
| 用户确认 | 恢复前展示摘要,用户确认继续 |
效果对比
| 项目 | 无 Context Catch | 有 Context Catch |
|---|---|---|
| 长面试支持 | ❌ 受 token 限制 | ✅ 支持任意长度 |
| 中断恢复 | ❌ 全部丢失 | ✅ 保留关键上下文 |
| 多轮面试 | ❌ 每次重新开始 | ✅ 累积候选人档案 |
| Token 消耗 | 线性增长 | 保持稳定 |