从源码拆解 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
夜雨聆风