精華筆記

· @aihub.tw

AI Agent 開發

Claude Agent SDK 實戰

Claude Agent SDK 實戰

第 2 課你已經知道工具介面怎麼設計,知道 agent loop 的每一轉在做什麼。現在打開一個新的 TypeScript 專案想動手——你要 import Anthropic、建 messages 陣列、寫 while 迴圈、處理 tool_use 訊息、把工具結果塞回去、再呼叫 API……光讓「第一轉」跑起來就要 100 行。還沒算錯誤處理、context 超出限制、並發工具執行、以及這個 loop 永不結束的無限迴圈 bug。每個問題都是一個排查一小時的深坑。

這就是 Claude Agent SDK 存在的理由。它把整個 agent loop 封裝成一個函式呼叫:你描述任務、宣告哪些工具可用,SDK 就自動跑完整個迴圈——包含工具執行、結果回填、context 管理、串流輸出。它不是另一個第三方框架,是 Anthropic 官方維護的函式庫,而且它就是 Claude Code 本身使用的同款引擎,2025 年 9 月從 Claude Code SDK 改名為 Claude Agent SDK,正式定位為「不只給寫 coding agent 用」的通用 agent 開發工具。

這堂課適合誰 適合:想跳過手寫 agent loop 樣板程式碼、直接在生產級架構上開發的工程師。需要基礎:會 TypeScript async/await 語法、看得懂 JSON schema、用過 Anthropic client SDK 呼叫過 API。前置課:第 1 課(Agent 架構)、第 2 課(工具設計)——觀念不熟直接跳過會很痛。

這堂學什麼

  • SDK 定位:Claude Agent SDK 和 Anthropic Client SDK 的分工,以及什麼情境下不需要用它
  • 安裝與第一個 agent:從零建立專案,用 query() 跑出第一個真正有工具呼叫的 agent
  • 內建工具全覽:Read/Write/Edit/Bash/WebSearch/WebFetch——每個的適用場景與限制
  • hooks 與權限控制:PreToolUse/PostToolUse 的實作,以及 permissionMode + allowedTools 如何鎖定範圍
  • 取捨判斷:Agent SDK vs 自寫 loop,什麼時候用哪個
  • 實戰:一個自動掃描專案、產出 PROJECT_OVERVIEW.md 的文件整理 agent

觀念一:SDK 定位——它包了什麼、留了什麼給你

Client SDK vs Agent SDK:誰負責 agent loop

Claude Agent SDK 官方文件(2026 年 7 月實況) 圖:Claude Agent SDK 官方文件(2026 年 7 月實況),來源:code.claude.com/docs

Anthropic 維護兩個不同層級的函式庫:

Anthropic Client SDK (@anthropic-ai/sdk) Claude Agent SDK (@anthropic-ai/claude-agent-sdk)
你管什麼 所有事:loop、tool dispatch、messages 只管任務描述和工具白名單
loop 你自己寫 SDK 自動跑
內建工具 無,你全部自己實作 Read/Write/Edit/Bash/WebSearch 等開箱即用
適合場景 單次呼叫、高度客製化 loop 需要多步驟自主執行的 agent
與 MCP 整合 需自行處理 原生支援

一句話判斷原則:如果你發現自己正在寫一個 while 迴圈來反覆呼叫 LLM 並執行工具,那就是 Agent SDK 最擅長的場景。如果你只是做一次分類、摘要、或提取,用 Client SDK 就夠,Agent SDK 反而多餘。


觀念二:Agent Loop 在 SDK 裡長什麼樣

從你呼叫 query() 到拿到結果,SDK 內部會自動跑這條路:

query() 內部的 agent loop

你的程式碼只需要處理 SDK 從 async generator yield 出來的訊息,不需要知道裡面跑了幾轉。這和第 1 課學到的 agent loop 理論完全一致——SDK 只是把那個 loop 的樣板程式碼幫你包好了。

訊息類型與它的意義:SDK 會 yield 四種主要訊息類型,理解它們才能寫出正確的輸出處理邏輯:

  • system:session 開始時傳送的系統訊息,通常不需要處理
  • assistant:模型的輸出,content 陣列裡可能包含 text(文字輸出)或 tool_use(模型決定呼叫工具);工具執行部分 SDK 自動處理,你通常只需要取 text
  • user:工具執行結果被回填進 messages 時會觸發,SDK 內部使用,外部程式通常不需要特別處理
  • result:任務結束訊號。subtype"success""error";若是 error,error 欄位會有詳細訊息

