上一篇 〈Building with the Claude API:提示工程的評估與迭代〉 把 Prompt 寫得更清楚,這一篇開始讓 Claude 碰到 Prompt 以外的世界。問題也跟著換了:如果 Claude 需要現在的時間、資料庫裡的內容,或某個外部 API 的結果,它要怎麼把需求交給你的程式?
本篇涵蓋 Claude Academy 的 Building with the Claude API 中 Tool use with Claude(使用 Claude 進行工具使用) 的第 20–31 堂,共 12 堂課與 Quiz 4。這一組從「設定提醒」的小專案開始,把工具函式、JSON schema、訊息區塊、多輪對話、串流,以及文字編輯和網路搜尋工具一路接起來。
20. Introducing tool use(工具使用簡介)
工具使用的分工可以先記成四步:
使用者問題 ↓Claude 判斷需要工具,提出 tool request ↓你的伺服器執行函式,取得外部資料 ↓把 tool result 送回 Claude ↓Claude 根據原問題與新資料產生回應例如使用者問舊金山的天氣,Claude 不會因為「知道怎麼回答」就突然取得即時天氣。它要先提出需要哪些資料,再由你的伺服器呼叫天氣 API,最後把結果放回對話。
這和一般聊天請求最大的差別,在於工具不是藏在模型裡的一個魔法按鈕。Claude 負責判斷要不要用、要傳哪些參數;實際執行權仍在你的程式。
21. Project overview(專案概覽)
課程用「設定未來日期的提醒」當作練習專案。表面上只是請 Claude 設定一個看醫生的提醒,實際上會碰到三個模型本身不一定可靠的地方:
| 模型缺口 | 對應工具 |
|---|---|
| 不一定知道精確的現在時間 | 取得目前日期時間 |
| 日期加法不適合完全交給模型猜 | 將時間長度加到日期時間 |
| 沒有內建提醒系統 | 設定提醒 |
這個專案的設計起點不是「我想展示三個工具」,而是「模型缺少哪一個能力」。當缺口被定義清楚後,工具才有明確的責任範圍。
22. Tool functions(工具函式)
Tool function 就是一個普通的 Python 函式,但它會被 Claude 提出的參數呼叫,所以輸入驗證和錯誤訊息都會成為模型可見的介面。
課程特別強調三件事:函式名和參數名要描述用途;無效輸入要拒絕;錯誤訊息要告訴 Claude 下一步可以怎麼修正。
def get_current_datetime(date_format="%Y-%m-%d %H:%M:%S"): if not date_format: raise ValueError("date_format cannot be empty") return datetime.now().strftime(date_format)如果 Claude 傳入空的 date_format,date_format cannot be empty 比單純的 500 error 更有用。Claude 看得到這段訊息,下一輪有機會帶著修正後的參數重新呼叫。
23. Tool schemas(工具結構)
函式寫好後,還要用 JSON schema 告訴 Claude:這個工具叫什麼、什麼時候該用、需要哪些輸入。
一份工具規格至少有三個重要欄位:
| 欄位 | 作用 |
|---|---|
name | 清楚、容易辨識的工具名稱 |
description | 工具做什麼、何時使用、會回傳什麼 |
input_schema | 參數的 JSON 結構、型別與描述 |
get_current_datetime_schema = { "name": "get_current_datetime", "description": "Returns the current date and time formatted according to the specified format", "input_schema": { "type": "object", "properties": { "date_format": { "type": "string", "description": "A string specifying the format of the returned datetime. Uses Python's strftime format codes.", "default": "%Y-%m-%d %H:%M:%S" } }, "required": [] }}這裡的 description 不是文件裡可有可無的註解,它是 Claude 判斷使用時機的重要依據。名稱寫得很清楚,但 description 沒有說明何時該用,模型仍可能在錯的情境呼叫工具。
24. Handling message blocks(處理訊息區塊)
一旦把 tools 傳進 Messages API,回應就不再保證只有一段文字。Claude 的 assistant message 可能同時包含:
| 區塊 | 用途 |
|---|---|
text | 給人看的說明,例如「我來查詢目前時間」 |
tool_use | 告訴程式要呼叫哪個工具,以及要傳哪些參數 |
tool_use 會帶有追蹤用的 ID、工具名稱和輸入字典。對話歷史要把完整的 response.content 存回去:
messages.append({"role": "assistant", "content": response.content})如果只把文字區塊留下來,下一次請求就失去 Claude 剛才要求執行哪個工具的結構,對話也無法正確接續。
25. Sending tool results(傳送工具結果)
工具執行完之後,結果會放在一則 user message 裡。這個格式乍看有點反直覺,但它代表「應用程式把外部世界的結果送回對話」。
messages.append({ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": response.content[1].id, "content": "15:04:22", "is_error": False }]})這裡最不能弄錯的是 tool_use_id。它必須和 Claude 原本提出的 tool_use ID 相同,才能把結果配回正確的工具請求。一次回應可以包含多個工具呼叫,即使結果回來的順序不同,也要靠 ID 配對,不能只靠陣列位置。
還有一個容易漏掉的細節:後續請求即使預期 Claude 會直接回答,也要繼續帶上工具 schema。Claude 需要這些 schema 才能理解對話歷史裡提到的工具。
26. Multi-turn conversations with tools(使用工具進行多輪對話)
如果問題只需要一個工具,流程是一次請求加一次結果;但「從今天起 103 天後是星期幾」需要先取得現在時間,再把天數加上去。這時一個問題會穿過多輪 API request:
使用者問題 → get_current_datetime → tool result → add_duration_to_datetime → tool result → Claude 的最終回答這就是為什麼工具使用不只是一個 if。應用程式必須保存 assistant 的完整回應、執行所有 tool_use、建立 tool result,再把更新後的 messages 送回 Claude。
27. Implementing multiple turns(實作多輪對話)
判斷 Claude 是否還要工具的關鍵欄位是 stop_reason:
if response.stop_reason != "tool_use": break # Claude is done, no more tools needed把它放進 while 迴圈後,工具使用就有了一個明確的終止條件:
def run_conversation(messages): while True: response = chat(messages, tools=[get_current_datetime_schema]) add_assistant_message(messages, response) print(text_from_message(response))
if response.stop_reason != "tool_use": break
tool_results = run_tools(response) add_user_message(messages, tool_results)
return messages這段迴圈就是整組課程最重要的模型:送出目前對話、保存 Claude 的訊息、判斷是否需要工具、執行工具並送回結果,直到 stop_reason 表示 Claude 已經完成回答。
實際執行「從今天起 103 天後是星期幾」時,我看到三次 API 請求:第一次取得目前時間,第二次把 103 天加到日期,第三次才產生最終回答。第二輪使用第一輪的工具結果,這才是 multi-turn 的具體樣子。
實際 run 的 log 長這樣:
uv run reminder_app.py
[第 1 輪] stop_reason=tool_use[text] 我來幫你計算從今天起 103 天後是星期幾。
首先,我需要獲得今天的日期,然後加上 103 天。[tool_use] name=get_current_datetime id=111 input={'date_format': '%Y-%m-%d %H:%M:%S'}[tool_result] id=111 is_error=False content=2026-10-05 21:26:56
[第 2 輪] stop_reason=tool_use[text] 現在我將今天的日期加上 103 天:[tool_use] name=add_duration_to_datetime id=222 input={'datetime_str':'2026-10-05 21:26:56', 'duration': 103, 'unit': 'days'}[tool_result] id=222 is_error=False content=2027-01-16 21:26:56
[第 3 輪] stop_reason=end_turn[text] 根據計算,從今天 (2026年10月5日) 起 103 天後是 **2027年1月16日**,這一天是 **星期六**。這段 log 可以直接看出對話如何累加:第二輪的 datetime_str 來自第一輪工具回傳的字串,而每個 tool_use 的 id 都和對應的 tool_result 一致。最後的「星期六」則是 Claude 從日期推導出來的結果,工具本身只負責日期加法。
28. Using multiple tools(使用多個工具)
在既有迴圈上增加工具,主要就是四個步驟:建立函式、定義 schema、把 schema 放進工具清單、在工具路由裡接上實作。
response = chat(messages, tools=[ get_current_datetime_schema, add_duration_to_datetime_schema, set_reminder_schema,])「幫我查詢現在時間,以及 2050 年 1 月 1 日之後的 177 天」這類請求,會讓 Claude 同時提出兩個互不依賴的工具請求。工具增加了,核心的對話迴圈不必跟著重寫,這是前面幾堂課把責任拆開的回報。
這裡其實有兩種工具使用模式。前面的「從今天起 103 天後」需要先知道現在時間,所以是依序連鎖:第一輪取得時間,第二輪才能把天數加上去。這次的日期是固定的 2050-01-01,get_current_datetime 和 add_duration_to_datetime 互不依賴,Claude 可以在同一輪提出兩個 tool_use,應用程式再把兩個結果放在同一則 user 訊息裡送回去。
這次實際 run 的 log:
[第 1 輪] stop_reason=tool_use[text] 我來幫你查詢這兩個問題。[tool_use] name=get_current_datetime id=111 input={}[tool_use] name=add_duration_to_datetime id=222 input={'datetime_str':'2050-01-01 00:00:00', 'duration': 177, 'unit': 'days'}[tool_result] id=111 is_error=False content=2026-10-05 21:36:45[tool_result] id=222 is_error=False content=2050-06-27 00:00:00
[第 2 輪] stop_reason=end_turn[text] 根據查詢結果:
1. 現在的時間是 2026-10-05 21:36:45(晚上9點36分45秒)2. 2050-01-01 00:00:00 之後 177 天是 2050-06-27 00:00:00(2050年6月27日)這次只有兩次 API 請求,比前面的三次少一輪,因為第一輪已經送出所有不需要互相等待的工具請求。兩個 tool_result 都帶著各自對應的 tool_use_id,第二輪收到後直接進入 end_turn。這個 commit 的程式沒有再修改,實際版本就是 cea16fb。
29. Fine grained tool calling(細粒度工具呼叫)
串流工具參數時,SDK 可能會提供 InputJsonEvent,其中包含一段增量 JSON 的 partial_json,以及目前累積內容的 snapshot。
課程介紹的 fine-grained tool calling 會停用 API 端的 JSON 驗證,讓工具參數更早抵達應用程式,減少頂層欄位之間的緩衝等待;代價是程式必須自己處理無效 JSON:
try: parsed_args = json.loads(chunk.snapshot)except json.JSONDecodeError: print("Received invalid JSON, continuing...")這個選項適合需要顯示工具參數即時進度,或對延遲非常敏感的情境。官方的結論仍然保守:多數應用程式使用預設驗證行為就足夠。
筆記另外把傳輸層補成 SSE(Server-Sent Events),並指出 SDK 的 input_json 和 snapshot 是高階封裝,而非 API 原生事件名稱。這段補充目前還沒有逐一對照官方文件確認事件名稱,因此先把它當成實作方向,不把它寫成已驗證的 API 契約。
30. The text edit tool(文字編輯工具)
文字編輯工具的特別之處,在於 Claude 已經知道一份內建 schema;但實際檢視、建立、替換、插入和撤銷檔案的程式碼,仍然要由你的應用程式負責。
目前 source repo 的實作依模型版本選擇工具版本字串:Claude 4 以後使用 text_editor_20250728 和 str_replace_based_edit_tool,較早模型則使用其他版本。這裡正好有一個課程教材和實作版本不同的例子:教材示範仍列出 text_editor_20250124、text_editor_20241022,不能直接把舊範例當成所有模型的通用設定。
換句話說,內建 schema 減少的是「你要怎麼描述工具」的工作,檔案沙盒、路徑檢查和每個 command 的實作責任仍在你的程式。
31. The web search tool(網路搜尋工具)
網路搜尋工具和文字編輯工具剛好相反:搜尋本身由 Claude 的伺服器端能力處理,你主要需要提供 schema 與使用限制。
web_search_schema = { "type": "web_search_20250305", "name": "web_search", "max_uses": 5}max_uses 用來限制一次任務最多搜尋幾次,因為 Claude 可能根據初始結果再發出後續搜尋。需要限定來源時,也可以加上 allowed_domains:
web_search_schema = { "type": "web_search_20250305", "name": "web_search", "max_uses": 5, "allowed_domains": ["nih.gov"]}這次實際 run 時,我們沒有執行搜尋函式;回應裡出現的是 server_tool_use 和 web_search_tool_result 區塊,stop_reason 直接是 end_turn。這和自訂工具的責任邊界不同,也提醒我:看到「工具」這個名稱時,還要先確認到底是應用程式執行,還是 Anthropic 伺服器端執行。
這次 logger 實際留下的核心 log 是:
[stop_reason=end_turn][server_tool_use][web_search_tool_result][text] ...(文字被拆成多個區塊,中間夾著引用)這裡沒有把搜尋 query 和結果內容印出來,所以這段只保留流程證據:搜尋在伺服器端完成,應用程式收到工具結果後直接進入最終回應。若要在產品裡顯示完整搜尋過程,就要像前面的自訂工具一樣,把區塊內容和引用一併記錄下來。
Course Quiz 4(工具使用測驗)
有 7 題。官方頁面本身是繁中,以下保留實際題目與正確答案。
1. 在使用 Claude 工具時,JSON schema 的主要目的是什麼?
答案:告訴 Claude 您的函式預期需要哪些參數以及如何使用它
2. Claude 內建的文字編輯器和網路搜尋工具與自訂工具有何不同?
答案:Claude 提供 schema,但您可能仍需要實作一些功能
3. Claude 預設只能存取其訓練資料中的資訊。是什麼讓 Claude 能夠取得即時、最新的資訊?
答案:使用工具存取外部資訊
4. 工具使用工作流程中正確的步驟順序是什麼?
答案:初始請求 → 工具請求 → 資料擷取 → 最終回應
5. 當 Claude 使用工具時,它會回傳什麼類型的訊息結構?
答案:包含文字和工具使用區塊的多區塊訊息
6. batch 工具解決了什麼問題?
答案:當需要多個工具時,它減少了來回通訊的次數
7. 您如何判斷 Claude 是否想在對話中進行另一次工具呼叫?
答案:查看 stop_reason 欄位是否為「tool_use」
實作地圖:每堂課一個 commit
完整程式碼放在 claude-academy-api-app。第 20、21、26 堂是概念或偽程式碼,沒有獨立 commit;第 22 堂開始沿著 reminder_app.py 一路累加。
| 課程 | 主題 | 實作內容 | Commit |
|---|---|---|---|
| 20 | Introducing tool use | 工具使用概念 | — |
| 21 | Project overview | 設定提醒專案的需求 | — |
| 22 | Tool functions | 工具函式與錯誤訊息 | 6d56234 |
| 23 | Tool schemas | name、description、input_schema | e1ac177 |
| 24 | Handling message blocks | 處理 text 與 tool_use | 8a12505 |
| 25 | Sending tool results | 以 tool_use_id 配對工具結果 | 4a1cd22 |
| 26 | Multi-turn conversations with tools | 多輪流程偽程式碼 | — |
| 27 | Implementing multiple turns | stop_reason 與 while 迴圈 | cd82a7f |
| 28 | Using multiple tools | 加入日期運算與提醒工具 | cea16fb |
| 29 | Fine-grained tool calling | 串流工具參數 | d5ec257 |
| 30 | The text edit tool | 文字編輯工具與沙盒 | a69ccf6 |
| 31 | The web search tool | 伺服器端網路搜尋工具 | 95bd02f |
小結
這門課專注在處理「對話」本身。外部工具的部分也很新鮮:先寫好一個 function,再寫一份 schema,描述 Claude 什麼時候使用它、需要帶哪些參數。schema 讓模型知道工具怎麼用,訊息區塊保留請求結構,tool_use_id 把結果送回正確位置,stop_reason 再決定要不要進入下一輪。
平常在 desktop 或 CLI 使用 Claude,我們知道 Context 會一路疊加;API 沒有記憶,所以由 App 保存 messages,再把前面的 Prompt、Claude 的回應和新的問題一起送出去。道理本身不難理解,真正有趣的是課程把這件事拆成一個可以親手實作的過程:先用很簡單的方式記下對話,再在下一次請求時把前面的歷史接回去,看著它怎麼串起來。
看到三輪提醒範例跑完時,我才真正感覺到 agent loop 可以從一個普通的 while 迴圈長出來。畫面上看起來只是 Claude 一段一段回覆,拆開 log 之後,背後其實是連續的 API request:先取得現在時間,再把結果交給下一個工具,最後才生成答案。工具算日期、模型推星期幾,兩件事的證據邊界仍然不同,這也提醒我不能只看最後答案正不正確。
因為程式碼在我手上,我刻意把每一次工具呼叫、工具名稱、參數和回傳結果都印出來。這個過程比只看最後答案更有感:你會很清楚看到 Claude 從使用者的意圖判斷要用哪些工具,收到結果後發現資訊還不夠,再提出下一次請求。那些看起來啪啦啪啦出現的文字,拆開來看,其實是連續不斷的工具調用,最後才組成我們看到的答案。把這個過程拆出來觀察,讓我印象非常深刻,也很有趣。
想把今天的範例一路跑起來,可以沿著實作地圖裡的逐堂 commit 重現。