3110 words
16 minutes
Context Catch
Context Catch
Context Catch 是 Claude Code 中上下文管理的技术,用于在上下文空间快用完时,智能地保留重要信息。
问题背景
Claude Code 上下文窗口限制
Token 上限(比如 200K tokens)
已使用:├─────────────────────────────────────────────────────────────┤│ 系统提示 │ 历史消息 │ 代码上下文 │ 工具输出 │ 剩余空间 │└─────────────────────────────────────────────────────────────┘ ↑ 快满了!如果继续添加 → 超出窗口 → 丢失早期上下文
Context Catch 的机制
┌─────────────────────────────────────────────────────────────┐│ Context Catch 工作流程 │├─────────────────────────────────────────────────────────────┤│ ││ Step 1: 检测上下文使用率 ││ ┌─────────────────────────────────────────────────────┐ ││ │ 当使用量 > 80% 时触发 catch 策略 │ ││ └─────────────────────────────────────────────────────┘ ││ ↓ ││ Step 2: 识别"可压缩"的内容 ││ ┌─────────────────────────────────────────────────────┐ ││ │ ❌ 工具执行结果(可重新执行) │ ││ │ ❌ 重复的日志输出 │ ││ │ ❌ 次要的中间过程 │ ││ │ ✅ 关键决策点 │ ││ │ ✅ 用户明确的需求 │ ││ │ ✅ 代码修改的核心内容 │ ││ └─────────────────────────────────────────────────────┘ ││ ↓ ││ Step 3: 执行压缩/摘要 ││ ┌─────────────────────────────────────────────────────┐ ││ │ "之前的搜索结果已保存,现在执行下一步..." │ ││ │ "已完成的代码变更:[文件A, 文件B]..." │ ││ └─────────────────────────────────────────────────────┘ ││ ↓ ││ Step 4: 释放空间,继续工作 ││ │└─────────────────────────────────────────────────────────────┘实际效果
压缩前(原始记录)
[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 中的记忆系统分层
┌─────────────────────────────────────────────────────────────┐│ Claude Code 记忆系统 │├─────────────────────────────────────────────────────────────┤│ ││ ┌─────────────────────────────────────────────────────┐ ││ │ Working Memory (当前上下文) │ ││ │ • 当前会话的完整上下文 │ ││ │ • 所有工具调用和结果 │ ││ │ • 代码修改历史 │ ││ └──────────────────┬──────────────────────────────────┘ ││ │ Context Catch ││ ↓ 压缩/摘要 ││ ┌─────────────────────────────────────────────────────┐ ││ │ Compressed Context │ ││ │ • 关键决策点摘要 │ ││ │ • 已完成/进行中的工作摘要 │ ││ │ • 重要文件状态 │ ││ └──────────────────┬──────────────────────────────────┘ ││ │ ││ ↓ ││ ┌─────────────────────────────────────────────────────┐ ││ │ Persistent Memory │ ││ │ • 项目知识 (README, 架构) │ ││ │ • 用户偏好设置 │ ││ │ • 长期项目上下文 │ ││ └─────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────┘Context Catch 的策略
| 策略 | 说明 |
|---|---|
| 工具输出压缩 | 工具执行结果压缩为”已执行”摘要 |
| 重复检测 | 连续相似的操作只保留一个 |
| 重要性评分 | 根据用户反馈、决策点等评分 |
| 层级保留 | 关键信息 > 中间过程 > 详细日志 |
使用示例
对话很长后,Claude 自动压缩
用户: 继续刚才的工作
Claude: (Context Catch 检测到上下文已压缩)
我记起来了:
- 正在实现用户注册功能
- 已完成 user-service.ts 和 user.ts 修改
- 需要完成: 验证逻辑 + 测试
让我继续…
总结:Context Catch 就是 Claude Code 在上下文快满时,自动压缩/摘要早期内容,保留关键信息,确保重要上下文不丢失的机制。
详细讲解:Context Catch 与 Claude Code 记忆系统
针对你在 AI-Interview 项目中的应用需求,以下是更完整的架构说明和代码实现。
整体架构:三层记忆系统
┌─────────────────────────────────────────────────────────────────┐│ Claude Code 记忆系统三层架构 │├─────────────────────────────────────────────────────────────────┤│ ││ ┌─────────────────────────────────────────────────────────┐ ││ │ L1: Working Context (活动层) │ ││ │ │ ││ │ • 当前 session 的完整上下文 │ ││ │ • 实时消息、工具调用、代码修改 │ ││ │ • Token 消耗大户 (会触发 catch) │ ││ │ │ ││ │ 容量:~200K tokens │ ││ │ 淘汰:超出时触发 Context Catch │ ││ └─────────────────────────────────────────────────────────┘ ││ │ ││ ▼ Context Catch ▼ ││ ┌─────────────────────────────────────────────────────────┐ ││ │ L2: Compressed Context (压缩层) │ ││ │ │ ││ │ • 压缩后的会话摘要 │ ││ │ • 关键决策点 (用户确认的选择、设计决策) │ ││ │ • 工作进度快照 (完成了什么、待完成什么) │ ││ │ • 文件变更摘要 │ ││ │ │ ││ │ 容量:~20K tokens (L1 的 10%) │ ││ │ 淘汰:Session 结束或长期不活跃 │ ││ └─────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌─────────────────────────────────────────────────────────┐ ││ │ L3: Persistent Memory (持久层) │ ││ │ │ ││ │ • 项目知识 (项目 README、架构文档、代码规范) │ ││ │ • 用户偏好 (编程语言偏好、注释风格) │ ││ │ • 跨 session 的长期状态 │ ││ │ │ ││ │ 容量:无限制 (存储在磁盘) │ ││ │ 淘汰:用户主动删除或项目结束 │ ││ └─────────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────┘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)
面试记忆系统架构
┌─────────────────────────────────────────────────────────────────┐│ AI-Interview 记忆系统设计 │├─────────────────────────────────────────────────────────────────┤│ ││ ┌─────────────────────────────────────────────────────────┐ ││ │ 用户会话层 (User Session) │ ││ │ • 面试进行中的实时上下文 │ ││ │ • 候选人的回答历史 │ ││ │ • 评分和笔记 │ ││ │ 触发 Context Catch 条件: │ ││ │ - Token 使用 > 80% │ ││ │ - 用户主动暂停/中断 │ ││ │ - 长时间无交互 (>30分钟) │ ││ └─────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌─────────────────────────────────────────────────────────┐ ││ │ 面试快照层 (Interview Snapshot) │ ││ │ 当用户中断面试时保存: │ ││ │ { │ ││ │ sessionId: "interview_2024_001", │ ││ │ candidateName: "张三", │ ││ │ position: "高级后端工程师", │ ││ │ currentQuestion: 5, │ ││ │ totalQuestions: 10, │ ││ │ answeredQuestions: [1,2,3,4], │ ││ │ scores: { q1: 4, q2: 3, q3: 5, q4: 2 }, │ ││ │ keyInsights: [ │ ││ │ "候选人对分布式系统理解深入", │ ││ │ "算法能力偏弱", │ ││ │ "项目经验描述不够具体" │ ││ │ ], │ ││ │ partialAnswer: { │ ││ │ questionId: 5, │ ││ │ content: "关于性能优化,我首先会..." │ ││ │ }, │ ││ │ overallImpression: "80%推荐进入下一轮", │ ││ │ nextActions: "继续第5题的系统设计部分" │ ││ │ } │ ││ └─────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌─────────────────────────────────────────────────────────┐ ││ │ 候选人档案层 (Candidate Profile) │ ││ │ • 候选人基础信息 (简历、教育背景) │ ││ │ • 历史面试记录 (如有) │ ││ │ • 技能评估雷达图 │ ││ │ • 面试偏好设置 │ ││ │ 应用: │ ││ │ - 下次面试快速预热 │ ││ │ - 跟踪候选人成长 │ ││ │ - 面试官间共享候选人信息 │ ││ └─────────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘核心 TypeScript 实现
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(); }}快照持久化存储
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); } } }}用户中断 → 恢复的完整流程
┌─────────────────────────────────────────────────────────────────┐│ AI-Interview Session 恢复流程 │├─────────────────────────────────────────────────────────────────┤│ 用户场景: 面试进行到一半,用户关闭浏览器 ││ ││ Step 1: 检测中断 ││ beforeunload 事件触发 → 自动保存快照 → 保存到 localStorage + 服务器 ││ ││ Step 2: 生成压缩快照 ││ ContextCatch.compress(interviewContext) → 生成 InterviewSnapshot ││ 保留关键洞察、进度、评分,压缩中间过程 ││ ││ Step 3: 持久化存储 ││ 保存到 IndexedDB (浏览器本地) + 服务器数据库,设置7天过期 ││ ││ ---------------------- 用户重新打开应用 ---------------------- ││ ││ Step 4: 检测未完成面试 ││ 应用启动 → 检查 localStorage → 发现未完成快照 → 询问用户是否恢复 ││ ││ Step 5: 恢复上下文 ││ snapshot = load(snapshotId); context = restore(snapshot) ││ 生成恢复消息,展示摘要给用户确认 ││ ││ Step 6: 继续面试 ││ "上次面试进行到第5题,候选人正在回答系统设计问题,回答内容关于缓存策略..." ││ 用户确认 → 继续面试流程 │└─────────────────────────────────────────────────────────────────┘关键设计要点
| 要点 | 说明 |
|---|---|
| 优先级 | 关键洞察 > 评分数据 > 进度 > 中间过程 |
| 压缩比 | L1 → L2 通常压缩到 10-15% |
| 保留决策 | 面试官的评价、候选人表现判断必须保留 |
| 过期机制 | 快照设置过期时间,定期清理 |
| 用户确认 | 恢复前展示摘要,用户确认继续 |
效果对比
| 项目 | 无 Context Catch | 有 Context Catch |
|---|---|---|
| 长面试支持 | ❌ 受 token 限制 | ✅ 支持任意长度 |
| 中断恢复 | ❌ 全部丢失 | ✅ 保留关键上下文 |
| 多轮面试 | ❌ 每次重新开始 | ✅ 累积候选人档案 |
| Token 消耗 | 线性增长 | 保持稳定 |
Context Catch
https://sgjki547.top/posts/context-catch/