開發時有一個常見疑問:「什麼時候我應該把 tool_useassistant 訊息裡取出來自己處理?」答案是:幾乎從不query() 的設計前提就是讓你不碰工具執行層,如果你需要攔截工具呼叫,該用 hooks,不是手動解析 tool_use block。


手把手實戰

安裝 SDK、建立專案

先確認環境:Node.js 20+ 或 Bun 1.1+,TypeScript 5+。

mkdir my-agent && cd my-agent
npm init -y
npm install @anthropic-ai/claude-agent-sdk
npm install -D typescript tsx @types/node
npx tsc --init --target ES2022 --moduleResolution bundler --esModuleInterop true

設定 API 金鑰(SDK 會自動讀取 ANTHROPIC_API_KEY 環境變數):

echo "ANTHROPIC_API_KEY=sk-ant-..." > .env

Python 用戶改裝:

pip install claude-agent-sdk        # 需要 Python 3.10+

本堂課以 TypeScript 為主,Python 的 API 介面幾乎一一對應,語法差異在每個程式碼範例後會補一行說明。

第一個 agent:Hello, 工具世界

建立 hello-agent.ts:

import { query } from "@anthropic-ai/claude-agent-sdk";

async function main() {
  const messages = query(
    "列出當前目錄下所有 .ts 檔案,然後告訴我總共有幾個",
    {
      cwd: process.cwd(),
      allowedTools: ["Bash", "Glob"],
      permissionMode: "dontAsk",
    }
  );

  for await (const message of messages) {
    // assistant 訊息:模型輸出(包含文字和工具呼叫)
    if (message.type === "assistant") {
      for (const block of message.message?.content ?? []) {
        if (block.type === "text" && block.text.trim()) {
          process.stdout.write(block.text);
        }
      }
    }
    // result 訊息:任務結束
    if (message.type === "result") {
      console.log(`\n\n[完成] subtype: ${message.subtype}`);
    }
  }
}

main().catch(console.error);

跑起來:

npx tsx hello-agent.ts

你會看到 agent 呼叫 Glob 找出 .ts 檔案、然後用自然語言回答你——整個工具呼叫的過程 SDK 全自動處理,你什麼都沒做。

訊息類型速查:assistant(模型輸出)、user(工具結果回填)、system(系統訊息)、result(任務結束,含 successerror subtype)。只需要關注 assistant.textresult.subtype 就能處理大多數場景。

Python 等效:把 query() 換成 async for message in query(...): 同樣邏輯。

認識內建工具全覽

Agent SDK 內建工具全覽

SDK 開箱即用的工具(2026 年 7 月):

工具名稱 功能 需要開放的 allowedTools 值
Read 讀取檔案內容 "Read"
Write 建立或覆寫整個檔案 "Write"
Edit 精確替換檔案中的片段 "Edit"
Glob 依路徑模式找出檔案列表 "Glob"
Grep 在檔案內容中搜尋字串 "Grep"
Bash 執行 shell 指令 "Bash"
WebSearch Bing 搜尋取得摘要結果 "WebSearch"
WebFetch 抓取並解析指定 URL 的內容 "WebFetch"
Agent 派遣子 agent 執行子任務 "Agent"

選工具的黃金原則:只開放任務真正需要的工具。開了 Bash 就等於給了 agent 一個 shell,確定你知道它會執行什麼指令再開放。Bash + Write 組合威力最強,也是最容易出事的組合。

幾個常見搭配的邏輯:

  • 純讀取分析任務:只開 Read + Glob + Grep,model 不能改任何東西,風險最低
  • 文件整理任務:開 Read + Write + Edit + Glob,不開 Bash,避免 agent 執行系統指令
  • 程式碼生成任務:開 Read + Write + Edit + Bash,但用 PreToolUse hook 過濾危險 Bash 指令
  • 研究任務:開 Read + WebSearch + WebFetch,不給寫檔權限,讓 agent 只輸出文字結果

每次增加一個新工具之前,都應該問自己:「這個工具讓 agent 多做了什麼?如果它亂用這個工具,最壞的情況是什麼?」這個思路和第 2 課的工具設計原則是同一件事——工具的邊界決定了 agent 的邊界。

Hooks 與權限控制

permissionMode 控制整體授權策略,allowedTools 列白名單,hooks 則讓你在每一次工具呼叫前後插入邏輯——這三層組合起來才是完整的權限架構。

permissionMode 五種模式的安全光譜

permissionMode 速查:

