第28章:Claude Code 的不足之处(以及你能修复什么)
为什么这很重要
前三章提炼了 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 级别——其复杂度完全来自处理"缓存段内可能出现的各种变动"。源码中的注释明确标记了引用关系:
// 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 注释是架构脆弱性的信号——组件之间通过隐式约定耦合,而非显式接口。
改进建议
集中构建提示词。将分散注入改为集中构建:
- 构建阶段:所有段落在一个中心函数中组装,组装完成后计算整体哈希
- 不可变约束:对缓存段内的内容实施编译期或运行时的不可变检查——任何会话中变化的内容强制放在缓存段之外
- 变更审计:提交前自动检测"是否在缓存段内添加了不稳定内容"
28.2 不足二:压缩信息丢失——9 段摘要模板无法保留所有推理链
问题描述
自动压缩(详见第9章)使用结构化提示词模板要求模型生成对话摘要。压缩提示词(restored-src/src/services/compact/prompt.ts)要求 <analysis> 块中包含:
// 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的。
改进建议
结构化信息提取 + 分层压缩:
- 结构化提取:压缩前用专门步骤提取结构化信息——文件修改列表、失败方法列表、决策图——存储为 JSON 而非自然语言摘要
- 分层压缩:对话分为"事实层"(文件修改、命令输出)和"推理层"(为什么这样做)。事实层用提取式压缩(直接提取),推理层用摘要式压缩(当前做法)
- 失败记忆:专门保留"已尝试但失败的方法"列表,防止压缩后模型重蹈覆辙
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)集成:
- 类型查找:通过 TypeScript Language Server 查询变量的推断类型
- 跳转到定义:处理 re-export、类型别名、动态导入的完整链路
- 查找引用:找到符号的所有使用位置,包括通过类型推断间接使用的
- 调用层次:查询函数的调用者和被调用者,建立调用图
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章"告知而非隐藏"原则——模型被告知截断发生了。但"告知"和"确保模型行动"是两件事。
问题的根源是注意力经济:模型在每一步都要决定下一步做什么。读取完整的截断文件意味着多一次工具调用、多等几秒——如果模型判断预览"足够好",它会跳过这步。但这个判断本身可能是错的,因为模型看不到截断点之后的内容。
改进建议
智能预览 + 主动建议:
- 结构化预览:不只截取前 N 字符,而是提取摘要——搜索结果的匹配总数、文件分布、前后 N 个匹配的上下文
- 相关性提示:在预览中添加"结果共 M 个匹配,当前只显示前 K 个。如果你在找特定文件或模式,建议查看完整内容"
- 自动分页:截断时不只保存到磁盘等模型来读——将结果分页,在预览中展示分页信息让模型按需继续
28.5 不足五:Feature Flag 复杂性——89 个 Flag 的涌现行为
问题描述
Claude Code 有 89 个 Feature Flag(详见第23章),通过两种机制控制:
- 构建时:
feature()函数编译时求值,dead code elimination 移除未启用分支 - 运行时: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 注释:
// 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 依赖图 + 互斥约束:
- 显式依赖声明:每个 flag 声明依赖的其他 flag(
KAIROS_DREAM依赖KAIROS),构建工具编译时验证依赖关系 - 互斥约束:声明不能同时启用的 flag 组合
- 组合测试:对关键 flag 组合进行自动化测试,至少覆盖所有两两组合
- 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:五个不足汇总
三层防御与五个不足的关系
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 工具需要新的外部依赖,但不改变核心架构。
反模式:分散注入
- 问题:多个独立注入点修改同一共享状态,状态变化不可预测
- 识别信号:需要复杂检测系统来解释"为什么状态变了"
- 解决方向:集中构建 + 不可变约束
反模式:有损压缩不可逆
- 问题:压缩后丢失的信息无法恢复
- 识别信号:压缩后模型重复之前已尝试过的失败方法
- 解决方向:结构化提取关键信息,分层存储
用户能做什么
你可以直接行动的
- 缓存脆弱性:通过 CLAUDE.md 控制你能控制的变量——保持项目 CLAUDE.md 稳定,避免频繁修改。监控 API 账单中的
cache_creationtoken 消耗 - 静默截断:在 CLAUDE.md 中添加指令:"当工具结果被截断时,始终使用 Read 工具查看完整内容"。不能保证 100% 遵守,但提高概率
- Grep 的局限:通过 MCP 服务器(详见第22章)添加 LSP 能力。社区已有 TypeScript LSP 和 Python LSP 的 MCP 集成
需要关注但无法直接修复的
- 压缩信息丢失:长会话中如果模型"忘记"了之前尝试过的方法,手动提醒。关键技术决策可记录在 CLAUDE.md 中(不会被压缩)
- 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):
// 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):
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 重新读取最新内容。
三层统一视图
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 构建者的启示
- 每条消息即时持久化,而非定期保存。CC 选择 JSONL 追加写入而非定期快照,因为 Agent 的每一步都可能修改文件系统——任何未持久化的步骤都是不可恢复的
- 检查点粒度 = 用户消息。文件历史快照与
messageId绑定,使得 rewind 的语义清晰:"回到这条消息时的文件状态" - 兜底计时器不可省略。
gracefulShutdown.ts的 failsafe timer 确保即使所有清理函数都卡住,进程最终也会退出——这对系统监控(systemd、Docker)的健康检查至关重要 - 补偿的 dry_run 模式。
rewind_files的dry_run参数让用户先预览变化再决定是否执行——这是不可逆操作的必备模式