◥ AI 互動教室 ‹ 生成式 AI 導論
下載 .py 單獨開啟實驗場 ↗ 留言回報
GENAI 進階補充 · C · CLAUDE AGENT SDK

Claude Agent SDK:
把 Claude Code 裝進你的程式

主線第 5 課你看過 agent 迴圈的骨架:模型吐一段 JSON、你的程式執行工具、 結果塞回對話,再問一次。Claude Agent SDK 把這個迴圈——連同 Claude Code 整套的讀檔、改檔、跑指令工具、 context 管理、權限與 hooks——包成一個 Python 函式 query()。 你不用再寫迴圈;剩下的工作只有一件:設邊界。

下面是同一個任務(「測試沒過,找出 bug、修好、跑測試確認」)、同一個模型, 只改一行設定的三次真實錄影。按「下一步」一則一則播:

實測紀錄:claude-haiku-4-5,claude-agent-sdk 0.2.159(內含 Claude Code 2.1.281),2026-09-24。 每次都從全新的專案複本開始;「測試結果」是跑完後我們自己再跑一次 unittest 的真實狀態,不是模型的自述。

實驗場是真的 Python(在你的瀏覽器裡跑,不用安裝任何東西、不需要任何帳號或金鑰)。 首次載入約需 30–60 秒,正好夠你讀完第 1 節。裡面有六種設定的完整錄影、一台權限閘門模擬器、 一台帳單計算機——選單、滑桿、開關隨便玩。

01 · 從迴圈到 HARNESS

SDK 替你做掉的事

一句話重點:Agent SDK =把 Claude Code 當函式庫用。迴圈、內建工具、context、權限、hooks、 子代理、session 全部內建(這整套叫 harness,馬具);你給的是 prompt 和一份 ClaudeAgentOptions。

主線第 5 課手寫的是最小迴圈;本站 LLM 應用開發系列把它寫成能跑的程式。 同一件事交給 SDK,分工變成這樣:

工作手寫 tool loopAgent SDK
迴圈自己寫 while、自己判斷何時停 query() 直接吐出一串訊息,停在 ResultMessage
工具每個都自己寫 schema、自己執行 內建 Read/Edit/Write/Bash/WebSearch/WebFetch/Agent…(這次錄影的清單有 29 個)
context自己管 messages、自己算長度 自動重送、自動開 prompt caching,快滿時自動壓縮(有 PreCompact hook 可接)
權限自己寫 if permission_mode、allowed_tools、disallowed_tools、can_use_tool
攔截與稽核自己包一層hooks:PreToolUse、PostToolUse、Stop…
帳單自己加總 usageResultMessage 給你 num_turns、total_cost_usd、逐模型的 model_usage

整支 agent 程式就這麼短(參考程式,不在課內執行;需要 pip install claude-agent-sdk 與 API key):

