控制協議
理解 CLI 和 SDK 之間的雙向 JSON RPC。
你將學到什麼
Claude Code 不是單一程序。它是兩個程序透過定義明確的協議進行協作。理解這種分離 — 以及在兩端之間流動的訊息 — 讓你能夠建構自訂前端、自動化工作流程,並除錯非預期行為。
完成後,你將了解:
- 為什麼 Claude Code 被分為 CLI 和 SDK 程序
- 雙向 JSON-RPC 訊息如何透過 stdin/stdout 傳遞
- 三種訊息類別:請求、回應和通知
- 串流在協議層面如何運作
- 權限提示如何在協議中傳遞
- 如何追蹤一則使用者訊息的完整往返過程
問題是什麼
大多數 CLI 工具是單體式的:一個程序讀取輸入、執行工作、印出輸出。但 Claude Code 有兩個截然不同的任務:
- 使用者介面 — 渲染終端 UI、處理鍵盤輸入、顯示串流文字
- AI 引擎 — 管理 API 呼叫、執行工具、強制權限、追蹤上下文
將兩者綁在一個程序中會造成緊密耦合。想把 Claude Code 嵌入 VS Code?你需要重寫 UI 層。想用腳本驅動它?你需要模擬終端輸入。想在遠端伺服器上執行引擎?不可能。
解決方案是乾淨的分離:
┌──────────────────┐ ┌──────────────────┐
│ CLI │ stdin │ SDK │
│ │ ──────► │ │
│ Terminal UI │ │ API Client │
│ User input │ stdout │ Tool Executor │
│ Permission UI │ ◄────── │ Context Manager │
│ Display │ │ Permission Logic │
└──────────────────┘ └──────────────────┘
CLI 處理使用者看到的一切。SDK 處理 AI 做的一切。它們透過一個控制協議溝通:經由 stdin 和 stdout 傳遞的雙向 JSON-RPC 串流。
如何運作
雙程序架構
當你啟動 claude 時,它會啟動兩個程序:
Terminal
│
▼
┌──────────────────────────────────────────────┐
│ CLI Process (Node.js) │
│ │
│ - Renders terminal UI (Ink/React) │
│ - Captures user keystrokes │
│ - Displays streaming assistant text │
│ - Shows permission prompts │
│ - Manages session history display │
│ │
│ stdin (pipe) ▼ ▲ stdout (pipe) │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ SDK Process (Node.js, child process) │ │
│ │ │ │
│ │ - Sends requests to Claude API │ │
│ │ - Parses streaming API responses │ │
│ │ - Executes tool calls (Read, Edit, │ │
│ │ Bash, etc.) │ │
│ │ - Manages message history │ │
│ │ - Resolves permissions │ │
│ │ - Handles context compaction │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
SDK 作為 CLI 的子程序運行。它們透過子程序的 stdin 和 stdout 管道通訊 — 沒有網路 socket、沒有 HTTP,只有以換行符分隔的原始 JSON 訊息管道。
訊息類型
此協議使用三種訊息類別,靈感來自 JSON-RPC 2.0:
1. 請求 — 預期會有回應。帶有 id 欄位。
{
"jsonrpc": "2.0",
"id": 1,
"method": "user_message",
"params": {
"content": "Fix the bug in auth.ts",
"session_id": "abc-123"
}
}
2. 回應 — 回答請求。帶有匹配的 id。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"status": "completed",
"usage": { "input_tokens": 1200, "output_tokens": 350 }
}
}
3. 通知 — 發送後即忘。沒有 id 欄位。
{
"jsonrpc": "2.0",
"method": "assistant_token",
"params": {
"token": "Let me ",
"message_id": "msg-456"
}
}
區分這些很重要:請求建立一個對話(一方等待回應),而通知是單向廣播。
串流模型
當 SDK 收到 API 回應時,它不會等待完整回應。每個 token 都作為通知串流出去:
SDK → CLI: {"method": "assistant_token", "params": {"token": "Let "}}
SDK → CLI: {"method": "assistant_token", "params": {"token": "me "}}
SDK → CLI: {"method": "assistant_token", "params": {"token": "check "}}
SDK → CLI: {"method": "assistant_token", "params": {"token": "the "}}
SDK → CLI: {"method": "assistant_token", "params": {"token": "file."}}
SDK → CLI: {"method": "tool_use_start", "params": {"name": "Read", ...}}
CLI 立即渲染每個 token,產生你在終端中看到的打字機效果。這類似於 Server-Sent Events (SSE) — 一串小訊息朝單一方向流動。
Claude API (SSE) SDK Process CLI Process
│ │ │
│── token: "Let " ────►│ │
│ │── notification ──►│── render "Let "
│── token: "me " ─────►│ │
│ │── notification ──►│── render "me "
│── tool_use ─────────►│ │
│ │── tool_start ────►│── show tool badge
│ │ │
│ │── (execute tool) ──│
│ │ │
│ │── tool_result ───►│── show result
權限流程
當 SDK 遇到需要使用者核准的工具呼叫時,一個請求-回應交換會處理它:
SDK → CLI: {
"jsonrpc": "2.0",
"id": 42,
"method": "permission_request",
"params": {
"tool": "Bash",
"command": "npm install express",
"risk_level": "medium"
}
}
(CLI shows permission prompt to user)
(User presses 'y')
CLI → SDK: {
"jsonrpc": "2.0",
"id": 42,
"result": {
"decision": "allow",
"scope": "session"
}
}
SDK 在此請求上阻塞。在 CLI 發回回應之前,它不會執行該工具。這就是權限系統的運作方式 — SDK 是決定什麼需要權限的權威,而 CLI 是決定是否授予權限的權威。
SDK CLI
│ │
│── permission_request ─────►│
│ (blocked) │── Show prompt
│ (waiting) │── User decides
│◄── permission_response ────│
│ │
│── execute tool │
│── tool_result ────────────►│── Display result
協議版本控制
交換的第一則訊息始終是能力協商:
CLI → SDK: {
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocol_version": "1.0",
"capabilities": {
"streaming": true,
"permissions": true,
"progress": true
}
}
}
SDK → CLI: {
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocol_version": "1.0",
"capabilities": {
"tools": ["Read", "Edit", "Bash", "Glob", "Grep", ...],
"mcp_servers": 3
}
}
}
這個握手確保雙方就支援哪些功能達成共識。如果 CLI 是較舊的版本,SDK 可以停用較新的功能。如果 SDK 引入了新的通知類型,較舊的 CLI 可以安全地忽略它。
關鍵洞見
控制協議是讓 Claude Code 既是 CLI 工具又是 SDK 的關鍵。 驅動終端 UI 的同一引擎可以被 VS Code、Web 介面或 Python 腳本驅動。協議就是邊界。
這有深遠的影響:
- 自訂前端。 建構一個能說協議語言的 GUI,你就能獲得 Claude Code 的全部能力而不需要終端。
- 程式化自動化。
@anthropic-ai/claude-codenpm 套件直接暴露 SDK — 你的程式碼變成 CLI,發送訊息並處理回應。 - 遠端執行。 協議與傳輸方式無關。今天它透過 stdin/stdout 管道運行。只需最少的修改就能透過 WebSocket 或 TCP 運行。
- 測試。 你可以透過發送腳本化的協議訊息來測試 SDK,完全不需要任何 UI。
協議不是實作細節 — 它就是架構本身。
實作範例
追蹤訊息的完整堆疊路徑
讓我們追蹤當你在 Claude Code 中輸入「What’s in README.md?」時會發生什麼:
Step 1: CLI captures input
─────────────────────────
User types: "What's in README.md?"
CLI packages it into a JSON-RPC request.
CLI → SDK:
{ "id": 5, "method": "user_message",
"params": { "content": "What's in README.md?" } }
Step 2: SDK calls Claude API
─────────────────────────────
SDK builds the full messages array (system prompt + history + new message)
SDK sends to Claude API via streaming HTTP.
Step 3: API streams back tokens + tool call
───────────────────────────────────────────
API response includes text and a tool_use block:
"Let me read that file."
tool_use: Read { file_path: "README.md" }
SDK forwards each token as a notification:
SDK → CLI: { "method": "assistant_token", "params": { "token": "Let " } }
SDK → CLI: { "method": "assistant_token", "params": { "token": "me " } }
...
SDK → CLI: { "method": "tool_use_start",
"params": { "name": "Read", "input": { "file_path": "README.md" } } }
Step 4: SDK checks permissions
──────────────────────────────
Read tool for README.md → allowed by default (no prompt needed).
SDK executes the tool directly.
Step 5: SDK feeds result back to API
─────────────────────────────────────
SDK appends tool_result to messages array.
SDK calls Claude API again with the updated context.
Step 6: API streams final response
───────────────────────────────────
API → SDK → CLI (token by token):
"The README.md contains a project description for..."
SDK → CLI: { "method": "turn_complete",
"params": { "stop_reason": "end_turn" } }
Step 7: SDK sends response to original request
───────────────────────────────────────────────
SDK → CLI:
{ "id": 5, "result": { "status": "completed",
"usage": { "input_tokens": 2400, "output_tokens": 180 } } }
以程式方式使用 SDK
因為協議是定義明確的,你可以將 Claude Code 作為函式庫使用:
import { claude } from "@anthropic-ai/claude-code";
// The SDK speaks the same protocol — your code is the "CLI"
const result = await claude("What's in README.md?", {
cwd: "/my/project",
allowedTools: ["Read", "Glob", "Grep"],
});
console.log(result.text);
// "The README.md contains..."
在底層,這會啟動 SDK 程序並透過 stdin/stdout 交換 JSON-RPC 訊息 — 與終端 CLI 的做法完全相同。
前後對比
| 單體式 CLI | 雙程序協議 |
|---|---|
| UI 和引擎緊密耦合 | 關注點清楚分離 |
| 只有一個前端(終端) | 任何能說 JSON-RPC 的前端 |
| 難以自動化 | 程式化 SDK 使用內建支援 |
| 難以測試 | 協議訊息可以腳本化 |
| 無法遠端執行引擎 | 傳輸方式無關的設計 |
| 權限邏輯與 UI 混合 | SDK 決定什麼需要權限,CLI 決定是否授予 |
下一堂課
你現在理解了 CLI 和 SDK 如何通訊。但工具不是擴展 Claude Code 的唯一方式。第 14 堂課介紹 MCP 整合 — Model Context Protocol 如何讓你透過標準協議接入無限的外部工具,使 Claude Code 具備無限的可擴展性。