乐于分享
好东西不私藏

CC 源码学习(1):框架设计与整体架构

CC 源码学习(1):框架设计与整体架构

这篇文章要解决什么问题

很多人打开 Claude Code 源码时,第一反应通常是“功能很多”,第二反应是“目录很多”,但真正难的地方不是目录本身,而是不知道这套系统是怎么跑起来的。

如果只盯着某个工具、某个命令或者某个 React 组件去读,很容易陷入局部细节,最后看了很多文件,仍然说不清楚:

  • 程序从哪里启动
  • 用户输入之后先经过谁
  • 模型调用和工具调用是谁在协调
  • 命令系统、工具系统、UI、服务层之间是什么关系
  • 为什么项目要拆出这么多模块

这篇文章的目标,就是先把 Claude Code 的整体框架搭起来,让你在读后至少能回答一句话:

它本质上是一个“以 CLI/REPL 为宿主、以大模型为核心决策器、以工具系统为执行层、以权限与策略系统为安全边界、再通过插件/技能/MCP 扩展能力”的工程化 Agent 框架。

先给一个全景图

如果把 Claude Code 当成一个系统来看,它大致可以拆成 7 层:

  1. 启动与初始化层
  2. 交互入口层
  3. 命令系统
  4. 工具系统
  5. Query / Agent 主循环
  6. 服务与基础设施层
  7. 扩展能力层

可以先用下面这张简化图理解:

用户输入 -> CLI 参数 / REPL 输入 -> 命令解析 / 普通提示词处理 -> QueryEngine -> query 主循环 -> 大模型输出文本或 tool_use -> 工具编排与执行 -> 权限校验 / 安全检查 -> 工具结果回流模型 -> UI 持续渲染结果

这也是理解整个项目最重要的一条主线。

启动入口:先把运行环境搭起来

Claude Code 的总入口在 src/main.tsx

这个文件不是简单地“读参数然后启动”,而是在非常早的阶段就做了大量工程化准备工作。它开头最值得注意的是两类动作:

  • 顶层 side effect 提前执行
  • 重模块尽量延迟加载

比如它会在其他模块大量导入之前,先启动:

  • startMdmRawRead():提前读取 MDM/托管配置
  • startKeychainPrefetch():提前读取 keychain 中的认证信息

这类写法的目标很明确:把启动链路中本来串行的 I/O 提前并行化,减少 CLI 冷启动成本。

如果你只看功能,会觉得这是“小优化”;但如果你从框架设计角度看,它其实说明这个项目已经把自己当成一个成熟产品,而不是一个 demo 工具。因为 CLI 产品的第一体验就是启动速度。

接下来 main.tsx 还会继续接管几件大事:

  • 读取系统上下文与用户上下文
  • 初始化遥测、GrowthBook、配置系统、策略限制
  • 加载命令与工具注册表
  • 处理交互模式与非交互模式
  • 最终决定是进入 REPL、执行命令,还是走 SDK / headless 路径

这一步可以理解成:先把“运行时世界”搭起来,后面模型和工具才有正确的上下文可用。

#初始化层:不仅是配置加载,而是基础设施预热

src/entrypoints/init.ts 体现了 Claude Code 很典型的工程思路:初始化不是一次配置读取,而是“基础设施预热”。

这个文件里至少能看到几类初始化工作:

  • 配置系统启用与校验
  • 安全相关环境变量注入
  • CA 证书与 mTLS 配置
  • 全局 HTTP agent / 代理设置
  • API 预连接
  • LSP、swarm、scratchpad 等资源的生命周期注册
  • 遥测与事件日志初始化

尤其值得注意的是,它区分了:

  • trust 建立前可以应用的“安全环境变量”
  • trust 建立后才能应用的完整配置

这说明项目不是把“配置”当成纯本地数据,而是把它视为一个可能影响网络、安全、执行边界的敏感输入。

从架构角度看,init.ts 的意义是:

  • 把跨模块共享的基础设施统一准备好
  • 把进程级别的行为提前固定住
  • 给后面的工具执行、模型调用、插件加载提供稳定运行环境

命令系统:把 CLI 能力组织成统一入口

src/commands.ts 是命令注册中心。

这个文件做的不是“实现命令逻辑”,而是把所有 slash command 和 CLI 命令统一注册起来,再根据运行环境、feature flag 和用户类型决定哪些命令真正可见。

你可以把它理解为一层“命令目录”。

它有几个很值得参考的设计点:

1. 注册中心统一收口

所有命令最终都在这里聚合,这样主程序不需要知道每个命令怎么实现,只需要知道“去命令表里取”。

这类模式的好处是:

  • 新增命令成本低
  • 入口清晰
  • 能统一做启用/禁用、过滤、懒加载和权限控制

2. feature flag 驱动裁剪

commands.ts 里有很多 feature('XXX') ? require(...) : null 形式的条件引入。

这不是单纯的“开关功能”,而是配合 Bun 的 dead code elimination,把不同发行版本真正裁剪成不同能力集合。

也就是说,这套架构从一开始就考虑了:

  • 内部版和外部版能力不同
  • 不同环境的功能集不同
  • 一部分重模块没必要在所有构建里都存在

3. 命令不是唯一入口,但它是用户最显性的入口

Claude Code 里有两类“用户动作”:

  • slash command,例如 /doctor/config/review
  • 普通自然语言输入,交给模型处理

命令系统本质上处理的是“明确动作”,而模型主循环处理的是“开放式任务”。这两者分开,是整个架构能同时兼顾可控性和灵活性的关键。

工具系统:真正把“Agent 能做什么”落到代码里

如果说命令系统是“用户显式入口”,那工具系统就是“模型可调用能力集合”。

核心注册文件是 src/tools.ts

这里最重要的一点是:Claude Code 并不是把工具散落在各处,而是通过统一注册表把工具声明出来,再在运行时统一交给模型使用。

从 getAllBaseTools() 可以看出,这些工具覆盖了几大类能力:

  • 文件读写与编辑
  • Shell / PowerShell 执行
  • 搜索与抓取
  • Web 搜索与网页访问
  • Agent / Team / Message 等多 Agent 能力
  • Skill、MCP、LSP 等扩展能力
  • 任务、计划、工作树、输出控制等流程型能力

也就是说,Claude Code 的“Agent 性能”不只来自模型本身,更来自工具层设计得足够完整。

工具系统为什么重要

因为在这类项目里,模型并不直接修改世界,它只能:

  • 生成文本
  • 发起 tool_use

真正和外部世界发生交互的,是工具层。

因此工具层承担了三个角色:

  • 能力边界:模型能做什么,取决于暴露了哪些工具
  • 安全边界:高风险操作必须在工具层拦截和约束
  • 抽象边界:底层实现再复杂,对模型暴露的仍是统一 schema

这一点对很多想做 Agent 框架的人很有启发:不要把重点只放在 prompt 或模型调用,真正决定系统上限的,往往是工具抽象是否清晰。

Tool 类型系统:把“可调用能力”标准化

src/Tool.ts 是全项目非常关键的基础文件。

它解决的不是某个具体功能,而是“一个工具应该以什么形式接入系统”。

这里面至少定义了几类关键抽象:

  • 工具输入 schema
  • 工具执行上下文 ToolUseContext
  • 权限上下文 ToolPermissionContext
  • 进度、通知、状态更新能力
  • 与消息系统、AppState、文件缓存之间的连接方式

这层抽象的价值在于:Claude Code 不是在调用一批随意函数,而是在调用一批满足统一协议的“能力模块”。

当你有了这层统一协议,后面很多事情才会变得容易:

  • 工具能否统一出现在系统 prompt 中
  • 工具能否统一参与权限审批
  • 工具能否统一接入状态管理和 UI 渲染
  • 工具能否在主线程、子 Agent、SDK 模式下复用

换句话说,Tool.ts 是工具框架成立的前提。

用户输入如何进入主循环

用户输入处理的关键文件之一是 src/utils/processUserInput/processUserInput.ts

它的职责不是“把字符串原样送给模型”,而是先做一轮输入分流:

  • 这是普通 prompt,还是 slash command
  • 是否包含附件、图片、IDE 选区、粘贴内容
  • 是否需要触发本地命令处理
  • 是否需要先跑 hook
  • 最终是否真的需要进入模型查询

这个阶段很像网关层。

也就是说,Claude Code 在模型前面先放了一层“输入整形与路由系统”,这样做有两个好处:

  • 把本地可直接处理的动作拦下来,不必每次都让模型参与
  • 把输入补成更完整的上下文,再交给模型,提高后续决策质量

这也是 Claude Code 区别于“把用户话术直接发给模型”的地方。它本质上是一个带前置编排层的 Agent 客户端。

QueryEngine:把一轮对话抽成可复用引擎

src/QueryEngine.ts 可以看成“会话级控制器”。

它最重要的价值,是把一次对话中的核心状态与流程从 UI 中抽出来,形成一个独立的引擎对象。

它负责管理的内容包括:

  • 当前会话消息
  • 文件读取缓存
  • 权限拒绝记录
  • 使用量统计
  • 当前 turn 的上下文
  • 用户输入提交后的整个处理流程

这里有个设计非常重要:它不是 React 组件,也不是某个命令的私有逻辑,而是一个可以被不同入口复用的对话引擎。

这意味着项目在架构上已经有意识地把以下两件事分开:

  • UI 怎么展示
  • 对话/工具/模型主循环怎么运行

这类拆分是大型应用能长期维护的关键,否则 REPL、SDK、桥接模式、子 Agent 模式很快就会互相缠死。

query 主循环:整个 Agent 框架真正的核心

如果要找全项目最接近“心脏”的位置,src/query.ts 一定算其中之一。

这个文件核心上在做一件事:

驱动“模型输出 -> 识别工具调用 -> 执行工具 -> 把结果塞回模型 -> 继续下一轮”的循环。

这就是典型的 agent loop。

在这个文件里,你可以看到很多围绕主循环的成熟工程处理:

  • 流式响应
  • tool use 结果回填
  • 自动 compact / 上下文压缩
  • token budget 控制
  • fallback model
  • stop hook / post hook
  • tool use summary
  • 错误恢复与最大输出恢复逻辑

这说明 Claude Code 并不是一个“简单请求模型然后展示结果”的程序,而是一个围绕长链路、不确定性、高上下文成本设计出来的执行系统。

工具执行编排:不是所有工具都串行执行

src/services/tools/toolOrchestration.ts 很值得单独看。

很多人做工具调用时,默认会把所有工具一个个串行跑完,但 Claude Code 在这里已经开始做更细的编排:

  • 能并发的工具并发跑
  • 有副作用的工具串行跑
  • 通过 isConcurrencySafe 判断是否可并发

这背后体现的是非常实用的工程思路:

  • “读操作”和“写操作”不该被一视同仁
  • 并发不是默认打开,而是按安全条件分批
  • 工具运行不仅要正确,还要兼顾吞吐与交互体验

这类设计特别值得复用,因为它刚好处在“简单 demo 不会考虑,但真实产品必须考虑”的位置。

权限系统不是附属模块,而是主链路的一部分

虽然安全专题会放到第二篇详细讲,但在架构层必须先看到一点:权限系统并不是外挂。

src/hooks/useCanUseTool.tsx 直接卡在工具执行之前,负责:

  • 判断是 allow、deny 还是 ask
  • 结合配置、规则、分类器结果做决策
  • 在交互式场景里发起用户审批
  • 在 swarm / coordinator 场景中走不同权限处理逻辑

这说明 Claude Code 的工具调用流程并不是:

模型 -> 工具

而是:

模型 -> 权限判断/审批 -> 工具

这个顺序非常关键,因为它意味着安全边界被纳入默认执行链路,而不是事后补丁。

REPL UI:Claude Code 不是脚本,而是终端应用

src/replLauncher.tsx 展示了另一个架构特点:UI 层被显式包装成 REPL 应用,而不是简单 console.log 输出。

Claude Code 使用 React + Ink 来构建终端界面,这带来两个直接收益:

  • 复杂状态和交互可以组件化
  • 工具进度、审批弹窗、状态面板、会话视图都能统一渲染

因此这个项目虽然跑在终端里,但整体组织方式更接近“前端应用 + Agent 运行时”的结合体,而不只是传统 CLI。

这也是为什么仓库里会有大量:

  • components/
  • screens/
  • hooks/
  • state/

你可以把它理解成:Claude Code 的外壳是 CLI,但内部已经是一个终端版应用框架。

服务层与扩展层:让核心闭环之外的能力可插拔

当启动、命令、工具、query loop 这些核心链路成立之后,项目还需要一层“外部能力接入层”。

Claude Code 在这方面拆得比较清楚,典型包括:

  • services/:API、OAuth、MCP、LSP、analytics、policy、remote settings 等
  • plugins/:插件机制
  • skills/:技能机制
  • bridge/:IDE / 外部宿主桥接
  • coordinator/utils/swarm/:多 Agent 协作

这类拆法的好处是:核心 agent loop 只关心“我现在有哪些能力可用”,至于这些能力来自内置模块、插件、MCP 还是 IDE bridge,并不需要全部耦合到同一层。

这是一种很典型的“核心稳定、外围扩展”的架构。

为什么这套架构值得参考

我觉得 Claude Code 最值得参考的,不是某一个具体工具,而是它在几个关键问题上的处理方式:

1. 把复杂系统拆成了明确层次

它没有把 UI、模型、工具、权限、扩展混在一起,而是拆成:

  • 入口层
  • 编排层
  • 执行层
  • 基础设施层
  • 扩展层

这让系统虽然大,但主干其实不乱。

2. 把“Agent”做成工程系统,而不是 prompt 脚本

从 QueryEngine 到 query loop,再到 tool orchestration、权限系统、上下文压缩、token budget,这些都说明它不是“让模型自己想办法”,而是把模型放进一个被严密编排的执行框架里。

3. 把安全和性能放进默认设计

无论是启动并行预热、懒加载,还是权限审批、策略限制、环境隔离,这些都不是后期附加,而是在主链路里提前布局。

4. 给扩展能力预留了足够空间

插件、技能、MCP、LSP、bridge、多 agent 这些能力能接进来,本质上是因为核心系统一开始就不是写死的。

如果你要读源码,建议按这个顺序看

如果你想自己继续读这个仓库,我建议按下面的顺序:

  1. src/main.tsx
  2. src/entrypoints/init.ts
  3. src/commands.ts
  4. src/tools.ts
  5. src/Tool.ts
  6. src/utils/processUserInput/processUserInput.ts
  7. src/QueryEngine.ts
  8. src/query.ts
  9. src/services/tools/toolOrchestration.ts
  10. src/hooks/useCanUseTool.tsx

按这个顺序看,你会先建立骨架,再进入主循环,最后再去看安全、扩展和具体工具实现。

总结

Claude Code 的整体框架,本质上不是“一个会调模型的 CLI”,而是一个分层比较完整的 Agent Runtime:

  • main.tsx 负责启动和模式分流
  • init.ts 负责基础设施预热
  • commands.ts 负责显式命令入口
  • tools.ts + Tool.ts 负责工具能力抽象
  • processUserInput.ts 负责输入路由
  • QueryEngine.ts 负责会话级编排
  • query.ts 负责 agent loop
  • toolOrchestration.ts 负责工具执行编排
  • useCanUseTool.tsx 负责把权限控制嵌进主链路

所以读这个项目时,最重要的不是把所有目录都看一遍,而是先抓住这条主线:

启动系统,接收输入,进入查询循环,让模型驱动工具,再通过权限和策略控制执行边界,最后把结果回流给用户。

这条线一旦建立起来,后面再去看安全、插件、技能、MCP、多 Agent,就不容易迷路了。

往期推荐

一个安全、简单的 SSH 管理方式 secssh每次给交换机做配置都像重来一遍?试试这个小工具AI 应用安全:在效率提升之外的安全思考