为什么要读 Claude Code 的源码
大多数人用 Claude Code,停留在”会用”这个层面。
会用,当然不错。但”会用”和”懂它”之间,有一条巨大的鸿沟。你在鸿沟的这边,它就是一个黑盒——你输入一句话,它输出一堆代码,中间发生了什么,你一无所知。
而当你跨过这条鸿沟,站到另一边,你会发现:Claude Code 的架构设计,本身就是一堂极好的软件工程课。
这是一个系列的第一篇。我会带你从入口文件开始,一路追踪到 API 调用和工具执行,把整个数据流完整地走一遍。
读完这篇文章,你再去读源码,效率会高很多——因为你已经有了一张地图。
从哪里开始
拿到一个陌生的代码库,很多人的第一反应是”从哪里开始看”。
这个问题其实有一个非常简单的答案:从入口文件和配置文件开始。
入口文件告诉你程序怎么启动的,配置文件告诉你程序依赖什么。这两样东西搞清楚了,你就知道了程序的”骨架”。剩下的,都是往骨架上填肉。
Claude Code 的入口文件是 src/entrypoints/cli.tsx。
第一层:cli.tsx —— 一切的起点
打开 cli.tsx,你首先看到的是一堆”polyfill”——在程序真正执行之前,它先往全局环境里注入了一些东西:
// cli.tsx 顶部
const feature = (_name: string) => false; // 所有 feature flag 全部返回 false
globalThis.MACRO = {
VERSION: "1.0.x",
// ... 构建时常量
};
这行 feature = () => false 非常关键。Claude Code 内部有大量的 feature flag:COORDINATOR_MODE(多 agent 协调)、DAEMON(后台守护进程)、VOICE_MODE(语音模式)……这些都是 Anthropic 内部用的功能。开源版本里,一个 () => false 把它们全关了。
这是一种非常实用的工程手段——用 feature flag 控制功能开关,发布时只需改一行代码就能裁剪掉所有内部功能。
真正的启动逻辑在文件底部:
// cli.tsx:319
void main();
// cli.tsx:59
async function main(): Promise<void> {
const args = process.argv.slice(2);
// 快速路径:--version 直接输出,零模块加载
if (args[0] === "--version") {
console.log(`${MACRO.VERSION} (Claude Code)`);
return;
}
// ...其他快速路径(大部分被 feature flag 跳过)
// 关键转换点:动态导入 main.tsx
const { main: cliMain } = await import("../main.jsx"); // L312
await cliMain(); // L314
}
注意这里用的是动态 import(),不是静态 import。这意味着只有真正需要启动完整 CLI 的时候,才会加载 main.tsx 及其所有依赖。如果你只是跑 --version,整个程序几乎零开销就退出了。
这就是软件工程里的”快速路径”思维:常见的简单场景,要尽量快地处理掉,不要让它去走复杂的完整流程。
第二层:main.tsx —— Commander 解析与初始化
main.tsx 大约有 1700 行,是整个 CLI 的定义中心。它用 Commander.js 来解析命令行参数,然后执行初始化流程。
核心调用链:
main.tsx:585 export async function main()
↓
main.tsx:4504 program.parseAsync(process.argv) // Commander 解析
↓
main.tsx:1006 .action(async (prompt, options) => { ... }) // 主命令处理器
↓
main.tsx:916 await init() // 初始化
↓
main.tsx:3798 await launchRepl(...) // 启动交互式 REPL
init() 定义在 src/entrypoints/init.ts:57,是一个 memoized 函数(只执行一次)。它做的事情很多,但每一件都有明确的目的:
export const init = memoize(async (): Promise<void> => {
enableConfigs(); // 加载配置系统
applySafeConfigEnvironmentVariables(); // 安全环境变量
applyExtraCACertsFromConfig(); // TLS 证书
setupGracefulShutdown(); // 优雅关闭
populateOAuthAccountInfoIfNeeded(); // OAuth 信息
configureGlobalMTLS(); // mTLS 配置
configureGlobalAgents(); // 代理配置
preconnectAnthropicApi(); // TCP 预连接 API
});
最后一行 preconnectAnthropicApi() 很有意思——在你还没输入任何内容的时候,程序就已经在跟 API 服务器建立 TCP 连接了。这种”预连接”策略,能显著减少你第一次提问时的等待时间。
初始化完成后,launchRepl() 启动交互式界面。它定义在 src/replLauncher.tsx:12:
export async function launchRepl(root, appProps, replProps, renderAndRun) {
const { App } = await import('./components/App.js');
const { REPL } = await import('./screens/REPL.js');
await renderAndRun(root, <App {...appProps}>
<REPL {...replProps} />
</App>);
}
没错,终端界面是用 React 写的。它用了一个叫 Ink 的库,把 React 的组件模型搬到了终端里。你在终端里看到的每一个输入框、每一条消息、每一个加载动画,都是一个 React 组件。
第三层:REPL.tsx —— 用户输入的入口
REPL.tsx 是交互界面的核心组件。当你在终端里输入一段话按下回车,触发的就是这里的 onQuery 回调。
关键调用链:
用户按下回车
↓
REPL.tsx:2859 onQuery(newMessages, ...)
↓
REPL.tsx:2922 → onQueryImpl(...)
↓
REPL.tsx:2772 → await Promise.all([ // 并行加载上下文
getSystemPrompt(...), // 系统 prompt
getUserContext(), // CLAUDE.md 等用户指令
getSystemContext() // git status 等环境信息
])
↓
REPL.tsx:2797 → for await (event of query({...})) // 流式查询
↓
REPL.tsx:2806 onQueryEvent(event) // 处理每个流式事件
这里有两个值得注意的设计:
第一,上下文是并行加载的。 Promise.all 同时获取系统 prompt、用户上下文(你项目里的 CLAUDE.md 文件)和系统上下文(git status、当前日期等)。串行加载也能工作,但并行加载更快。
第二,查询是流式的。 for await...of 消费一个 AsyncGenerator,每收到一个事件就立刻处理。这就是为什么你能看到 Claude 的回答一个字一个字地”打”出来——不是等全部生成完再显示,而是边生成边显示。
还有一个容易忽略的细节:REPL.tsx 直接调用 query(),不经过 QueryEngine。QueryEngine(定义在 QueryEngine.ts:186)是给 SDK/headless 模式用的编排器。两条路径最终都汇入同一个 query() 函数。
第四层:query.ts —— 对话循环的心脏
query.ts 是整个系统的核心——一个 while(true) 循环。
// query.ts:219
export async function* query(params: QueryParams) {
yield* queryLoop(params, consumedCommandUuids);
}
// query.ts:241
async function* queryLoop(params, consumedCommandUuids) {
const deps = params.deps ?? productionDeps();
// 主循环:每次迭代 = 一次 API 调用 + 工具执行
while (true) {
// 1. 调用模型
for await (const message of deps.callModel({
messages, systemPrompt, tools, signal, ...
})) {
// 处理流式事件:text、thinking、tool_use...
}
// 2. 执行工具调用
const toolUpdates = runTools(toolUseBlocks, ...);
for await (const update of toolUpdates) {
yield update.message;
}
// 3. 判断:有 tool_use → continue,end_turn → return
}
}
这个循环的逻辑用一句话概括就是:调用 API → 收到回复 → 如果回复里包含工具调用,就执行工具,把结果塞回去,再调一次 API → 直到模型说”我说完了”(end_turn)。
这就是所谓的 Agentic Loop——Agent 的核心模式。Claude 不只是回答你的问题,它会通过调用工具(读文件、写文件、执行命令)来主动完成任务。每调用一次工具,就是循环的一次迭代。
deps.callModel 是一个依赖注入点。在生产环境中,它指向 queryModelWithStreaming(来自 src/services/api/claude.ts):
// query/deps.ts:33
export function productionDeps(): QueryDeps {
return {
callModel: queryModelWithStreaming, // → claude.ts
microcompact: microcompactMessages,
autocompact: autoCompactIfNeeded,
uuid: randomUUID,
};
}
依赖注入在这里的好处是:测试的时候可以替换成 mock 的 API 调用,不用真的去请求 Claude API。 这是写可测试代码的标准做法。
第五层:claude.ts —— 真正发出请求的地方
services/api/claude.ts 是最底层,直接调用 Anthropic SDK。
// claude.ts:753
export async function* queryModelWithStreaming({ messages, systemPrompt, ... }) {
yield* queryModel(messages, systemPrompt, ...);
}
// claude.ts:1018
async function* queryModel(messages, systemPrompt, ...) {
// ... 构建请求参数、计算 beta 头、过滤工具 ...
// 最终 API 调用
const result = await anthropic.beta.messages
.create(
{ ...params, stream: true },
{ signal, headers }
)
.withResponse();
// 遍历流式事件
for await (const event of stream) {
// message_start, content_block_delta, tool_use, message_stop...
// 构建 AssistantMessage 并 yield 回 query.ts
}
}
这里的 stream: true 就是流式请求的关键。它让 API 不是等所有内容生成完再返回,而是一个 token 一个 token 地往回推。
Claude Code 还支持多个 Provider:Anthropic Direct、AWS Bedrock、Google Vertex、Azure。你可以通过配置切换不同的后端。
完整数据流:一次交互的全景
把五层串起来,一次完整的交互是这样的:
用户在终端输入 "帮我写一个排序函数"
│
│ cli.tsx:319 void main()
│ cli.tsx:312 动态 import main.tsx
│ cli.tsx:314 cliMain()
▼
main.tsx:585 main() 启动
main.tsx:916 init() → 加载配置、认证、预连接 API
main.tsx:3798 launchRepl() → 渲染 React/Ink 组件树
│
▼ 用户输入 → React 组件捕获
REPL.tsx:2859 onQuery()
REPL.tsx:2772 并行加载:系统 prompt + CLAUDE.md + git status
REPL.tsx:2797 for await (event of query({...}))
│
▼
query.ts:307 while (true) { ← 主循环开始
query.ts:659 调用 API(流式)
│ ↓
claude.ts:1823 anthropic.beta.messages.create({stream: true})
claude.ts:1880+ 遍历流式事件 → yield 回 query.ts
│ ↑
query.ts:1385 如果有 tool_use → 执行工具(读文件、写文件等)
query.ts 把工具结果塞回消息 → continue(再调一次 API)
query.ts 如果 end_turn → return(退出循环)
│
▼
REPL.tsx:2806 onQueryEvent(event) → 更新终端 UI
REPL.tsx:2851 resetLoadingState() → 恢复输入状态
技术栈选型:每个选择都有原因
| 技术 | 为什么选它 |
|---|---|
| Bun | 比 Node.js 快,原生支持 TSX/ESM,启动速度极快 |
| React + Ink | 用 React 组件模型构建终端 UI,复用前端开发者的心智模型 |
| React Compiler | 自动 memoization,减少不必要的终端重渲染 |
| Commander.js | 成熟的 CLI 参数解析框架 |
| Anthropic SDK | 官方 SDK,原生支持流式响应 |
| Zustand 风格 Store | 轻量状态管理,适合单进程 CLI |
| MCP 协议 | Anthropic 的工具扩展协议,允许接入外部工具服务器 |
其中最有意思的选择是 React + Ink。用 React 写终端 UI,听起来有点奇怪,但想想看——终端 UI 本质上也是一棵组件树,也需要状态管理、也需要响应式更新。React 的声明式编程模型在这里完全适用。
而且 Claude Code 甚至 fork 了 Ink 框架,做了自己的 reconciler 和虚拟列表实现。这说明他们对终端渲染性能有很高的要求。
工具系统:Agent 的”手”
Claude Code 的核心能力来自工具系统。每个工具就是一个函数:
interface Tool {
name: string
description: string
inputSchema: JSONSchema // 参数定义
call(input, context): Result // 执行逻辑
}
核心工具包括:
- BashTool —— 执行 shell 命令
- FileReadTool / FileEditTool / FileWriteTool —— 文件操作
- GlobTool / GrepTool —— 搜索
- AgentTool —— 启动子 agent(Agent 可以启动 Agent!)
- WebFetchTool / WebSearchTool —— 网络操作
当 Claude 的回复中包含 tool_use 时,query.ts 的主循环会执行对应的工具,把结果作为 tool_result 消息追加到对话历史中,然后再次调用 API。这个”调用-执行-反馈”的循环,就是 Agent 能够自主完成复杂任务的秘密。
读源码的方法论
最后,分享一下我读这个代码库的方法。
第一步:读入口文件和配置文件。 这给了你程序的骨架——它从哪里开始,依赖什么,怎么构建。
第二步:追踪主数据流。 找到用户输入到达系统的入口点,然后一路追踪它经过的每个模块,直到最终的输出。不要一开始就陷入细节,先把主干走通。
第三步:深入关键模块。 主干走通之后,再挑你感兴趣的模块深入。比如工具系统、权限系统、自动压缩机制等。
这和读一本书的方法是一样的:先看目录,再通读一遍,最后精读重点章节。 很多人读源码的时候,上来就一行一行地啃,啃了半天也不知道自己在看什么。那是因为没有先建立全局认知。
Claude Code 的代码库大约有 1341 个 TypeScript 错误(因为是反编译的),但完全不影响 Bun 运行。这也是一个有趣的工程现实:类型系统是工具,不是目的。能跑才是硬道理。
下一篇,我会深入分析工具系统和权限机制——Claude Code 是怎么决定该不该执行一个危险操作的。
敬请期待。