# 第28章：Claude Code 的不足之处（以及你能修复什么）

> **定位**：本章客观审视 Claude Code 源码中 5 个可观测的工程设计不足及其改进建议。前置依赖：无，可独立阅读。适用场景：想客观了解 CC 工程局限性的读者，避免盲目照搬。

## 为什么这很重要

前三章提炼了 Claude Code 的优秀设计——驾驭工程原则、上下文管理策略、生产级编码模式。但一本严肃的技术分析不能只讲"它做对了什么"，还必须客观审视"它在哪里做得不够好"。

本章列出 5 个从源码中可观测到的设计不足。每个不足包含三部分：**问题描述**（它是什么）、**源码证据**（为什么说它是问题）、**改进建议**（可以怎么做）。

需要强调的是：这些分析完全基于工程设计层面，不涉及对 Anthropic 团队的能力评价。每一个"不足"都是在特定工程权衡下的合理选择——只是这些选择有可观测的代价。

---

## 源码分析

### 28.1 不足一：缓存脆弱性——分散的注入点制造缓存中断风险

#### 问题描述

Claude Code 的提示词缓存系统依赖核心假设：**`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 之前的内容在整个会话中保持不变**。但多个分散的注入点可以修改这个区域：

- `systemPromptSections.ts` 中的条件性段落：基于 Feature Flag 或运行时状态决定是否包含
- MCP 连接/断开事件：`DANGEROUS_uncachedSystemPromptSection()` 显式标记"会破坏缓存"
- 工具列表变化：MCP 服务器上下线导致 `tools` 参数哈希改变
- GrowthBook Flag 切换：远程配置变更导致序列化的工具 Schema 变动

#### 源码证据

缓存中断检测系统需要追踪近 20 个字段（`restored-src/src/services/api/promptCacheBreakDetection.ts:28-69`）就是直接证据——如果缓存是稳定的，不需要如此复杂的检测系统来解释"为什么中断了"。

`DANGEROUS_uncachedSystemPromptSection()` 的命名本身是警示标记——函数名中的 `DANGEROUS` 前缀说明团队清楚它会破坏缓存，但在某些场景下（MCP 状态变化）没有更好的替代方案。

Agent 列表曾内联在系统提示词中，占全球 `cache_creation` token 的 10.2%（详见第15章）。虽然后来被移至附件，但这说明即使是经验丰富的团队，也会无意中在缓存段内放入不稳定内容。

`splitSysPromptPrefix()`（`restored-src/src/utils/api.ts:321-435`）的三条代码路径——MCP tool-based、global+boundary、默认 org 级别——其复杂度完全来自处理"缓存段内可能出现的各种变动"。源码中的注释明确标记了引用关系：

```typescript
// restored-src/src/constants/prompts.ts:110-112
// WARNING: Do not remove or reorder this marker without updating
// cache logic in:
// - src/utils/api.ts (splitSysPromptPrefix)
// - src/services/api/claude.ts (buildSystemPromptBlocks)
```

这种跨文件的 `WARNING` 注释是架构脆弱性的信号——组件之间通过隐式约定耦合，而非显式接口。

#### 改进建议

**集中构建提示词**。将分散注入改为集中构建：

1. **构建阶段**：所有段落在一个中心函数中组装，组装完成后计算整体哈希
2. **不可变约束**：对缓存段内的内容实施编译期或运行时的不可变检查——任何会话中变化的内容强制放在缓存段之外
3. **变更审计**：提交前自动检测"是否在缓存段内添加了不稳定内容"

---

### 28.2 不足二：压缩信息丢失——9 段摘要模板无法保留所有推理链

#### 问题描述

自动压缩（详见第9章）使用结构化提示词模板要求模型生成对话摘要。压缩提示词（`restored-src/src/services/compact/prompt.ts`）要求 `<analysis>` 块中包含：

```typescript
// restored-src/src/services/compact/prompt.ts:31-44
"1. Chronologically analyze each message and section of the conversation.
    For each section thoroughly identify:
    - The user's explicit requests and intents
    - Your approach to addressing the user's requests
    - Key decisions, technical concepts and code patterns
    - Specific details like:
      - file names
      - full code snippets
      - function signatures
      ..."
