# 策展 · X (Twitter) 🔥

> 作者：小八 (@IceBearMiner) · 平台：X (Twitter) · 日期：2026-03-28

> 原始來源：https://x.com/IceBearMiner/status/2037888800341610684

## 中文摘要

# 從零開始兩天建構一個 Claude Code：帶你拆解 AI CLI 的每一層

前兩天突發奇想：一個生產級的 agentic CLI 到底需要哪些組件？每一層的具體怎麼實作？SSE 緩衝區怎麼管理、system prompt 怎麼分段、工具權限怎麼攔截、上下文滿了怎麼壓縮。這些問題靠讀文件回答不了，靠逆向混淆程式碼效率極低。

所以選擇了另一條路：以 Claude Code 為參照系，從零重建一個功能等價的實作——純 TypeScript，零框架，唯一的依賴是 fast-glob（因為原生 glob 在跨平台路徑處理上有已知缺陷）。

兩天之後的結果是 46 個檔案，一萬行 TypeScript。這篇文章記錄的是這個過程中每一層的技術決策和實作細節。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1774720927255-iaHEgGP82aQAAZIbQjpg.jpg)

## 整體架構

在開始寫任何程式碼之前，先要在腦子裡跑通一個最小迴圈：使用者輸入一句話，CLI 怎麼把它變成一次 API 呼叫，API 回應怎麼變成終端機輸出或工具執行，工具結果怎麼再送回模型。把這個迴圈畫清楚，架構就基本定了。

核心流程如下：

使用者輸入 → 組裝請求 → API 呼叫（SSE）→ 解析回應事件流 → 根據 stop_reason 決定分支：

- end_turn — 輸出文字，結束本輪

- tool_use — 執行工具呼叫 → 將 tool_result 追加到 messages → 反覆迭代

目錄結構按層劃分：

- core/ — 引擎層，包含 Agent Loop、SSE 客戶端、context 管理、compact 邏輯

- tools/ — 工具系統，包含所有內建工具的實作和 MCP 客戶端

- ui/ — 終端機渲染層，處理流式文字輸出、進度指示、顏色主題

- plugins/ — 擴充系統，允許執行時注入工具和 hook

- skills/ — 技能庫，對應 Claude Code 的 slash command 進階功能

- commands/ — 處理 / 前綴命令的解析和分發

這六層之間的依賴是單向的，core/ 不依賴 ui/，tools/ 不依賴 skills/。

技術選型的核心理由是 Node.js 22 的原生能力。fetch 和 ReadableStream 在 22 版本中已經穩定，不需要 node-fetch 或任何 HTTP 客戶端程式庫。TextDecoder 處理 UTF-8 位元組流，Buffer 處理二進位，child_process.exec 執行 Shell 命令——所有這些都是標準程式庫。唯一無法繞過的是檔案系統 glob，fast-glob 在處理 gitignore 規則和大型目錄的效能上比原生實作好一個數量級，這個依賴是值得的。

## 多平台 LLM 相容

預設支援御三家和自訂平台，自訂模型（OpenAI 相容格式）是一個附加需求。部分本地模型（如 Ollama、LM Studio）公開 OpenAI 相容介面，事件格式與 其他格式不同。處理方式是在 SSE 客戶端初始化時傳入 format: 'openai' 參數，在事件解析層做格式適配，將 OpenAI 的 delta 結構轉換成統一的內部事件類型。這樣 Agent Loop 層完全不感知 API 格式差異。

## System Prompt 分段架構與 Prompt Caching

最直覺的 system prompt 寫法是一個大字串，把所有指令拼湊在一起傳給 API。這個方式在原型階段沒問題，但在生產環境有兩個顯著缺陷：

1. 每輪對話 system prompt 幾乎不變，但如果以單一字串傳入，API 快取無法有效命中

2. 部分內容（如目前目錄、Git 狀態、CLAUDE.md 檔案內容）每輪都會變化，與靜態內容混雜在一起會污染快取

將 system 參數設為 block 陣列，每個 block 可以獨立設定 cache_control。

按可變性將 system prompt 分為兩類：

靜態段（程序生命週期內不變，標記上 cache_control 後首次寫入快取，後續命中）：

