乐于分享
好东西不私藏

第一次读 Claude Code 源码,别从最大的文件开始

第一次读 Claude Code 源码,别从最大的文件开始
很多人第一次打开 Claude Code 源码,会本能地做一件事:

先找最大的文件,觉得那里一定最重要。

这个直觉不算错,但很容易把人带偏。

因为 Claude Code 不是一个单点模块,而是一条任务链路。你如果一上来就扎进几千行的 main.tsx 或 REPL.tsx,很快会被命令参数、UI 状态、权限模式、MCP、插件、恢复会话这些细节淹没。

这篇文章想讲清楚一个判断:

读 Claude Code 源码,第一步不是找“最重要的文件”,而是先画出一次用户输入经过系统的路线图。

看完这一篇,你至少应该知道四个文件分别站在哪里:

cli.tsx -> main.tsx -> REPL.tsx -> query.ts

它们不是四个孤立文件,而是一条从“启动”到“执行”的主线。


一、为什么源码地图比逐行精读更重要

先给一个现实一点的事实。

在当前这份 Claude Code 源码里,这几个文件大概是这样的规模:

src/entrypoints/cli.tsx   约 401 行src/main.tsx              约 7049 行src/screens/REPL.tsx      约 6410 行src/query.ts              约 1775 行

如果你没有地图,直接打开 main.tsx,很容易产生两个错觉。

第一个错觉是:这个系统的核心就是 CLI 参数。

因为你会看到大量 Commander 配置、命令注册、模式判断、初始化逻辑。

第二个错觉是:这个系统的核心就是终端 UI。

因为你继续读 REPL.tsx,又会看到输入框、消息渲染、快捷键、状态同步、权限弹窗。

这些都重要,但它们不是主线。

主线应该是:

用户输入如何进入系统系统如何准备上下文和工具模型如何被调用工具结果如何回到 messages什么时候继续下一轮什么时候停止

也就是说,你不是在读一堆 TypeScript 文件。

你是在追踪一次 Agent turn 的生命周期。

源码地图的价值,不是告诉你文件名,而是告诉你先读什么、后读什么,以及每个文件在任务链路里负责哪一段。


二、先看 cli.tsx:它不是主角,但它决定你走哪条路

src/entrypoints/cli.tsx 是真正的入口。

但它的职责很克制:先判断有没有快速路径。

比如:

--version--dump-system-prompt--computer-use-mcp--daemon-workerremote-controldaemon

如果命中了这些路径,它就直接动态 import 对应模块,然后提前返回。

如果没有命中,才进入完整 CLI 主流程,也就是加载 main.tsx

你可以把 cli.tsx 理解成机场安检前的分流口:

只查版本的人,不需要进入候机大厅
启动 MCP server 的人,走专门通道
真正要进入交互式编程助手的人,才继续往里走

这解释了一个很关键的设计。

为什么 CLI 入口要这么薄?

因为启动速度很重要。

--version 这种命令如果也加载完整 React、工具系统、MCP、配置、权限,用户会明显感觉慢。Claude Code 把这些快速路径提前切走,本质上是在减少无意义模块加载。

所以读 cli.tsx 时,不要陷入每个分支的业务细节。

你只需要抓住一句话:

cli.tsx 负责决定这次启动是不是要进入完整 Agent 系统。


三、再看 main.tsx:它负责把“命令行程序”变成“会话”

如果 cli.tsx 是分流口,main.tsx 就是完整 CLI 的调度大厅。

这里会发生很多准备工作:

解析命令行参数注册子命令准备配置和认证加载工具和权限模式处理会话恢复准备 MCP 和插件决定进入 REPL 还是 headless 模式

这也是为什么 main.tsx 很长。

它不是因为“核心算法都在里面”,而是因为它承担了大量启动前的胶水工作。

读这个文件时,最容易犯的错是逐个子命令往下读。

比如你看到 mcpauthplugindoctordaemon,每个都想展开。这样读一天也很难形成主线。

