..

从Claude Code源码中学到的

一根针掉在地上

2026 年 3 月 31 日,有人在 Anthropic 的 npm 包里发现了一份 .map 文件。源码映射指向一个 R2 存储桶,里面躺着完整的、未混淆的 TypeScript 源码——Claude Code 的全部家当。1

1900 个文件,51 万行代码。Anthropic 官方的 Claude Code 实现,就这样摊开在所有人面前。

于是用 Claude Code 来读 Claude Code,有一种艾舍尔的感觉😹。之前在Claude Code is Engineering rooted in Ingenium里通过反编译了解过 Claude Code 的工作原理,但这次是直接看源代码,细节层面完全不同。就像之前在看一辆车的外观和发动机舱,这次直接把车拆了,看到每个零件的设计和装配。忽略UI和安全相关的部分,重点看了系统架构、查询引擎、上下文管理、多代理系统、工具编排的实现。

骨架:工具、命令、查询引擎

Claude Code 的基本结构很干净。三根柱子撑起整个系统:

工具系统——src/tools/ 下大约 40 个自包含模块。每个工具定义输入 schema、权限模型和执行逻辑。BashTool 跑 shell,FileEditTool 改文件,GrepTool 用 ripgrep 搜内容,AgentTool 生成子代理。工具之间互不依赖,注册表统一调度。

命令系统——100 多个斜杠命令。/commit 提交代码,/review 做审查,/compact 压缩上下文,/resume 恢复会话,/vim 切换编辑模式——从代码操作到辅助调试,从记忆管理到团队协作,几乎覆盖了开发工作流的每个环节。这部分没有特别的。

QueryEngine——1300 行的会话引擎封装。它管理整个对话的生命周期:消息队列、AbortController 层级传播、会话恢复与持久化、权限模式管理。它不是直接做 API 调用的地方,而是更高层的编排者,负责将用户输入转化为 query 循环并管理其全生命周期。query.ts 负责跟模型对话并执行工具的内循环,QueryEngine.ts 负责管理整个会话的外循环。

查询循环:一个永不停歇的 while(true)

query.ts 是整个系统的脊柱,1700 多行,一个巨大的 while(true) 循环。