"default"          → 標準模式,依 settings.json 規則決定是否需要確認
"acceptEdits"      → 自動同意所有檔案編輯(Read/Write/Edit),其他照預設
"dontAsk"          → 白名單外的工具直接拒絕,不詢問
"bypassPermissions"→ 跳過所有權限檢查(開發測試用,絕對不進生產)
"auto"             → 用分類模型自動評估每次工具呼叫是否允許

建立 hooks-demo.ts,展示 PreToolUse 過濾敏感檔案 + PostToolUse 寫稽核日誌:

import { query } from "@anthropic-ai/claude-agent-sdk";
import { appendFile } from "fs/promises";

async function main() {
  const SENSITIVE_PATTERN = /\.env|secret|credential|private_key/i;

  const messages = query("讀取專案所有設定檔並列出內容摘要", {
    cwd: process.cwd(),
    allowedTools: ["Read", "Glob", "Grep"],
    permissionMode: "dontAsk",
    hooks: {
      PreToolUse: [
        {
          // 攔截所有 Read 工具呼叫
          matcher: "Read",
          callback: async ({ tool_name, tool_input }) => {
            const filePath: string = tool_input.file_path ?? "";

            if (SENSITIVE_PATTERN.test(filePath)) {
              console.warn(`[BLOCKED] ${tool_name} 嘗試讀取敏感檔案: ${filePath}`);
              return {
                permissionDecision: "deny" as const,
                permissionDecisionReason: `安全限制:禁止讀取敏感檔案 ${filePath}`,
              };
            }

            return { permissionDecision: "allow" as const };
          },
        },
      ],
      PostToolUse: [
        {
          // 記錄所有工具使用到稽核日誌
          matcher: ".*",
          callback: async ({ tool_name, tool_input }) => {
            const timestamp = new Date().toISOString();
            const target = tool_input.file_path ?? tool_input.query ?? "";
            await appendFile(
              "agent-audit.log",
              `${timestamp}\t${tool_name}\t${target}\n`
            );
          },
        },
      ],
    },
  });

  for await (const message of messages) {
    if (message.type === "assistant") {
      for (const block of message.message?.content ?? []) {
        if (block.type === "text") process.stdout.write(block.text);
      }
    }
  }
}

main().catch(console.error);

PreToolUse 可以做的事:

  • 回傳 permissionDecision: "deny" 阻止工具執行
  • 回傳 permissionDecision: "allow" 放行
  • 回傳 updatedInput 修改工具的輸入參數後再執行

PostToolUse 在工具已成功執行後觸發,input 同時包含 tool_input(送進去的參數)和 tool_response(工具回傳值),適合做日誌、監控、或把結果轉換成不同格式再送回 context。

實戰:自動整理專案文件的 agent

這個 agent 會:掃描任意專案目錄 → 分析結構與重要檔案 → 在根目錄產出 PROJECT_OVERVIEW.md

建立 doc-organizer.ts:

import { query } from "@anthropic-ai/claude-agent-sdk";
import { appendFile } from "fs/promises";
import { resolve } from "path";

