Claude Code 源码解读之 架构
01 - Claude Code Architecture
一、概念解释
什么是 Claude Code?
Claude Code 是 Anthropic 官方推出的 CLI Agent(命令行智能体)。它不是一个简单的命令行工具,而是一个在终端中运行的、具备自主编程能力的 AI 编程助手。
与普通 CLI 工具的区别:
| 维度 | 普通 CLI 工具(如 git, npm) | CLI Agent(Claude Code) |
|---|---|---|
| 交互模式 | 命令 -> 结果,单轮 | 对话式,多轮持续交互 |
| 能力边界 | 执行预定义命令 | 自主决策、调用工具、编写代码 |
| 输入方式 | 固定参数和选项 | 自然语言 + 上下文理解 |
| 输出方式 | 文本/JSON | 代码修改、文件操作、终端命令执行 |
| 状态管理 | 无状态或简单状态 | 完整的对话历史和上下文管理 |
核心能力
Claude Code 能够:
- 读写文件 – 浏览项目结构,阅读源码,编辑和创建文件
- 执行命令 – 在沙箱中运行 shell 命令(构建、测试、部署等)
- 搜索代码 – 全局搜索文件名(Glob)和文件内容(Grep)
- 管理任务 – 创建、追踪和管理子任务,支持多 Agent 并行
- 访问网页 – 获取网页内容、搜索互联网
- 操作 Jupyter Notebook – 读取和编辑 notebook 单元格
- 连接 MCP 服务 – 通过 Model Context Protocol 扩展能力
- 多 Agent 协作 – 通过 Task 工具启动子 Agent 并行处理
解决什么问题?
Claude Code 的本质是将大型语言模型(LLM)的能力与本地开发环境深度集成,解决以下核心问题:
-
AI 与开发环境的隔阂 – 传统 AI 编程助手只能通过 Web 界面交互,无法直接操作本地文件和终端。Claude Code 让 AI 直接在开发者的终端环境中工作。
-
工具调用的编排 – LLM 本身只能生成文本。Claude Code 构建了一套完整的工具系统,让 LLM 能够"动手操作" – 读写文件、执行命令、搜索代码。
-
上下文管理 – 大型项目的代码量远超 LLM 上下文窗口。Claude Code 实现了上下文构建、压缩、历史管理等机制,让 LLM 始终能看到最相关的信息。
-
多步骤任务的自主执行 – 从理解需求到编写代码、运行测试、修复 bug,Claude Code 能自主完成多步骤的开发任务,而不需要人类逐步指挥。
二、核心流程图
分层架构图
各层职责说明:
| 层次 | 核心模块 | 职责 |
|---|---|---|
| 用户交互层 | cli.tsx, commands/, components/ | 接收用户输入,渲染终端 UI |
| 应用控制层 | main.tsx | 初始化应用,管理 REPL 主循环,路由命令 |
| 核心引擎层 | QueryEngine, query.ts, context.ts | 管理对话生命周期,执行 Agent Loop,构建上下文 |
| 工具层 | Tool.ts, tools.ts, tools/* | 定义工具接口,注册和执行具体工具 |
| 服务层 | services/* | API 通信、认证、分析、MCP 协议等 |
| 基础设施层 | 第三方依赖 | 提供 React 渲染、API 调用、数据校验等基础能力 |
核心模块关系图
数据流概览
三、架构解决的核心问题
Claude Code 的架构围绕以下核心问题展开设计:
-
AI 与开发环境的隔阂 – 传统 AI 编程助手只能通过 Web 界面交互,无法直接操作本地文件和终端。Claude Code 的分层架构(CLI 入口 -> 应用控制 -> 核心引擎 -> 工具层)让 AI 直接在开发者的终端环境中工作。
-
工具调用的编排 – LLM 本身只能生成文本。通过统一的 Tool 接口和注册中心,Claude Code 构建了一套完整的工具系统,让 LLM 能够"动手操作" – 读写文件、执行命令、搜索代码。工具系统采用策略模式,新增工具只需实现接口并注册,不影响核心循环。
-
上下文管理 – 大型项目的代码量远超 LLM 上下文窗口。Claude Code 实现了多级上下文构建(系统提示 + 项目规则 + 对话历史)和自动压缩机制,让 LLM 始终能看到最相关的信息。
-
多步骤任务的自主执行 – Agent Loop 以
while(true)循环驱动"思考-行动-观察"的 ReAct 模式,从理解需求到编写代码、运行测试、修复 bug,全程自主完成。 -
启动性能优化 – 通过 Fast-path 分发模式,简单请求(如
--version)毫秒级响应,复杂请求按需加载重量级模块。
四、核心代码详解
4.1 启动流程
Claude Code 的启动分为三个阶段:引导注入、参数分发、主循环初始化。
阶段 1: 引导注入
bootstrap-entry.ts --> bootstrapMacro.ts
(注入构建元数据到全局对象)
阶段 2: 参数分发
cli.tsx
(解析命令行参数,决定走哪条路径)
阶段 3: 主循环初始化
main.tsx
(注册命令、初始化 Ink、进入 REPL)
阶段 1:引导注入
src/bootstrap-entry.ts 是整个应用的绝对入口,仅有约 5 行代码:
// bootstrap-entry.ts -- 整个 Claude Code 的起点
// 核心思想:在加载任何业务代码之前,先注入构建时元数据
import { ensureBootstrapMacro } from "./bootstrapMacro.js";
// 第一步:确保全局 MACRO 对象存在
// 在原始构建流程中,这些值由打包工具在编译时注入
// 在恢复版中,我们从 package.json 读取默认值
ensureBootstrapMacro();
// 第二步:动态导入 CLI 主入口
// 使用动态 import 是为了确保 MACRO 注入先于 cli.tsx 的执行
import("./entrypoints/cli.js");
src/bootstrapMacro.ts 注入的全局对象结构:
// bootstrapMacro.ts -- 向 globalThis.MACRO 注入构建元数据
declare global {
var MACRO: {
version: string; // 例如 "999.0.0-restored"
buildTimestamp: string; // 构建时间戳
};
}
export function ensureBootstrapMacro() {
// 如果全局已存在(真正构建的版本),则跳过
if (globalThis.MACRO) return;
// 恢复版:从 package.json 填充默认值
globalThis.MACRO = {
version: "999.0.0-restored",
buildTimestamp: new Date().toISOString(),
};
}
教学要点:为什么需要引导注入?
在真正的构建流程中,版本号、构建时间等是在 CI/CD 打包时确定的。但源码中的模块可能会在启动时就读取这些值(例如 MACRO.version)。因此必须在所有模块加载之前完成注入。这就是为什么 bootstrap-entry.ts 要先用同步代码注入,再动态 import 主入口。
阶段 2:参数分发(Fast-path 模式)
src/entrypoints/cli.tsx 实现了高效的参数分发策略。它并不是把所有请求都交给主应用处理,而是对常见场景实现了"快速路径":
// cli.tsx -- 参数分发逻辑(伪代码,展示核心思路)
async function main() {
const args = process.argv.slice(2);
// Fast-path 1: --version
// 零依赖:不加载 React、不加载 SDK、不加载任何重量级模块
if (args.includes("--version")) {
console.log(globalThis.MACRO.version);
process.exit(0);
}
// Fast-path 2: --dump-system-prompt
// 输出系统提示词后退出,用于调试和审计
if (args.includes("--dump-system-prompt")) {
const context = await import("../context.js");
console.log(await context.buildSystemPrompt());
process.exit(0);
}
// Fast-path 3: --claude-in-chrome-mcp
// 启动 Chrome MCP 服务器(浏览器自动化能力)
if (args.includes("--claude-in-chrome-mcp")) {
const { startChromeMCP } = await import("../services/mcp/chrome.js");
await startChromeMCP();
return;
}
// Fast-path 4: --daemon-worker
// 守护进程模式,用于后台持续运行
if (args.includes("--daemon-worker")) {
const { runDaemonWorker } = await import("../services/daemon.js");
await runDaemonWorker();
return;
}
// Fast-path 5: 后台会话管理命令
// ps / logs / attach / kill -- 管理后台运行的 Claude Code 会话
if (args[0] === "ps") { /* 列出会话 */ }
if (args[0] === "logs") { /* 查看日志 */ }
if (args[0] === "attach") { /* 连接到会话 */ }
if (args[0] === "kill") { /* 终止会话 */ }
// Fast-path 6: 远程控制 / Bridge 模式
// 用于 IDE 集成,接受远程指令
if (args.includes("--remote-control") || args.includes("--bridge")) {
const bridge = await import("../bridge/index.js");
await bridge.start();
return;
}
// ---- 默认路径:启动完整的 CLI 应用 ----
// 这是最重的路径,会加载 React、Ink、所有工具等
const mainModule = await import("../main.js");
await mainModule.cliMain();
}
教学要点:Fast-path 设计模式
这是一种常见的 CLI 优化策略。--version 之类的操作用户期望瞬间完成,如果每次都要加载整个应用(React 渲染引擎、50 个工具、SDK 初始化等),响应时间可能达到数秒。通过在入口处提前拦截简单请求,可以做到毫秒级响应。
启动时间对比:
--version (Fast-path):
bootstrap-entry.ts -> cli.tsx -> 打印版本 -> 退出
耗时:< 100ms
默认路径 (Full-path):
bootstrap-entry.ts -> cli.tsx -> main.tsx ->
加载 React/Ink -> 初始化工具 -> 连接 API -> 进入 REPL
耗时:1-3s
阶段 3:主循环初始化
当请求进入默认路径时,main.tsx 中的 cliMain() 被调用:
// main.tsx 中的 cliMain 函数(简化展示核心步骤)
export async function cliMain() {
// 步骤 1: 初始化配置和认证
// - 读取 ~/.claude/ 下的配置文件
// - 检查 OAuth 认证状态
// - 确认 API Key 可用
await initializeConfig();
// 步骤 2: 注册所有 CLI 命令
// 使用 Commander.js 注册约 90 个命令
const program = registerCommands();
// 步骤 3: 启动 Ink 渲染
// Ink 是一个用 React 组件渲染终端 UI 的框架
// 将整个应用渲染为 React 组件树
const { waitUntilExit } = render(
<App program={program} />
);
// 步骤 4: 进入 REPL 循环
// Read-Eval-Print Loop:读取用户输入、处理、输出结果、循环
await waitUntilExit();
}
4.2 main.tsx — 主控制器
main.tsx 是整个应用最核心的文件(约 4690 行),承担了以下职责:
| 职责 | 说明 |
|---|---|
| 命令注册 | 通过 Commander.js 注册所有 CLI 命令 |
| Ink 渲染 | 用 React 组件树渲染终端 UI |
| REPL 循环 | 持续接收用户输入并处理 |
| 会话管理 | 管理对话历史和上下文 |
| 工具调度 | 根据用户意图选择并执行工具 |
| 错误处理 | 全局错误边界和异常恢复 |
为什么 main.tsx 有 4690 行?在原始项目中,这个文件很可能被拆分成很多模块。但在源码恢复过程中,它被还原为一个单体文件。这提醒我们:真实项目中应该保持模块化,单个文件控制在 800 行以内。
4.3 QueryEngine — 对话生命周期
QueryEngine 是对话的"管理者",负责一次完整对话的生命周期:
QueryEngine 生命周期:
创建 QueryEngine
|
v
初始化上下文 -----> 加载对话历史
| |
v v
等待用户输入 <----- 恢复会话状态
|
v
构建 API 请求
(系统提示 + 历史消息 + 用户输入 + 可用工具定义)
|
v
调用 Anthropic API
|
v
接收 LLM 响应
|
+---> 纯文本 --> 渲染给用户 --> 等待下一轮输入
|
+---> 工具调用 --> 执行工具 --> 结果加入上下文 --> 再次调用 API
|
v
持久化消息(保存对话历史)
QueryEngine 的核心职责:
- 消息管理 – 维护消息数组(用户消息、助手消息、工具结果)
- 上下文窗口控制 – 当消息超过上下文窗口时触发压缩(compact)
- 持久化 – 将对话保存到磁盘,支持断点恢复
- 成本追踪 – 记录每次 API 调用的 token 消耗和费用
4.4 context.ts — 上下文构建
context.ts 负责构建发送给 LLM 的系统提示和上下文信息。它决定了 LLM 能看到什么信息,直接影响回答的质量。
系统提示的主要构成:
系统提示 (System Prompt)
|
+-- 身份定义:你是 Claude Code,一个编程助手
|
+-- 环境信息:操作系统、工作目录、Shell 类型
|
+-- 项目信息:目录结构、Git 状态
|
+-- 工具描述:每个工具的名称、功能、参数说明
|
+-- 项目规则:来自 CLAUDE.md 的项目定制规则
|
+-- 全局规则:来自 ~/.claude/rules/ 的用户偏好
|
+-- 上下文状态:当前 token 使用量、窗口余量
4.5 query.ts — Agent Loop
query.ts(约 1729 行)实现了 Claude Code 最核心的 Agent Loop。这是一个 while(true) 循环,模拟了"思考 -> 行动 -> 观察"的循环。Agent Loop 的完整实现分析详见 02-Agent Loop。
Agent Loop 核心循环伪代码:
// query.ts 中 Agent Loop 的核心逻辑(伪代码)
async function agentLoop(queryEngine: QueryEngine) {
// 构建初始消息列表
let messages = [...historyMessages, userMessage];
while (true) {
// ---- 第 1 步:调用 LLM ----
// 将完整的上下文发送给 Anthropic API
const response = await anthropicClient.messages.create({
model: selectedModel,
system: systemPrompt, // 来自 context.ts
messages: messages, // 对话历史 + 最新用户输入
tools: toolDefinitions, // 来自 tools.ts 的工具 schema
max_tokens: 8192,
});
// 记录 token 消耗
trackCost(response.usage);
// ---- 第 2 步:解析响应 ----
// LLM 的响应可能包含多个 content block:
// TextBlock: 纯文本输出(展示给用户的内容)
// ToolUseBlock: 工具调用请求(需要执行的操作)
const assistantMessage = response.content;
messages.push({ role: "assistant", content: assistantMessage });
// 收集所有工具调用
const toolCalls = assistantMessage.filter(
(block) => block.type === "tool_use"
);
// ---- 第 3 步:如果没有工具调用,循环结束 ----
// LLM 认为不需要再做任何操作,对话轮次结束
if (toolCalls.length === 0) {
break;
}
// ---- 第 4 步:执行所有工具调用 ----
const toolResults = await Promise.all(
toolCalls.map(async (toolCall) => {
try {
const tool = getToolByName(toolCall.name);
const result = await tool.execute(toolCall.input);
return {
type: "tool_result",
tool_use_id: toolCall.id,
content: result,
};
} catch (error) {
return {
type: "tool_result",
tool_use_id: toolCall.id,
content: "Error: " + error.message,
is_error: true,
};
}
})
);
// ---- 第 5 步:将工具结果加入上下文 ----
messages.push({ role: "user", content: toolResults });
// ---- 继续循环 ----
// LLM 将基于工具结果决定:继续调用工具?还是回复用户?
}
}
工具系统 采用经典的接口 -> 注册 -> 实现三层架构,通过统一的 Tool 接口让 Agent Loop 不需要知道具体工具的细节。工具注册了约 50 个工具,按功能分为文件操作、命令执行、网络访问、任务管理、用户交互、模式切换、系统扩展等类别。(详见 03-工具系统)
上下文压缩 – 当对话历史超过 LLM 的上下文窗口时,Claude Code 会触发压缩机制:
压缩前(可能 100K+ tokens):
[系统提示] [用户消息1] [助手回复1] [工具调用1] [工具结果1]
[用户消息2] [助手回复2] [工具调用2] [工具结果2]
... (大量历史消息) ...
[最新用户消息]
压缩后(约 20-30K tokens):
[系统提示]
[压缩摘要:前 20 轮对话的关键信息概括]
[最近 5 轮对话的完整内容]
[最新用户消息]
压缩服务位于 src/services/compact/,它会调用 LLM 对历史消息生成摘要,用摘要替换原始消息,从而为新的对话腾出空间。
五、启动追踪示例
用户说:“帮我修复 src/utils.ts 中的 bug”
循环第 1 轮:
LLM 思考:我需要先看看这个文件的内容
LLM 行动:调用 FileReadTool 读取 src/utils.ts
观察:获得文件内容
循环第 2 轮:
LLM 思考:我发现了第 42 行的空指针问题
LLM 行动:调用 FileEditTool 修复代码
观察:文件修改成功
循环第 3 轮:
LLM 思考:修复完成,运行测试验证
LLM 行动:调用 BashTool 运行 npm test
观察:测试通过
循环第 4 轮:
LLM 思考:所有工作完成,向用户汇报
LLM 行动:(无工具调用,纯文本回复)
-> 循环结束
六、关键文件索引
核心文件职责
| 文件 | 行数(约) | 职责 |
|---|---|---|
| main.tsx | 4690 | 主控制器,命令注册,REPL 循环 |
| query.ts | 1729 | Agent Loop 核心实现 |
| QueryEngine.ts | 1295 | 对话生命周期管理 |
| context.ts | – | 系统提示和上下文构建 |
| Tool.ts | – | 工具接口定义 |
| tools.ts | – | 工具注册中心 |
| commands.ts | – | 命令注册和路由 |
| bootstrapMacro.ts | 29 | 全局构建元数据注入 |
| bootstrap-entry.ts | 5 | 应用绝对入口 |
| cli.tsx | 300 | CLI 入口和参数分发 |
目录结构概览
src/
|
+-- entrypoints/ # 应用入口点
| +-- cli.tsx # CLI 主入口(参数分发)
| +-- mcp.ts # MCP 服务器入口
| +-- sdk/ # Agent SDK 入口
|
+-- bootstrap/ # 启动初始化逻辑
|
+-- commands/ # CLI 命令实现(约 90 个命令)
| +-- install-slack-app/
| +-- init/
| +-- ...
|
+-- tools/ # 工具实现(约 50 个工具)
| +-- BashTool.ts
| +-- FileEditTool.ts
| +-- GrepTool.ts
| +-- ...
|
+-- components/ # React/Ink UI 组件
|
+-- services/ # 服务模块
| +-- api/ # Anthropic API 通信
| +-- mcp/ # MCP 协议实现
| +-- oauth/ # OAuth 认证
| +-- analytics/ # 使用分析
| +-- compact/ # 上下文压缩
| +-- voice/ # 语音输入
| +-- plugins/ # 插件系统
|
+-- hooks/ # React Hooks(约 70 个)
| +-- IDE 集成 hooks
| +-- 快捷键 hooks
| +-- 设置 hooks
| +-- ...
|
+-- skills/ # 技能系统
| +-- 加载器
| +-- 内建技能内容
|
+-- assistant/ # 助手行为模块
|
+-- bridge/ # 桥接连接(IDE、远程)
|
+-- ink/ # Ink 渲染扩展
|
+-- state/ # 应用状态管理
|
+-- types/ # TypeScript 类型定义
|
+-- screens/ # 全屏 UI 界面
|
+-- server/ # 服务端逻辑
|
+-- query/ # 查询处理
|
+-- vim/ # Vim 模式支持
|
+-- utils/ # 工具函数
+-- api/ # API 工具
+-- auth/ # 认证工具
+-- shell/ # Shell 工具
+-- agent-context/ # Agent 上下文工具
核心技术栈
| 技术 | 用途 | 说明 |
|---|---|---|
| TypeScript | 开发语言 | 严格模式关闭,ESM 模块系统 |
| React + Ink | 终端 UI | 用 React 组件模型渲染终端界面 |
| Anthropic SDK | API 调用 | 与 Claude 模型通信 |
| Commander.js | 命令解析 | CLI 命令注册和路由 |
| Zod | 数据校验 | 工具参数的 schema 定义和校验 |
| WebSocket | 实时通信 | IDE 集成、远程控制 |
| Bun | 运行时 | 包管理和开发运行 |
设计模式
| 模式 | 应用位置 | 说明 |
|---|---|---|
| ReAct 模式 | Agent Loop | LLM 在循环中交替进行推理和行动 |
| 策略模式 | 工具系统 | 所有工具实现统一的 Tool 接口,可自由增删替换 |
| 注册表模式 | tools.ts, commands.ts | 集中管理所有可扩展的功能点 |
| 观察者模式 | React Hooks | 约 70 个 Hooks 管理 UI 状态和副作用 |
| 快速路径模式 | CLI 入口 | 对简单请求零依赖处理,避免加载重量级模块 |
关键设计决策
-
为什么选择 React/Ink 而不是传统终端库? React 的组件模型使得复杂终端 UI 的管理变得可行。Claude Code 的终端界面包含流式文本输出、进度条、工具调用展示等复杂交互,React 的声明式 UI 和状态管理非常适合这种场景。
-
为什么使用 Zod 而不是手动校验? Zod 同时提供 TypeScript 类型推导和运行时校验。工具参数的 schema 定义一次,就可以同时在编译时获得类型检查、在运行时获得数据校验、还能直接转换为 JSON Schema 发送给 LLM。一举三得。
-
为什么 Agent Loop 使用 while(true) 而不是递归? while 循环比递归更安全 – 它不会导致栈溢出,内存使用更可控。Agent Loop 可能执行数十甚至上百轮迭代(处理大型代码库时),递归在这种场景下是有风险的。
延伸阅读
如果你希望深入了解以下主题,可以继续阅读:
- Agent Loop 完整实现:02-Agent Loop
- 工具系统详解:03-工具系统
- 上下文压缩机制:阅读
src/services/compact/了解如何在有限上下文窗口中管理长对话 - MCP 协议集成:阅读
src/services/mcp/了解如何通过 Model Context Protocol 扩展工具能力 - React/Ink 终端 UI:阅读
src/components/和src/hooks/了解如何用 React 构建终端界面 - 命令系统:阅读
src/commands/了解 90+ 命令的实现方式
本文档基于 Claude Code 恢复版源码(version 999.0.0-restored)撰写。恢复版是从 source maps 重构的代码树,部分模块使用兼容性垫片或降级实现,与原始上游实现可能存在差异。
更多推荐



所有评论(0)