引擎:主迴圈與 Prompt 編排
query.ts 1729 行狀態機、10 步迴圈、Streaming Tool Execution,以及 Prompt 是怎麼組裝出來的
query.ts:1729 行的狀態機
整個 Claude Code 的執行引擎集中在 query.ts,1729 行,是程式碼庫裡最重的單一檔案。
它不是一個簡單的「發請求、等回應」函式。它是一個狀態機——管理對話歷史、工具呼叫、串流回應、錯誤恢復、Token 預算,每一輪都要走完一套固定的步驟才算結束。
10 步迴圈
每次使用者發送訊息,主迴圈依序執行這 10 個步驟:
- 載入上下文:從記憶體和磁碟拿到當前的對話歷史、系統 Prompt、工具清單
- 注入動態內容:把當前工作目錄、Skill 指令、MCP 工具說明等動態資訊插入 Prompt
- Token 預算計算:根據當前上下文長度決定這輪能用多少 Token
- 發送 API 請求:串流方式呼叫模型
- 處理串流事件:每個 chunk 到來時更新 UI、累積工具呼叫請求
- Speculative Classifier:在模型輸出結束前預測它要呼叫的工具,提前跑權限檢查
- 工具執行:依序或並行執行工具,收集結果
- 結果回注:把工具執行結果加入對話歷史,準備下一輪
- Token 稽核:檢查是否超出預算,決定是否需要壓縮上下文
- 終止判斷:模型說話結束且沒有待執行工具 → 迴圈結束,等待下一次使用者輸入
步驟 6 和 7 的順序值得注意:權限檢查發生在工具執行之前,而且是在串流還沒結束時就預測性地啟動。這讓 Claude Code 的回應感覺比實際延遲更低——使用者看到模型還在輸出時,後台已經在準備工具執行了。
Streaming Tool Execution
傳統的 LLM 呼叫是:等模型輸出完整 → 解析工具呼叫 → 執行工具 → 把結果加回去 → 再呼叫一次模型。每個步驟都是串行的,延遲疊加。
Claude Code 的做法不同。串流回應到來時,query.ts 同時做兩件事:把文字輸出推給 UI 渲染,以及解析工具呼叫請求的結構。一旦工具呼叫的參數齊全(不需要等模型輸出完整),就立刻送進工具執行 Pipeline。
結果是:使用者看到模型「說話」的同時,工具已經在跑了。體感延遲顯著降低,尤其是需要連續多輪工具呼叫的任務。
Prompt 怎麼組裝
每次呼叫模型前,query.ts 需要組裝一個完整的 Prompt。這個 Prompt 不是靜態字串,而是由多個來源動態拼接的。
靜態部分在啟動時確定,包含行為規範、工具說明的框架結構、輸出格式要求。這些內容幾乎不隨使用者輸入改變,適合被快取。
動態部分每輪都可能不同。SYSTEM_PROMPT_DYNAMIC_BOUNDARY 是程式碼裡的一個明確標記,用來分隔靜態和動態內容。它的存在讓快取策略能精確定位:標記之前的內容嘗試命中快取,標記之後的內容每次重新計算。
動態內容的來源包括:當前工作目錄和 git 狀態、已載入的 Skill 指令集、MCP 工具的行為說明、Memory 系統預取的相關記憶片段、以及當前對話的 Tool result 摘要。
行為規範層
Prompt 的靜態部分有一塊專門處理行為規範——告訴模型什麼能做、什麼不能做、遇到邊界情況怎麼處理。
這不是一段「請你友善地回答」之類的軟性描述,而是具體的操作規則。例如:不要在沒有使用者確認的情況下刪除檔案、在工具執行前說明意圖而不是執行後解釋、遇到衝突的指令優先遵循更具體的那條。
行為規範層和工具的 inputSchema 共同構成了對模型行為的雙重約束:Prompt 層面的規範告訴模型「應該怎麼做」,工具層面的 schema 在輸入不合法時直接拒絕執行。兩層互相補充,任何一層單獨都不夠。
為什麼狀態機而非簡單函式
最簡單的實作是一個 async function query(messages),發請求、回傳結果。夠用但有問題:它沒有辦法在中途被打斷、沒有辦法處理工具執行失敗後的重試、也沒有辦法在 Token 快用完時透明地做上下文壓縮。
狀態機的設計讓每一步都是明確的狀態轉移,錯誤發生時知道從哪裡恢復,使用者取消時知道在哪裡乾淨地退出。這個代價是複雜度——1729 行就是這個代價。換來的是在各種邊界情況下的可預測行為。
參考來源: 本文內容參考 Xiao Tan(@tvytlx)的《Claude Code 源碼架構深度解析 V2.0》,基於原報告的分析框架和研究成果整理。