async function organizeProjectDocs(projectPath: string) {
  const absPath = resolve(projectPath);

  const prompt = `
你是一個專案文件整理助理,目標是讓任何新進開發者 5 分鐘內看懂這個專案。

請按照以下步驟完成任務(每步前先說明你要做什麼):

1. 用 Glob 列出根目錄的所有檔案(不含 node_modules、.git、dist)
2. 讀取 package.json 或 pyproject.toml 取得專案名稱與依賴
3. 找出所有現有的 .md 文件,列出它們的標題
4. 判斷哪些目錄是核心原始碼、哪些是測試、哪些是設定
5. 在專案根目錄寫入 PROJECT_OVERVIEW.md,結構如下:
   # {專案名稱} — 總覽

   ## 專案簡介
   (一段說明,從 package.json 或 README 提取)

   ## 目錄結構
   (文字版樹狀結構,只列兩層深度)

   ## 重要檔案速查
   | 檔案 | 用途 |
   (列出 10 個以內最重要的檔案)

   ## 開始開發
   (安裝指令、本機跑起來的指令,從 package.json scripts 提取)

   ## 文件索引
   (列出所有現有 .md 的連結與一行說明)

如果 PROJECT_OVERVIEW.md 已存在,請先讀取再決定是否更新,不要無腦覆寫。
`.trim();

  const messages = query(prompt, {
    cwd: absPath,
    model: "claude-opus-4-5",           // 複雜分析任務選 Opus
    allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
    permissionMode: "acceptEdits",       // 自動接受檔案寫入
    hooks: {
      PreToolUse: [
        {
          matcher: "Bash",
          callback: async ({ tool_input }) => {
            const cmd: string = tool_input.command ?? "";
            // 只允許唯讀 shell 指令,禁止 rm、curl、sudo 等
            const DANGEROUS = /\b(rm|rmdir|curl|wget|sudo|chmod|chown|dd|mkfs)\b/;
            if (DANGEROUS.test(cmd)) {
              console.warn(`[BLOCKED Bash] 指令包含危險操作: ${cmd}`);
              return {
                permissionDecision: "deny" as const,
                permissionDecisionReason: `不允許在文件整理任務中執行系統操作指令`,
              };
            }
            return { permissionDecision: "allow" as const };
          },
        },
      ],
      PostToolUse: [
        {
          matcher: "Write|Edit",
          callback: async ({ tool_input }) => {
            const path: string = tool_input.file_path ?? "";
            await appendFile(
              "doc-organizer-audit.log",
              `${new Date().toISOString()}\tWRITE\t${path}\n`
            );
            console.log(`  [已寫入] ${path}`);
          },
        },
      ],
    },
  });

  console.log(`\n開始整理專案文件:${absPath}\n${"─".repeat(50)}`);

  for await (const message of messages) {
    if (message.type === "assistant") {
      for (const block of message.message?.content ?? []) {
        if (block.type === "text" && block.text.trim()) {
          process.stdout.write(block.text);
        }
      }
    }
    if (message.type === "result") {
      if (message.subtype === "success") {
        console.log(`\n${"─".repeat(50)}\n任務完成!PROJECT_OVERVIEW.md 已更新。`);
      } else {
        console.error(`\n任務失敗:${message.subtype}`);
      }
    }
  }
}

// 從命令列參數取目錄路徑,預設當前目錄
const targetPath = process.argv[2] ?? ".";
organizeProjectDocs(targetPath).catch(console.error);

使用方式:

# 整理當前目錄
npx tsx doc-organizer.ts .

# 整理指定專案
npx tsx doc-organizer.ts /path/to/your-project

這個實戰展示了三個關鍵設計決策:

  1. model 選 Opus:文件分析需要多步驟推理,Opus 在複雜任務上比 Sonnet 更可靠,成本換正確率值得。
  2. acceptEdits + Bash 過濾:整個任務需要寫檔,用 acceptEdits 省掉反覆確認;但 Bash 指令用 hook 加白名單過濾,防止意外的系統操作。
  3. 判斷再寫、不覆寫:prompt 明確要求 agent 先讀取現有文件再決定怎麼更新,避免反覆執行時每次都重寫一遍。

Agent SDK vs 自寫 Loop:選哪個?

Agent SDK vs 自寫 loop:三題決策

實際開發中有一種常見的錯誤衝動:「Agent SDK 太黑盒了,我要自己寫才放心」。但黑盒本身不是問題,問題是你不熟悉它的邊界。以下是真正需要自寫 loop 的場景:

  • 工具的並發策略需要完全掌控:例如你需要同時執行 20 個 API 呼叫並合併結果,Agent SDK 的並發行為不符合你的需求
  • context 管理需要高度客製化:例如你需要對 messages 做複雜的壓縮或語意去重,而不是簡單的截斷
  • 成本控制極度嚴格:Agent SDK 讓 agent 自主決定用幾轉,如果你需要硬性限制每個任務最多 N 次 API 呼叫,自寫 loop 更好控制

以上三點之外,Agent SDK + hooks 幾乎都能處理。第 4 課會進一步討論當任務大到需要 orchestrator + subagent 架構時,SDK 的 Agent 工具如何串接。

還有一個常見的誤判:「我需要自訂工具所以要自己寫 loop」。這個邏輯不成立。Agent SDK 完全支援客製化工具,你可以在 MCP server 裡定義自己的工具然後透過 mcpServers 選項接入,或直接在 prompt 裡描述工具行為讓 SDK 執行對應的 Bash 指令。自寫 loop 的真正理由只有上面三個,「我有自訂工具」不是其中之一。


常見坑

坑 1:bypassPermissions 進了生產環境

症狀:agent 在本機測試順利,你為了方便改成 permissionMode: "bypassPermissions",部署後 agent 刪了它不該刪的檔案,或執行了危險的 bash 指令。