```

这是精心设计的清单，但有一个根本限制：**模型的推理链和失败尝试在压缩中丢失**。

具体丢失的信息类型：

- **失败的方法**：模型尝试方法 A 但失败，转而使用方法 B 成功——压缩后只保留"使用方法 B 解决了问题"，方法 A 的失败经验丢失
- **决策上下文**：为什么选方法 B 而非方法 A 的推理被简化为结论
- **精确引用**：具体的文件路径和行号在摘要中可能被泛化——"修改了认证模块"而非"修改了 `auth/middleware.ts:42-67`"

#### 源码证据

压缩的 token 预算是 `MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000`（`restored-src/src/services/compact/autoCompact.ts:30`）。压缩比可能高达 7:1 或更高——在这种压缩比下，信息丢失不可避免。

压缩后文件恢复机制（`POST_COMPACT_MAX_FILES_TO_RESTORE = 5`，`restored-src/src/services/compact/compact.ts:122`）部分缓解了问题，但只恢复文件内容，不恢复推理链。

`NO_TOOLS_PREAMBLE`（`restored-src/src/services/compact/prompt.ts:19-25`）的存在暗示了另一个压缩质量问题：模型在压缩时有时会尝试调用工具而非生成摘要文本（Sonnet 4.6 上发生率 2.79%），需要显式禁止。这意味着压缩任务本身对模型来说不是trivial的。

#### 改进建议

**结构化信息提取 + 分层压缩**：

1. **结构化提取**：压缩前用专门步骤提取结构化信息——文件修改列表、失败方法列表、决策图——存储为 JSON 而非自然语言摘要
2. **分层压缩**：对话分为"事实层"（文件修改、命令输出）和"推理层"（为什么这样做）。事实层用提取式压缩（直接提取），推理层用摘要式压缩（当前做法）
3. **失败记忆**：专门保留"已尝试但失败的方法"列表，防止压缩后模型重蹈覆辙

---

### 28.3 不足三：Grep 不是抽象语法树（AST）——文本搜索遗漏语义关系

#### 问题描述

Claude Code 的代码搜索完全基于 GrepTool（文本正则匹配）和 GlobTool（文件名模式匹配）。在大多数场景下工作良好，但无法覆盖**语义级别的代码关系**：

- **动态导入**：`require(variableName)` 中的变量是运行时值，文本搜索无法追踪
- **Re-export**：`export { default as Foo } from './bar'` 在搜索 `Foo` 定义时不被正确追踪
- **字符串引用**：工具名作为字符串注册（`name: 'Bash'`），搜索工具使用点需同时搜字符串和变量名
- **类型推断**：TypeScript 的类型推断意味着很多变量没有显式注解，搜索特定类型的使用位置不完整

#### 源码证据

Claude Code 自身的工具列表包含 40+ 个工具（详见第2章），但没有 AST 查询工具。系统提示词明确引导模型使用 Grep 而非 Bash 中的 grep（详见第8章）——但这只是将文本搜索从一个工具移到另一个，没有提升搜索的语义层次。

在 Claude Code 自身的代码库（1,902 个 TypeScript 文件）中，这些遗漏的影响是可观的。例如：Feature Flag 通过 `feature('KAIROS')` 调用使用——搜索字符串 `KAIROS` 可以找到使用点，但搜索函数 `feature` 的调用则会返回所有 89 个 flag 的结果，噪音巨大。没有 AST 查询，无法表达"找到 `feature()` 调用中参数值为 `KAIROS` 的位置"。

#### 改进建议

**添加 LSP（Language Server Protocol）集成**：

1. **类型查找**：通过 TypeScript Language Server 查询变量的推断类型
2. **跳转到定义**：处理 re-export、类型别名、动态导入的完整链路
3. **查找引用**：找到符号的所有使用位置，包括通过类型推断间接使用的
4. **调用层次**：查询函数的调用者和被调用者，建立调用图

LSP 集成的基础设施在源码中已有迹象——Feature Flag 分析（详见第23章）中可以观察到部分 LSP 相关的实验性代码路径，但尚未广泛启用。Grep + LSP 的组合将比纯 Grep 或纯 LSP 更强大：Grep 负责快速的全文搜索和模式匹配，LSP 负责精确的语义查询。

---

### 28.4 不足四：截断告知不等于行动——大结果写入磁盘但模型可能不会重新读取

#### 问题描述

当工具结果超过 50K 字符（`DEFAULT_MAX_RESULT_SIZE_CHARS`，`restored-src/src/constants/toolLimits.ts:13`）时，处理策略是：完整结果写入磁盘，返回预览消息（详见第12章）。

问题在于：**模型可能不会重新读取**。模型基于预览做判断——如果预览看起来"足够"（例如搜索结果的前 50K 字符已包含一些相关结果），模型可能不去读完整内容。但关键信息可能恰好在截断点之后。

#### 源码证据

`restored-src/src/utils/toolResultStorage.ts` 实现了大结果持久化逻辑。截断时模型收到：

```
[Result truncated. Full output saved to /tmp/claude-tool-result-xxx.txt]
[Showing first 50000 characters of N total]
```

这遵循了第25章"告知而非隐藏"原则——模型被告知截断发生了。但"告知"和"确保模型行动"是两件事。

问题的根源是**注意力经济**：模型在每一步都要决定下一步做什么。读取完整的截断文件意味着多一次工具调用、多等几秒——如果模型判断预览"足够好"，它会跳过这步。但这个判断本身可能是错的，因为模型**看不到**截断点之后的内容。

#### 改进建议

**智能预览 + 主动建议**：

1. **结构化预览**：不只截取前 N 字符，而是提取摘要——搜索结果的匹配总数、文件分布、前后 N 个匹配的上下文
2. **相关性提示**：在预览中添加"结果共 M 个匹配，当前只显示前 K 个。如果你在找特定文件或模式，建议查看完整内容"
3. **自动分页**：截断时不只保存到磁盘等模型来读——将结果分页，在预览中展示分页信息让模型按需继续

---

### 28.5 不足五：Feature Flag 复杂性——89 个 Flag 的涌现行为

#### 问题描述

Claude Code 有 89 个 Feature Flag（详见第23章），通过两种机制控制：

1. **构建时**：`feature()` 函数编译时求值，dead code elimination 移除未启用分支
2. **运行时**：GrowthBook `tengu_*` 前缀的 flag 通过 API 获取

问题在于 flag 之间的**交互效应**。89 个二值 flag 理论上产生 2^89 种组合。即使只有 10% 的 flag 之间存在交互，组合空间也是巨大的。

#### 源码证据

以下是源码中可观察到的 flag 交互示例：

| Flag A | Flag B | 交互关系 |
|--------|--------|---------|
| `KAIROS` | `PROACTIVE` | 助手模式和主动工作模式有重叠的唤醒机制 |
| `COORDINATOR_MODE` | `TEAMMEM` | 都涉及多 agent 通信，使用不同消息传递机制 |
| `BRIDGE_MODE` | `DAEMON` | 桥接模式需守护进程支持，但生命周期管理独立 |
| `FAST_MODE` | `ULTRATHINK` | 加速输出和深度思考在 effort 配置中可能冲突 |

**表 28-1：Feature Flag 交互示例**

锁存机制（详见第25章原则六）是对 flag 交互复杂性的缓解——固定某些状态来减少运行时组合。但锁存本身也增加理解难度：系统当前行为不仅取决于 flag 当前值，还取决于**会话历史中 flag 值的变化序列**。

工具 Schema 缓存（`getToolSchemaCache()`，详见第15章）是另一个缓解措施——每会话计算一次工具列表，防止会话中途 flag 切换导致 Schema 变动。但这意味着会话中途切换的 flag 不会影响工具列表——既是特性也是限制。

`promptCacheBreakDetection.ts` 中每个 flag 相关的锁存字段都带有 `Tracked to verify the fix` 注释：

```typescript
// restored-src/src/services/api/promptCacheBreakDetection.ts:47-55
/** AFK_MODE_BETA_HEADER presence — should NOT break cache anymore
 *  (sticky-on latched in claude.ts). Tracked to verify the fix. */
