乐于分享
好东西不私藏

从源码拆解 Claude Code 的工程架构

本文最后更新于2026-03-31,某些文章具有时效性,若有错误或已失效,请在下方留言或联系老夜

从源码拆解 Claude Code 的工程架构

深度解析

拆解 Claude Code 的工程架构

一个生产级 AI Agent CLI 工具,背后的设计决策与工程实践

AI Agent 工具正在从「玩具」走向「生产力工具」,但它的工程实现鲜有人深入讨论。本文基于 Claude Code 的公开源代码快照,从架构设计、核心机制、性能优化三个维度做系统性拆解,重点关注可复用的工程思路。

目标读者:有一定工程经验,想了解 AI Agent 工具「为什么这么设计」的开发者。

一、整体架构:分层 + 插件化的组合拳

Claude Code 的整体架构并不复杂,采用经典的分层架构,同时通过工具/命令的插件化设计实现横向扩展。理解这个架构,比读任何一段具体代码都更有价值。

 1.1 五层分层模型

层级

职责

关键模块

表现层

CLI 参数解析 + 终端 UI 渲染

Commander.js + React/Ink

应用层

会话管理、消息路由、工具编排

QueryEngine.ts

工具层

40+ 可扩展工具实现

tools/ 目录

服务层

外部服务集成(API、OAuth、MCP)

services/ 目录

基础层

工具函数、状态管理、类型定义

utils/ state/ types/

��架构选型的取舍

React/Ink 用于终端 UI 是个有趣的选择——用声明式 UI 框架管理 TUI 状态,避免了手写 ANSI 转义码的噩梦,代价是引入了 React 的全套心智模型。

Bun 替代 Node.js 主要收益在启动速度(快 2-4x)和内置 Bundle 支持,这对于 CLI 工具体验至关重要。

 1.2 插件化的工具注册机制

工具和命令都通过中心化注册表(tools.ts / commands.ts)统一管理,每个工具是一个遵循统一接口的独立模块:

// Tool 接口定义(src/Tool.ts)

export type Tool = {

name: string

description: string // 供 LLM 理解工具用途的自然语言描述

inputSchema: ToolInputJSONSchema // JSON Schema,LLM 据此生成结构化调用

isEnabled: () => boolean // 动态开关,支持按环境/权限启用

isDangerous: () => boolean // 标记危险操作,触发用户确认流程

needsPermissions: () => boolean // 权限声明

validateInput: (input) => ValidationResult // 运行时输入校验(Zod)

execute: (input, context) => Promise<ToolResult>

}

这个接口设计有几个值得关注的点:

description 不是给人看的注释——它直接进入 system prompt,LLM 通过这段文本决定「何时调用这个工具、传什么参数」,写得好不好直接影响 AI 的工具调用准确率

inputSchema 双重作用:既用于 LLM 生成结构化调用参数(类似函数签名),也用于运行时 Zod 校验,一份 Schema 两处复用

isDangerous() 和 needsPermissions() 分离设计,允许细粒度控制:有的操作危险但不需要每次确认(如果用户已授权),有的操作安全但需要声明权限

��值得讨论

插件化工具注册是 AI Agent 框架的通行设计(LangChain Tools、OpenAI Function Calling 都是这个思路)。但一个容易忽视的细节是:description 的质量直接影响 Agent 表现。你在实际项目中,有没有遇到过因为 Tool description 写得不好导致 LLM 总是调错工具的问题?

二、核心引擎:QueryEngine 的「Agentic Loop」

QueryEngine 是整个系统的心脏,实现了 AI Agent 的核心执行循环(Agentic Loop)。理解这个循环,才能真正理解 AI Agent 和普通「调用 API」的本质区别。

 2.1 Agentic Loop 的执行流程

��核心循环逻辑(伪代码)

async function* agenticLoop(userMessage) {

messages.push(userMessage)

while (true) {

const response = await callAPI(messages, tools) // 流式调用

yield* streamResponse(response) // 实时推送给 UI

if (response.stopReason === ‘end_turn’) break // AI 主动结束

if (response.stopReason === ‘tool_use’) {

const results = await executeTool(response.toolCalls) // 执行工具

messages.push(toolResults) // 结果作为新消息

// 继续下一轮,让 AI 基于工具结果继续推理

}

if (turnCount > maxTurns) break // 防止无限循环

}

}

这个循环有几个关键的工程决策:

① 流式响应 + 增量渲染

每个 API 调用都是流式的(SSE),QueryEngine 用异步生成器(async generator)将响应 token 逐步 yield 给上层 UI。这确保了用户能看到「实时打字」效果,而不是等 AI 生成完整回复后一次性输出。

② 工具调用的权限拦截

 executeTool 前有一个权限检查层(canUseTool),这里实现了「沙箱」策略:

// 工具执行前的权限决策

async function canUseTool(tool, input): Promise<PermissionResult> {

if (tool.isDangerous()) {

return await promptUserForConfirmation(tool, input) // 阻塞等待用户确认

}

if (hasStoredPermission(tool, input)) {

return { allowed: true } // 用户之前已授权,直接放行

}

return { allowed: false, reason: ‘permission_denied’ }

}

③ AbortController 的传递

AbortController 从 QueryEngine 创建后,被传入每个工具的 context。用户按下 Ctrl+C 时,信号会向下传播,所有进行中的 Shell 命令、HTTP 请求都能被优雅取消。这是 CLI 工具必须处理好的细节,否则会出现僵尸进程。

��可借鉴的设计:上下文对象统一传递

ToolUseContext 将所有工具执行所需的「环境」封装成一个对象传递:

• abortController — 取消信号

• readFileState — 文件读取缓存(避免重复 IO)

• getAppState / setAppState — 应用状态读写

• mcpClients — MCP 连接池

这种「上下文对象」模式(Context Object Pattern)在大型项目中比全局变量和参数透传都更清晰,值得在自己的项目里采用。

 2.2 上下文构建:让 LLM 「看懂」工程环境

每次用户发送消息,系统会并行收集一批环境信息拼入 system prompt:

// context.ts — 并行收集上下文,使用 memoize 避免重复采集

export async function buildSystemPrompt(): Promise<string[]> {

const [gitStatus, userCtx, memoryPrompt] = await Promise.all([

getGitStatus(), // git branch/status/log

getUserContext(), // OS、cwd、环境变量

getMemoryPrompt(), // 跨会话持久化记忆

])

return [systemContext, gitStatus, userCtx, memoryPrompt]

}

// memoize 确保同一会话内多次调用只采集一次

export const getGitStatus = memoize(async () => {

const [branch, status, log] = await Promise.all([

execFile(‘git’, [‘branch’, ‘–show-current’]),

execFile(‘git’, [‘status’, ‘–short’]),

execFile(‘git’, [‘log’, ‘–oneline’, ‘-n’, ‘5’]),

])

return formatGitContext(branch, status, log)

})

上下文质量直接决定 LLM 输出质量。Claude Code 把当前 Git 状态(分支、未提交变更、最近提交)都注入到每次对话的上下文里——这解释了为什么 AI 能在不问用户的情况下「知道」你在哪个分支工作。

三、工具系统:20+ 工具的分类与设计逻辑

Claude Code 提供了覆盖编程任务全流程的工具集。工具可以按「副作用风险」分为三类:

风险等级

工具举例

典型用途

权限策略

只读

FileReadTool、GrepTool、GlobTool

读文件、搜索代码

默认允许

写操作

FileWriteTool、FileEditTool、NotebookEditTool

创建/修改文件

首次需确认

执行操作

BashTool、AgentTool、TeamCreateTool

运行命令、创建子 Agent

每次确认或白名单

网络操作

WebFetchTool、WebSearchTool、MCPTool

访问外部资源

按工具策略

 3.1 FileEditTool:精准编辑而不是全量替换

FileEditTool 不是「读文件→修改→写回」的全量替换,而是基于字符串匹配的精准编辑(类似 sed 的 s 命令):

// 输入 Schema 设计

{

file_path: string,

old_str: string, // 必须在文件中唯一出现——防止错误替换

new_str: string, // 替换为新内容(空字符串 = 删除)

}

要求 old_str 在文件中唯一出现是个精妙的约束:它强迫 LLM 在调用工具时必须提供足够多的上下文(不能只写 「}」 这种到处都有的字符串),从而大幅降低了误替换的概率。这是把「模型约束」嵌入「工具接口设计」的典型做法。

 3.2 BashTool:Shell 执行的安全边界

BashTool 允许 AI 直接执行 Shell 命令,是最强大也最危险的工具。它的安全机制分三层:

命令白名单/黑名单:某些危险命令(rm -rf /、:(){ :|:& };: 等)在运行前被拦截

超时控制:默认 120s 超时,防止 AI 启动一个永不退出的进程

用户确认流:isDangerous() 返回 true,每次执行前中断会话等待用户 y/n 确认

值得注意的是,BashTool 会把命令的 stdout、stderr、exit code 都返回给 LLM,让 AI 能基于命令执行结果继续推理(比如看到编译错误后自动修复代码)。

 3.3 多代理工具:AgentTool + TeamCreateTool