- 身份聲明（參考龍蝦的 User、Soul）

- 工具使用規範（何時用 Bash vs 讀取檔案、何時拒絕執行）

- 程式撰寫風格（規範、註解原則）

- 安全執行規則（禁止執行的命令類型）

這個就是官方訂閱為什麼呼叫 API 能省下不少錢，因為快取命中率高。

動態段（不帶 cache_control，每輪重新計算）：

- 目前工作目錄和系統環境（每次啟動可能不同）

- Git 儲存庫狀態（git status 輸出，每輪可能變化）

- CLAUDE.md 內容（使用者可隨時修改）

- MCP 伺服器的自訂指令（執行時發現）

完整請求主體的 system 欄位最終是一個有序 block 陣列，順序固定：身份 → 工具指南 → 程式撰寫規範 → 安全規則 → 風格指南 → 環境資訊 → Git 上下文 → CLAUDE.md → MCP 指令。Anthropic 的 prompt caching 按照 block 陣列的前綴匹配來識別快取，排在前面的靜態內容越穩定，快取命中率越高。

## Agent Loop：while 迴圈背後的状态機

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1774720927321-iaHEgGVl2aMAAdxGdjpg.jpg)

Agent Loop 的骨架是一個有上限的 while 迴圈，最大迭代次數 25 次，每次迭代對應一輪模型呼叫——25 輪足以完成大多數真實任務（讀取檔案、分析、修改、驗證通常在 10 輪內完成），同時防止失控迴圈耗盡 API 額度。

每次迭代的流程：

1. 檢查是否需要 compact

2. 建構完整 prompt

3. 發起流式請求

4. 即時處理事件流

5. 檢查 stop_reason — tool_use 則執行工具，end_turn 或 max_tokens 則結束迴圈

6. 建構 tool_result message，追加到 messages 陣列，進入下一輪

工具執行管線是 Agent Loop 中最複雜的部分，共六個階段：

1. renderToolCall — 在終端機展示將要執行的工具名稱和參數

2. permissionCheck — 根據工具類型和參數決定是否需要使用者確認

3. preHook — plugin 系統的前置攔截點

4. checkpoint — 對於破壞性操作，在執行前快照相關檔案狀態

5. executeTool — 呼叫實際工具函數

6. postHook — plugin 後置鉤子

自動 compact 機制處理上下文視窗溢出。在每輪迭代開始時，估算目前 messages 陣列的 token 數量（總字元數除以 4），如果超過模型上下文限制的 85%，觸發壓縮——發起一次獨立的 API 呼叫產生摘要，用 [{role: 'user', content: summary}, {role: 'assistant', content: 'Understood.'}] 替換原來的 messages 陣列。（這點跟 Claude Code 是一樣的）

Prompt Caching 在 Agent Loop 層有三個施力點：

- system prompt blocks — 如前文所述，靜態段走快取

- tools 陣列 — 最後一個 tool definition 標記上 cache_control

- 最後一條 tool_result message — 作為快取斷點

三層疊加的實際效果是每輪 API 呼叫只有少量 token 是真正的輸入計費，大部分走快取價格（約為正常輸入價格的 10%，這個官方 API 都帶，中轉站的快取命中率一般都不會很高）。

## 21 個內建工具

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1774720927333-iaHEgGXr3bAAAVFVfjpg.jpg)

工具系統的入口是 TOOL_DEFINITIONS，一個 JSON Schema 陣列，描述每個工具的名稱、用途和參數結構。模型透過這個陣列"知道"有哪些工具可以呼叫以及如何呼叫。陣列中包含 21 個內建工具定義，加上執行時從 MCP 伺服器動態注入的工具。（json scheme能保證輸出格式，LLM json也更易讀）

執行入口是單一的 executeTool 函數，內部使用 switch 按工具名稱分發。MCP 工具透過 mcp__ 前綴識別，走獨立的呼叫路徑。

核心工具的實作細節各有側重：

- Read — fs.readFile 讀取檔案後加上行號前綴，支援 offset 和 limit 參數分頁讀取

- Write — 寫入前 fs.mkdir({ recursive: true }) 確保父目錄存在