autoModeActive: boolean
/** Overage state flip — should NOT break cache anymore (eligibility is
 *  latched session-stable in should1hCacheTTL). Tracked to verify the fix. */
isUsingOverage: boolean
/** Cache-editing beta header presence — should NOT break cache anymore
 *  (sticky-on latched in claude.ts). Tracked to verify the fix. */
cachedMCEnabled: boolean
```

三个字段、三次 `should NOT break cache anymore`、三次 `Tracked to verify the fix`——说明这些 flag 的状态变化**曾经**导致缓存中断，团队逐个修复后添加追踪来验证修复是否有效。这是典型的"打地鼠"模式——没有系统性解决 flag 交互问题，而是逐个修复暴露出来的案例。

#### 改进建议

**Flag 依赖图 + 互斥约束**：

1. **显式依赖声明**：每个 flag 声明依赖的其他 flag（`KAIROS_DREAM` 依赖 `KAIROS`），构建工具编译时验证依赖关系
2. **互斥约束**：声明不能同时启用的 flag 组合
3. **组合测试**：对关键 flag 组合进行自动化测试，至少覆盖所有两两组合
4. **Flag 状态可视化**：调试模式下输出所有 flag 值和锁存状态，帮助诊断行为异常

---

## 模式提炼

### 五个不足汇总表

| 不足 | 源码证据 | 改进建议 |
|------|---------|---------|
| 缓存脆弱性 | `promptCacheBreakDetection.ts` 追踪 18 个字段 | 集中构建 + 不可变约束 |
| 压缩信息丢失 | `compact/prompt.ts` 压缩比 7:1+ | 结构化提取 + 分层压缩 |
| Grep 不是 AST | 40+ 工具中无 AST 查询工具 | LSP 集成 |
| 截断告知不足 | `toolResultStorage.ts` 预览不保证被读取 | 智能预览 + 自动分页 |
| Flag 复杂性 | 3 个 `Tracked to verify the fix` 注释 | Flag 依赖图 + 互斥约束 |

**表 28-2：五个不足汇总**

### 三层防御与五个不足的关系

```mermaid
graph TD
    subgraph "提示词层"
        A["不足2: 压缩信息丢失<br/>摘要模板的局限性"]
        B["不足4: 截断告知不足<br/>告知了但模型可能不行动"]
    end
    subgraph "工具层"
        C["不足3: Grep 不是 AST<br/>文本搜索的语义盲区"]
    end
    subgraph "基础设施层"
        D["不足1: 缓存脆弱性<br/>分散的注入点"]
        E["不足5: Flag 复杂性<br/>组合爆炸与打地鼠"]
    end

    D --> A
    D --> B
    E --> C
    E --> D