这是 Claude Code 最有野心的功能——支持递归地创建子 Agent,让 AI 可以把复杂任务分解后并行处理:

// AgentTool:创建单个子 Agent(串行子任务)

AgentTool.execute({

task: ‘扫描 src/ 目录下所有 TODO 注释并汇总’,

tools: [‘FileReadTool’, ‘GrepTool’] // 子 Agent 可用的工具子集

})

// TeamCreateTool:创建多个并行 Agent(并行子任务)

TeamCreateTool.execute({

agents: [

{ role: ‘security-reviewer’, task: ‘检查 SQL 注入风险’ },

{ role: ‘perf-analyzer’, task: ‘找出 N+1 查询问题’ },

{ role: ‘style-checker’, task: ‘检查命名规范违反’ },

]

})

coordinator/ 模块负责管理这些子 Agent 的生命周期,通过 SendMessageTool 实现 Agent 间通信。这是「AI 编排 AI」的具体实现,也是当前 Agent 架构演进的重要方向。

��值得讨论

 Agent 并行看起来很强,但实际工程中有个棘手问题:子 Agent 的 Token 消耗是叠加的,一个稍微复杂的多 Agent 任务很容易烧掉十倍于单 Agent 的 API 费用。Claude Code 的 maxBudgetUsd 参数就是为此设计的。你们团队如何控制 AI 工具的 API 成本?有没有好的实践?

四、MCP 协议:AI 工具的「USB 接口」

MCP(Model Context Protocol)是 Anthropic 主导的开放协议,目标是让 AI 工具和外部服务的连接标准化——类似于 LSP(Language Server Protocol)之于 IDE 和语言工具链的关系。

 4.1 MCP 的连接模型

// MCP 服务器支持三种传输层

type MCPTransport =

| { type: ‘stdio’ } // 本地进程,最低延迟

| { type: ‘sse’, url } // HTTP SSE,适合远程服务

| { type: ‘http’, url } // HTTP,适合无状态服务

// 连接建立后的能力协商

const connection = await mcpClient.connect(transport)

const { tools, resources, prompts } = await connection.initialize()

// MCP 工具动态注册为 Claude Code 的原生工具

const mcpTools = tools.map(t => new MCPTool(t, connection))

registry.registerAll(mcpTools)

MCP 工具通过 MCPTool 作为代理被注册进工具系统,对 QueryEngine 完全透明——LLM 看到的就是一个普通工具,不感知底层是本地代码还是远程 MCP 服务。这是代理模式(Proxy Pattern)的经典应用。

 4.2 为什么 MCP 值得关注

生态正在形成:GitHub、Slack、Postgres 等已有官方 MCP Server,意味着 AI 工具可以直接操作这些服务而不需要为每个工具单独写集成代码

协议标准化带来可移植性:为 Claude Code 写的 MCP Server,理论上也能接入其他支持 MCP 的 AI 工具(如 Cursor、Windsurf)

安全边界清晰:MCP Server 自己管理凭证和权限,AI 工具只通过协议交互,减少密钥暴露风险

五、性能优化:CLI 工具的体验工程

CLI 工具的核心用户体验指标是「感知速度」——用户从输入命令到看到第一个有意义的输出,这段时间必须尽可能短。Claude Code 的优化策略值得系统梳理。

 5.1 启动时间优化:并行预取

main.tsx 里有一个有意思的模式:在 import 语句执行的同时,以副作用方式启动异步预取:

// main.tsx — 这两行在模块加载时立即执行,不等 main() 函数调用

startMdmRawRead() // 异步读取 MDM 企业配置(macOS)

startKeychainPrefetch() // 异步读取钥匙串中的 API Key

// 等到真正需要这些数据时,大概率已经 ready 了

async function main() {

const apiKey = await getKeychainValue() // 通常已经 resolved

// …

}

这是「利用模块加载时间做预热」的技巧。在 Node/Bun 环境里,import 链执行有一定耗时,把不阻塞 critical path 的 IO 操作安排在这期间并行执行,可以有效减少启动延迟。

 5.2 运行时优化:分层缓存策略

缓存类型

实现

适用场景

函数记忆化

lodash-es/memoize 包装纯函数

getGitStatus、getUserContext 等一次性采集

文件状态缓存

FileStateCache (Map<path, {content, hash}>)

避免同一文件在一次会话中重复读盘

上下文缓存

Anthropic API 的 cache_control 参数

system prompt 内容不变时节省 Token 费用

 5.3 功能开关与死代码消除(Tree-shaking)

Claude Code 使用 Bun 的 bun:bundle feature API 实现编译时功能开关,未激活的功能分支在打包时被完全移除:

import { feature } from ‘bun:bundle’

// 构建时 feature(‘VOICE_MODE’) 被求值为 true/false

// false 分支的代码整个被 tree-shaking 掉

const voiceCommand = feature(‘VOICE_MODE’)

? require(‘./commands/voice/index.js’).default

: null

// 当前已知的功能开关

// PROACTIVE | KAIROS | BRIDGE_MODE | DAEMON | VOICE_MODE | AGENT_TRIGGERS

这和 React Native 的 __DEV__ 、webpack 的 DefinePlugin 思路相同,但 Bun 的实现更原生。对于需要维护多个发布版本(基础版/专业版/企业版)的 CLI 工具,这种机制比运行时 if-else 检查更干净,因为最终产物里根本不包含未启用的代码。

 5.4 上下文压缩:对话历史的滑动窗口

LLM 的上下文窗口是有限的(Claude 3.5 是 200K tokens)。长对话不加处理迟早会超出限制。Claude Code 的解法是:

实时 Token 计数:每次 API 响应后更新 totalUsage,接近阈值时触发压缩

摘要式压缩:调用 compact/prompt.ts,用一次 LLM 调用把历史对话压缩成结构化摘要

选择性保留:工具调用的 input/output 是压缩重点保留的内容(相比闲聊更有信息密度)

压缩本身也消耗 Token,所以需要权衡「压缩时机」——太早浪费钱,太晚会遇到上下文溢出错误。Claude Code 的策略是在剩余 20-30% 窗口时触发,这个参数在实际项目中可能需要根据对话特性调整。

六、设计模式在 AI Agent 中的应用

Claude Code 的代码里能清晰看到几个经典设计模式的应用,在 AI Agent 场景下有些变体值得注意。

设计模式

应用位置

AI Agent 场景下的特殊考量

策略模式

Tool 接口

每个工具是一个独立策略,LLM 动态选择调用哪个——选择者是 AI 而非代码逻辑

代理模式

MCPTool

远程 MCP 服务的本地代理,对 QueryEngine 透明——跨进程/网络的工具调用

组合模式

系统提示构建

多个上下文片段组合成完整 prompt——组合的粒度和顺序影响 LLM 理解效果

观察者模式

流式响应渲染

QueryEngine 产生事件,UI 订阅渲染——生成器替代传统事件总线

命令模式

斜杠命令系统

命令封装为对象,支持 prompt/action/jsx 三种执行类型

��一个 AI 特有的模式:Prompt as Interface

 AI Agent 开发中有个传统软件不存在的「模式」:工具的 description 字段是 AI 与工具之间的接口契约。

传统开发中接口通过类型系统约束(编译器检查);Agent 开发中接口通过自然语言约束(LLM 理解)。

这意味着:description 不是可选的文档,而是决定工具能否被正确调用的核心代码。

实践建议:description 里要说清楚「什么时候用」「什么时候不用」「输入输出是什么格式」,就像写精确的函数文档一样。

七、工程经验总结:可直接用于自己项目的思路

如果你在做 AI Agent 相关的开发,以下几点可以直接借鉴:

1.Tool description 是一等公民:像写 API 文档一样写工具描述,明确「何时调用」「何时不该调用」,这直接决定 Agent 工具调用的准确率。

2. JSON Schema 统一 LLM 参数和运行时校验:一份 Schema 给 LLM 生成调用参数,同时用 Zod 做运行时校验,减少重复定义。

3.Context Object 模式统一传递执行环境:把 abortController、缓存、状态读写打包成一个 context 对象向下传递,比参数透传和全局变量都更清晰。

4.危险工具的权限分层:只读工具默认允许,写操作首次确认,命令执行每次确认(或白名单机制)。这是 Agent 工具安全边界的最小可行实现。

5.上下文窗口管理要主动,不要等到溢出:提前实现 Token 计数 + 对话摘要机制,超出 70-80% 阈值时主动压缩,别等 API 报错了再处理。

6.启动时间优化:把不阻塞 critical path 的 IO(配置读取、认证信息预热)安排在模块加载阶段并行执行,CLI 工具的体验感差距很多时候就在这里。

��值得讨论

Claude Code 的架构反映了当前 AI Agent 工程实践的一个基本判断:Agent 的能力边界由工具系统决定,工具系统的质量由接口设计决定,接口设计的质量由 description 和 schema 决定。你在做 AI 相关项目时,遇到过哪些「设计上的坑」?欢迎评论区交流。

如果这篇分析对你有帮助,欢迎转发给团队里做 AI 工具开发的同事

点「在看」是对内容最直接的支持��

本文基于 Claude Code 公开源代码快照分析 | 2026.03