Claude Code 源码深度剖析(一):架构全景与核心循环
51 万行 TypeScript 太庞大?社区开发者用 Rust 重写了 Claude Code 的核心,只有 2 万行。从这个精简版入手,反而更容易看清一个 AI 智能体到底需要什么。
一、为什么要看 Rust 重写版?
2026 年 3 月 31 日凌晨,Claude Code 的完整源码因一次 npm 打包事故意外泄露。Anthropic 在发布 @anthropic-ai/claude-code v2.1.88 时,不小心把 59.8MB 的 source map 文件打包进了 npm 包。source map 本是开发调试用的,它能将压缩后的代码映射回原始 TypeScript 源码。也就是说,任何人安装这个包,就能还原出完整的约 51 万行原始代码。(注意:Claude Code 的 GitHub 官方仓库一直存在,但里面只有文档和打包后的 bundle,正常情况下看不到可读源码。)
消息传开后,大量分析文章涌现,但几乎都在拆解那个 51 万行的 TypeScript 原版。QueryEngine 4.6 万行、40+ 工具、复杂的插件系统……信息量大得让人迷失在细节中。
泄露发生后,韩国开发者社区 instructkr 的 Sigrid Jin 迅速行动,先用 Python 做了 clean-room 重写,随后在 dev/rust 分支推进了 Rust 版本,代号 claw-code(项目地址:github.com/instructkr/claw-code)。这个社区驱动的重写版只有约 2 万行 Rust 代码,却实现了 Claude Code 的完整核心功能:Agent 循环、工具系统、权限管控、系统提示词、配置管理、API 通信、会话管理、MCP 协议、子 Agent。
claw-code 是社区项目,与 Anthropic 官方无关。它的价值在于:重写本身就是一次提炼本质的过程,51 万行浓缩到 2 万行,留下来的就是真正重要的东西。
我选择从 Rust 重写版入手,就是因为它已经帮你做了”减法”,核心架构一目了然。
这篇文章不只是拆解源码,每个模块我都会聊聊”为什么这样设计”,以及我觉得可以复用的经验。
二、架构全景:6 个模块的分层设计
claw-code 由 6 个模块组成,形成分层架构:
claw-code/├── runtime/ # 核心运行时:对话循环、权限、配置、会话、提示词├── api/ # Anthropic API 客户端、SSE 流式解析、OAuth├── tools/ # 工具注册表与执行分发(18 个内置工具)├── commands/ # 斜杠命令注册(/help, /cost 等)├── compat-harness/ # TS→Rust 迁移兼容适配层└── rusty-claude-cli/ # CLI 入口、REPL、终端渲染
这 6 个模块不是平级的,有明确的依赖方向:
┌─────────────────────────────────────────────┐│ CLI / REPL(用户交互层) │├─────────────────────────────────────────────┤│ MCP 协议 · 子 Agent(扩展层) │├─────────────────────────────────────────────┤│ API 客户端 · 会话管理(通信/存储层) │├─────────────────────────────────────────────┤│ 系统提示词 · 配置系统(上下文层) │├─────────────────────────────────────────────┤│ Agent Loop · 工具系统 · 权限系统(核心层) │└─────────────────────────────────────────────┘
这里有一个值得注意的设计决策:runtime 模块是最底层的,但它不绑定任何具体实现。它只定义了两个接口,ApiClient(跟 LLM 通信)和 ToolExecutor(执行工具)。真正的实现在最顶层的 CLI 模块。
这意味着同一套核心循环代码,测试时用 mock 实现,生产时用真实实现,一行代码都不用改。整个架构的可测试性就建立在这个分离上。
三、Agent Loop:88 行代码的核心循环
如果你只看 Claude Code 的一个文件,就看 conversation.rs。整个智能体的核心循环只有 88 行。
3.1 核心数据结构
先看它需要什么:
AgentRuntime { session: 消息数组(唯一的状态) api_client: LLM 通信接口 tool_executor: 工具执行接口 permission_policy: 权限策略 system_prompt: 系统提示词 max_iterations: 防无限循环(默认无上限) usage_tracker: Token 用量追踪}
注意,整个运行时的唯一状态就是一个消息数组。没有复杂的状态机,没有 FSM,就是一个不断追加的消息列表。第一次看到的时候我也觉得”不可能就这么简单”,但它确实就是这样。
3.2 核心循环:run_turn()
当用户输入一条消息后,发生了什么?
def run_turn(user_input): # 1. 追加用户消息到会话 session.messages.append(UserMessage(user_input)) while True: # 2. 防无限循环 if iterations > max_iterations: raise Error("超过最大迭代次数") # 3. 把系统提示词 + 全部消息发给 LLM response = api_client.stream(system_prompt, session.messages) # 4. 解析 LLM 的响应(文本 + 工具调用) assistant_message = parse_response(response) session.messages.append(assistant_message) # 5. 提取工具调用请求 tool_calls = extract_tool_uses(assistant_message) # 6. 没有工具调用 → LLM 认为任务完成,退出循环 if not tool_calls: break # 7. 逐个执行工具 for tool_name, input in tool_calls: # 7a. 权限检查 permission = authorize(tool_name, input) if permission == Allow: result = tool_executor.execute(tool_name, input) session.messages.append(ToolResult(result)) else: # 拒绝原因也作为结果喂回 LLM session.messages.append(ToolResult(deny_reason, is_error=True)) # 回到循环顶部,带着工具结果再次调用 LLM
就这样。整个 Claude Code 的”智能”行为,都建立在这个循环之上。
3.3 一个完整对话回合
用一个具体例子来看这个循环是怎么跑的。用户问”2+2 等于多少?”:
|
|
|
|
|---|---|---|
|
|
[User("2+2?")] |
|
|
|
+ Assistant("让我算一下" + ToolUse) |
|
|
|
+ ToolResult{output: "4"} |
|
|
|
+ Assistant("答案是 4") |
|
|
|
|
|
整个过程的终止条件只有一个:LLM 自己决定不再调用工具。max_iterations 只是个安全网,默认值是 usize::MAX(相当于无上限),正常情况下不会触发。
3.4 流程图
用户输入 │ ▼┌──────────────────┐│ 追加 User 消息 │└────────┬─────────┘ ▼┌──────────────────┐│ 调用 LLM(流式) │◄─────────────────────┐└────────┬─────────┘ │ ▼ │┌──────────────────┐ 是 ┌─────────┴────────┐│ 有工具调用? ├─────────→│ 权限检查 → 执行工具 │└────────┬─────────┘ │ 结果追加到消息数组 │ │ 否 └──────────────────┘ ▼┌──────────────────┐│ 返回结果,结束 │└──────────────────┘
3.5 设计启发
这段代码给我两个比较深的印象。
消息数组即状态。 不需要设计复杂的状态机来追踪”当前在做什么”。消息数组本身就是完整的状态,可以序列化保存(会话恢复),可以截断压缩(上下文管理),可以回放调试(问题排查)。一个 append-only 的数组,把状态管理、持久化、调试三件事都解决了。
拒绝也是结果。 当权限检查拒绝了一个工具调用,系统不会直接报错或中断循环,而是把拒绝原因作为 ToolResult(is_error=true) 喂回 LLM。这样 LLM 知道”这个工具不能用”,可以自己调整策略,比如换一个不需要高权限的工具,或者直接告诉用户”我没有权限执行这个操作”。
四、工具系统:18 个工具的统一分发
Agent Loop 是循环本身,工具系统是循环中实际干活的部分。Claude Code 的 Rust 版实现了 18 个内置工具,全部在一个文件中(4240 行)。
4.1 三层结构
┌──────────────────────────────────────┐│ 第 1 层:工具注册表 ││ 定义工具名称 / 描述 / 参数 / 权限 │ → 发给 LLM,让它知道有哪些工具├──────────────────────────────────────┤│ 第 2 层:统一分发器 ││ match tool_name → 路由到具体实现 │ → 一个 switch/case 搞定├──────────────────────────────────────┤│ 第 3 层:具体实现 ││ 每个工具一个 Input 结构体 + 执行函数 │ → 实际干活的地方└──────────────────────────────────────┘
4.2 工具注册表:LLM 与工具之间的约定
每个工具用一个 ToolSpec 来描述自己:
{ "name": "bash", "description": "执行 shell 命令", "input_schema": { "command": "string", "timeout": "number?" }, "required_permission": "DangerFullAccess"}
input_schema 是标准的 JSON Schema 格式,直接发给 Anthropic API 的 tools 字段。LLM 按 schema 生成参数 JSON,工具按 schema 反序列化参数。双方按约定交互,互不知道对方怎么实现的。
这个解耦做得比较干净:你可以换一个完全不同的 LLM(只要它支持 function calling),工具不用改;你也可以换一个完全不同的工具实现,LLM 不用改。
4.3 18 个工具按权限分类
|
|
|
|
|---|---|---|
|
|
read_file
glob_search, grep_search, WebFetch, WebSearch 等 10 个 |
|
|
|
write_file
edit_file, NotebookEdit, TodoWrite, Config |
|
|
|
bash
REPL, PowerShell, Agent |
|
权限从低到高:只读 < 工作区写入 < 完全访问。每个工具在注册时就绑定了所需权限级别。
4.4 统一分发器
def execute_tool(name, input): match name: "bash" -> parse_as(BashInput, input) -> run_bash() "read_file" -> parse_as(ReadInput, input) -> run_read_file() "write_file" -> parse_as(WriteInput, input) -> run_write_file() "edit_file" -> parse_as(EditInput, input) -> run_edit_file() "glob_search" -> parse_as(GlobInput, input) -> run_glob_search() "grep_search" -> parse_as(GrepInput, input) -> run_grep_search() "Agent" -> parse_as(AgentInput, input) -> spawn_agent_job() # ... 其余工具 _ -> Error("不支持的工具: {name}")
每个工具的流程完全一致:JSON 反序列化 → 执行逻辑 → 返回字符串结果。新增一个工具只需要三步:定义 Input 结构体、实现执行函数、加一行 switch case。大约 30 行代码就搞定了。
4.5 最复杂的工具:子 Agent
18 个工具中,Agent 是唯一需要”开新线程、创建新运行时”的:
def spawn_agent_job(input): # 在新线程中运行 # 创建一个新的 AgentRuntime,复用同一套核心循环 runtime = AgentRuntime( session = 新的空会话, api_client = 新的 API 客户端, tool_executor = 白名单限制的工具集, # 关键区别 permission = 完全访问但不能问用户, # prompter = null system_prompt = 主提示词 + "你是后台子 Agent,自主完成任务", ) runtime.run_turn(input.prompt) # 结果写入磁盘文件
子 Agent 不是一个新系统,就是”用不同参数 new 一个 AgentRuntime”。相同的核心循环,不同的工具集和权限。
所有子 Agent 的工具白名单中都不包含 Agent 工具,这就防止了子 Agent 再生子 Agent 造成无限递归。约束很简单,但堵住了一个潜在的严重问题。
4.6 设计启发
工具系统有两点值得记住。
JSON Schema 作为接口约定。 LLM 和工具之间、内部和外部工具之间,统一用 JSON Schema。不需要共享代码、不需要 import 彼此的类型。一份 schema 就是全部的接口定义。后面的 MCP 协议把这个思路推得更远,让任何语言写的工具都能接入。
子 Agent 复用核心循环。 很多项目会为子任务写一套全新的执行逻辑,但 Claude Code 没有。子 Agent = 主 Agent – 部分工具 + 不同提示词。同一套 run_turn(),换个参数就行。
五、权限系统:233 行代码的安全模型
一个能执行 shell 命令的智能体,安全是绕不过去的问题。Claude Code 的权限系统只有 233 行代码,但覆盖得很完整。
5.1 五个权限级别
ReadOnly // 最低,只能读WorkspaceWrite // 中等,可以写工作区文件DangerFullAccess // 最高常规权限,可以执行任意命令Prompt // 特殊,每次都问用户Allow // 特殊,无条件放行
这里有一个 Rust 的小技巧(思路是通用的):这 5 个级别按声明顺序自动获得”大小比较”能力。代码中直接用 当前权限 >= 所需权限 判断是否放行,不需要手写任何比较逻辑。
5.2 决策矩阵
|
|
|
|
|
|---|---|---|---|
| Allow |
|
|
|
| 完全访问 |
|
|
|
| 工作区写 |
|
|
|
| 只读 |
|
|
|
| Prompt |
|
|
|
“问用户”:有交互界面就弹窗询问,没有交互界面就直接拒绝。
5.3 核心逻辑
def authorize(tool_name, input): current = 当前权限级别 required = 该工具所需的权限级别 # 情况 1:权限足够 → 直接放行 if current == Allow or current >= required: return Allow # 情况 2:差一级 → 可以问用户提权 if current == WorkspaceWrite and required == DangerFullAccess: if 有用户交互界面: return 问用户是否允许 else: return Deny("需要用户批准") # 情况 3:差距太大 → 直接拒绝,连问都不问 return Deny("需要更高权限")
5.4 子 Agent 的权限设计
还记得子 Agent 的创建参数吗?
permission: DangerFullAccess // 权限级别设到最高prompter: null // 但没有用户交互界面
效果是:白名单内的工具都能直接放行(因为权限足够),但如果子 Agent 试图做任何超出白名单的事情(这不会发生,因为白名单已经限制了),系统会自动拒绝(因为没有 prompter 来问用户)。
两个独立的机制(权限级别 + prompter 可选)组合出了精确的安全策略,不需要为子 Agent 单独写权限逻辑。
5.5 设计启发
权限系统里最值得借鉴的是「提权而非全有全无」。
很多系统的权限要么全给(不安全),要么全禁(不好用)。Claude Code 的做法是:差一级权限可以问用户”我需要执行这个命令,允许吗?”,差太多就直接拒绝(ReadOnly 想执行 bash?连问都不问)。
用户不会被每个操作都弹窗打扰(只有跨级操作才问),但也不会在不知情的情况下执行危险命令。
六、上篇小结
回顾一下,Claude Code 的核心层由三个组件构成:
┌──────────────────────────────────────────┐│ 核心三件套 ││ ││ Agent Loop 工具系统 权限系统 ││ 88 行循环 18 个工具 5 级权限 ││ 消息即状态 JSON Schema 提权不全给 ││ LLM 自决终止 match 分发 prompter 可选 │└──────────────────────────────────────────┘
这三个组件加在一起,就是一个最小可用的智能体。理论上,只要有这三个组件 + 一个 API 客户端 + 一个系统提示词,你就可以构建一个能读写文件、执行命令、搜索代码的 AI 编程助手。
但”能用”和”好用”之间差距很大。下篇会深入 Claude Code 的上下文工程:系统提示词怎么构建?配置怎么多层合并?对话过长时怎么压缩?以及从 2 万行源码中挑出来的一些设计经验。
下篇预告:Claude Code 源码深度剖析(二):上下文工程与设计启发
相关仓库
• Rust 重写版:github.com/instructkr/claw-code(社区项目,与 Anthropic 无关) • Claude Code 官方:github.com/anthropics/claude-code(发布仓库,非可读源码)
我是 Antony,关注 AI 智能体与工程实践。如果你也在研究这个方向,欢迎加我一起交流。

夜雨聆风