Claude Code 是 Anthropic 推出的官方 CLI AI 编程助手。它不是一个简单的“终端聊天机器人”,而是一个能直接读写文件、执行命令、搜索代码、甚至派出多个 AI Agent 协同工作的全功能开发工具。
本文基于对 Claude Code 的 TypeScript 源码的完整逆向分析——涵盖 1,905 个源文件、47 个工具模块、89 个命令、94 个 Feature Flag——尝试还原 Anthropic 是如何一步步将一个 LLM 对话框架,迭代成一个成熟的 Agentic AI 工程系统的。这并非使用教程,而是一份聚焦于架构拆解与工程哲学的分析报告,旨在为对人工智能系统设计感兴趣的开发者提供洞见。
一、技术全景
先用一张表看清全貌:
| 维度 |
选型 |
| 语言 |
TypeScript 5.7 (strict mode) |
| 运行时 |
Bun(启动快、内置 bundler、支持编译时 DCE) |
| 终端 UI |
React 19 + Ink(自定义 reconciler 驱动终端渲染) |
| 校验 |
Zod(Schema-first,运行时校验 + 类型生成) |
| CLI 框架 |
Commander.js |
| AI API |
Anthropic SDK v0.50 + Bedrock SDK + Vertex SDK |
| 扩展协议 |
MCP (Model Context Protocol) SDK v1.11 |
| Feature Flag |
编译时 Bun DCE + 运行时 GrowthBook |
| 可观测性 |
OpenTelemetry (traces / metrics / logs) |
两个值得注意的选型决策:
为什么用 React 写终端 UI? Ink 库让 React 组件可以渲染到终端而非浏览器。Claude Code 的 UI 复杂度已经超出了传统 CLI 框架的能力——权限对话框、多 Agent 状态面板、流式 Markdown 渲染——React 的组件模型和状态管理在这里是合理选择。
为什么用 Bun 而不是 Node.js? Bun 的启动速度更快,且内置 bundler 支持 feature() 函数的编译时求值,使得 Dead Code Elimination 可以在打包阶段完成,而非运行时。这对一个有 94 个 Feature Flag 的项目至关重要。
二、架构五层模型
我们可以将其核心架构抽象为一个五层模型:
┌─────────────────────────────────────────────────────────┐
│ CLI Layer │ Commander.js → 89 slash commands │
├─────────────────┼───────────────────────────────────────┤
│ UI Layer │ React/Ink 组件树 → 自定义 Reconciler │
├─────────────────┼───────────────────────────────────────┤
│ Query Engine │ LLM 流式交互 → Tool 调度 → 压缩 │
├─────────────────┼───────────────────────────────────────┤
│ Tool/Skill │ 47 Tools + Skill System + MCP │
├─────────────────┼───────────────────────────────────────┤
│ Service Layer │ API / OAuth / MCP / Analytics / LSP │
└─────────────────┴───────────────────────────────────────┘
核心模块的代码量分布揭示了项目的重心:
- main.tsx(4,683 行)—— 启动编排,是整个系统的引导器。
- QueryEngine.ts(1,295 行)—— LLM 交互循环,是运行时核心。
- Tool.ts(792 行)—— 工具类型系统,是扩展性基石。
接下来,我们将按照推导出的 9 个关键迭代阶段逐一拆解其设计思路。
传统 LLM 只能生成文本。Claude Code 使用 Tool Use 模式:AI 可以发出“调用工具”的结构化指令,系统执行后将结果喂回 AI,形成闭环。
用户: "帮我修复 login.ts 的 bug"
→ AI 思考 → 调用 FileReadTool 读取文件
→ 读取结果喂回 AI → AI 分析 bug
→ 调用 FileEditTool 修改代码
→ 修改结果喂回 AI → AI 确认修复完成
→ 回复用户
这个循环就是所谓的 Query Loop,也是所有 Agentic AI 系统的基础架构。
AsyncGenerator:流式循环的实现
QueryEngine 的 submitMessage() 使用 TypeScript 的 async * 语法实现流式输出:
async *submitMessage(): AsyncGenerator<SDKMessage> {
for await (const message of query({
messages, systemPrompt, canUseTool, maxTurns
})) {
switch (message.type) {
case 'assistant': // AI 的回复(可能包含 tool_use block)
yield normalizedMessage
break
case 'user': // 工具结果(包装为 user 消息喂回 AI)
this.mutableMessages.push(message)
yield message
break
case 'progress': // 工具执行进度
yield progressMessage
break
}
}
yield { type: 'result', usage: this.totalUsage, num_turns }
}
为什么用 AsyncGenerator 而非普通 async 函数?普通 async 函数要等全部完成才能返回,用户面对的是长时间空白后突然出现的大段结果。AsyncGenerator 每产生一条消息就 yield 出去,UI 立即渲染——这就是 Claude Code 实现“打字机效果”的底层机制。
工具的泛型类型系统
每个工具都是 Tool 三参数泛型:
Tool<Input, Output, P extends ToolProgressData>
- Input:用 Zod Schema 定义,运行时严格校验。
- Output:执行结果类型,编译时检查。
- Progress:进度数据类型,支持流式进度条。
配合 buildTool() 函数的默认值注入(Builder Pattern 变体),工具开发者只需要定义核心逻辑,其他安全属性使用默认值:
const TOOL_DEFAULTS = {
isConcurrencySafe: () => false, // 默认不可并行(保守)
isReadOnly: () => false,
isDestructive: () => false,
checkPermissions: () => Promise.resolve({ behavior: 'allow' }),
}
function buildTool(def) { return { ...TOOL_DEFAULTS, ...def } }
工具排序——一个容易忽略的性能细节
工具注册表 assembleToolPool() 在合并内置工具和 MCP 工具时,会按名称排序:
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name',
)
原因在于 Claude API 支持 Prompt Caching。如果两次请求的 system prompt 和 tools 定义完全一致,API 会复用缓存以节省计算。工具顺序不稳定会导致缓存失效——排序是为了保证幂等性。
四、Phase 2:权限系统 —— 信任是分层的
四级权限模式
| 模式 |
行为 |
| default |
每次调用都要确认 |
| plan |
只读自动通过,写操作需确认 |
| auto |
安全操作自动通过,破坏性操作需确认 |
| bypass |
全部自动通过(管理员模式) |
三路由架构
权限检查不是简单的 if/else,而是根据运行模式路由到不同处理器:
const result = await hasPermissionsToUseTool(tool, input, context)
switch (result.behavior) {
case 'allow': return grant()
case 'deny': return reject(result.reason)
case 'ask':
if (isCoordinatorMode()) return handleCoordinatorPermission()
if (isSwarmWorker()) return handleSwarmWorkerPermission()
return handleInteractivePermission()
}
Swarm Worker(多 Agent 场景中的工作者 Agent)的权限处理尤其值得关注:
async function handleSwarmWorkerPermission() {
// 1. 先用分类器尝试自动批准
const autoApproval = await bashClassifier(input)
if (autoApproval) return grant()
// 2. 先注册回调,再发送请求(防竞态!)
const promise = registerPermissionCallback(requestId)
// 3. 通过文件 mailbox 发请求给 leader
await sendToMailbox(leaderName, { type: 'permission_request', tool, input })
// 4. 等待 leader 审批
const response = await promise
return response.approved ? grant() : reject()
}
注意第 2 步在第 3 步之前——先注册监听,再发消息。如果顺序反过来,leader 的回复可能在注册之前到达,导致消息丢失。这是并发编程中经典的竞态条件防护。
25+ Hook 事件
系统定义了 25 个以上的 Hook 事件(PreToolUse, PostToolUse, PermissionRequest, SessionStart 等),用户和插件可以拦截几乎所有操作。例如 PreToolUse Hook 可以返回 { behavior: 'deny' } 来阻止某个工具执行,这为自定义安全策略提供了入口。
五、Phase 3:五代上下文压缩 —— 最大的工程挑战
上下文管理是整个项目中工程量最大、迭代次数最多的部分。核心矛盾很简单:
压缩太激进 → AI 忘记关键信息 → 做出错误决策
压缩太保守 → Token 超限 → API 报错
五代压缩技术就是在这两个极端之间不断逼近最优解的过程。
第一代:Full Compaction
调用一个“压缩 Agent”把整个对话历史总结为 9 段式摘要(目标/概念/文件/错误/推理/用户消息/待办/当前/下一步),然后用摘要替换原始对话。关键设计在于:压缩 Agent 复用主对话的 Prompt Cache,不额外消耗完整的 system prompt 传输。
第二代:Auto-Compact
当 Token 使用量超过 85% 时自动触发压缩。加入了 Circuit Breaker(熔断器):连续 3 次失败后停止尝试,避免无限重试导致的资源浪费。
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
return // 停止尝试,等待人工 /compact
}
第三代:Cached Microcompact(最精妙的设计)
不修改本地消息列表,而是生成 cache_edits 指令告诉 API “在缓存层删除某些工具结果”:
return {
type: 'cache_edits',
edits: toDelete.map(id => ({ action: 'delete', tool_use_id: id }))
}
为什么巧妙?传统压缩修改消息列表,不可逆。Cached Microcompact 只在 API 缓存层操作,本地数据完好——如果 API 调用失败需要重试,完整上下文仍然可用。这是关注点分离的典范。只有 8 种“可压缩”工具的输出会被清理(如 FileRead、Shell 等),因为它们的输出要么可以重新获取,要么已经生效。
第四代:Time-Based Microcompact
利用 Prompt Cache 的 TTL(约 1 小时)过期时机:既然缓存已经过期、下次请求必须重发完整前缀,那就趁这个机会顺便清理旧数据。这是一种机会主义优化——零额外成本。
第五代:Context Collapse
与 Auto-Compact 互斥运行。不是简单的“压缩全部”或“删除旧的”,而是基于语义理解哪些上下文当前有用,进行选择性折叠,体现出更深度的深度思考。
六、Phase 4:模型无感迁移 —— 别名 + 幂等迁移
Claude 的模型一直在更新(Opus 4.0 → 4.1 → 4.5 → 4.6),如何让用户无感升级?
别名系统:用户配置中写 'opus' 而非 'claude-opus-4-6-...',别名在运行时解析为最新版本。
幂等迁移函数:系统有 11 个迁移函数,启动时自动运行。每个都遵循相同模式:
function migrateLegacyOpusToCurrent() {
if (config.hasMigrated) return // 幂等守卫
const settings = getUserSettings() // 只读 user 级(不读 merged)
if (legacyModels.includes(settings.model)) {
updateSettingsForSource('user', { model: 'opus' })
}
markCompleted() // 即使无需修改也标记
logEvent('migration_completed')
}
一个微妙但关键的细节:迁移只读 userSettings 而不读合并后的 mergedSettings。如果读 merged 写 user,project 级别的设置会被意外提升到 user 级别,影响所有项目。配置系统的层级语义必须在迁移中得到尊重。
七、Phase 5:Plan Mode —— 从“直接做”到“先想再做”
Plan Mode 引入了 EnterPlanModeTool / ExitPlanModeTool / VerifyPlanExecutionTool 三个工具,实现了“规划 → 确认 → 执行 → 验证”的四步工作流。
在 Plan 权限模式下,AI 可以自由搜索和阅读代码(只读操作自动通过),但修改文件和执行命令需要等用户批准计划后才能进行。
后续还引入了 ULTRAPLAN(扩展规划)和 ULTRATHINK(深度思考),以及 Thinking Configuration 的三种模式(Adaptive / Enabled / Disabled)——让 AI 在规划阶段投入更多“思考预算”。
八、Phase 6:多 Agent 协作 —— AsyncLocalStorage 隔离
演进路径
单 Agent → SubAgent → Team 系统 → Coordinator Mode
核心挑战:同进程隔离
多个 Agent 跑在同一个 Node.js 进程内,如何隔离上下文?答案是 AsyncLocalStorage:
const teammateContext = new AsyncLocalStorage<TeammateIdentity>()
function runWithTeammateContext(identity, fn) {
return teammateContext.run(identity, fn)
}
AsyncLocalStorage 类似 Java 的 ThreadLocal,但适用于 Node.js 的异步模型。每个异步调用链拥有独立的“局部变量空间”——Agent A 调用 readFile() 时看到的身份是 “researcher”,Agent B 同时调用 readFile() 看到的是 “coder”,互不干扰。
双路径通信
Agent 之间的通信有两条路径:
- 内存回调(in-process agent,优先路径)——直接把权限请求放入 leader 的 React UI 确认队列。
- 文件 mailbox(跨进程 agent,退路)——写入
~/.claude/teams/{team}/mailbox/ 目录,每 500ms 轮询。
文件系统是唯一可靠的跨进程通信通道(Agent 可能跑在不同的 tmux pane 中)。
共享任务列表
团队成员通过 ~/.claude/tasks/{team-name}/ 目录共享任务。工作流:完成任务 → 标记完成 → 检查列表 → 认领下一个未阻塞的任务。
九、Phase 7:从封闭到开放 —— MCP / Skills / Plugins
MCP:AI 工具接入的 USB 标准
MCP (Model Context Protocol) 的设计灵感来自 LSP (Language Server Protocol):
LSP:任何编辑器 ←→ 任何语言服务器
MCP:任何 AI Agent ←→ 任何工具服务器
在 Claude Code 中,MCP 工具和内置工具对 AI 完全等价——它们出现在同一个工具列表中,AI 自主决定调用哪个。第三方能力成为一等公民。
Skill 系统
Skill 是 Markdown 定义的可复用工作流模板。用户可以在 ~/.claude/skills/ 下创建自定义 Skill(比如 deploy-staging.md),之后用一句话触发整个流程。压缩时 Skill 有专门的 Token 预算(每个 5K,总计 25K),确保压缩后 AI 仍然记得可用的 Skill。
十、Phase 8:全场景覆盖 —— 不止是终端
Claude Code 从纯 CLI 扩展到了多个运行环境:
| 模式 |
Feature Flag |
描述 |
| 终端 CLI |
— |
原始形态 |
| IDE 插件 |
BRIDGE_MODE |
VS Code / JetBrains 双向通信(JWT 认证) |
| 远程容器 |
CCR_AUTO_CONNECT |
代码在远程容器中执行 |
| 服务器模式 |
DIRECT_CONNECT |
作为服务端运行 |
| 后台常驻 |
DAEMON |
消除冷启动,毫秒级响应 |
| SSH 远程 |
SSH_REMOTE |
远程 SSH 执行 |
| 自带计算 |
BYOC_ENVIRONMENT_RUNNER |
用户自带计算环境 |
Bridge 架构使用 JWT 认证的双向消息协议,IDE 端可以发送权限请求、文件附件,CLI 端返回执行结果。
十一、Phase 9:KAIROS —— 从工具到助手
KAIROS 是目前最新的方向,标志着 Claude Code 从“被动的工具”向“主动的助手”转型:
| 特性 |
Feature Flag |
描述 |
| 持久化会话 |
KAIROS |
跨会话记忆和上下文 |
| 主动推送 |
KAIROS_PUSH_NOTIFICATION |
AI 主动通知(如“你的 PR 构建失败了”) |
| GitHub Webhook |
KAIROS_GITHUB_WEBHOOKS |
监听代码库变化 |
| Channel 系统 |
KAIROS_CHANNELS |
类 Slack 频道通信 |
| 每日摘要 |
KAIROS_BRIEF |
总结当天发生的事 |
| Dream 模式 |
KAIROS_DREAM |
空闲时自主探索项目 |
这不再是“你问一句我答一句”,而是一个有记忆、会主动思考、能监听外部事件的 AI 同事。
十二、启动优化 —— 毫秒级的工程较量
一个 CLI 工具的启动速度直接影响用户体验。Claude Code 在这方面做了极致优化。
Fast Path(快速退路)
claude --version 不加载 React、Ink、SDK——直接打印编译时内联的版本常量后退出,整个过程 < 10ms。
Parallel Prefetch(并行预取)
在模块 import 之前就启动 I/O 操作:
// main.tsx 顶部(import 之前的副作用)
startMdmRawRead() // 启动 MDM 子进程
startKeychainPrefetch() // 启动 Keychain 读取
// 然后才 import React, Ink 等模块(~135ms)
// MDM 和 Keychain 在这 135ms 内已经完成
串行需要 240ms,并行只需要 135ms——I/O 被模块加载时间“吸收”了。
TLS 证书时序陷阱
一个容易被忽略的细节:Bun 使用 BoringSSL,它在第一次 TLS 握手时缓存证书库。如果自签名 CA 证书在首次连接之后才设置,BoringSSL 不会重新加载——企业内网连接直接失败。所以 applyExtraCACertsFromConfig() 必须在任何网络请求之前执行。
十三、Feature Flag —— 编译时 DCE 的工程哲学
Claude Code 有 94 个 Feature Flag,使用双层架构:
编译时(Bun DCE):feature() 在外部构建中被 stub 为 return false,Bun 的 bundler 直接消除不可达分支。代码不存在于产物中——零运行时开销、减小包体积、消除攻击面。
// 源码
const VoiceUI = feature('VOICE_MODE')
? require('./voice.js')
: null
// 外部构建产物
const VoiceUI = null
// voice.js 及其所有依赖完全不打包
运行时(GrowthBook):支持 A/B 测试、按比例灰度发布、实时开关。不需要重新构建和发布。
这种双层设计解决了一个根本矛盾:编译时 Flag 效率最高但不灵活,运行时 Flag 灵活但有开销。两者结合,各取所长。
十四、状态管理 —— React 在终端中的实践
Claude Code 使用自定义的 Zustand-like Store:
// Selector-based 精确订阅
const model = useAppState(s => s.mainLoopModel)
// 只有 model 变化时重渲染,其他 50 个字段变化不影响
内部使用 React 18 的 useSyncExternalStore,开发环境还有安全检查——如果 selector 返回整个 state 而非 slice,直接抛错。
大部分状态用 DeepImmutable 约束(递归 readonly),但 tasks 字段例外——因为它包含函数引用,不能被 readonly 约束。
十五、从源码中提炼的设计模式
最后,总结 12 个值得学习的工程模式,它们不仅适用于 AI 系统,也具有普适性:
| 模式 |
Claude Code 中的应用 |
通用场景 |
| AsyncGenerator 流式输出 |
QueryEngine.submitMessage() |
SSE 推送、流式 API |
| Circuit Breaker 熔断器 |
Auto-Compact 3 次失败后停止 |
API 调用、外部服务 |
| Builder Pattern |
buildTool() 默认值注入 |
复杂对象创建 |
| Memoize 单次执行 |
init() 只跑一次 |
昂贵的初始化 |
| Parallel Prefetch |
启动时并行读 MDM + Keychain |
CLI 启动、服务器 warmup |
| Selector Subscription |
useAppState(s => s.field) |
React 状态性能优化 |
| AsyncLocalStorage |
多 Agent 进程内隔离 |
多租户、请求级跟踪 |
| 幂等迁移 |
完成标记 + 层级语义 |
数据库迁移、配置升级 |
| 编译时 DCE |
feature() + Bun bundler |
多环境构建 |
| 机会主义优化 |
Cache TTL 过期时顺带清理 |
缓存刷新时附带操作 |
| 竞态条件防护 |
先注册回调再发消息 |
异步通信 |
| 配置层级语义 |
迁移只读 user 不读 merged |
多层配置系统 |
结语
Claude Code 的迭代思路可以用一句话概括:
先让 AI 能动手,再让 AI 安全地动手,再让 AI 聪明地动手,再让一群 AI 一起动手,最后让 AI 成为你的长期搭档。
从 Tool Loop 到 KAIROS,Anthropic 走的每一步都遵循渐进增强的原则——用 Feature Flag 门控实验、用幂等迁移保证兼容、用编译时 DCE 控制产物体积。没有“大爆炸式重写”,只有持续的、有意识的工程演进。这种基于源码逆向分析提炼出的务实设计哲学,或许是构建复杂 AI 系统最值得借鉴的地方,也欢迎在 云栈社区 交流更多技术见解。
本文基于 Claude Code TypeScript 源码(1,905 个文件)的完整逆向分析。所有代码片段均来自实际源码,经简化后用于说明架构设计。