這個 mode 的設計用途是本機快速驗證邏輯,不是生產設定。生產環境一律用 "dontAsk" + 明確的 allowedTools 白名單。如果某個動作需要彈性,用 PreToolUse hook 動態判斷,不要用 bypass 一刀切。


坑 2:cwd 設錯導致 agent 找不到任何檔案

症狀:agent 跑了很久,最後回應「找不到指定的目錄」或「沒有符合條件的檔案」,但你知道檔案明明在那裡。

Error: ENOENT: no such file or directory, scandir '/wrong/path'

原因通常有兩個:一是傳入了相對路徑,而 SDK 在不同執行環境下的工作目錄不一樣;二是 cwd 省略了,預設是 process.cwd(),但 CI 環境或 Docker 容器裡 cwd 不是你以為的地方。

修法:永遠傳入 resolve(targetPath) 的絕對路徑。

import { resolve } from "path";
const messages = query(prompt, {
  cwd: resolve(process.argv[2] ?? "."),  // 一律轉成絕對路徑
  // ...
});

坑 3:allowedTools 沒包含 "Agent" 導致多代理任務失敗

症狀:你的 prompt 期待 agent 把複雜任務拆給子 agent,但 agent 始終自己硬做或回應「無法派遣子任務」。

原因:Agent 工具本身也在 allowedTools 白名單管轄範圍內。搭配 dontAsk 模式時,沒有列在白名單的工具一律被拒絕,Agent 工具也不例外。

allowedTools: ["Read", "Glob", "Agent"],  // 記得加 "Agent"
permissionMode: "dontAsk",

這個坑在第 4 課多代理架構時會反覆踩到,先記住這個規則。


坑 4:async generator 沒完整消費導致任務被截斷

症狀:agent 明明還沒完成任務,程式就結束了;或只收到部分輸出,沒有看到 result 訊息。

原因:query() 回傳的是 async generator,必須用 for await...of 完整迭代到底。如果你在中途 break 或忘記 await,generator 不會繼續推進,底層的 API 呼叫也會被中斷。

// 錯誤:只取第一個訊息就結束
const gen = query(prompt, options);
const first = await gen.next();  // ← 任務在這裡停住,不繼續跑

// 正確:完整迭代
for await (const message of query(prompt, options)) {
  // 處理每個訊息,讓 generator 自然結束
}

坑 5:長任務 context 爆掉,agent 在中途就停下來

症狀:任務做到一半,agent 突然輸出「由於 context 限制我無法繼續」或直接以不完整的結果結束。result.subtype"error" 且錯誤訊息含 context_window_exceeded

根本原因:每一次工具呼叫的結果都會被追加進 messages 陣列,如果你讓 agent 讀了很多大檔案,context 很快就滿了。

應對策略:

  1. 縮小工具輸入範圍:prompt 明確指示「只讀需要的那幾行,不要讀整個檔案」
  2. 拆分任務:把一個大任務分成幾個小的 query() 呼叫,每次只做一段
  3. 過濾工具輸出:用 PostToolUse hook 把工具回傳的巨大輸出截斷再送回 context
  4. 用 Grep 取代 Read:先搜尋定位再讀取,避免把整個大檔案塞進 context

作業

  1. 複製本課的 doc-organizer.ts,對你自己的一個專案執行它,確認產出的 PROJECT_OVERVIEW.md 內容準確。
  2. 加一條 PostToolUse hook,讓每次 WriteEdit 之後印出「已寫入多少 bytes」的提示(提示:tool_response.content 裡有寫入資訊)。
  3. 試試把 model 改成 claude-sonnet-4-5 重跑一次,比較速度與品質的差異,然後根據你的任務複雜度做出選擇。
  4. 進階挑戰:讓 doc-organizer 接受第二個命令列參數 --dry-run,在此模式下只印出「我打算寫什麼」但不實際執行 Write/Edit(提示:用 PreToolUse hook 攔截並 deny)。

下一課預告

你已經有了一個可以自主操作檔案、執行指令的 agent。但當任務規模繼續擴大——要同時研究三個主題、分析五個資料夾、寫十份文件——單一 agent 處理起來又慢又容易 context 爆掉。

第 4 課進入多代理架構:orchestrator 怎麼把任務分解派給 subagent、subagent 的結果怎麼合併回主流程、以及最重要的一個問題——多個 agent 同時對同一份檔案操作時怎麼不互相踩壞。你在這課做的 doc-organizer 會在第 4 課被改造成一個可以同時整理多個專案的並行版本。

#AI Agent 開發#Claude Agent SDK#agent loop#hooks#TypeScript

← 回所有文章