图解 Claude Code 剖析源码
这几天 Claude Code 源码泄露的事传得挺广的。一个 59.8MB 的 npm 包,source map 没删干净,五十多万行 TypeScript 代码就那么摊在那儿。我第一时间翻了一遍,想看看这工具到底怎么设计的。
看之前我以为就是个高级点的 API 封装。看完了发现——它的设计深度比我预想的大不少。尤其是上下文管理、子 Agent 构建、System Prompt 动态拼接这些细节,每一条放在你自己的 Agent 项目里,都是可以直接拿用的思路。
下面我拆几个核心问题,结合源码说下自己的理解,也顺手分享一些实操中踩过的坑。
那个“边想边做”的循环,到底怎么转的?
Claude Code 的基座就是一个 while(true) 循环,没什么特别的。
源码在 src/query.ts(泄露版本 v2.1.88,大约 1729 行)。核心函数 query() 是个 async generator,每次迭代做三件事:
1. 调大模型,把当前消息列表发过去,拿到 completion。
2. 从返回里解析 tool_use 块。
3. 执行对应的工具,把结果塞回消息列表。
然后下一轮。
说起来简单,但循环里加了不少实用细节。
Token 预算
大模型有输出上限。Claude Code 用 createBudgetTracker() 追踪每轮用了多少 token。眼看要超了,模型还没给完整回复,就自动续,每次 +500k token 上限。同时绑了一个最多 3 次的重试机制(MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3),超过 3 次直接报错,防止模型在一个工具调用上无限拖下去。我在自己的 Agent 项目里也遇到过类似问题——加个预算追踪和重试上限,至少系统不会死在那儿。
Thinking 块的排序
模型返回的 thinking 和 redacted_thinking 块,在 trajectory 里必须按原始顺序保留,不能乱。代码里有专门的校验逻辑。原因可能是:一旦打乱顺序,模型后续的推理质量会明显下降。
错误恢复
源码里藏了好几层恢复机制:
- HTTP 5xx 网络重试。
- 工具执行失败重试(脚本报错了重新调)。
- 上下文爆炸后自动压缩(后面细说)。
最外层还有 QueryEngine 的类封装(src/QueryEngine.ts),把那个 async generator 包成更易用的接口,加上了事件钩子。
Plan Mode 怎么“先规划再执行”?
Plan Mode 不是什么特殊的框架模式——它就是两个工具:EnterPlanMode 和 ExitPlanMode,在同一个 Tool-Use Loop 里实现。模型调用 EnterPlanMode 就跟调用 Read 读文件一样自然,引擎层不需要特殊处理。
流程大致三步
1. 模型自主判断或用户手动触发。复杂任务(比如“重写整个模块”)模型会调用 EnterPlanMode;简单任务(修 typo)就直接跳过。用户也能通过 Shift+Tab 手动切换。
2. 进入 Plan Mode 后,模型权限立刻降为只读。它只能用 Read、Grep、Glob 这类工具探索代码库,不能写文件、改代码、跑命令。探索完后,把规划写进 .claude/plans/ 目录。
有个有意思的细节:每 5 轮对话,系统会给模型塞一张“小纸条”,提醒它“你现在还在 Plan Mode,别手痒改代码”。原因是对话轮次一多,模型容易忘记当前模式,突然调用写文件工具。如果你在做 Agent 开发,这个“模式提醒”的思路值得借鉴。
3. 模型调用 ExitPlanMode,用户确认后权限恢复,模型按计划实施。
这个设计好在哪?
“工具即能力”——对模型来说,Plan Mode 不是一种特殊的模式切换,只是调用了一个叫 EnterPlanMode 的工具。系统不需要为它单独写调度逻辑,所有工具调用的流程完全复用。我最初自己做“规划+执行”两阶段 Agent 时,在引擎层加了状态机(普通模式、规划模式、执行模式),结果状态流转各种复杂。后来看了 Claude Code 的写法才意识到:直接把“进入规划模式”和“退出规划模式”做成工具,让模型自己管理状态,引擎层无感。
踩过的坑
模型在 Plan Mode 里写计划但质量差。原因是 System Prompt 里对 Plan Mode 的行为定义不够详细。Claude Code 的 System Prompt 专门有一节讲 Plan Mode 的规则,包括必须写计划文件、必须探索足够信息、必须让用户审批等等。缺少这些约束,模型在 Plan Mode 里就划划水过去了。
System Prompt 怎么写的?8700 token 都说了啥?
它不是静态的,而是由十几个 Section 动态拼接,还做了缓存优化。
动态组装
源码里一堆 prompt 片段,每个对应一个 Section,大概包括:
- 身份定义。
- 行为规范。
- 可用工具列表(名称、参数、描述、使用规则)。
- 安全约束。
- Plan Mode 规则。
- 子 Agent 规则。
- 文件系统规则。
- Shell 命令规则。
所有 Section 拼起来,泄露版本大约 8700 token。
这些 Section 不是每次都全量拼的。Claude Code 先检查哪些部分在上次请求后没有变化,直接复用缓存。工具定义之类的几乎不变,每次都重拼会浪费 token 和时间。
缓存失效策略
有个 ant-only 调试场景,允许通过环境变量注入自定义 prompt 覆盖默认行为。正常场景下,System Prompt 的缓存靠内容哈希控制。每个 Section 生成一个 hash,hash 没变就直接取缓存的字节;只有 hash 变了才重新编码。缓存粒度很细——每个 Section 独立缓存。这样即使某个 Section 变了(比如用户改了 CLAUDE.md),其他部分都不用重新编码。
面试常被问的两个点
- **“System Prompt 怎么保证模型不越权?”**
回答:Claude Code 的 System Prompt 对每个工具的调用条件做了详细约束,比如 DeleteFile 只在特定目录下可用,Bash 不能执行非白名单命令。而且这些约束不止写在 prompt 里,在工具执行层还有对应的安全检查(后面会讲 23 层安全检查)。
- **“如果用户改了 CLAUDE.md,System Prompt 会重新构建吗?”**
回答:会的。CLAUDE.md 是 System Prompt 的一个注入 Section。文件修改后 hash 变了,这个 Section 就会重新编码。但其他 Section 不受影响,缓存依然有效。
我的实操反馈
我借鉴了“分段缓存”的思路,但踩过一个坑:缓存 key 的计算漏了环境变量。比如某个 Section 的行为依赖 ALLOW_DELETE 环境变量,但没把它纳入 hash 计算,导致用户改了环境变量但 System Prompt 没更新,模型继续用旧的规则——这有安全隐患。Claude Code 的做法是把所有可能影响行为的输入都纳入 hash,包括环境变量、功能开关、用户指令等。
200K 上下文窗口怎么管理?压缩机制是啥?
很多人只知道用摘要压缩,但 Claude Code 的压缩比摘要复杂。
5 层压缩金字塔
Auto-Compact 机制不是单层压缩,而是分 5 层:
1. Drop 层:直接丢弃低价值消息。比如模型产生的中间推理步骤(thinking 块),用户消息里的大段代码如果已经执行过也可以考虑丢弃。这一层只删不压缩。
2. Summary 层:对历史对话做结构化摘要。按“用户消息-模型响应”对压缩,每对对话保留关键决策和工具调用结果。
3. Truncation 层:截断过长消息。工具调用的返回结果如果特别长,只保留首尾关键部分。
4. Redaction 层:替换敏感信息。绝对路径变相对路径,API key 变 placeholder,既安全又能减少 token。
5. Reinjection 层:将压缩后的关键信息重新注入。不是把所有东西丢掉,而是提取出关键事实、决策、未完成任务,用清晰结构写道 System Prompt 或附件里,让模型后续能引用。
这 5 层不是每次都全跑,而是根据上下文窗口的紧张程度逐步触发。窗口占用到 70% 时只跑 Drop 层,到 85% 时跑 Summary 层,到 95% 时跑 Truncation 层……逐级递进。
什么时候触发压缩?
有一个 autoCompactTracking 对象,追踪每次请求后的 token 使用情况。临近上下文窗口上限时(阈值可配置),会在下一次模型调用前触发压缩。注意:压缩发生在工具执行完成后、下一次模型调用前,不会打断正在进行的工具调用。
摘要 Prompt 怎么设计的?
摘要不是简单的“请总结上面的对话”,而是一段专门的 prompt,引导模型做结构化压缩。告诉模型保留什么、丢掉什么:
- 保留:所有已做出的决策、用户明确的要求、未完成的任务、重要的文件路径。
- 丢掉:工具调用的详细输出(code diff 和日志)、模型的中间推理步骤、已经确认过的事实。
同时要求模型输出特定格式的摘要,方便后续解析和注入。
压缩完怎么接续对话?
压缩完成后,消息列表被替换成一条压缩后的 System Prompt(包含压缩后的历史),紧跟几条最近消息的原始版本(保留最近几轮对话,不压缩),最后是用户当前消息。这样模型不丢失长期记忆,又能对最近的对话保持完整上下文。
多 Agent 怎么协作的?Fork、Coordinator、Swarm 啥区别?
三种多 Agent 机制,适用不同场景。
Subagent:父子结构
主 Agent 遇到独立子任务时(比如“查查这个模块的依赖图”),可以派一个 Subagent 去做。Subagent 完成任务后返回结果。
关键设计:Fork 机制。子 Agent 会直接继承父 Agent 的大部分 System Prompt 缓存,包括已渲染好的字节,所以启动成本极低——大约只有正常启动一个 Agent 的 10%。直接复制了父级渲染后的 System Prompt 字节,包括所有 Feature Flag 的冷热状态。成本优化本身就是能力:成本降下来,你就可以更频繁地调 Subagent,整个系统的能力边界就扩大了。
Coordinator 模式:主 Agent 退化成协调者
任务很大、需要一堆 Agent 并行工作时,父子结构太慢。
需要同时满足两个条件才能开启:编译时的功能开关和运行时的环境变量 CLAUDE_CODE_COORDINATOR_MODE=1。
开启后主 Agent 模式彻底改变:它不再自己写代码、读代码,只负责拆分任务、派活、汇总结果。真正执行的工作全部由 Worker Agent 完成。如果你给主 Agent 一个任务“重构整个模块”,它会拆成多个子任务,派多个 Worker 并行执行(如果环境支持),每个 Worker 有自己的上下文窗口,互不干扰。
Swarm 模式:多 Leader 多 Worker,集群作战
更激进的方案:多个独立 Agent(每个有自己的 Leader)组成团队,每个 Agent 有自己独立的终端窗口,用颜色边框区分,你可以同时看到所有 Agent 在干什么。
Swarm 最精妙的设计是分布式权限系统。多个 Worker 并行工作,每个都需要删除文件、修改配置时,如果每个 Worker 都在自己的终端弹出权限对话框,用户需要在多个窗口之间来回切换审批。Swarm 的解法是:所有 Worker 的权限请求通过一个“邮箱系统”发给 Leader,Leader 在终端展示统一的权限对话框。你只需要盯着 Leader 的终端,所有权限请求都在那里。
Worker 遇到权限提示
→ 发送 permission_request 到 Leader 邮箱
Leader 在终端展示对话框(用户审批)
→ 发送 permission_response 到 Worker 邮箱
Worker 收到响应,继续或中止Swarm 的 Agent 之间不直接调用,而是通过文件级邮箱传递消息。原因是 Swarm 支持三种运行方式:tmux 分屏、iTerm2 分屏、同进程隔离。前两种方式 Agent 在不同进程,必须用文件通信。为了保证协议一致性,即使同进程,也用文件邮箱。牺牲一点性能,换来协议的一致性和可靠性。
三种模式怎么选?
- 简单的场景(单任务偶尔查个资料):用 Subagent,Fork 模式省钱。
- 大型重构/迁移,任务可明确拆分且互不依赖:用 Coordinator 模式,并行执行。
- 多人在多个领域同时操作的复杂项目(比如代码迁移+数据库迁移+基础设施变更):用 Swarm 模式。
CLAUDE.md 记忆系统是怎么让 AI“认识”项目的?
很多人以为 CLAUDE.md 只是一个简单的配置文件,实际上它是一个完整的记忆系统。
多层递归发现
Claude Code 从多个位置自动发现并加载 CLAUDE.md:
1. ~/.claude/CLAUDE.md — 跨所有项目生效。
2. 项目根目录的 CLAUDE.md — 项目级。
3. 当前工作目录的 CLAUDE.md — 目录级。
4. 当前目录到项目根之间所有父目录的 CLAUDE.md — 递归向上查找。
所有找到的文件会被合并注入,越深层的目录越晚注入。内容冲突时深层规则覆盖浅层规则,让子目录可以针对自己的场景补充或覆盖上层约束。
CLAUDE.md 能写什么?
典型内容:项目约束(代码风格、禁止修改的文件、命名规则)、常用命令(build / test / lint 命令)、架构说明(关键模块的职责和依赖关系)、团队规范(commit message 格式、PR 流程)、跨会话记忆(模型可以主动往里写内容)。
最后一点很关键:模型不是只被动读取,它可以在任务执行中主动往里写内容。比如模型发现了一个函数的副作用,或者找到了某个 bug 的根因,可以写在 CLAUDE.md 里,供下一次会话复用。这就是真正的“跨会话学习”。
踩过的坑
我一开始以为 CLAUDE.md 只在启动时加载一次,后来发现每次 autoCompact 压缩后,CLAUDE.md 内容也会作为 attachment 重新注入。这意味着如果你写了很长的内容,每次压缩都会带上它。写太多太杂反而占用上下文窗口。建议只写最关键的、不会变的部分。一次性的发现应该丢到 .claude/plans/ 或者对话里,不要全塞进 CLAUDE.md。
23 层安全检查到底怎么防的?
安全系统分层设计,从源码里能看出一份完整的检查列表。
四种权限模式
以 toolPermissionContext.mode 为核心:
- default:每次工具调用前弹出确认对话框(交互式 IDE 使用)。
- accept_all:自动接受所有工具调用(自动化/CI 场景)。
- deny_all:拒绝所有工具调用(只读探索场景)。
- custom:自定义规则,指定哪些工具需要确认、哪些可以自动执行。
deny > ask > allow 评估顺序
安全检查不是简单的白名单/黑名单,而是三层:
1. Deny 列表:操作在 deny 列表里(比如 rm -rf /),直接拒绝,不弹对话框。
2. Ask 列表:操作在 ask 列表里(比如删除文件、执行不确定命令),弹出对话框让用户确认。
3. Allow 列表:操作在 allow 列表里(比如读取文件、搜索代码),自动放行。
优先级 deny > ask > allow。
具体的检查项
从文件路径合法性、命令白名单、网络请求限制、环境变量覆盖,到子 Agent 的权限继承……大概有 23 层顺序检查,每一层对应一个具体的检查函数。权限系统还会考虑上下文:同一个操作在不同模式下可能结果不同,比如 Plan Mode 下写文件操作被直接 deny。
面试常考的点
安全分为两层:第一层是 prompt 层的“软约束”,告诉模型什么能做什么不能做;第二层是执行层的“硬约束”,在工具执行前做完整的权限检查。软约束避免模型产生违规意图,硬约束防止模型违规执行。
还有什么是我没想到的?
几个读源码过程中发现的零碎设计。
双模型策略
主模型用 Claude 3.5 Sonnet/Opus,但某些子任务(比如简单的文件搜索、正则匹配)会用更便宜的模型。Subagent 的模型选择可以独立配置。
遥测监控
内置了详细的遥测系统。每次工具调用、每次模型请求、每次上下文压缩都有追踪事件,调试时可以直接通过日志看模型在干什么。
可观测性设计
源码里大量使用 hook 事件(比如 InstructionsLoaded、ToolCalled、CompactionDone),方便外部系统接入。如果你想把它集成到自己的 IDE 或 CI 流水线里,这些 hook 会很实用。
一点个人感受
看 Claude Code 源码最大的收获不是某个具体技巧,而是一种务实的工程风格。它没有追求“看起来牛逼”的设计,每一步都在权衡:缓存的粒度、权限的层级、费用的控制、协议的一致性。即使完全不用 Claude Code,这套思路也值得借鉴到自己的 Agent 项目里。那些在专栏和教程里看不到的工程细节——比如 token 预算追踪、Fork 的缓存继承、分布式权限的邮箱通信——才是让一个系统从“能跑”到“好用”的关键。
没有完美的架构,只有不断逼近完美的迭代。源码是死的,思路是活的。
读者评论 5