OFFICIAL SOURCE ANALYSIS

运行时机制

工具执行编排(并行 vs 串行)、Zustand 状态管理、上下文压缩、会话持久化与恢复。

4. 工具执行编排 — 并行 vs 串行

当模型在一次响应中请求多个工具调用时(Claude 支持在一次回复中同时请求多个工具),Claude Code 需要决定:这些工具是并行执行还是串行执行?错误决策会导致竞态条件或不必要的性能损失。

并行执行条件

一个工具要被纳入并行执行池,必须同时满足两个条件:

isReadOnly = true
+
isConcurrencySafe = true
=
可并行
工具属性含义示例
isReadOnly 工具不会修改任何文件或系统状态 Read, Grep, Glob, LS
isConcurrencySafe 多个实例同时运行不会相互干扰 大多数只读工具都是并发安全的
并行上限:最多 10 个。即使模型请求了 15 个并行读取操作,也会分成两批执行(10 + 5)。这个限制是为了避免打开过多子进程导致系统资源耗尽。

串行执行:写入工具

任何涉及文件写入或系统状态修改的工具都必须串行执行。原因很简单:

混合场景怎么处理?

如果模型在一次响应中同时请求了 3 个 Read 和 1 个 Edit,Claude Code 的行为是:

  1. 先并行执行 3 个 Read(它们是只读且并发安全的)
  2. 等所有 Read 完成后,再串行执行 Edit

实际上调度逻辑会将工具调用分为两组:可并行组和必须串行组,先执行并行组,再依次执行串行组。

重试机制

工具执行层和 API 调用层都有各自的重试策略。这里重点说 API 层的重试,因为它直接影响用户体验:

错误类型重试策略最大等待时间
429 (Rate Limited) 指数退避重试,每次等待时间翻倍 最多 10 次,累计最长约 5 分钟
529 (Overloaded) 指数退避重试 最多 3 次(fast mode 下仅 1 次)
ECONNRESET 网络层连接重置,立即重试 仅 1 次,失败则报错
401 (Auth Error) 不重试,认证问题不会自动恢复
Prompt Too Long 不重试,需要先触发上下文压缩
指数退避的具体行为:第 1 次重试等 1 秒,第 2 次 2 秒,第 3 次 4 秒... 每次翻倍,加上随机抖动(jitter)避免多个客户端同时重试导致「惊群效应」。在无人值守(headless)模式下,429/529 会无限重试,并每 30 秒发送 keep-alive 防止连接超时。

5. 状态管理 — Zustand Store

Claude Code 使用 Zustand 作为状态管理方案。如果你用过 React,应该知道 Zustand——它是一个极简但强大的状态管理库,核心思想是不可变状态 + 观察者模式

为什么不用全局变量?

在一个有多个子系统(UI 渲染、API 调用、工具执行、设置管理)需要共享状态的应用中,全局变量会迅速变成维护噩梦。Zustand 提供了三个关键保证:

  1. 不可变更新:每次状态变更都产生新的状态对象,旧状态不会被修改。这使得时间旅行调试和变更追踪成为可能。
  2. 精确订阅:组件只订阅自己关心的状态切片,不会因为无关状态变更而重新渲染。
  3. 中间件支持:可以在状态变更时执行副作用(持久化、日志、通知 IDE 等)。

AppState 核心结构

以下是 Zustand store 中管理的核心状态:

状态字段类型说明
settings 对象 用户设置,包括主题、权限模式、自定义 API endpoint 等
model 字符串 当前使用的模型标识(如 claude-sonnet-4-20250514
permissions 对象 allow/deny 规则集合,来自配置文件 + 运行时用户选择
tasks 数组 TodoWrite 创建的任务追踪列表
fastMode 布尔值 是否启用快速模式(使用 Haiku 等轻量模型)
remoteSessionUrl 字符串 | null 远程开发会话的 URL(如通过 SSH 连接时)
mcpServers Map 已连接的 MCP 服务器及其状态
conversationId 字符串 当前会话唯一标识,用于持久化和恢复

变更回调:状态变化触发的副作用

Zustand 的中间件机制让 Claude Code 能在状态变更时自动执行副作用。以下是三个最重要的回调:

1. 持久化设置

settings 状态变更时,变更会自动序列化并写入 ~/.claude/settings.json。这保证了用户的偏好在下次启动时依然有效。写入是防抖的(debounced),多次快速变更只触发一次磁盘写入。

2. 刷新 Feature Gate

当某些关键状态(如 modelsettings)变更时,feature gate 会重新评估。某些功能可能只对特定模型或设置组合开放。例如,切换到 Opus 模型可能解锁额外的工具或更高的并行限制。

3. 通知 IDE

当 Claude Code 作为 VS Code / JetBrains 扩展运行时,状态变更需要同步到 IDE 层。例如:

  • 模型切换 → 更新状态栏显示
  • 权限变更 → 更新权限指示器
  • 任务列表变更 → 更新 IDE 侧边栏面板
  • 会话状态变更 → 更新活动指示器

这种通知通过 JSON-RPC 消息发送到 IDE 宿主进程。

6. 上下文压缩 — Auto-compact 机制

这是 Claude Code 中最精巧的子系统之一。在长时间交互中,上下文会不断增长,最终触及模型的窗口上限。Auto-compact 机制在这发生之前自动介入,在保留关键信息的同时大幅缩小上下文。

触发阈值

触发条件: 当前上下文 token 数 > contextWindow - 13000 其中 contextWindow 取决于当前模型: ┌─────────────────────────┬─────────────┬──────────────┐ │ 模型 │ 上下文窗口 │ 触发阈值 │ ├─────────────────────────┼─────────────┼──────────────┤ │ Claude Sonnet │ 200k tokens │ ~187k tokens │ │ Claude Opus │ 200k tokens │ ~187k tokens │ │ Claude Haiku │ 200k tokens │ ~187k tokens │ └─────────────────────────┴─────────────┴──────────────┘ 留出 13000 tokens 的安全余量,确保压缩后 模型仍有足够空间生成回复 + 工具定义

压缩流程

一旦阈值被触发,以下步骤按顺序执行:

检测阈值
Strip Images
LLM 摘要
替换旧消息
重新注入关键信息
步骤 1:Strip Images — 移除图片内容

图片是上下文中最大的 token 消耗者。一张中等大小的截图可能占用数千 token。压缩的第一步是将所有图片内容从消息历史中移除,替换为简短的文本占位符(如「[已移除的图片:login-page-screenshot.png]」)。

步骤 2:LLM 摘要 — 用模型压缩对话

这是最核心的步骤。Claude Code 会调用一次额外的 LLM 请求(用同一个模型),将整个对话历史发送过去,要求模型生成一个结构化摘要。关键参数:

  • max_output_tokens = 20000:摘要最多 2 万 token,确保信息密度足够高
  • 指令重点:保留所有文件路径、代码变更、错误信息、用户偏好、未完成的任务
  • 格式要求:摘要必须是自包含的——读完摘要的人应该能接着完成之前的工作
步骤 3:替换所有旧消息

摘要生成后,messages[] 数组中的所有旧消息会被替换为一条系统消息,内容就是摘要。这一步将上下文从可能的 18 万 token 压缩到 2 万 token 以内。

步骤 4:重新注入关键工具和技能

压缩后,一些关键的上下文信息会被重新注入:

  • 当前激活的 skill 的完整内容(不只是名字)
  • 最近一次工具调用的完整结果(模型可能需要继续之前的操作)
  • 当前文件的编辑状态(如果有未完成的编辑)

Circuit Breaker — 熔断机制

如果压缩连续失败(例如 LLM 摘要请求本身因为上下文过大而失败),Claude Code 不会无限重试。连续 3 次失败后,circuit breaker 被触发,压缩功能暂时禁用。此时用户会收到提示,建议手动 /clear 对话或开始新会话。

为什么需要熔断?如果压缩一直失败但仍不断重试,每次重试本身也消耗 API 额度和时间。熔断机制避免了在不可恢复的情况下持续浪费资源。

完整对话保存到 .transcripts/

重要的是:压缩不等于数据丢失。在执行压缩之前,完整的对话历史(包括所有图片、工具调用和结果)会被序列化并保存到 ~/.claude/.transcripts/ 目录。每个文件以会话 ID 和时间戳命名。

这意味着即使当前上下文被压缩到只剩摘要,原始对话依然可以完整回溯。这对调试、审计和了解 Claude 的完整思考过程非常有价值。

7. 会话持久化与恢复

Claude Code 的会话管理不是事后想到的功能——它从架构层面就设计为「随时可中断、随时可恢复」的系统。

磁盘写入策略

前面在 QueryEngine 部分提到过:每条用户消息在 API 响应返回之前就写入磁盘。具体来说,写入的内容包括:

写入时机写入内容存储位置
用户发送消息后 完整的 messages[] 数组快照 ~/.claude/conversations/{id}.json
工具执行完成后 更新后的 messages[](含工具结果) 同上(覆盖写入)
模型回复完成后 包含模型回复的 messages[] 同上(覆盖写入)
会话结束/中断时 最终状态 + 元数据(模型、用量、时间戳) 同上 + transcript 副本
为什么是覆盖写入而不是追加?追加写入(append log)在恢复时需要重放所有事件,对于长会话来说成本很高。覆盖写入(snapshot)让恢复变成简单的反序列化操作,代价是每次写入量更大,但考虑到现代 SSD 的写入速度和对话数据的相对小体量(通常几百 KB 到几 MB),这是一个合理的权衡。

/resume — 恢复历史会话

/resume 命令可以恢复任何之前的会话。执行流程:

/resume
列出历史会话
用户选择
加载 messages[]
恢复状态
恢复时发生了什么?
  1. 扫描会话文件:读取 ~/.claude/conversations/ 下所有会话文件,按最后活跃时间排序
  2. 展示选择列表:显示每个会话的首条消息摘要、时间戳和消息数量
  3. 反序列化:将选中会话的 JSON 文件解析为 messages[] 数组
  4. 恢复 Zustand 状态:重建会话相关的所有状态(模型选择、权限决策历史等)
  5. 重建上下文:由于 CLAUDE.md 和 skill 信息可能已经更新,system prompt 会基于当前最新配置重新组装
  6. 继续对话:用户可以像刚才离开一样继续交互

/rewind — 回退代码和对话状态

/rewind 是一个比 /resume 更强大的命令——它不仅回退对话,还回退代码变更。

/rewind 的独特之处:大多数 AI 编程工具只能回退对话,但 Claude Code 的 /rewind 还会 git checkout 到对应时间点的代码状态。这意味着你可以说「那个重构方向不对,回到 3 步之前」,不仅对话恢复到那个时候,文件内容也回去了。
/rewind 的执行步骤
  1. 确定回退点:展示对话历史中每一步的概要,用户选择要回退到哪里
  2. Git 状态检查:检查当前是否有未提交的变更,如果有,提醒用户先 stash 或 commit
  3. 代码回退:使用 Git 将文件系统恢复到选定时间点的状态
  4. 对话回退:截断 messages[] 数组到选定点,丢弃之后的所有消息
  5. 保存截断后状态:将新的 messages[] 写入磁盘
  6. 继续对话:现在用户可以从回退点开始走一条不同的路径

架构全景图

最后,让我们把所有模块串在一起,看一条消息从输入到输出经过的完整路径:

用户输入: "帮我把 auth 模块从 JWT 迁移到 session" │ │ ① processUserInput │ - 不是斜杠命令 → 继续 │ - 无附件 → 继续 │ - 模型: claude-sonnet → 确认 │ │ ② 上下文装配 │ System Prompt = 核心规则 │ + CLAUDE.md (全局 + 项目) │ + Skill 索引 (15 个 skill 名) │ + MCP 工具列表 (8 个工具名) │ Messages = [之前的 23 条消息] + [本次用户消息] │ Tools = [38 个工具定义] (过滤掉了 4 个被 deny 的) │ │ ③ 写入磁盘 (API 调用前) │ conversations/abc-123.json → 已保存 │ │ ④ API 调用 → Claude 返回 │ stop_reason: "tool_use" │ 工具调用: [Grep("auth"), Grep("jwt"), Read("src/auth/index.ts")] │ │ ⑤ 工具编排 │ 3 个都是 ReadOnly + ConcurrencySafe → 并行执行 │ ┌─→ Grep("auth") → 12 个匹配文件 │ ├─→ Grep("jwt") → 5 个匹配文件 │ └─→ Read("src/auth/index.ts") → 文件内容 │ │ ⑥ 工具结果追加到 messages[] → 再次调用 API │ Claude 返回: "我来修改 3 个文件..." │ stop_reason: "tool_use" │ 工具调用: [Edit("src/auth/index.ts", ...), Edit("src/auth/session.ts", ...)] │ │ ⑦ Permission Gate │ Edit 不是只读 → 检查 allow rules → 未匹配 → 弹出确认 │ 用户: "是,允许编辑这两个文件" │ │ ⑧ 串行执行 Edit × 2 │ src/auth/index.ts → 已修改 │ src/auth/session.ts → 已修改 │ │ ⑨ 工具结果追加 → 再次调用 API │ Claude 返回: "迁移完成,你可以运行测试验证..." │ stop_reason: "end_turn" → 循环结束 │ │ ⑩ 最终状态写入磁盘 │ Token 统计: input 45,230 / output 3,890 │ conversations/abc-123.json → 已更新 │ │ ⑪ 上下文检查 │ 当前总 token: 52,000 < 187,000 阈值 → 无需压缩 │ └─→ 等待用户下一条输入
关键洞察:一次用户输入「帮我迁移 auth 模块」在底层触发了 3 次 API 调用、5 次工具执行、2 次磁盘写入和 1 次权限确认。用户感知到的是一个流畅的对话,但背后的编排复杂度远超表面。