更好的读法是只追一个问题:

普通交互式会话是怎么启动 REPL 的?

你会看到它最后会准备一批 initialMessagessessionConfiginitialState,然后调用类似 launchRepl(...) 的路径,把系统带到终端交互界面。

在这个阶段,Claude Code 还没有真正进入 Agent Loop。

它只是在做一件事:

把一个命令行启动,整理成一场可以继续对话、可以调用工具、可以保存状态的会话。

这一步很像开工前整理工位。

工具放哪里,权限怎么设,历史记录要不要恢复,当前目录是什么,模型用哪个,这些都要准备好。

否则后面的 query.ts 再强,也没有稳定的工作现场。


四、然后看 REPL.tsx:它不是输入框,而是用户和 Agent Loop 的桥

很多人看到 REPL.tsx,会以为它只是终端 UI。

这个理解太浅了。

REPL.tsx 确实负责输入框、消息展示、键盘交互、加载状态,但它更重要的职责是:

把用户输入变成 messages准备 systemPrompt / userContext / systemContext把 tools 和 permission 上下文交给 query消费 query 返回的事件用 setMessages 更新终端界面

这就不是普通 UI 了。

它是用户和 Agent Loop 之间的桥。

在源码里,你会看到类似这样的路径:

用户提交输入  ↓REPL 追加 user message  ↓准备 system prompt、上下文、工具、权限  ↓for await (const event of query(...))  ↓onQueryEvent(event)  ↓setMessages(...)

这里有一个很容易被忽略的点:REPL.tsx 不只是“展示模型输出”,它还要不断接收 query 里的流式事件。

比如 assistant 文本来了,要显示。

工具调用开始了,要显示。

工具结果回来了,要显示。

权限需要用户确认,也要在 UI 层接住。

所以读 REPL.tsx 时,不要把它当成普通 React 组件。

更准确的理解是:

REPL.tsx 负责把 Agent Loop 的内部事件,翻译成用户能看见、能操作、能打断的终端体验。


五、最后看 query.ts:这里才是 Agent turn 的心脏

到 query.ts,主线才真正进入 Agent Loop。

这个文件里有两个特别值得先抓住的点:

export async function* query(...)async function* queryLoop(...)

为什么是 async function*

因为它不是一次性返回最终答案,而是不断产出事件。

比如:

stream_request_startassistant messagetool_usetool_resultattachment最终 terminal reason

这正好对应我们前几篇讲过的模型:

messages -> model -> tool_use -> 本地工具 -> tool_result -> messages -> model

在 queryLoop 里,你会看到一个持续调度结构。

它每一轮大致做这些事:

准备 messagesForQuery请求模型并消费 streaming收集 assistantMessages识别 tool_use blocks执行 runTools(...)把 tool_result 转回可继续发送给模型的 user message合成下一轮 state.messages继续下一轮

这里最关键的不是某一行代码,而是状态如何流动。

尤其是这件事:

下一轮 messages = 上一轮 messages + assistantMessages + toolResults

如果这一步没发生,工具结果就不会进入下一轮模型推理。

这也是为什么我一直强调:tool_result 不是给用户看的日志,它是下一轮推理的燃料。

query.ts 的难点,不是它会调用模型,而是它要在流式输出、工具执行、上下文压缩、权限、停止条件之间维持一条不断裂的消息轨道。


六、这四个文件应该按什么顺序读

如果你是第一次读,我建议不要按文件大小读,也不要按 import 跳来跳去。

按这条路线读:

1. cli.tsx   只看启动分流:哪些路径会提前返回,什么时候进入 main.tsx2. main.tsx   只追交互式会话:参数、配置、工具、权限、session 如何准备,什么时候 launchRepl3. REPL.tsx   只追一次用户输入:输入如何进入 messages,query 如何被调用,事件如何更新 UI4. query.ts   只追一轮 Agent turn:模型如何返回 tool_use,工具如何执行,tool_result 如何回写,为什么继续下一轮