- Edit — 精確字串替換，關鍵約束是 old_string 必須在檔案中唯一出現，多次出現則報錯——迫使模型提供足夠精確的定位字串

- Bash — child_process.exec 執行，120 秒逾時，輸出超 500 行時截斷（保留前 200 + 後 100）

- Grep — 自行實作 regex 引擎，支援三種輸出模式、上下文行、跨行匹配，不依賴系統 grep

- WebFetch / WebSearch — fetch + HTML 剝離 + 截斷，搜尋使用 DuckDuckGo

Deferred Tools 是一個效能最佳化。低頻工具（如 NotebookRead、TodoWrite）不放入每次請求的 tools 陣列，而是標記為 deferred——模型需要時透過 ToolSearch 工具按關鍵字查詢取得完整 schema，然後在下一輪呼叫。這個機制將 tools 陣列的固定開銷降低了約 40%。

## 權限系統：三種模式與兩階段分類器

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1774720927277-iaHEgGZudacAApmOMjpg.jpg)

權限系統是 agentic CLI 的安全核心。設計不當要么讓使用者一直點擊確認直到放棄，要么給模型太多自主權造成不可逆損壞。

三種模式：

- default — safe 類工具（Read/Glob/Grep/WebFetch）自動執行，dangerous 類（Bash/Agent）和 write 類（Write/Edit）需要使用者確認

- auto — 繞過所有互動式提示，適合 CI 環境——但 deny rules 仍然生效，底線不可逾越

- plan — 唯讀沙盒，safe 工具放行，dangerous 和 write 工具被靜默拒絕，使用者可以先看執行計畫再切換到 default

工具分類在註冊時靜態聲明：

- safe — Read, Glob, Grep, WebFetch, WebSearch 等

- dangerous — Bash, Agent（副作用範圍不可預測）

- write — Write, Edit（檔案修改是最常見的需要稽核的操作）

- bypass — PlanMode 切換，始終無需確認

兩階段分類器增強 auto 模式下的安全性：

Stage 1 — 純模式匹配：維護已知安全命令和已知危險命令的規則表，命中即返回，涵蓋 90% 以上的情況，零延遲。

Stage 2 — 處理未涵蓋的情況：將命令字串發送給 Haiku 模型，要求返回 allow/deny/ask_user 三種之一。Haiku 延遲約 300-500ms，相比模型主迴圈幾乎可以忽略。ask_user 路徑讓自動模式在遇到模糊操作時能夠優雅降級。

## MCP 動態工具與 LSP 整合

MCP（Model Context Protocol）本質上是一個標準化的工具發現協議。傳統做法是把工具硬程式碼在 CLI 裡，MCP 讓工具變成可以獨立部署的程序，透過統一介面被任何相容客戶端發現和呼叫。

協議層是 JSON-RPC 2.0 over stdio：啟動 MCP server 程序，透過 stdin/stdout 交換 JSON-RPC 消息。啟動序列固定：initialize 握手 → tools/list 取得工具定義陣列。工具定義包含 JSON Schema 格式的 inputSchema，與 Anthropic API 的工具格式直接相容。McpManager 管理多個 server 的生命週期，工具名稱加上 mcpserver_name 前綴做命名空間隔離。

LSP 整合解決不同的問題：不是擴充工具，而是給模型提供即時的程式碼診斷資訊。LspClient 實作 Language Server Protocol 客戶端側，使用 Content-Length 幀協議通訊。LspManager 維護檔案副檔名到語言伺服器的路由表，Write/Edit 執行後自動通知對應語言伺服器，診斷結果作為 lsp_diagnostics section 注入下一輪 system prompt。

這個設計讓模型在修改程式碼後能立即看到編譯器回饋，而不需要單獨的"檢查錯誤"工具呼叫，縮短了發現問題到修復問題的路徑。

## plugin 系統與 Skills

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1774720927274-iaHEgGu9VbgAANTTdjpg.jpg)

plugin 系統回答的問題是：如何在不修改 CLI 核心程式碼的情況下擴充能力？答案是 manifest 驅動的目錄結構。每個 plugin 是一個目錄，包含 plugin.json，聲明提供的六類擴充點：

- skills — 注入可呼叫的技能

- agents — 注入自訂 Agent 定義

- hooks — 前置/後置鉤子

- commands — 自訂斜線命令

- mcpServers — 注入 MCP 伺服器，啟動時自動連線

- lspServers — 注入 LSP 伺服器

Skills 是比自訂 Agent 更輕量的重複使用單元——本質上是參數化的 prompt 模板。使用者輸入 /commit 時，skill 系統展開模板、注入上下文，提交給目前 agent loop。8 個內建 skill：

- commit / pr / review — Git 操作類，讀取 diff 後產生規範化訊息或 PR 描述

- init — 掃描專案結構，產生設定檔

- simplify — 審查變更程式碼，查找重複/低效邏輯，直接修復

- loop / schedule — 自動化類，持續執行或定時觸發

- update-config — 修改 CLI 自身設定

Skill 查找遵循固定優先級：內建 → plugin → 專案 .clio/skills/。專案級 skill 可以覆蓋 plugin 級，但不能覆蓋內建，防止安全敏感的內建 skill 被意外替換。

## 補上之前提到的 Agent Teams：多 Agent 協作

單 Agent 的瓶頸在於串行執行。當任務可以分解為多個獨立子任務時，多 Agent 並行能顯著縮短完成時間。

子 Agent 透過 executeSubAgent() 執行，它是主 agent loop 的簡化版本：排除團隊管理工具防止無限巢狀，最多 15 輪迭代。隔離模式可選——isolation: "worktree" 建立 Git worktree，子 Agent 在獨立分支上操作，完成後由主 Agent 決定是否合併。

背景 Agent 透過 run_in_background: true 標記，Promise 存入 Map，完成時以 tool_result 形式通知主 Agent。

自訂 Agent 定義在 .clio/agents/*.md 檔案中，YAML front-matter 聲明工具子集、模型選擇、最大迭代次數。

完整協作流程：主 Agent 呼叫 TeamCreate 建立團隊 → SendMessage 分發任務 → 等待完成 → 彙總結果 → TeamDelete 釋放資源。

這三個工具構成了一個最小化的多 Agent 協調協議，沒有訊息佇列，沒有共享狀態，只有顯式的訊息傳遞。

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1774720927331-iaHEgHkekaoAAaxxujpg.jpg)

## Auto Memory：跨會話記憶

LLM 本身是無狀態的。每次對話從空白上下文開始，之前建立的偏好、專案約定、使用者回饋全部消失。Auto Memory 用檔案系統模擬持久記憶，不引入資料庫，不設計新的儲存格式。

記憶檔案儲存在專案專屬目錄下，每個是帶有 YAML frontmatter 的 Markdown 檔案，四種類型：

- user — 使用者偏好（角色、習慣、專長）

- feedback — 行為糾正（使用者確認或否定的做法）

- project — 專案約定（不可從程式碼/git 推導的上下文）

- reference — 外部資源指標（URL、看板、文件連結）

MEMORY.md 是索引檔案，每次對話開始時注入 system prompt，模型按需透過 Read 工具讀取具體內容。

記憶的寫入完全重複使用現有工具鏈：Write 建立、Edit 更新、Read 讀取。零新增程式碼路徑，工具權限模型自動適用。生命週期：

1. 首次執行 — 建立索引

2. 後續對話 — 自動載入，按需讀取

3. 過時記憶 — 模型主動更新或刪除

記憶檔案是普通文字，使用者可以直接編輯，不存在格式鎖定。

整個專案驗證了一個認知：Claude Code 的工程品質確實高於平均水準。分段 prompt 的三層快取設計——靜態系統提示快取、工具定義快取、動態內容不快取——這個粒度的 cache 意識在大多數 LLM 應用中是缺失的。

這兩天的體驗就是，想建構 Agent 工具/產品，最重要的一個結論是：核心難點在於 Harness Engineering。畢竟呼叫 API 是十行程式碼的事，只有把工具呼叫結果正確地回饋給模型、在流式輸出中間插入使用者互動、處理長任務中的錯誤恢復

## 標籤

Claude Code, CLI, 教學資源, 開源專案
