2026 年 3 月 31 日下午,一条推特引爆了整个技术圈——@Fried_rice 曝光了 Claude Code 的完整源码泄露事件。消息一出,开发者社区瞬间沸腾。
说实话,作为一个长期关注 AI Agent 架构的工程师,看到这条消息的第一反应不是吃瓜,而是——终于可以验证我对 Claude Code 的猜想了。之前用它写代码时,一直好奇它的"手感"为什么和其他 AI 编程工具完全不一样:上下文似乎永远不会丢、工具调用快得不像是串行的、权限管控粒度细到单条命令。既然源码有了,就了解下吧~
一、Claude Code 到底是什么?
如果你只把 Claude Code 当成"命令行版的 Copilot",那就严重低估它了。
从源码角度看,Claude Code 是一个完整的 Agent 运行时系统——它有自己的 Agent 主循环、工具注册与调度框架、多层上下文管理策略、权限控制体系、插件与技能扩展机制、子 Agent 编排能力,甚至还有一套基于 Ink(React for Terminal)的完整终端 UI 框架。
它和 ChatGPT、GitHub Copilot 的本质区别在于:
| 维度 | ChatGPT / Copilot | Claude Code |
|---|---|---|
| 交互模式 | 对话 / 补全 | 自主 Agent 循环 |
| 工具能力 | 无 / 有限 | 47+ 内置工具,支持 MCP 扩展 |
| 上下文管理 | 简单截断 | 5 层上下文压缩管道 |
| 代码修改 | 输出代码片段 | 直接编辑文件系统 |
| 权限控制 | 无 | 3 层权限架构 + AST 级命令分析 |
| 子任务 | 无 | 多 Agent 并发编排 |
Claude Code 值得研究的原因很简单:它是目前工程化程度最高的 AI Coding Agent 实现之一,其架构设计中蕴含了大量可复用的工程模式。
二、整体架构拆解
在深入源码之前,先看 Claude Code 的全局架构。我画了一个分层视图:
┌─────────────────────────────────────────────────────────────────┐
│ 用户交互层 (Ink/React Terminal UI) │
│ ┌──────────┐ ┌────────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │PromptInput│ │MessageList │ │ Spinner │ │PermissionDialog │ │
│ └──────────┘ └────────────┘ └──────────┘ └──────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ REPL 编排层 (screens/REPL.tsx) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ useReplBridge · useCanUseTool · useTypeahead │ │
│ └──────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Agent 核心层 │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ query() │ │ QueryEngine │ │ SubAgent 编排 │ │
│ │ Agent 主循环 │ │ 会话编排器 │ │ (AgentTool) │ │
│ └──────┬───────┘ └──────────────┘ └───────────────────┘ │
│ │ │
│ ┌──────▼───────────────────────────────────────────────┐ │
│ │ 工具调度层 │ │
│ │ toolOrchestration → toolExecution │ │
│ │ StreamingToolExecutor (流式并行执行) │ │
│ └──────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ 工具实现层 (47+ Tools) │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ Bash │ │ Edit │ │ Read │ │ Grep │ │ Agent│ │ Skill│ ... │
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
├─────────────────────────────────────────────────────────────────┤
│ 上下文管理层 │
│ ToolResultBudget → SnipCompact → MicroCompact │
│ → ContextCollapse → AutoCompact → ReactiveCompact │
├─────────────────────────────────────────────────────────────────┤
│ 基础设施层 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────────┐ │
│ │State │ │Perms │ │Memory │ │Plugins │ │MCP/Bridge │ │
│ │Store │ │System │ │System │ │/Skills │ │Integration │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────┘
下面逐层拆解。
三、Agent 主循环:一个精密的异步状态机
这是 Claude Code 最核心的代码,位于 src/query.ts。
3.1 整体结构
整个 Agent 循环被实现为一个异步生成器(async generator),核心就是一个 while(true) 循环:
// src/query.ts
export async function* query(params: QueryParams): AsyncGenerator<
StreamEvent | RequestStartEvent | Message | TombstoneMessage | ToolUseSummaryMessage,
Terminal
> {
const consumedCommandUuids: string[] = []
const terminal = yield* queryLoop(params, consumedCommandUuids)
for (const uuid of consumedCommandUuids) {
notifyCommandLifecycle(uuid, 'completed')
}
return terminal
}
query() 是一个薄壳,实际逻辑在 queryLoop() 里。注意返回类型 Terminal——这是一个离散枚举,表示循环退出的原因(completed、aborted、max_turns、prompt_too_long 等)。
3.2 循环状态
每次循环迭代之间传递的可变状态被封装在一个 State 对象中:
// src/query.ts
type State = {
messages: Message[]
toolUseContext: ToolUseContext
autoCompactTracking: AutoCompactTrackingState | undefined
maxOutputTokensRecoveryCount: number
hasAttemptedReactiveCompact: boolean
maxOutputTokensOverride: number | undefined
pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
stopHookActive: boolean | undefined
turnCount: number
transition: Continue | undefined // 上一次迭代为什么继续了
}
我在读这段代码时注意到一个设计细节:transition 字段记录了上一次循环"为什么继续"。注释说这是为了让测试能断言恢复路径(recovery path)是否触发了,而不需要检查消息内容。这是一种很实用的可观测性设计。
3.3 单次迭代的完整流程
每一次 while(true) 迭代就是一个完整的 think-act 周期。我把读源码时整理出来的流程画成了这张图:
┌────────────────────────────────────────────────────┐
│ 单次迭代开始 │
│ │
│ 1. 上下文预处理管道 │
│ applyToolResultBudget (大结果持久化到磁盘) │
│ ↓ │
│ snipCompact (裁剪历史消息) │
│ ↓ │
│ microcompact (消息级微优化) │
│ ↓ │
│ contextCollapse (上下文折叠投影) │
│ ↓ │
│ autoCompact (全对话摘要) │
│ │
│ 2. 调用模型 API(流式) │
│ deps.callModel() → streaming │
│ ↓ │
│ 检测 tool_use blocks → StreamingToolExecutor │
│ (模型还在生成时就开始执行工具!) │
│ │
│ 3. 错误恢复 │
│ prompt-too-long → 上下文折叠 / 响应式压缩 │
│ max-output-tokens → 递增限制重试(最多3次) │
│ 模型故障 → fallback 到备用模型 │
│ │
│ 4. 工具执行 │
│ partitionToolCalls → 并发安全 / 串行批次 │
│ runToolsConcurrently / runToolsSerially │
│ │
│ 5. 续行判断 │
│ 有 tool_use → needsFollowUp=true → continue │
│ stopHook 注入 → retry │
│ token budget 未耗尽 → continue │
│ 以上都不是 → return Terminal │
│ │
└────────────────────────────────────────────────────┘
3.4 为什么用异步生成器?
这个选择非常值得说道。用 async function* 而不是回调或 Promise 链有几个关键好处:
- 背压控制(backpressure):调用方可以按自己的节奏消费事件,REPL 可以渲染 UI,SDK 可以序列化传输
- 生命周期语义清晰:
yield是中间事件,return是终态,throw 是异常——三种语义天然分离 - 可组合:
yield*可以把子生成器的所有事件透传给父生成器 - 取消传播:通过
.return()可以级联关闭所有嵌套生成器
对比之下,很多 Agent 框架用的是 callback 或 EventEmitter,在错误恢复和取消传播上会复杂得多。
3.5 流式工具执行——Claude Code 快的秘密
我在读源码时发现了一个"偷跑"机制:StreamingToolExecutor。
// src/services/tools/StreamingToolExecutor.ts
export class StreamingToolExecutor {
private tools: TrackedTool[] = []
private hasErrored = false
private siblingAbortController: AbortController
addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
// 工具从流中解析出来时就立刻加入执行队列
// 不需要等待完整的模型响应
}
}
当模型的流式响应中出现 tool_use block 时,StreamingToolExecutor 立刻开始执行这个工具,不等模型响应完成。这意味着当模型还在生成第 2、3 个工具调用时,第 1 个工具可能已经执行完了。
更精妙的是它的并发控制:
- 并发安全的工具(如 Read、Grep、Glob)可以并行执行
- 非并发安全的工具(如 Bash、Edit)独占执行
- 当某个 Bash 工具报错时,
siblingAbortController立即杀掉兄弟进程,但不影响父级——查询循环继续运行
这个设计让 Claude Code 在多文件读取等场景下有明显的速度优势。
四、工具系统:47 把瑞士军刀
4.1 Tool 抽象层
所有工具统一由 buildTool() 工厂函数构建,核心类型定义在 src/Tool.ts:
// src/Tool.ts (简化)
export type Tool<Input, Output, Progress> = {
name: string
inputSchema: ZodSchema // Zod schema 验证输入
call(args, context, canUseTool): Promise<ToolResult<Output>>
prompt(options): Promise<string> // 贡献到系统 prompt
checkPermissions(input, context): Promise<PermissionResult>
isConcurrencySafe(input): boolean // 能否并行执行?
isReadOnly(input): boolean // 是否写文件系统?
isDestructive?(input): boolean // 是否不可逆?
maxResultSizeChars: number // 超过多少持久化到磁盘
// ... 还有渲染、进度、UI 等 ~40 个方法
}
关键设计:默认值是 fail-closed 的。
const TOOL_DEFAULTS = {
isConcurrencySafe: (_input?: unknown) => false, // 默认不安全
isReadOnly: (_input?: unknown) => false, // 默认假设会写
isDestructive: (_input?: unknown) => false,
}
也就是说,如果一个新工具忘了声明自己是并发安全的,它会自动被串行执行。这种"安全默认值"的设计在大型系统中非常重要。
4.2 工具注册:条件编译与死代码消除
src/tools.ts 中的 getAllBaseTools() 是所有内置工具的注册中心。我在读这段代码时被它的条件编译机制震撼到了:
// src/tools.ts
import { feature } from 'bun:bundle'
const SleepTool = feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool
: null
const CtxInspectTool = feature('CONTEXT_COLLAPSE')
? require('./tools/CtxInspectTool/CtxInspectTool.js').CtxInspectTool
: null
// Ant 内部专用工具
const REPLTool = process.env.USER_TYPE === 'ant'
? require('./tools/REPLTool/REPLTool.js').REPLTool
: null
feature() 来自 bun:bundle,是一个构建时常量。Bun 打包器会在编译期求值这些表达式,对外部构建,feature('KAIROS') 永远是 false,整个 require() 分支会被彻底消除——连字符串字面量都不会留在产物中。
这意味着 Claude Code 的外部发布版和内部版本共享同一套源码,但通过构建时 feature flag 产出完全不同的制品。这比运行时 feature flag 干净得多。
4.3 工具调度:智能分批
src/services/tools/toolOrchestration.ts 实现了工具调度的核心逻辑:
// src/services/tools/toolOrchestration.ts
function partitionToolCalls(
toolUseMessages: ToolUseBlock[],
toolUseContext: ToolUseContext,
): Batch[] {
return toolUseMessages.reduce((acc: Batch[], toolUse) => {
const tool = findToolByName(toolUseContext.options.tools, toolUse.name)
const isConcurrencySafe = tool?.isConcurrencySafe(parsedInput) ?? false
// 连续的并发安全工具合并为一个批次(并行)
// 非安全工具各自一个批次(串行)
if (isConcurrencySafe && lastBatch?.isConcurrencySafe) {
lastBatch.blocks.push(toolUse)
} else {
acc.push({ isConcurrencySafe, blocks: [toolUse] })
}
return acc
}, [])
}
这里的关键洞察:并发安全性是 per-invocation 的,不是 per-tool-type 的。同一个 BashTool,ls 是并发安全的,rm -rf 不是。这种粒度的控制是通过 isConcurrencySafe(input) 实现的——传入的是具体的调用参数。
4.4 几个值得深究的工具实现
FileEditTool:搜索替换而不是行号编辑
// src/tools/FileEditTool/FileEditTool.ts (简化)
// 输入: file_path, old_string, new_string, replace_all
Claude Code 选择了搜索替换而不是行号编辑来修改文件。为什么?
因为行号编辑有一个致命缺陷:当模型产生的上下文与实际文件有偏差时(比如文件被外部修改了),行号会对不上。搜索替换则更鲁棒——只要目标文本存在,不管它在哪一行。
这个工具内置了多重安全机制:
- Must-Read-First Guard:如果模型没有读过某个文件,就不允许编辑它,防止盲改
- Stale Write Detection:通过
readFileState追踪文件的修改时间戳。如果文件在上次读取后被外部修改了,编辑被拒绝 - Duplicate Match Safety:如果
old_string在文件中匹配多处但replace_all为 false,操作被拒绝并返回匹配数量 - Quote Normalization:
findActualString()处理中英文引号不一致的情况 - File History:每次编辑前创建备份用于 undo
BashTool:最复杂的工具
BashTool 是整个系统中最复杂的工具实现,约 1800 行代码。它的权限系统 (bashPermissions.ts) 特别值得关注:
- 使用 tree-sitter 对 Bash 命令进行 AST 级解析
- 将管道命令拆分为子命令(硬限 50 个,防止 DoS)
- 每个子命令独立匹配权限规则(精确匹配、前缀匹配、通配符匹配)
- 在
auto权限模式下,还有一个 LLM 分类器对命令进行安全评估
还有一个有趣的细节:_simulatedSedEdit 内部字段。当 Bash 命令包含 sed 编辑时,权限对话框会预先计算 sed 的执行结果,让用户在审批时就能看到"这条命令会把文件改成什么样"。
AgentTool:子 Agent 编排
AgentTool 是多 Agent 架构的核心,支持多种执行模式:
// src/tools/AgentTool/AgentTool.tsx (输入 schema 简化)
z.object({
description: z.string(), // 3-5 词任务描述
prompt: z.string(), // 完整任务提示
subagent_type: z.string(), // 专用 Agent 类型
model: z.enum(['sonnet', 'opus', 'haiku']),
run_in_background: z.boolean(),
isolation: z.enum(['worktree']), // git worktree 隔离
})
支持的模式包括:
- 同步子 Agent:行内执行,返回结果
- 异步后台 Agent:立即返回
agentId,后台运行 - Worktree 隔离:创建临时 git worktree,子 Agent 在隔离的仓库副本上工作
- Coordinator 模式:主 Agent 只有 AgentTool / SendMessageTool / TaskStopTool,worker Agent 拥有实际工具——同一个循环代码,通过工具集配置变成完全不同的角色
五、上下文管理:5 层压缩管道
这可能是 Claude Code 源码中最精妙的部分。上下文窗口管理不是简单的"截断旧消息",而是一个 5 层逐级压缩的管道,每一层有不同的触发条件、代价和粒度。
5.1 管道全景
Layer 1: Tool Result Budget (每条消息限额)
↓ 大的工具结果持久化到磁盘,替换为预览
Layer 2: Snip Compact (历史裁剪)
↓ 移除对话中间的旧消息
Layer 3: Microcompact (消息级微优化)
↓ 编辑单条消息内容,不破坏 prompt cache
Layer 4: Context Collapse (上下文折叠)
↓ 读时投影,摘要存在独立的 collapse store
Layer 5: Auto Compact (全对话摘要)
↓ Fork 一个独立 Agent 生成对话摘要
↓ (应急) Reactive Compact
当 API 返回 prompt-too-long 时紧急触发
5.2 每层的实现细节
我在源码中找到了 Auto Compact 的触发阈值计算:
// src/services/compact/autoCompact.ts
export function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model)
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS // 缓冲 13,000 tokens
}
当估计 token 数超过 contextWindow - 13000 - maxOutputTokens 时触发。Auto Compact 使用一个独立的 API 调用(fork agent)来生成摘要,避免阻塞主循环。
更巧妙的是 Reactive Compact——当 API 返回 prompt-too-long 错误时,这个错误会被**扣留(withhold)**不传给调用方,然后紧急触发压缩。如果压缩成功,循环无感知地继续;如果失败,错误才会浮出。
// src/query.ts 中的错误恢复逻辑(简化)
// prompt-too-long 恢复路径:
// 1. 先尝试 context collapse drain
// 2. 再尝试 reactive compact
// 3. 都失败了才让错误浮出
Auto Compact 还有一个熔断器:连续失败 3 次后,停止重试。这防止了在上下文真的无法压缩时(比如系统 prompt 本身就很大)陷入无限重试。
5.3 为什么需要 5 层?
每一层解决不同的问题:
| 层 | 解决的问题 | 代价 |
|---|---|---|
| Tool Result Budget | 单条消息太大 | 磁盘 I/O |
| Snip Compact | 历史太长 | 丢失旧上下文 |
| Microcompact | 缓存命中率优化 | 微小的信息损失 |
| Context Collapse | 渐进式上下文缩减 | 摘要质量 |
| Auto Compact | 整体超限 | 一次 API 调用 + 信息压缩 |
层次化设计意味着轻量操作先执行。如果 Snip 就够了,就不会触发昂贵的 Auto Compact。如果 Context Collapse 把 token 数压到了阈值以下,Auto Compact 直接跳过。
六、权限系统:三层纵深防御
Claude Code 能直接修改文件和执行命令,权限系统就是安全命脉。源码中实现了三层纵深防御。
6.1 Layer 1: 规则匹配
// src/utils/permissions/permissions.ts (简化)
async function hasPermissionsToUseToolInner(tool, input, context) {
// 1a. 整个工具被 deny?
const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
if (denyRule) return { behavior: 'deny' }
// 1b. 整个工具需要 ask?
const askRule = getAskRuleForTool(appState.toolPermissionContext, tool)
if (askRule) return { behavior: 'ask' }
// 1c. 工具自身的权限检查
return await tool.checkPermissions(parsedInput, context)
}
规则来自多个源,按优先级排序:policySettings → userSettings → projectSettings → cliArg → command → session。
规则语法支持模式匹配:
Bash(git *)— 允许所有 git 开头的命令Edit(/src/**)— 允许编辑 /src/ 下的文件mcp__server1— 禁用某个 MCP 服务器的所有工具
6.2 Layer 2: 工具专属检查
每个工具实现自己的 checkPermissions()。比如文件工具会检查:
- 文件路径是否在允许的工作目录内
- 是否是 UNC 路径(防止 Windows NTLM 凭据泄露)
- 是否包含敏感文件(
.env、凭据文件等)
BashTool 的检查更复杂:
- tree-sitter AST 解析命令
- 每个子命令独立匹配规则
- 路径约束检查
- Sed 编辑检测与验证
6.3 Layer 3: 交互式审批
当前两层返回 behavior: 'ask' 时,进入交互式流程:
hasPermissionsToUseToolInner() → 'ask'
↓
useCanUseTool hook
↓
handleCoordinatorPermission() (协调者 Agent)
↓
Bash Classifier (LLM 分类器预判)
↓
handleInteractivePermission() (用户审批 UI)
Bash Classifier 是一个值得注意的设计:在 auto 模式下,一个轻量 LLM 会对命令进行安全评估。如果分类器认为安全,可以跳过用户确认。这是对"安全性"和"流畅性"的折中——既不像 bypassPermissions 那样完全放弃安全检查,也不像 default 那样每次都弹窗。
七、状态管理:34 行代码的极简 Store
Claude Code 没有用 Redux、Zustand 或任何状态管理库,而是用 34 行代码实现了自己的 store:
// src/state/store.ts — 完整源码
type Listener = () => void
type OnChange<T> = (args: { newState: T; oldState: T }) => void
export type Store<T> = {
getState: () => T
setState: (updater: (prev: T) => T) => void
subscribe: (listener: Listener) => () => void
}
export function createStore<T>(
initialState: T,
onChange?: OnChange<T>,
): Store<T> {
let state = initialState
const listeners = new Set<Listener>()
return {
getState: () => state,
setState: (updater: (prev: T) => T) => {
const prev = state
const next = updater(prev)
if (Object.is(next, prev)) return // 引用相等跳过
state = next
onChange?.({ newState: next, oldState: prev })
for (const listener of listeners) listener()
},
subscribe: (listener: Listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
},
}
}
用 React 的 useSyncExternalStore 搭配选择器订阅,只有被选中的 state 切片变化时才触发 re-render。这套方案在 CLI 场景下足够了——不需要中间件、devtools、时间旅行调试等 Web 应用常用的功能。
AppState 本身使用了 DeepImmutable<T> 类型约束,编译期强制不可变。
八、提示词工程:缓存友好的双层结构
8.1 System Prompt 的组装
src/constants/prompts.ts 中的 getSystemPrompt() 返回一个 string[]——字符串数组而非单个字符串。这是为了支持两层缓存:
// src/constants/prompts.ts (简化)
export async function getSystemPrompt(tools, model): Promise<string[]> {
return [
// --- 静态内容(全局可缓存)---
getSimpleIntroSection(), // 身份介绍
getSimpleSystemSection(), // 核心系统规则
getSimpleDoingTasksSection(), // 编码行为指导
getActionsSection(), // 可逆性/影响范围指导
getUsingYourToolsSection(tools), // 工具使用偏好
getSimpleToneAndStyleSection(), // 沟通风格
// === 动态内容边界 ===
...(shouldUseGlobalCacheScope()
? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY]
: []),
// --- 动态内容(每会话)---
...resolvedDynamicSections, // Memory、环境信息、MCP 指令等
].filter(s => s !== null)
}
SYSTEM_PROMPT_DYNAMIC_BOUNDARY 之前的内容可以跨用户/跨会话复用缓存(Anthropic API 的 prompt caching 功能),之后的内容每个会话独立。这直接影响 API 成本——缓存命中的 token 价格远低于全量输入。
8.2 动态 Prompt Section 的记忆化
每个 prompt section 都通过 systemPromptSection() 包装,实现会话级记忆化——相同参数只计算一次:
// src/constants/systemPromptSections.ts (概念)
export function systemPromptSection(computeFn) {
// 一个 session 内只计算一次
// 后续调用直接返回缓存结果
}
还有 DANGEROUS_uncachedSystemPromptSection()——每个 turn 重新计算。用于 MCP 服务器指令这种可能在会话中途变化的内容(MCP 服务器可以动态连接/断开)。
8.3 Latch 机制
Bootstrap state 中有多个"锁存器"(latch)变量:
afkModeHeaderLatched
fastModeHeaderLatched
cacheEditingHeaderLatched
thinkingClearLatched
这些变量一旦设为 true 就不会回退为 false。目的:如果 fast mode 在会话中途开启然后关闭,发送给 API 的 header 保持"开启过"状态,避免因为 header 变化导致 prompt cache 失效。
这种对缓存命中率的极致优化在整个代码库中随处可见。
九、技能与插件系统
9.1 Skill 系统
Skills 是 Markdown 文件驱动的能力扩展:
// src/skills/bundledSkills.ts (简化)
export type BundledSkillDefinition = {
name: string
description: string
whenToUse?: string // 模型何时应该调用这个 Skill
allowedTools?: string[] // 限制可用工具
model?: string // 覆盖模型
context?: 'inline' | 'fork' // 内联执行 or fork 子 Agent
hooks?: HooksSettings // 每 Skill 的 hook 配置
getPromptForCommand: (args, context) => Promise<ContentBlockParam[]>
}
Skill 的加载源有明确优先级:
bundled— 编译时内置plugin— 插件提供skills—.claude/skills/目录的 Markdown 文件managed— 组织策略管理
一个特别有趣的特性是条件激活:activateConditionalSkillsForPaths() 会在 FileEditTool、FileReadTool、FileWriteTool 每次操作文件时被调用。当用户操作某些目录或文件模式时,对应的 Skill 会自动激活。
9.2 插件系统
插件分三个层级:
- Built-in plugins:随 CLI 发布,用户可启用/禁用
- Bundled skills:硬编码的复杂功能
- Marketplace plugins:从 Git 仓库加载,支持 SHA 版本锁定
一个插件可以提供:Commands、Agents、Skills、Hooks、MCP Servers、LSP Servers、Output Styles、Settings——几乎可以扩展系统的所有方面。
错误处理方面,源码定义了 25+ 种 PluginError 类型的联合类型,每种都携带特定的上下文信息。
9.3 Memory 系统
src/memdir/ 实现了文件级的持久化记忆:
- 存储路径:
~/.claude/projects/<项目哈希>/memory/ - 入口文件:
MEMORY.md(始终加载到上下文,限 200 行 25KB) - 独立话题文件:按需通过 AI 侧查询召回(用 Sonnet 模型选择最多 5 个相关文件)
- 四种记忆类型:
user(用户信息)、feedback(纠正/确认)、project(项目上下文)、reference(外部系统指针)
安全方面,validateMemoryPath() 拒绝相对路径、根路径、UNC 路径、空字节、长度不足 3 的路径;并且明确排除 projectSettings 作为 autoMemoryDirectory 的来源——防止恶意仓库将内存写入重定向到 ~/.ssh。
十、任务系统与多 Agent 协作
10.1 Task 系统
src/tasks/ 不是简单的 Todo List,而是一个并发任务执行管理器,支持 7 种任务类型:
type TaskType =
| 'local_bash' // 后台 shell
| 'local_agent' // 本地子 Agent
| 'remote_agent' // 远程 Agent
| 'in_process_teammate' // 进程内协作者
| 'local_workflow' // 工作流
| 'monitor_mcp' // MCP 监控
| 'dream' // 自动记忆整理
Task ID 使用 randomBytes(8) 生成(36^8 ≈ 2.8 万亿种组合),注释中明确提到这是为了防止符号链接攻击。
10.2 DreamTask:AI 的"睡眠整理"
DreamTask 是一个自动记忆整理子 Agent——当触发条件满足时,它会回顾最近的对话,将有价值的信息提炼到持久化记忆文件中。如果被中途 kill,它会回滚整理锁的时间戳,让下次会话可以重试。这个设计灵感明显来自人类的睡眠记忆整理机制。
10.3 后台主会话
当用户按两次 Ctrl+B 时,LocalMainSessionTask 把当前对话推到后台。后台运行的查询有独立的 transcript 文件,发送进度更新(工具数、token 数),完成时发出 XML 通知。这让用户可以在等待长任务时继续交互。
10.4 Stall Watchdog
LocalShellTask 内置了一个卡死检测器:
- 每 5 秒检查后台命令是否有新输出
- 45 秒无输出后,检查最后 1024 字节是否包含交互提示(
(y/n)、Press any key、Continue?) - 如果检测到,通知用户建议使用非交互标志
这种主动监控机制在其他 Agent 工具中很少见到。
十一、Claude Code 强大的关键原因
从源码分析角度,我总结 Claude Code 强于其他 AI Coding Agent 的根本原因:
11.1 端到端的工程化闭环
不是简单地把 LLM 包一层 API——从输入解析、提示词组装、上下文管理、工具调度、权限控制、错误恢复到输出渲染,每个环节都经过精心设计。
11.2 上下文管理的工程深度
5 层压缩管道 + 缓存优化 + reactive 恢复 + 熔断器。这套系统解决的核心问题是:让模型在有限的上下文窗口中始终看到最相关的信息。
大多数 Agent 框架的上下文管理是"满了就截断",Claude Code 的策略是"分层递进压缩,轻量操作优先,重量操作兜底"。
11.3 安全模型的深度
三层权限系统 + AST 级命令分析 + LLM 分类器 + UNC 路径防护 + stale write 检测 + must-read-first guard。这些不是"加个确认弹窗"能比的——每一层都在解决不同的攻击面。
11.4 Streaming 执行的性能优势
模型生成和工具执行重叠进行,加上智能并发分批,让 Claude Code 在多文件操作场景下的延迟远低于"等生成完再执行"的串行方案。
11.5 设计上的 trade-off 意识
- 用异步生成器而非 callback——牺牲一点学习成本,换来背压控制和生命周期清晰
- 用 fail-closed 默认值——牺牲一点灵活性,换来安全底线
- 用 5 层上下文管道而非简单截断——增加了复杂度,但换来了上下文利用率的质的飞跃
- 用构建时 feature flag——维护一套源码而非两个仓库,但需要 Bun 打包器支持
十二、可复用的设计模式
从 Claude Code 源码中提炼出的可迁移经验:
12.1 Agent Loop 设计模式
async function* agentLoop(params):
while (true):
preprocess context // 上下文预处理管道
response = callModel() // 调用模型
if error: recover or break // 错误恢复
if tool_use:
results = executeTool() // 工具执行
yield results // 向外暴露事件
continue // 继续循环
else:
return terminal // 循环结束
关键点:
- 用生成器做主循环,
yield暴露中间事件 - 状态封装在
State对象中,continue 站点统一赋值 - 错误恢复在循环内部处理,不向外抛出
12.2 Tool 抽象模式
Tool = {
inputSchema, // 输入验证(Zod)
call(), // 执行逻辑
checkPermissions(), // 权限检查
isConcurrencySafe(), // 并发安全性声明
isReadOnly(), // 只读声明
prompt(), // 对系统 prompt 的贡献
}
关键点:
- 每个工具自描述(包括 prompt 贡献和安全属性)
- 默认值 fail-closed
- 并发安全性是 per-invocation 的
12.3 分层上下文管理模式
轻量级(零/低 API 成本)→ 中量级(局部优化)→ 重量级(全量压缩)→ 应急(reactive)
关键点:
- 每层独立判断是否需要介入
- 前面的层如果解决了问题,后面的层直接跳过
- 最后一层是应急手段,有熔断器
12.4 权限 Layered Defense
Layer 1: 静态规则匹配(快,零成本)
Layer 2: 工具专属检查(中等,可能有 I/O)
Layer 3: LLM 分类器 + 用户交互(慢,但最灵活)
关键点:
- 大部分请求在 Layer 1 就已决定(命中 allow/deny 规则)
- 只有不确定的情况才向上层升级
- 每一层都可以独立运作
12.5 State Store 模式
34 行的极简 store + DeepImmutable 类型约束 + useSyncExternalStore 选择器。证明了在很多场景下,你不需要 Redux。
12.6 Skill 扩展模式
用 Markdown + YAML frontmatter 定义能力扩展。降低扩展门槛——写一个 .md 文件就能给 Agent 添加新能力,不需要写代码。条件激活机制让 Skill 能根据当前操作的文件类型自动加载。
十三、如果我要自己实现一个类似系统
技术选型建议
| 组件 | 推荐 | 原因 |
|---|---|---|
| 运行时 | Bun | 启动快,原生 TS,feature() 构建时消除 |
| Agent Loop | async generator | 背压控制,生命周期清晰 |
| 输入验证 | Zod | 运行时验证 + 类型推导 |
| CLI UI | Ink (React for terminal) | 组件化,React 生态复用 |
| 命令解析 | Commander.js | 成熟稳定 |
| 状态管理 | 自建微型 Store | 30 行就够,别引入 Redux |
| 文件搜索 | ripgrep (rg) 子进程 | 速度碾压原生 Node glob |
| 命令分析 | tree-sitter | AST 级精度 |
| LLM API | Anthropic SDK + streaming | 原生流式支持 |
实现路径建议
- 先实现最小 Agent Loop:
while(true) → callModel → executeTools → yield,不需要一开始就搞 5 层上下文管理 - Tool 系统用 interface + factory:一开始 3-5 个核心工具(Read、Edit、Bash、Glob、Grep)就够了
- 权限系统从 Layer 1 开始:静态规则匹配覆盖 80% 场景,分类器和交互式审批后加
- 上下文管理逐层加:先做简单截断,然后加 Auto Compact,再加微优化层
- 子 Agent 能力放最后:单 Agent 先跑通,多 Agent 编排是进阶能力
十四、结语
读完 Claude Code 的源码,我最大的感受不是"它有多少 fancy 的技术",而是每一个设计决策背后都有明确的工程权衡。
异步生成器不是为了炫技,是因为它真的解决了 Agent 循环中事件流控制的核心问题。5 层上下文管道不是过度设计,是因为 LLM 的上下文窗口就是 Agent 系统最关键的资源瓶颈。34 行的 Store 不是偷懒,是因为 CLI 工具确实不需要 Redux 那套东西。
Claude Code 证明了一件事:AI Agent 的竞争力不仅在模型能力上,更在工程化的深度上。同样的基座模型,配上精心设计的工具系统、上下文管理、权限控制和错误恢复,体验可以天差地别。
这也意味着,AI Agent 领域的竞争正在从"谁的模型更强"转向"谁的 Agent 工程做得更好"。Claude Code 的源码为这个方向提供了一个高水平的参考实现——无论你是在构建自己的 Coding Agent,还是在设计其他领域的 Agent 系统,都能从中找到可借鉴的工程模式。
源码面前,了无秘密。但理解设计意图,需要一点工程直觉。希望这篇文章能帮你建立这种直觉。