这条路线的好处是,你不会一开始就被工具实现、MCP、插件、远程控制、任务系统打散。

那些模块当然重要,但它们应该排在主线之后。

读源码最怕的是把支线当主线。

Claude Code 这种系统尤其如此。它有很多能力,但第一遍你只需要回答一个问题:

一句用户输入,是怎么一步步变成 Agent Loop 的?

只要这个问题通了,后面再读 Tool、Permission、MCP、Skills、Context、Memory,才不会散。


七、这一篇我们把源码地图也跑成 Demo

第五篇的代码不是重写一个新项目,而是在第四篇的基础上继续加。

上一章我们已经有:

messagesMockModelToolRegistryReadFileToolSearchFileToolAgentLoop

这一章新增一个教学工具:

SourceMapTool

它做的事很简单:当模型判断用户想要“Claude Code 源码地图”时,先返回一份源码地图文件路径:

src/main/resources/demo/ClaudeCodeSourceMap.md

然后 Agent Loop 继续调用 ReadFileTool 读取这份地图,最后由 MockModel 总结阅读顺序。

最小链路是:

UserMessage("帮我画一张 Claude Code 源码地图...")  ↓MockModel 返回 ToolUse(source_map, claude-code)  ↓SourceMapTool 返回 ClaudeCodeSourceMap.md  ↓MockModel 返回 ToolUse(read_file, ClaudeCodeSourceMap.md)  ↓ReadFileTool 读取源码地图  ↓MockModel 输出阅读建议

你会发现,这跟第四篇的结构完全一样。

变化的只是工具和任务。

这就是 Agent Harness 的一个重要特征:

主循环稳定以后,新增能力通常不是重写 Loop,而是新增工具、状态和模型可理解的任务协议。


本篇实践任务

Milestone:M01(005-008)
目标:让 Agent 基于源码地图工具,输出 Claude Code 的源码阅读路径。
代码地址:https://gitee.com/learn_java_together/mini-claude-code.git
当前分支:chapter/005-source-map
JDK 版本:17
输入:"帮我画一张 Claude Code 源码地图,告诉我 cli.tsx、main.tsx、REPL.tsx、query.ts 应该从哪里读起"
输出:终端打印三轮链路:源码地图工具 -> 读取源码地图 -> 最终阅读建议

运行方式:

git clone https://gitee.com/learn_java_together/mini-claude-code.gitcd mini-claude-codegit checkout chapter/005-source-mapmvn -q -DskipTests compile exec:java

你应该能看到类似链路:

loop step 1 -> source_maploop step 2 -> read_fileloop step 3 -> final answer

验收标准:

1
第一轮必须看到 source_map 的 tool_use 和 tool_result
2
第二轮必须看到 read_file 读取 ClaudeCodeSourceMap.md
3
第三轮必须输出 cli.tsx -> main.tsx -> REPL.tsx -> query.ts 的阅读顺序
4
messages 中必须保留用户输入、工具请求、工具结果和最终回答

失败注入:

mvn -q -DskipTests compile exec:java -Dexec.args="--max-steps=1"

这个命令会故意让 Agent 只跑一轮。

你要观察的是:系统是否能在拿到 source_map 结果后暂停,并保留中间 messages。

这能帮助你理解一个真实工程问题:

Agent 不只要能完成任务,也要能在任务未完成时留下可恢复、可调试的轨迹。


写在最后

如果只记住一句话:

读 Claude Code 源码,不要从文件大小开始,而要从一次用户输入的流向开始。

对第一次读源码的人,我建议下一步只做一件事:

把这四个文件按顺序打开,分别回答一个问题:

cli.tsx:这次启动走哪条路径?main.tsx:这次会话怎么准备?REPL.tsx:用户输入怎么进入 query?query.ts:tool_use 和 tool_result 怎么形成下一轮?

回答完这四个问题,你就已经建立了 Claude Code 的第一张源码地图。

下一篇,我们继续拆:

CLI 入口为什么这么薄:快速路径、动态 import、启动分流。