架构全景
从外部视角勾勒 Claude Code 的全貌:在不深入源码细节的前提下,厘清技术栈选型、TAOR 核心循环、五大子系统构成、一次请求的完整旅程与整体代码规模。后续核心系统 / 高级架构 / 方法论板块会以此为坐标系,逐层拆解各系统的设计决策。
技术栈:每个选择都有理由
翻开 Claude Code 源码,技术选型是第一个让人停下来思考的地方——有些在意料之中,有些让人意外。
| 组件 | 选择 | 为什么 |
|---|---|---|
| 运行时 | Bun | 比 Node.js 快,冷启动和子进程创建尤为明显 |
| UI 框架 | React + Ink | 终端 UI 的状态管理需求和 Web 前端本质相同 |
| 语言 | 严格模式 TypeScript | 19000 个文件的项目,没有类型系统会崩溃 |
| Schema 验证 | Zod v4 | 运行时类型检查,防止 AI 返回格式错误的数据 |
| 入口文件 | main.tsx(785KB) | 单文件打包,减少模块解析开销 |
| 重模块加载 | 懒加载 | OpenTelemetry、gRPC 等按需加载 |
下面拆开看每个选择背后 的工程考量。
为什么选 Bun 不选 Node.js
Claude Code 对运行时有两个压力敏感点:频繁启停子进程(每次 Bash 工具调用都是一个子进程)和冷启动延迟——这是命令行工具,敲回车到响应的延迟直接影响体验。
Bun 在这两项上比 Node.js 强不少,冷启动快 3–5 倍。每天用几百次的工具,每次快 200ms 累积起来感受明显。
为什么终端界面用 React
乍看意外——终端程序用 React?
但 Claude Code 的交互复杂度解释了这一点:实时进度条、可折叠代码 diff、权限确认弹窗、嵌套的工具调用展示、多 Agent 分屏状态……这些和 Web 前端是同一个状态管理问题。用 console.log 拼字符串处理它们,代码会变成不可维护的面条。
React + Ink 让 Anthropic 用组件化思路构建终端:每个 UI 元素是一个组件,状态变自动重渲染。这是复杂度管理的选择,不是炫技。
社区在源码泄露后还发现了一些底层优化——ink/screen.ts 和 ink/optimizer.ts 借鉴游戏引擎技术:用 Int32Array 字符池代替字符串操作,用位掩码编码样式元数据;自清除的行宽缓存在 token 流式传输场景下减少了约 50 倍的 stringWidth 计算。Claude Code 每秒可能 重绘几十次,这些是流畅体验的必要条件。
为什么 Schema 验证用 Zod
TypeScript 的类型在运行时被擦除,AI 返回的 JSON 在运行时可能是任何形状——模型输出是不确定的,不能假设它每次都返回正确格式。
Zod 在「不确定的 AI 输出」和「确定性的程序逻辑」之间建立了一道防线:提供运行时类型检查和转换。
单文件入口与懒加载
main.tsx 单文件 785KB 看似违反"分模块"直觉,但这是有意的——单文件打包减少模块解析开销,启动时不走标准模块解析流程。OpenTelemetry、gRPC 等重模块则采用懒加载,仅在需要时加载,避免拖慢启动。
核心循环:TAOR
Claude Code 的心脏是一个叫 TAOR 的 Agent 循环:Think → Act → Observe → Repeat。
你在终端输入一句话后发生的所有事情,都是这个循环在驱动:模型先理解你要什么(Think),选一个工具执行操作(Act),观察工具返回的结果(Observe),判断任务是否完成。没完成就回到 Think 继续。一个任务可能要转几十圈才结束。
TAOR 不是 Claude Code 独创的概念,但 Claude Code 的实现里有几个值得记住的设计细节。
工具调用是唯一的「行动」方式
模型不能直接操作文件系统或运行命令,必须通过工具间接执行。所有操作都经过一层中间件,可以在中间层做权限检查、日志记录、安全审查——这是 harness 工程中权限系统存在的前提。
每次循环都是一次完整的 API 调用
每个 Think-Act 周期都遵循同一个节奏:
- 把当前上下文发给 API
- 等待模型返回工具调用指令
- 执行工具,把结果加入上下文
- 再发一次 API 调用
这解释了为什么复杂任务耗费大量 token——不是因为模型「想太多」,而是循环结构决定了每一步都要把完整上下文发过去。这也是 harness 工程中上下文管理如此重要的原因。
停止条件是模型自己决定的
循环什么时候停?当模型判断任务完成时,它不再调用工具,直接输出文本回答。
这正是给 Claude 明确的验证标准特别重要的原因——如果需求描述模糊,模型不知道什么时候算「做完了」,循环就会一直转下去。
五大子系统
TAOR 是骨架,让 Claude Code 真正好用的是围绕这个骨架构建的五个子系统:
| 子系统 | 职责 | 代码量 |
|---|---|---|
| 系统提示(System Prompt) | 定义 AI 的身份、能力边界和行为准则 | — |
| 工具系统(Tools)⭐ | 59 个工具的注册、调用和权限管理 | ~50,000 行 |
| 查询引擎(Query Engine)⭐ | 所有 LLM API 调用、流式传输、缓存和编排 | ~54,000 行 |
| 权限系统(Permissions) | 操作的安全审查和访问控制 | — |
| 记忆系统(Memory) | 跨会话的持久化偏好和上下文 | — |
工具系统 和 查询引擎 是两个最大、最具体的子系统——一个管「能做什么」,一个管「怎么调模型」。其余三个更接近策略层:System Prompt 决定 AI 怎么回答,权限系统决定哪些操作能放行,记忆系统让 AI 跨会话保持连续性。
除了这五个核心子系统,还有几个重要的辅助模块:
- IDE Bridge:VS Code 和 JetBrains 与 CLI 之间的双向通信层。用 JWT 做认证,支持两个方向的消息传递。IDE 可以向 CLI 发任务,CLI 也可以向 IDE 推状态更新——这是 Claude Code 能同时作为独立 CLI 和 IDE 插件运行的基础。
- 上下文管理器:负责在 对话变长时进行结构化压缩,以及管理 prompt 缓存策略。
- 多 Agent 协调器:管理多个 Agent 的创建、通信、隔离和结果合并。
一次请求的完整旅程
把前面的概念串起来:当你输入「帮我写一个排序函数」时,完整旅程是什么样?
-
System Prompt 拼装 系统从缓存加载静态 prompt 段(身份定义、安全准则、工具说明),再拼接动态段(你的 CLAUDE.md、当前目录信息、记忆文件、git 状态)。这可能是一个数千 token 的巨大 prompt。
-
API 调用(Think) 你的消息连同 system prompt 和对话历史一起发给 Claude API。查询引擎处理流式传输,你看到文字逐渐出现。
-
工具调用(Act) 模型决定先读一下当前目录的文件结构(调用 Glob 工具),再看看有没有已有的排序相关代码(调用 Grep 工具)。每个工具调用都经过权限系统审查。
-
结果注入(Observe) 工具的返回结果被添加到对话上下文中。模型现在知道了目录结构和现有代码。
-
循环继续(Repeat) 模型决定用 Write 工具创建文件,然后用 Bash 工具运行测试。每一步都是一次新的 API 调用,带上所有之前的上下文。
-
完成 测试通过后,模型判断任务完成,输出总结文本。如果对话触发了记忆提取条件,后台还会 fork 一个子 agent 来提取值得记住的偏好信息。
看起来简单?实际上即使是「写一个排序函数」这样的小任务,也可能涉及 3–5 次 TAOR 循环,每次循环都是一次完整的 API 调用。复杂任务可能有几十次循环,消耗几万 token。
但这就是 TAOR 的魅力:它不是在执行预设的脚本,而是在实时做决策。每一步都根据最新的观察调整策略——试了一个方案发现不行,回退换另一条路。这不是 bug,是设计的一部分。
代码规模总览
最后看几个数字,帮你建立对这个项目规模的直觉:
| 指标 | 数量 | 说明 |
|---|---|---|
| 源文件总数 | ~1,900 | 全部 TypeScript |
| 代码总行数 | ~510,000 | 不含 node_modules |
| 工具系统 | ~50,000 行 | 最复杂的模块之一 |
| 查询引擎 | ~54,000 行 | 最大的单一模块(services/ 目录) |
| 内置工具 | 59 | 每个独立权限控制 |
| 斜杠命令 | ~50 | 用户可调用的快捷命令 |
| Feature Flags | 44 | 控制未发布功能 |
src/ 子目录 | 41 | 高度模块化的项目结构 |
这是一个严肃的工程项目——不是三五个工程师周末写的 hobby project,而是一个大团队持续投入、精心设计的产品级软件。
Hacker News 上有人评论说 Anthropic 的代码像是 vibe coded——「感觉对了就行」的写法。这个评价是否公允另说,但它说明一件事:Claude Code 的竞争力不在代码是否 优雅,而在系统设计的决策是否正确。做对了几个关键的架构选择,比把每一行代码写得漂亮重要得多。
后面核心系统 / 高级架构 / 方法论三个板块,会逐一拆解这些关键决策。