每一轮迭代做这些事:

  1. 对消息施加工具结果预算(applyToolResultBudget
  2. 历史剪裁(snipCompactIfNeeded
  3. 微压缩——清理过期的工具结果
  4. 上下文折叠——非破坏性的投影视图
  5. 自动压缩——在接近上下文窗口极限时触发
  6. 调用模型,流式接收响应
  7. 执行工具调用
  8. 处理停止钩子
  9. 注入附件消息(内存、技能发现、队列命令)
  10. 回到步骤 1

大的框架符合直觉,没啥特别的。

上下文:你能塞多少东西进窗口

整个代码库中最让人好奇的是上下文管理子系统。好的上下文管理,占成功的 90%。三种策略协同工作:

传统压缩——用 Claude 自己生成对话摘要,替换掉旧消息。当 token 快要超限时触发,兜底方案。

API 微压缩——利用 Anthropic API 原生的上下文编辑能力,不碰本地消息。两种策略并行:clear_tool_uses_20250919 基于 token 阈值触发,达到上限时按保留数量清理早期工具调用的输入和结果;clear_thinking_20251015 管理思考块的生命周期——在 thinking 模式下保留思考轮次,空闲超过 1 小时(缓存大概率已失效)时只留最后一轮。两种策略作为 context_management.edits 数组传给 API,由服务端在模型推理前执行。值得一提的是,工具清理策略目前仅限 Anthropic 内部用户(USER_TYPE === 'ant'),外部用户只能用思考块清理。

上下文折叠——这是新东西。受 CONTEXT_COLLAPSE feature flag 保护,在外部构建中目前尚未开启。projectView 函数创建一个消息的「投影视图」,不修改原始数组,通过增量提交日志来重放。像版本控制系统一样——每次 commit 一个折叠,需要时回放。具体实现在 src/services/contextCollapse/operations.ts 中,通过懒加载引入。从 query.tssessionRestore.tsREPL.tsx 的调用方式看,它是一个增量式、非破坏性的上下文变换工具,允许系统在不丢失细节的情况下,动态调整可见上下文的范围和粒度。

三层策略的优先级经过仔细编排:折叠先于自动压缩。如果折叠就能把 token 控制在阈值内,自动压缩就不会触发——保留了更细粒度的上下文,而不是一刀切的摘要。

多代理:从子代理到团队

Agent 系统分三层。

子代理(SubAgent)——进程内的轻量任务委托。Claude 调用 AgentTool 时,系统创建一个完全独立的 query() 循环,有自己的 AbortController、转录文件、工具池和权限模式。但通过 CacheSafeParams 模式共享父级的提示缓存——系统提示、用户上下文、工具上下文原封不动传过去,让 API 看到同样的缓存前缀。

子代理之间不共享上下文。每个代理有自己的消息历史。这是设计选择,不是疏忽——隔离保证了可靠性。

团队系统(Team)——多进程协调层。TeamCreateTool~/.claude/teams/ 创建团队描述文件,包含成员定义、领导者 ID 和会话上下文。成员之间通过文件系统邮箱通信——SendMessageTool 把消息写到指定代理的收件箱。

一个细节:团队领导者不设置 CLAUDE_CODE_AGENT_ID,所以 isTeammate() 返回 false。这防止了领导者轮询自己的收件箱。

协调器模式(Coordinator)——不是基础设施,是提示工程。getCoordinatorSystemPrompt() 注入一个约 250 行的系统提示(整个 coordinatorMode.ts 有 369 行),教 Claude 如何编排工作:任务分解、并行管理、先综合再委派。它区分了 coordinator 和 worker 的关系——coordinator 通过 AgentTool 创建 worker,通过 SendMessageTool 继续 worker 的上下文,通过 TaskStopTool 终止 worker。Worker 完成后通过 <task-notification> XML 格式向 coordinator 回报结果。受 COORDINATOR_MODE feature flag 和 CLAUDE_CODE_COORDINATOR_MODE 环境变量双重门控,它运行在团队系统之上,是同一套管道的不同操作模式。

子代理是临时创建的。generateAgent 在运行时根据任务需求构建代理定义——工具列表、系统提示、权限模式都是动态生成的。系统提示由 LLM 生成,但受框架约束。

Harness:十二层机制

找了下源码注释里有「harness」相关的地方。大致看了下周边的实现,得以了解下生产级 AI Agent 需要的全部约束和编排机制。大概十二层:

层次 核心
工具系统 40+ 自包含模块,统一接口
查询引擎 流式响应、工具循环、重试
会话持久化 JSONL 记录,parentUuid 链表
上下文压缩 六级压缩管道
代理系统 独立 query() 循环,缓存共享
团队系统 文件系统邮箱,多进程协调
协调器模式 300 行提示工程
权限系统 七步判定管道 + YOLO 分类器后处理
工具编排 流式并发执行器
记忆系统 文件型分类法,200 行索引限制
桥接系统 IDE 双向通信,JWT 认证
技能系统 Markdown + YAML,一等子代理

每一层都有明确的边界和故障恢复策略。比如自动压缩有电路断路器——连续失败 3 次就停止重试。max_output_tokens 恢复也限制 3 次。Token 预算有递减收益检测——连续 3 次续写且增量低于 500 token,就判定为空转。

工具并发执行是另一个精巧设计。StreamingToolExecutor(530 行)在模型响应流到达时就开始执行工具,不等完整响应。并发安全的工具并行跑,非并发安全的独占执行。内部维护 siblingAbortController——当 Bash 工具报错时,终止同级子进程。结果按到达顺序缓冲并有序输出。

提示词:静态与动态的分界线

src/constants/prompts.ts 里有一个特殊标记:

export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
  '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'

它把系统提示切成两部分。标记之前是静态内容——工具定义、行为规范、通用指引,跨会话不变,可以被 Anthropic API 全局缓存。标记之后是动态内容——工作目录、Git 状态、用户记忆、环境变量,每次会话都不同。

splitSysPromptPrefix 函数在这里分割提示词,返回 SystemPromptBlock[],每个块带 cacheScope 标记。三种模式:

  1. MCP 工具存在时——跳过全局缓存,所有块标记 cacheScope: 'org'(组织级缓存)
  2. 全局缓存模式(1P 专用)——边界前的静态内容标记 cacheScope: 'global'(跨组织缓存),边界后 cacheScope: null(不缓存)
  3. 默认模式(3P 提供商)——最多 3 个块,org 级缓存

global 意味着所有使用相同静态前缀的用户共享缓存命中——工具定义和行为规范对所有人都一样,没必要每个用户各存一份。这是 Anthropic 自己的产品代码里写的缓存策略。

提示词还根据功能标志和工具可用性做条件化生成。如果 AskUserQuestionTool 可用,就注入多选题的使用指南。如果有自定义技能,就列出可用技能列表。没有一行废话——每个段落都只在需要时出现。

一些工程细节

特性标志做死代码消除——feature('VOICE_MODE') 来自 bun:bundle,是编译期开关。条件为 false 的分支在构建时被物理删除。这意味着外部发布版和内部版本是不同的二进制。源码里可以看到 KAIROS(一部 assistant 模式的内部代号,含 cron 调度和 proactive 主动干预能力)、COORDINATOR_MODEBRIDGE_MODEVOICE_MODECONTEXT_COLLAPSE 等子系统在外部版本里根本不存在。讽刺的是,做了这么多,源代码泄漏了。

会话持久化用 JSONL——每条消息带 parentUuid 指针,形成链表结构。支持对话分叉和 /resume 恢复。compact boundary 处 parentUuid 重置为 null,标记新链的起点。子代理的转录存在独立的 sidechain 文件里。远程持久化用 Last-Uuid 做乐观并发控制。

记忆系统的约束——MEMORY.md 入口文件限制 200 行或 25KB(先到者为准)。截断优先按行,再按字节,在最后一个换行处切割避免行被截断。AutoDream(autoDream.ts)负责记忆整合——自动在后台总结对话为持久记忆,但 KAIROS 模式下用独立的 disk-skill dream 路径。

启示

分为两部分,「啊哈,我也是这么做的」和「哦!原来如此」。

  • 也是这么做的:
    • API 微压缩和上下文折叠,应用中也做了类似实现。projectView 的具体实现被 CONTEXT_COLLAPSE feature flag 保护,外部构建中做了死代码消除。从 query.tssessionRestore.tsREPL.tsx 等调用点看,它通过增量 commit log 重放折叠,像版本控制一样管理上下文的可见性——具体的折叠边界和回放策略很好奇。
    • 工具 reflection 这条线,也在用,确实能在长链路任务里提高自我修正能力。
    • Coordinator模式设计和在做的也很像,甚至也用了类似的 <task-notification> XML 格式来规范 worker 向 coordinator 的结果回报,当时之所以这么干的目的是LLM对这种格式比较敏感。
  • 要学习的:

    目前关注比较多的是上下文管理和多代理系统的设计。

    • 多 agent 设计里如何共享上下文,还需要更细化的分层处理。现在 Swarm 模式下,仍然是共享整段上下文,粒度偏粗。Claude Code 的做法是子代理完全隔离上下文,但通过 CacheSafeParams(含 systemPrompt、userContext、systemContext、toolUseContext、forkContextMessages)共享提示缓存前缀——既保证隔离,又不浪费缓存。
    • cache 的控制粒度还可以更细。splitSysPromptPrefix 的三模式设计(global / org / null)提供了很好的参考——特别是 global 级别缓存让所有用户共享静态提示词的缓存命中,这在成本上是质的区别。
    • 工具并发调用我也试过,但做得不够好。大部分时候还是串行执行,里面既有工程实现问题,也有模型规划能力的约束。StreamingToolExecutor 的设计给了一个思路:流式接收时就开始执行,用 siblingAbortController 做错误传播,按顺序缓冲结果——并发的难点不在并发本身,在于错误处理和顺序保证。

最后,不得不说,这个世界,真的一个巨大的艹台班子。


EOF 🤞