乐于分享
好东西不私藏

Claude Code 源码深度剖析(一):架构全景与核心循环

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?")]
用户提问
第 1 轮 API
+ Assistant("让我算一下" + ToolUse)
LLM 决定调用计算工具
第 1 轮工具
+ ToolResult{output: "4"}
工具返回结果
第 2 轮 API
+ Assistant("答案是 4")
LLM 回答,不再调用工具
结束
4 条消息,2 轮迭代
没有工具调用 → 退出循环

整个过程的终止条件只有一个: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_searchgrep_searchWebFetchWebSearch 等 10 个
读文件、搜代码、搜网页
工作区写入
write_file

edit_fileNotebookEditTodoWriteConfig
写文件、编辑、配置
完全访问
bash

REPLPowerShellAgent
执行命令、启动子 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 智能体与工程实践。如果你也在研究这个方向,欢迎加我一起交流。

微信二维码