import asyncio from claude_agent_sdk import (query, ClaudeAgentOptions, AssistantMessage, ToolUseBlock, ResultMessage) async def main(): options = ClaudeAgentOptions( model="claude-haiku-4-5", cwd="shop-demo", # agent 在哪個資料夾工作 allowed_tools=["Read", "Edit", "Bash"], # 這三個工具不用問、直接放行 ) async for msg in query(prompt="測試沒過,找出 bug、修好、跑測試確認。", options=options): if isinstance(msg, AssistantMessage): for block in msg.content: if isinstance(block, ToolUseBlock): print("→", block.name, block.input) # 模型要求的工具呼叫 elif isinstance(msg, ResultMessage): print(msg.subtype, msg.num_turns, msg.total_cost_usd) asyncio.run(main())

query() 吐出來的訊息串,就是 hero 播放器裡那一則一則的卡片: SystemMessage(init)(這次有哪些工具、什麼模式)→ AssistantMessage(模型說的話 TextBlock、要求的呼叫 ToolUseBlock)→ UserMessage(工具結果 ToolResultBlock)→ …… → ResultMessage(帳單)。 執行工具、把結果塞回去、再問模型——主線第 5 課你手寫的那幾行,全在 SDK 裡面。

兩個預設值要知道:system prompt 預設是空的(SDK 啟動 Claude Code 時傳 --system-prompt "";要 Claude Code 完整的行為準則,寫 system_prompt={"type": "preset", "preset": "claude_code"}); setting_sources 預設會載入你電腦上的設定(~/.claude、 專案的 CLAUDE.md)。錄影時我們一律設 setting_sources=[], 讓結果只反映程式本身——正式上線時也建議這樣做,不然換一台機器行為就變。

02 · 權限

六道閘門:誰說了算

一句話重點:模型每要求一次工具,SDK 依固定順序過六道閘門,第一個做出決定的說了算—— hooks → deny 規則 → ask 規則 → 權限模式 → allow 規則 → can_use_tool。
  1. Hooks:你的 PreToolUse 函式最先執行,可以直接擋(hook 的「放行」不會跳過後面的規則)。
  2. deny 規則:disallowed_tools。裸名稱("Bash")乾脆把工具從清單拿掉;有範圍的("Bash(rm *)")在這裡擋。
  3. ask 規則:settings.json 才能設,符合的一律送去問 can_use_tool。
  4. 權限模式:permission_mode(下表)。
  5. allow 規則:allowed_tools。另外,工作目錄內讀檔、find/ls 這類唯讀指令本來就免審。
  6. can_use_tool:前面都沒決定的,才來問你的函式;沒設函式就沒人可問——拒絕。
permission_mode行為(官方文件,2026-09 查證)
default模式本身不放行任何東西;需要核准又沒被 allow 規則放行的,送去問 can_use_tool
acceptEdits工作目錄內的改檔,以及 mkdir/touch/rm/mv/cp/sed 自動放行
plan只看、只規劃:改檔一律送審,連 allow 規則都不算
dontAsk不問人:沒被規則放行的一律拒絕,can_use_tool 永遠不會被叫
bypassPermissions全部放行——但 hooks、deny、ask 規則排在它前面,照樣擋得住
auto由一個模型分類器決定放行或拒絕(可用性見官方文件)

錄影裡實際撞到的三件反直覺的事:

① headless 預設=能看不能動。錄影①什麼都沒設:Read 和 find 直接跑,改檔卻被拒——「Claude requested permissions to write to ~/shop-demo/shop/cart.py, but you haven't granted it yet.」 因為沒有人可以按「允許」。模型最後只能把修法用文字告訴你,測試照樣是紅的——而 subtype 仍是 success。

② allowed_tools 是放行清單,不是限制清單。設 bypassPermissions 再把 allowed_tools 只寫 ["Read"], Bash 照樣執行(實測:python3 -c "print(6*7)" 輸出 42)。沒列到的工具掉到權限模式, bypass 一律放行。

③ deny 規則和 hook 連 bypass 都擋得住。同樣是 bypassPermissions, 加上 disallowed_tools=["Bash(rm *)"],rm -rf build 被擋下: 「Permission to use Bash with command rm -rf build && echo "清理完成" has been denied.」——它們排在權限模式前面。

03 · 你的程式當守門員

hooks 與 can_use_tool

一句話重點:hook 在每一次工具呼叫前都會跑(第一道閘門),適合護欄與稽核; can_use_tool 只在「沒有規則放行、需要問人」時才被叫(最後一道),適合人工審批。

錄影③的 hook:一個記錄每次呼叫,一個規定 Bash 只准跑 unittest(參考程式):

async def bash_guard(input_data, tool_use_id, context): cmd = input_data["tool_input"].get("command", "") if not cmd.strip().startswith("python3 -m unittest"): return {"hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "這個專案只准用 `python3 -m unittest` 跑測試;其他 shell 指令一律擋下。", }} return {} # 回空物件=不插手 options = ClaudeAgentOptions( allowed_tools=["Read", "Edit", "Bash"], hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[audit]), # 每次呼叫都記一筆 HookMatcher(matcher="Bash", hooks=[bash_guard])]}, )

錄影裡它擋了三次:兩個 find、一個 ls -la——模型只是想找檔案。 原因:在 Linux/macOS 上 Claude Code 預設不給 Glob、Grep 工具,搜尋走 Bash 的 find/grep,所以照樣經過你的 Bash 護欄。 擋下的理由會回給模型:它讀到之後改用 Read 讀檔、用 python3 -m unittest discover -v 跑測試,最後照樣修好。

錄影⑤換成審批函式:

async def approver(tool_name, tool_input, context): path = str(tool_input.get("file_path", "")) if tool_name in ("Edit", "Write") and "/shop/" in path: return PermissionResultAllow() return PermissionResultDeny(message="使用者拒絕:這次只准修改 shop/ 底下的程式碼,不准執行任何指令。") options = ClaudeAgentOptions(can_use_tool=approver)

函式被問了兩次:一次 python -m pytest(拒絕)、一次改 shop/cart.py(放行); Read 一次都沒來問——它在第⑤道就放行了。 陷阱:同時設 allowed_tools=["Bash"] 和 can_use_tool, Bash 在第⑤道就被放行,你的函式永遠不會被叫到;SDK 會警告你(實測原文見測驗第 4 題)。 要「每一次都檢查」,用 hook。

plan 模式要配審批函式。錄影④只設了 permission_mode="plan": 沒接 can_use_tool 時,init 的工具清單裡根本沒有 ExitPlanMode (29 個;接了函式的⑤⑥是 32 個,多了 AskUserQuestion、EnterPlanMode、 ExitPlanMode)。模型寫好計畫卻交不出去,四處找工具、兩度嘗試改檔被擋, 繞了 22 輪、花了 $0.198,檔案一個字沒改——plan 模式確實守住了「不動手」,但流程卡死。 錄影⑥加上審批函式:模型呼叫 ExitPlanMode 交出計畫 → 函式核准 → 改檔 → 測試全過,14 輪、$0.107。

04 · 帳單與記憶

ResultMessage 與 session

一句話重點:ResultMessage 是整趟的收據(輪數、時間、token、花費、被擋清單); subtype="success" 只代表迴圈正常結束,不代表任務完成。

六段錄影的 subtype 全是 success,包括檔案一個字沒改的①④。 判斷有沒有做成要看 permission_denials(①有 4 筆),再自己驗證——跑測試、看 diff。 要設上限:max_turns 用完會得到 subtype="error_max_turns" (實測只給 2 輪:errors=["Reached maximum number of turns (2)"]), max_budget_usd 超過則是 error_max_budget_usd。

花費的大頭是 context。錄影②的第一次 API 呼叫就送出約 1.46 萬個 token——幾乎全是 29 個工具的說明書; 之後每一輪重送全部歷史(主線第 5 課的「上下文帳」),但 Claude Code 自動開了 prompt caching: 整趟送出約 17 萬個輸入 token,其中 16.2 萬是便宜的 cache_read(牌價的 0.1 倍)、 8,504 個是寫入快取(2 倍),完全沒快取的只有 82 個。整趟 $0.0495,用 Haiku 4.5 牌價自己重算對得上。

記憶在 session,不在模型。第一次跟它說「專案代號是藍鯨」,拿到 ResultMessage.session_id; 第二次 resume=session_id 問「代號是什麼」→「藍鯨」。第三次不帶 resume,它不知道—— 兩次錄影一次回「shop-demo」(拿資料夾名稱瞎猜)、一次回「找不到明確的專案代號」。 resume 一個不存在的 session 會直接丟例外:「No conversation found with session ID: …」。

05 · 擴充

自訂工具、子代理

一句話重點:@tool+create_sdk_mcp_server 把 Python 函式變成工具 (同一個行程裡的 MCP server,不用另外架伺服器);AgentDefinition 定義專職的子代理。
from claude_agent_sdk import tool, create_sdk_mcp_server @tool("lookup_order", "查詢訂單狀態。輸入訂單編號(例如 A1023),回傳出貨狀態、物流商與預計到貨日。", {"order_id": str}) async def lookup_order(args): oid = args["order_id"].strip().upper() if oid in ORDERS: return {"content": [{"type": "text", "text": json.dumps(ORDERS[oid], ensure_ascii=False)}]} return {"content": [{"type": "text", "text": f"查無訂單 {oid}"}], "is_error": True} shop = create_sdk_mcp_server(name="shop", version="1.0.0", tools=[lookup_order]) options = ClaudeAgentOptions( tools=[], # 不給任何內建工具 mcp_servers={"shop": shop}, allowed_tools=["mcp__shop__lookup_order"], # 名字會變成 mcp__伺服器__工具 )

問「A1023 和 B77 的狀態」:模型呼叫兩次 mcp__shop__lookup_order,B77 拿到 is_error 的「查無訂單」照樣接得住,3 輪、$0.0058——因為 tools=[] 拿掉了內建工具,第一次呼叫只有 1,339 個 token。 (訂單資料是教學用的假資料;MCP 協定本身的細節在補充 D。)

options = ClaudeAgentOptions( agents={"test-runner": AgentDefinition( description="執行專案的單元測試並回報哪些測試失敗。需要跑測試時使用。", prompt="你只負責執行 python3 -m unittest -v,列出失敗的測試……不要修改任何檔案。", tools=["Bash", "Read"], model="haiku")}, allowed_tools=["Read", "Bash", "Agent"], )

子代理有自己乾淨的 context,只把最終報告交回主代理;它自己的呼叫在串流裡帶 parent_tool_use_id。2.1.281 的坑:子代理預設丟到背景跑—— 第一次錄影主代理回一句「已在後台派遣,請稍候」就結束了(ResultMessage 先到); prompt 寫明「等它跑完拿到結果再回答」,模型才以 run_in_background: false 前景等待。 把「怎麼做某件事」打包成可重用知識的 Skills 在補充 E; 在終端機裡用 Claude Code 開發的實務在補充 F。

06 · 免費跑法

SDK 免費,模型要錢——或者換一個

一句話重點:claude-agent-sdk 本身免費(pip install), 花錢的是背後的模型。Claude Code 講的是 Anthropic Messages API:用 ANTHROPIC_BASE_URL 把它指到任何 Anthropic 相容端點,就能接免費的開源模型。
跑法要錢嗎我們驗證了什麼
Claude API(ANTHROPIC_API_KEY) 按 token 計費;官方定價頁寫新帳號有少量免費試用額度 本課所有 Claude 錄影(Haiku 4.5,修一次 bug 約 $0.05)
本機 Ollama(v0.14 起有 /v1/messages) 免費,用自己的電腦 沒在本機實測;設定照 Ollama 官方文件:ANTHROPIC_BASE_URL=http://localhost:11434、 ANTHROPIC_AUTH_TOKEN=ollama,建議模型 context ≥ 32K
Ollama Cloud 免費方案 含入門額度、只能用入門模型、同時 1 個請求(ollama.com/pricing,2026-09) 沒實測
自架 vLLM(0.26.0 有 /v1/messages) 免費,用自己的 GPU ✅ 實測 qwen3.5-2b:見下文與實驗場 5️⃣

我們把同一個修 bug 任務接到 vLLM 上的 qwen3.5-2b(20 億參數,context 只有 16,384)錄了三次, 撞到的兩個錯誤就是你自己接小模型時會撞到的:

A. 什麼都不調:Claude Code 對不認得的模型預設要 32,000 個輸出 token,伺服器直接回 500 「max_completion_tokens=32000 cannot be greater than max_model_len…」。 B. 用 CLAUDE_CODE_MAX_OUTPUT_TOKENS=4096 調小:換 prompt 爆掉—— 光內建工具的說明書就「at least 12289 input tokens」。 C. 只開 3 個工具+短 system prompt:第一次呼叫降到 2,068 個 token,小模型真的讀檔、改對公式、跑測試全過 (9 輪、約 3.5 分鐘)。A、B 兩次的 subtype 也是 success (is_error=True):錯誤被包成一句文字,不會丟例外。

options = ClaudeAgentOptions( model="你拉下來的模型名", # 例:Ollama 官方文章建議的 qwen3-coder、gpt-oss:20b env={ "ANTHROPIC_BASE_URL": "http://localhost:11434", # 本機 Ollama "ANTHROPIC_AUTH_TOKEN": "ollama", # 要有值,Ollama 不檢查 "ANTHROPIC_API_KEY": "", "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "4096", # 預設 32000,小 context 模型會 500 }, tools=["Read", "Edit", "Bash"], # 只開需要的:工具說明書越少,prompt 越小 system_prompt="你是程式助理……先讀檔、再修改,最後用 python3 -m unittest 驗證。", allowed_tools=["Read", "Edit", "Bash"], )

接本機模型時 total_cost_usd 沒有意義:錄影 C 報 $0.1306、 costBasis 寫 unknown——剛好是拿 $4/$20(Opus 5.5 的牌價)去乘 token 數算出來的, 你真正付的是電費。完整可跑的錄製腳本在 GitHub: spike_genai_agent_sdk.py (free 段預設就指向本機 Ollama,用環境變數 FREE_BASE_URL/FREE_MODEL 改; 我們實測時指向的是 vLLM)。

另一條規則要知道:你把 Agent SDK 做成產品給別人用時,官方要求用 API key(或 Bedrock/Vertex/Foundry)認證; 除非事先獲准,不能讓你的使用者用 claude.ai 帳號登入、吃他們的訂閱額度(Agent SDK 官方文件,2026-09 查證)。

07 · 速查

本課速查卡

query() / ClaudeSDKClient 跑一趟 agent,吐出訊息串;ClaudeSDKClient 可多輪對話、中途 interrupt()、改權限模式。
訊息串 SystemMessage(init) → AssistantMessage(TextBlock/ToolUseBlock)→ UserMessage(ToolResultBlock)→ … → ResultMessage。
六道閘門 hooks → deny → ask → 權限模式 → allow → can_use_tool;第一個做決定的說了算。
allowed_tools / disallowed_tools 放行清單 / 禁止清單。allowed_tools 不限制 bypassPermissions;要禁止用 disallowed_tools。
hooks 每次呼叫前(PreToolUse)/後(PostToolUse)跑你的函式;deny 連 bypass 都擋得住。
can_use_tool 沒被規則放行、需要問人時才叫你的函式;會被整個工具的 allow 規則遮蔽。
@tool + create_sdk_mcp_server Python 函式變工具,同一個行程裡的 MCP server;名字是 mcp__server__tool。
AgentDefinition 專職子代理:自己的 prompt、工具、模型;2.1.281 起預設背景執行。
ResultMessage 收據:subtype、is_error、num_turns、total_cost_usd、permission_denials、session_id。success ≠ 任務完成。
ANTHROPIC_BASE_URL 把 harness 接到任何 Anthropic 相容端點(Ollama、vLLM);小模型記得調輸出上限、少開工具。
08 · 實戰

換你動手

LEVEL 1

在實驗場 1️⃣ 選「③ 放行+PreToolUse hook」播到底:hook 擋了幾次、擋的是哪些指令?明明只想管「跑什麼測試」,為什麼連找檔案都被擋?模型怎麼繞過去的?

LEVEL 2

在 2️⃣ 模擬器選「Bash:rm -rf build」+ bypassPermissions:找出兩種還擋得住它的設定,再找出一種「看起來會擋、其實擋不住」的。

LEVEL 3

幫 CI 設計一組設定:可以讀檔、可以改 shop/cart.py、可以跑 python3 -m unittest,但 rm -rf build 一定要被擋,而且沒有人在旁邊按「允許」。用 2️⃣ 把五個呼叫逐一驗過。

卡住了?三題在實驗場最後一格都有折疊解答——先自己做,再打開對照。

09 · 驗收

情境測驗

離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。

Q1 情境題

你要在 CI 上讓 agent 自動修測試:可以讀檔、改程式、跑 python3 -m unittest,但絕對不能刪檔,而且 CI 上沒有人可以按「允許」。哪組設定最合適?

B:dontAsk 讓「沒被規則放行的一律拒絕」,不需要有人在場;allowed_tools 精準放行三件事;disallowed_tools 的 Bash(rm *) 是雙保險——deny 規則排在權限模式前面,哪天有人改成 bypass 也擋得住。A 是本課實測過的陷阱:allowed_tools 是放行清單不是限制清單,bypass 底下沒列到的 Bash 照樣執行(錄影 g3)。C 的結果就是錄影①:headless 沒人按允許,改檔被拒、測試還是紅的,而且 prompt 不是護欄。D 最陰險:acceptEdits 會自動放行工作目錄內的 rm、mv 這類檔案操作,「不准刪檔」只剩一句拜託。

Q2 情境題

產品需求:「agent 先交修改計畫,主管在網頁上按核准,它才可以動手改檔。」下面哪個設計做得到?

B,這正是錄影⑥:模型呼叫 ExitPlanMode 交出計畫 → 你的函式(這裡可以換成等主管按鈕)核准 → 改檔 → 測試全過,14 輪、$0.107。A 是錄影④:沒接 can_use_tool 時工具清單裡根本沒有 ExitPlanMode,模型寫好計畫交不出去,繞了 22 輪、花了 $0.198,一個字沒改。C 把「先審後做」變成「先做後審」,需求直接不成立。D 看似合理,其實是把主管的核准步驟自動化掉——allow 規則放行的呼叫不會來問任何人。

Q3 情境題

同事的 CI 腳本這樣判斷 agent 有沒有修好 bug:if msg.subtype == "success": mark_green()。看過本課錄影後,你會建議怎麼改?

C。success 只代表迴圈正常結束:錄影①的 subtype 是 success、is_error 也是 False,但被擋了 4 次(2 次跑測試、2 次改檔)、測試還是紅的。permission_denials 告訴你「它想做卻被擋的事」,而真正的驗收要看世界的狀態——我們每段錄影跑完都重跑 unittest,就是這個原因。A 不夠:①的 is_error 也是 False。B 沒意義:①跑了 10 輪。D 相信模型的自述——模型可能宣稱成功卻沒做到,驗收不能靠它自己說。

Q4 錯誤診斷

你寫了一個審批函式,想讓每一個 Bash 指令都先經過它,結果它一次都沒被叫到、指令照樣執行。啟動時 SDK 印出這段警告(實測原文)。最可能的原因與修法?

options = ClaudeAgentOptions( allowed_tools=["Bash"], can_use_tool=my_approver, ) CanUseToolShadowedWarning: can_use_tool will not be invoked for: Bash. An allowed_tools entry that allows a whole tool auto-approves it before the callback is consulted. To gate every tool call, use a PreToolUse hook; or narrow the entry so calls fall through to can_use_tool. Allow rules from settings files can also shadow the callback but are not visible here.

B。閘門順序是 hooks → deny → ask → 模式 → allow → can_use_tool:裸名稱的 allow 規則會在第⑤道放行整個工具,第⑥道的函式根本沒機會發言——這正是警告在說的事(本課錄影 g5:函式一次都沒被叫,print(6*7) 照樣執行)。修法二選一:縮小 allow 的範圍,讓其他呼叫掉到函式;或改用 hook,它在第一道、每次都跑。A 是對的好習慣但不是這裡的病因;C 在 0.2.159 不成立,我們實測 query() 搭配 can_use_tool 可以正常執行;D 恰好相反——bypass 會在第④道就全部放行,函式更輪不到。

Q5 錯誤診斷

你用 ANTHROPIC_BASE_URL 把 Agent SDK 接到本機一個 context 16K 的開源模型,也照教學把 CLAUDE_CODE_MAX_OUTPUT_TOKENS 調成 4096,工具沒特別設定。ResultMessage 回來是這樣(實測原文,端點位址已遮)。下一步該怎麼做?

subtype='success' is_error=True num_turns=1 result: API Error: 500 This model's maximum context length is 16384 tokens. However, you requested 4096 output tokens and your prompt contains at least 12289 input tokens, for a total of at least 16385 tokens. Please reduce the length of the input prompt or the number of requested output tokens. (parameter=input_tokens, value=12289). This is a server-side issue, usually temporary — try again in a moment. If it persists, check your inference gateway (<你的端點>).

B。任務還沒開始,第一次呼叫就要 12,289 個輸入 token——使用者的 prompt 才幾十字,其餘是 20 個內建工具的說明書。本課實測:只開 3 個工具、配短 system prompt,第一次呼叫降到 2,068 token,qwen3.5-2b 真的把 bug 修好、測試全過;Ollama 官方也建議 context ≥ 32K。A 治標不治本:12,289 + 1,024 塞得下第一輪,但每一輪都重送全部歷史,幾輪之後照樣爆,還讓模型沒空間回答。C 被錯誤訊息的套話誤導:這個 500 是確定性的長度超限,重試一百次結果一樣。D 與實測相反:同一個 2B 模型在精簡設定下完成了讀檔、改檔、跑測試。另外注意 subtype 仍是 success——要看 is_error。

Python 環境載入中(首次約 30–60 秒)…讀完第 1 節它就好了