```

**图 28-1：五个不足在三层防御中的分布**

基础设施层的两个不足（缓存脆弱性、Flag 复杂性）最深层——影响系统整体行为，修复成本最高。提示词层的两个不足（压缩信息丢失、静默截断）更容易缓解——改进压缩模板或预览格式不需要大规模重构。工具层的不足（Grep 不是 AST）介于两者之间——添加 LSP 工具需要新的外部依赖，但不改变核心架构。

### 反模式：分散注入

- **问题**：多个独立注入点修改同一共享状态，状态变化不可预测
- **识别信号**：需要复杂检测系统来解释"为什么状态变了"
- **解决方向**：集中构建 + 不可变约束

### 反模式：有损压缩不可逆

- **问题**：压缩后丢失的信息无法恢复
- **识别信号**：压缩后模型重复之前已尝试过的失败方法
- **解决方向**：结构化提取关键信息，分层存储

---

## 用户能做什么

### 你可以直接行动的

1. **缓存脆弱性**：通过 CLAUDE.md 控制你能控制的变量——保持项目 CLAUDE.md 稳定，避免频繁修改。监控 API 账单中的 `cache_creation` token 消耗
2. **静默截断**：在 CLAUDE.md 中添加指令："当工具结果被截断时，始终使用 Read 工具查看完整内容"。不能保证 100% 遵守，但提高概率
3. **Grep 的局限**：通过 MCP 服务器（详见第22章）添加 LSP 能力。社区已有 TypeScript LSP 和 Python LSP 的 MCP 集成

### 需要关注但无法直接修复的

4. **压缩信息丢失**：长会话中如果模型"忘记"了之前尝试过的方法，手动提醒。关键技术决策可记录在 CLAUDE.md 中（不会被压缩）
5. **Feature Flag 复杂性**：内部架构问题，但了解它有助于理解为什么 Claude Code 的行为有时"不一致"——可能是 flag 交互导致

---

### 不足是权衡的另一面

| 不足 | 权衡的另一面 |
|------|-------------|
| 缓存脆弱性 | 提示词的灵活组合能力 |
| 压缩信息丢失 | 能在 200K 窗口中持续工作数百轮 |
| Grep 不是 AST | 零外部依赖、跨语言通用 |
| 截断告知不足 | 防止上下文被单个大结果淹没 |
| Flag 复杂性 | 快速迭代和 A/B 测试能力 |

**表 28-3：五个不足与其对应的工程权衡**

理解这些权衡比单纯批评不足更有价值。在你自己的 AI Agent 系统中，你可能面临同样的选择——而 Claude Code 的经验可以帮助你预见每种选择的长期代价。

---

## CC 的容错架构：三层防护

学术报告将 Agent 系统的容错划分为三层：检查点（checkpoint）、可恢复执行（durable execution）、幂等/补偿事务（idempotency/compensation）。Claude Code 在这三层都有工程实现，但本书前文分散在不同章节中，未作为统一架构呈现。

### 第一层：检查点（Checkpointing）

CC 在两个维度做持久化检查点：

**文件历史快照**（`fileHistory.ts:39-52`）：

```typescript
// restored-src/src/utils/fileHistory.ts:39-52
export type FileHistorySnapshot = {
  messageId: UUID
  trackedFileBackups: Record<string, FileHistoryBackup>
  timestamp: Date
}
```

每次工具修改文件后，CC 都会创建一个快照：文件的内容哈希 + 修改时间 + 版本号。备份存储在 `~/.claude/file-backups/`，以内容寻址方式防止重复存储。最多保留 100 个快照（`MAX_SNAPSHOTS = 100`）。

**会话 Transcript 持久化**（`sessionStorage.ts`）：

每条消息以 JSONL 格式追加到 `~/.claude/projects/{project-id}/sessions/{sessionId}.jsonl`。这不是定期保存——是每条消息即时持久化。崩溃后，JSONL 文件即是恢复源。

### 第二层：可恢复执行（Graceful Shutdown + Resume）

**信号处理**（`gracefulShutdown.ts:256-276`）：

CC 注册了 SIGINT、SIGTERM、SIGHUP 三种信号处理器。更巧妙的是**孤儿检测**（第 278-296 行）：每 30 秒检查 stdin/stdout 的 TTY 有效性，在 macOS 上当终端关闭（文件描述符被撤销）时主动触发优雅关闭。

**清理优先级序列**（`gracefulShutdown.ts:431-511`）：

```
1. 退出全屏模式 + 打印 resume 提示（立即）
2. 执行注册的清理函数（2 秒超时，超时抛 CleanupTimeoutError）
3. 执行 SessionEnd hooks（允许用户自定义清理）
4. 刷新遥测数据（500 毫秒上限）
5. 兜底计时器：最大 5 秒 + SessionEnd 超时后强制退出
```

**崩溃后恢复**：`claude --resume {sessionId}` 从 JSONL 文件加载完整消息历史、文件历史快照和 attribution 状态（`sessionRestore.ts:99-150`）。恢复后的会话与崩溃前状态一致——用户可以从中断点继续工作。

### 第三层：补偿事务（File Rewind）

当模型修改出错时，CC 提供两种补偿机制：

**SDK Rewind 控制请求**（`controlSchemas.ts:308-315`）：

```typescript
SDKControlRewindFilesRequest {
  subtype: 'rewind_files',
  user_message_id: string,  // 回退到哪条消息的文件状态
  dry_run?: boolean,        // 先预览变化，不实际执行
}
```

Rewind 算法（`fileHistory.ts:347-591`）找到目标快照，逐文件对比当前状态与快照状态：如果文件在目标版本不存在则删除，如果内容不同则从 `~/.claude/file-backups/` 恢复。

**压缩后文件恢复**（`compact.ts:122-129`，详见 ch10）：

| 常量 | 值 | 用途 |
|------|---|------|
| `POST_COMPACT_MAX_FILES_TO_RESTORE` | 5 | 最多恢复 5 个文件 |
| `POST_COMPACT_TOKEN_BUDGET` | 50,000 | 恢复内容总预算 |
| `POST_COMPACT_MAX_TOKENS_PER_FILE` | 5,000 | 单文件上限 |

恢复时按访问时间排序（最近访问优先），跳过已在保留消息中的文件，使用 FileReadTool 重新读取最新内容。

### 三层统一视图

```mermaid
graph TD
    subgraph "第一层：检查点"
        A[文件历史快照<br/>MAX_SNAPSHOTS=100]
        B[JSONL Transcript<br/>每消息即时持久化]
    end
    subgraph "第二层：可恢复执行"
        C[信号处理<br/>SIGINT/SIGTERM/SIGHUP]
        D[孤儿检测<br/>30秒 TTY 轮询]
        E[claude --resume<br/>完整状态恢复]
    end
    subgraph "第三层：补偿事务"
        F[rewind_files<br/>SDK 控制请求]
        G[压缩后恢复<br/>5 文件 × 5K tokens]
    end

    A --> E
    B --> E
    C --> B
    D --> C
    A --> F
    A --> G
```

**图 28-x：CC 容错架构三层防护**

### 对 Agent 构建者的启示

1. **每条消息即时持久化，而非定期保存**。CC 选择 JSONL 追加写入而非定期快照，因为 Agent 的每一步都可能修改文件系统——任何未持久化的步骤都是不可恢复的
2. **检查点粒度 = 用户消息**。文件历史快照与 `messageId` 绑定，使得 rewind 的语义清晰："回到这条消息时的文件状态"
3. **兜底计时器不可省略**。`gracefulShutdown.ts` 的 failsafe timer 确保即使所有清理函数都卡住，进程最终也会退出——这对系统监控（systemd、Docker）的健康检查至关重要
4. **补偿的 dry_run 模式**。`rewind_files` 的 `dry_run` 参数让用户先预览变化再决定是否执行——这是不可逆操作的必备模式
