← note 专题系列 · 2026-04-11

Context Catch

Claude Code 在上下文接近上限时通过 Context Catch 压缩与摘要保留关键信息,本文整理其三层记忆机制并给出 AI-Interview 面试系统的快照恢复应用设计。

7 min read

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% / 用户主动暂停中断 / 长时间无交互(&gt;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 消耗线性增长保持稳定