Claude Code 源码解读 01:从零理解 Agent 架构——代码不是拼出来的,是长出来的

张开发
2026/5/17 21:48:17 15 分钟阅读
Claude Code 源码解读 01:从零理解 Agent 架构——代码不是拼出来的,是长出来的
Claude Code 源码解读 01从零理解 Agent 架构——代码不是拼出来的是长出来的当我第一次读到 Claude Code 的源码时脑海里冒出一个问题为什么它的代码读起来像是一篇精心结构的论文而不是一堆功能的堆砌答案藏在它的架构设计哲学里。引言架构不是效果图很多人在学一个项目的源码时会问这个函数做了什么、“那个类有什么用”。这些问题当然重要但如果只盯着局部你永远理解不了为什么这个系统能跑得稳、扩展得开、维护得久。看 Claude Code 源码我学到最重要的东西不是某个具体实现而是一个问题这个设计是为了解决什么问题而存在的Claude Code 不是一个AI 写代码工具那么简单。它的核心挑战是如何在保证安全的前提下让一个 AI Agent 能够在真实代码库里长时间、自主地工作。这个问题域决定了它的架构走向。让我从最底层开始一层层剥开它的设计思路。从入口说起一个命令的完整生命周期当你敲下claude这四个字母发生了什么claude帮我重构这个模块这个命令首先进入cli.tsx这是 Claude Code 的真正入口。但它并不是立刻开始加载整个程序——它先做了一件事快速路径判断。// cli.tsx - 快速路径零模块加载if(args.length1(args[0]--version||args[0]-v)){console.log(${MACRO.VERSION}(Claude Code));return;// 就这么简单退出了什么都没加载}注意这里没有任何import。MACRO.VERSION是在构建时由 Bun 直接内联到字节码里的。这意味着claude --version的响应时间是毫秒级的。这不是吹毛求疵的优化这叫工程哲学用户体验中的快速路径值得专门对待。快速路径不只是--version。--daemon-worker、--claude-in-chrome-mcp、后台会话管理ps/logs/attach/kill……这些分支都用了动态import()而不是静态导入// 内部 MCP 服务器 - 动态导入只在需要时加载if(process.argv[2]--claude-in-chrome-mcp){const{runClaudeInChromeMcpServer}awaitimport(../utils/claudeInChrome/mcpServer.js);awaitrunClaudeInChromeMcpServer();return;}对于只在特定条件下执行的代码路径延迟加载避免了普通交互场景中不必要的模块求值开销。架构全景Claude Code 是怎么组织的把 Claude Code 的源码结构摊开大致是这样的src/ ├── entrypoints/ # 程序入口点 │ ├── cli.tsx # CLI 入口快速路径 │ └── main.tsx # 主程序入口Commander.js 解析 ├── query.ts # Agent 循环核心 ├── QueryEngine.ts # 对话状态管理 ├── Tool.ts # 工具接口定义 ├── tools/ # 各种工具实现 │ ├── BashTool/ │ ├── FileReadTool/ │ ├── AgentTool/ # 递归 Agent 派生 │ └── MCPTool/ # MCP 协议工具包装 ├── setup.ts # 初始化序列 ├── bootstrap/state.ts # 全局状态单例 ├── hooks/ # React hooksTUI 界面 ├── services/ │ ├── mcp/ # MCP 客户端 │ └── compact/ # 上下文压缩 ├── coordinator/ # 多 Agent 协调模式 ├── skills/ # 技能系统 └── plugins/ # 插件系统这个结构里藏着第一个设计智慧职责边界清晰。入口是入口循环是循环工具是工具界面是界面。没有一个文件既做这个又做那个。核心三角Agent 循环、工具系统、上下文管理Claude Code 的复杂功能可以归结为三个核心问题的答案1. Agent 循环queryLoop怎么让 AI 持续工作不是问一句答一句而是调用模型 → 检测到工具调用 → 执行工具 → 收集结果 → 再次调用模型 → …… 直到任务完成。这是query.ts和QueryEngine.ts的职责。2. 工具系统Tool如何安全地给 Agent 提供能力每个工具都要回答一系列问题它现在可以调用吗调用前需要用户确认吗它会修改文件还是只读数据多个工具能同时运行吗结果如何在终端 UI 上渲染这是Tool.ts和tools.ts的职责。3. 上下文管理Auto-Compact对话历史越来越长怎么办Token 是有限的。Claude Code 在对话过程中主动把历史压缩成摘要原始内容备份到磁盘在保持上下文连续性的同时控制 Token 消耗。这是services/compact/autoCompact.ts的职责。这三角关系里有一个微妙的依赖方向Agent 循环依赖工具系统来执行动作工具系统依赖上下文管理来理解场景上下文管理又在 Agent 循环的每一轮被调用。三者互相支撑缺一不可。全局状态不得不用的魔鬼Claude Code 有一个全局状态单例写在src/bootstrap/state.ts里// DO NOT ADD MORE STATE HERE - BE JUDICIOUS WITH GLOBAL STATE这不是客气话。全局状态是架构腐化的温床——一旦养成往全局丢状态的习惯代码很快就会变得难以测试、难以推理。Claude Code 团队用注释明确表达了这种约束意识。但为什么还是要有全局状态因为在某些场景下不引入全局状态意味着要把所有东西一层层往下传——那更丑。状态初始化的细节很有意思// 处理符号链接和 Unicode 规范化try{resolvedCwdrealpathSync(rawCwd).normalize(NFC);}catch{// iCloud Drive 等挂载点可能 lstat 失败resolvedCwdrawCwd.normalize(NFC);}realpathSync解析符号链接.normalize(NFC)处理 macOS 文件系统的 Unicode 特殊行为。在 iCloud Drive 挂载点上lstat可能会失败所以有 try/catch 降级处理。每个细节不起眼但每一个都是真实 Bug 的补丁。两条执行路径交互式 vs 无头Claude Code 支持两种执行模式交互式 REPL用户敲一行Agent 处理一行终端实时渲染进度。这需要 React/Ink TUI 渲染。无头模式-p接收一条 prompt执行输出结果退出。这对 CI/CD 集成至关重要。# 基本用法claude-p解释这段代码的功能main.py# 输出 JSON 格式claude-p--output-format json列出所有 TODO 注释# 流式 JSONclaude-p--output-format stream-json重构这个函数|jq.content两种模式共享完全相同的核心逻辑——同样的 Agent 循环、同样的工具调用、同样的上下文管理。区别只在于有没有 TUI、输出格式怎么处理、用户确认对话框怎么弹。这是经典的策略模式把不变的部分封装起来把变化的部分抽象成接口。设计哲学代码是长出来的不是拼出来的看完 Claude Code 的架构我最大的感受是它是有机的不是拼凑的。很多工具是功能 功能 功能堆起来的。Claude Code 不是它的每一个设计决策都指向同一个核心问题如何让 AI Agent 在真实代码库里安全、长时间、自主地工作安全 → 多层权限系统长时间 → 上下文压缩自主 → 工具系统 Agent 循环真实代码库 → 文件系统边界、路径验证、git 状态快照架构不是效果图不是先画好蓝图再填代码。它是问题域的投影。当你对问题理解得足够深架构自然会浮现。总结这一章我们从整体上理解了 Claude Code 的架构设计快速路径哲学非主路径不拖累主路径零依赖的快速操作优先处理三角架构Agent 循环、工具系统、上下文管理三者互相支撑全局状态的克制不得不用的魔鬼用注释严格约束策略模式交互式/无头模式共享核心逻辑只在表现层分化问题域驱动架构是问题理解深度的投影不是先验的完美设计下一章我们将深入 CLI 入口与启动流程理解 Claude Code 从命令行到Agent 醒来的完整初始化过程。附架构核心文件索引模块文件职责入口src/entrypoints/cli.tsx快速路径判断主入口src/entrypoints/main.tsxCommander.js 参数解析全局状态src/bootstrap/state.ts运行时状态单例初始化src/setup.tsAgent 初始化序列REPL 渲染src/repl/replLauncher.tsxInk/React TUI 启动

更多文章