运行时机制
工具执行编排(并行 vs 串行)、Zustand 状态管理、上下文压缩、会话持久化与恢复。
4. 工具执行编排 — 并行 vs 串行
当模型在一次响应中请求多个工具调用时(Claude 支持在一次回复中同时请求多个工具),Claude Code 需要决定:这些工具是并行执行还是串行执行?错误决策会导致竞态条件或不必要的性能损失。
并行执行条件
一个工具要被纳入并行执行池,必须同时满足两个条件:
| 工具属性 | 含义 | 示例 |
|---|---|---|
isReadOnly |
工具不会修改任何文件或系统状态 | Read, Grep, Glob, LS |
isConcurrencySafe |
多个实例同时运行不会相互干扰 | 大多数只读工具都是并发安全的 |
串行执行:写入工具
任何涉及文件写入或系统状态修改的工具都必须串行执行。原因很简单:
- Edit 工具:基于
old_string匹配进行替换。如果两个 Edit 同时修改同一个文件,第二个 Edit 的old_string可能已经因为第一个 Edit 的修改而不存在了。 - Bash 工具:可能改变工作目录、环境变量、文件系统等全局状态,并行执行结果不可预测。
- Write 工具:覆盖整个文件,并行写同一文件必然导致数据丢失。
混合场景怎么处理?
如果模型在一次响应中同时请求了 3 个 Read 和 1 个 Edit,Claude Code 的行为是:
- 先并行执行 3 个 Read(它们是只读且并发安全的)
- 等所有 Read 完成后,再串行执行 Edit
实际上调度逻辑会将工具调用分为两组:可并行组和必须串行组,先执行并行组,再依次执行串行组。
重试机制
工具执行层和 API 调用层都有各自的重试策略。这里重点说 API 层的重试,因为它直接影响用户体验:
| 错误类型 | 重试策略 | 最大等待时间 |
|---|---|---|
| 429 (Rate Limited) | 指数退避重试,每次等待时间翻倍 | 最多 10 次,累计最长约 5 分钟 |
| 529 (Overloaded) | 指数退避重试 | 最多 3 次(fast mode 下仅 1 次) |
| ECONNRESET | 网络层连接重置,立即重试 | 仅 1 次,失败则报错 |
| 401 (Auth Error) | 不重试,认证问题不会自动恢复 | — |
| Prompt Too Long | 不重试,需要先触发上下文压缩 | — |
5. 状态管理 — Zustand Store
Claude Code 使用 Zustand 作为状态管理方案。如果你用过 React,应该知道 Zustand——它是一个极简但强大的状态管理库,核心思想是不可变状态 + 观察者模式。
为什么不用全局变量?
在一个有多个子系统(UI 渲染、API 调用、工具执行、设置管理)需要共享状态的应用中,全局变量会迅速变成维护噩梦。Zustand 提供了三个关键保证:
- 不可变更新:每次状态变更都产生新的状态对象,旧状态不会被修改。这使得时间旅行调试和变更追踪成为可能。
- 精确订阅:组件只订阅自己关心的状态切片,不会因为无关状态变更而重新渲染。
- 中间件支持:可以在状态变更时执行副作用(持久化、日志、通知 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
当某些关键状态(如 model 或 settings)变更时,feature gate 会重新评估。某些功能可能只对特定模型或设置组合开放。例如,切换到 Opus 模型可能解锁额外的工具或更高的并行限制。
3. 通知 IDE
当 Claude Code 作为 VS Code / JetBrains 扩展运行时,状态变更需要同步到 IDE 层。例如:
- 模型切换 → 更新状态栏显示
- 权限变更 → 更新权限指示器
- 任务列表变更 → 更新 IDE 侧边栏面板
- 会话状态变更 → 更新活动指示器
这种通知通过 JSON-RPC 消息发送到 IDE 宿主进程。
6. 上下文压缩 — Auto-compact 机制
这是 Claude Code 中最精巧的子系统之一。在长时间交互中,上下文会不断增长,最终触及模型的窗口上限。Auto-compact 机制在这发生之前自动介入,在保留关键信息的同时大幅缩小上下文。
触发阈值
压缩流程
一旦阈值被触发,以下步骤按顺序执行:
步骤 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 对话或开始新会话。
完整对话保存到 .transcripts/
重要的是:压缩不等于数据丢失。在执行压缩之前,完整的对话历史(包括所有图片、工具调用和结果)会被序列化并保存到 ~/.claude/.transcripts/ 目录。每个文件以会话 ID 和时间戳命名。
这意味着即使当前上下文被压缩到只剩摘要,原始对话依然可以完整回溯。这对调试、审计和了解 Claude 的完整思考过程非常有价值。
7. 会话持久化与恢复
Claude Code 的会话管理不是事后想到的功能——它从架构层面就设计为「随时可中断、随时可恢复」的系统。
磁盘写入策略
前面在 QueryEngine 部分提到过:每条用户消息在 API 响应返回之前就写入磁盘。具体来说,写入的内容包括:
| 写入时机 | 写入内容 | 存储位置 |
|---|---|---|
| 用户发送消息后 | 完整的 messages[] 数组快照 | ~/.claude/conversations/{id}.json |
| 工具执行完成后 | 更新后的 messages[](含工具结果) | 同上(覆盖写入) |
| 模型回复完成后 | 包含模型回复的 messages[] | 同上(覆盖写入) |
| 会话结束/中断时 | 最终状态 + 元数据(模型、用量、时间戳) | 同上 + transcript 副本 |
/resume — 恢复历史会话
/resume 命令可以恢复任何之前的会话。执行流程:
恢复时发生了什么?
- 扫描会话文件:读取
~/.claude/conversations/下所有会话文件,按最后活跃时间排序 - 展示选择列表:显示每个会话的首条消息摘要、时间戳和消息数量
- 反序列化:将选中会话的 JSON 文件解析为 messages[] 数组
- 恢复 Zustand 状态:重建会话相关的所有状态(模型选择、权限决策历史等)
- 重建上下文:由于 CLAUDE.md 和 skill 信息可能已经更新,system prompt 会基于当前最新配置重新组装
- 继续对话:用户可以像刚才离开一样继续交互
/rewind — 回退代码和对话状态
/rewind 是一个比 /resume 更强大的命令——它不仅回退对话,还回退代码变更。
git checkout 到对应时间点的代码状态。这意味着你可以说「那个重构方向不对,回到 3 步之前」,不仅对话恢复到那个时候,文件内容也回去了。
/rewind 的执行步骤
- 确定回退点:展示对话历史中每一步的概要,用户选择要回退到哪里
- Git 状态检查:检查当前是否有未提交的变更,如果有,提醒用户先 stash 或 commit
- 代码回退:使用 Git 将文件系统恢复到选定时间点的状态
- 对话回退:截断 messages[] 数组到选定点,丢弃之后的所有消息
- 保存截断后状态:将新的 messages[] 写入磁盘
- 继续对话:现在用户可以从回退点开始走一条不同的路径
架构全景图
最后,让我们把所有模块串在一起,看一条消息从输入到输出经过的完整路径: