CC 源码学习(1):框架设计与整体架构
这篇文章要解决什么问题
很多人打开 Claude Code 源码时,第一反应通常是“功能很多”,第二反应是“目录很多”,但真正难的地方不是目录本身,而是不知道这套系统是怎么跑起来的。
如果只盯着某个工具、某个命令或者某个 React 组件去读,很容易陷入局部细节,最后看了很多文件,仍然说不清楚:
- 程序从哪里启动
- 用户输入之后先经过谁
- 模型调用和工具调用是谁在协调
- 命令系统、工具系统、UI、服务层之间是什么关系
- 为什么项目要拆出这么多模块
这篇文章的目标,就是先把 Claude Code 的整体框架搭起来,让你在读后至少能回答一句话:
它本质上是一个“以 CLI/REPL 为宿主、以大模型为核心决策器、以工具系统为执行层、以权限与策略系统为安全边界、再通过插件/技能/MCP 扩展能力”的工程化 Agent 框架。
先给一个全景图
如果把 Claude Code 当成一个系统来看,它大致可以拆成 7 层:
- 启动与初始化层
- 交互入口层
- 命令系统
- 工具系统
- Query / Agent 主循环
- 服务与基础设施层
- 扩展能力层
可以先用下面这张简化图理解:

用户输入 -> 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 这些能力能接进来,本质上是因为核心系统一开始就不是写死的。
如果你要读源码,建议按这个顺序看
如果你想自己继续读这个仓库,我建议按下面的顺序:
src/main.tsxsrc/entrypoints/init.tssrc/commands.tssrc/tools.tssrc/Tool.tssrc/utils/processUserInput/processUserInput.tssrc/QueryEngine.tssrc/query.tssrc/services/tools/toolOrchestration.tssrc/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 looptoolOrchestration.ts负责工具执行编排useCanUseTool.tsx负责把权限控制嵌进主链路
所以读这个项目时,最重要的不是把所有目录都看一遍,而是先抓住这条主线:
启动系统,接收输入,进入查询循环,让模型驱动工具,再通过权限和策略控制执行边界,最后把结果回流给用户。
这条线一旦建立起来,后面再去看安全、插件、技能、MCP、多 Agent,就不容易迷路了。
往期推荐
一个安全、简单的 SSH 管理方式 secssh每次给交换机做配置都像重来一遍?试试这个小工具AI 应用安全:在效率提升之外的安全思考
夜雨聆风