💥 Claude Code 原始碼外洩?我把這個最強 AI Coding Agent 的架構扒乾淨了

2026 年 3 月 31 日下午,一條推特引爆了整個技術圈——@Fried_rice 曝光了 Claude Code 的完整原始碼外洩事件。消息一出,開發者社群瞬間沸騰。

image.png

說實話,作為一個長期關注 AI Agent 架構的工程師,看到這條消息的第一反應不是吃瓜,而是——終於可以驗證我對 Claude Code 的猜想了。之前用它寫程式時,一直好奇它的「手感」為什麼和其他 AI 編程工具完全不一樣:上下文似乎永遠不會丟、工具呼叫快得不像是串行的、權限管控粒度細到單條命令。既然原始碼有了,就來了解一下吧~

一、Claude Code 到底是什麼?

如果你只把 Claude Code 當成「命令列版的 Copilot」,那就嚴重低估它了。

從原始碼角度看,Claude Code 是一個完整的 Agent 執行時系統——它有自己的 Agent 主迴圈、工具註冊與調度框架、多層上下文管理策略、權限控制體系、外掛與技能擴充機制、子 Agent 編排能力,甚至還有一套基於 Ink(Terminal 用的 React)的完整終端 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 鏈有幾個關鍵好處:

  1. 背壓控制(backpressure):呼叫方可以按自己的節奏消費事件,REPL 可以順序渲染 UI,SDK 可以序列化傳輸。
  2. 生命週期語義清楚:yield 是中間事件,return 是終態,throw 是例外——三種語義天然分離。
  3. 可組合:yield* 可以把子產生器的所有事件透傳給父產生器。
  4. 取消傳播:透過 .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>                // 貢獻到 system 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

// 內部專用工具
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 選擇了搜尋替換而不是行號編輯來修改檔案。為什麼?

因為行號編輯有一個致命缺陷:當模型產生的上下文與實際檔案不同步時(例如檔案被外部修改了),行號會不對。搜尋替換則更穩健——只要目標文字存在,不論它在哪一行。

此工具內建多重安全機制:

  1. Must-Read-First Guard:如果模型沒有先讀過某個檔案,就不允許編輯它,防止盲改。
  2. Stale Write Detection:透過 readFileState 追蹤檔案的修改時間戳。如果檔案在上次讀取後被外部修改,編輯會被拒絕。
  3. Duplicate Match Safety:如果 old_string 在檔案中匹配多處但 replace_all 為 false,操作會被拒絕並回傳匹配數量。
  4. Quote Normalization:findActualString() 處理中英文引號不一致問題。
  5. 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 次後,停止重試。這防止在上下文真的無法壓縮時(比如 system prompt 本身就很大)陷入無限重試。

5.3 為什麼需要 5 層?

每一層解決不同的問題:

  • 層:Tool Result Budget → 解決單條訊息太大,代價是磁碟 I/O
  • 層:Snip Compact → 歷史太長,代價低
  • 層:Microcompact → 提升 prompt cache 命中率,代價極低
  • 層: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 = (args: { newState: T; oldState: T }) => void

export type Store = {
  getState: () => T
  setState: (updater: (prev: T) => T) => void
  subscribe: (listener: Listener) => () => void
}

export function createStore<T>(
  initialState: T,
  onChange?: OnChange,
): 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 切片變動時才觸發重新渲染。這套方案在 CLI 場景下足夠了——不需要 middleware、devtools、時間旅行除錯等 Web 應用常用功能。

AppState 本身使用了 DeepImmutable 類型約束,在編譯期強制不可變。

八、提示詞工程:快取友好的雙層結構

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 中有多個「鎖存器」變數:

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 的載入來源有明確優先順序:

  1. bundled — 編譯時內建
  2. plugin — 外掛提供
  3. skills — ~/.claude/skills/ 目錄的 Markdown 檔案
  4. 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/<project>/memory/
  • 入口檔案:MEMORY.md(始終載入到上下文,限制 200 行 25KB)
  • 獨立話題檔案:按需透過 AI 端查詢召回(用 Sonnet 模型選最多 5 個相關檔案)
  • 四種記憶類型:user(使用者資訊)、feedback(糾正/確認)、project(專案上下文)、reference(外部系統指標)

安全方面,validateMemoryPath() 拒絕相對路徑、根路徑、UNC 路徑、空字元、長度不足 3 的路徑;並且明確排除 projectSettings 作為 autoMemoryDirectory 的來源——防止惡意倉庫將記憶寫入重定向到 ~/.ssh。

十、任務系統與多 Agent 協作

10.1 Task 系統

src/tasks/ 不是簡單的代辦清單,而是一個並發任務執行管理器,支援 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(),             // 對 system 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(啟動快、原生 TypeScript、feature() 支援建置時剔除)
  • Agent Loop:async generator(背壓控制,生命週期清晰)
  • 輸入驗證:Zod(運行時驗證 + 型別推導)
  • CLI UI:Ink(Terminal 用的 React,元件化,能重用 React 生態)
  • 命令解析:Commander.js(成熟穩定)
  • 狀態管理:自建微型 Store(30 行就夠,別一開始就引入 Redux)
  • 檔案搜尋:ripgrep (rg)(子進程速度優於 Node 原生 glob)
  • 命令分析:tree-sitter(AST 級精度)
  • LLM API:Anthropic SDK + streaming(原生流式支援)

實作路徑建議

  1. 先實作最小 Agent Loop:while(true) → callModel → executeTools → yield,不需要一開始就做 5 層上下文管理。
  2. Tool 系統用 interface + factory:一開始 3-5 個核心工具(Read、Edit、Bash、Glob、Grep)就夠了。
  3. 權限系統從 Layer 1 開始:靜態規則比對覆蓋 80% 場景,分類器與互動式審核後續補上。
  4. 上下文管理逐層加:先做簡單截斷,然後加 Auto Compact,再加微優化層。
  5. 子 Agent 能力放最後:單 Agent 先跑通,多 Agent 編排是進階能力。

十四、結語

讀完 Claude Code 的原始碼,我最大的感受不是「它有多少花俏的技術」,而是每一個設計決策背後都有明確的工程權衡。

非同步產生器不是為了炫技,而是因為它真的解決了 Agent 迴圈中事件流控制的核心問題。5 層上下文管道不是過度設計,而是因為 LLM 的上下文視窗就是 Agent 系統最關鍵的資源瓶頸。34 行的 Store 不是偷懶,而是因為 CLI 工具確實不需要 Redux 那套東西。

Claude Code 證明了一件事:AI Agent 的競爭力不僅在模型能力上,更在工程化的深度上。同樣的基座模型,配上精心設計的工具系統、上下文管理、權限控制和錯誤復原,使用體驗可以天差地別。

這也意味著,AI Agent 領域的競爭正在從「誰的模型更強」轉向「誰的 Agent 工程做得更好」。Claude Code 的原始碼為這個方向提供了一個高水準的參考實作——無論你是在構建自己的 Coding Agent,還是在設計其他領域的 Agent 系統,都能從中找到可借鑑的工程模式。

原始碼面前,了無秘密。但理解設計意圖,需要一點工程直覺。希望這篇文章能幫你建立這種直覺。


原文出處:https://juejin.cn/post/7623242804392902665


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝9   💬10   ❤️3
318
🥈
我愛JS
📝2   💬6   ❤️2
114
🥉
💬1  
4
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登