Claude 能不能自己執行函式?在 Tool use 裡,Claude 負責提出工具請求,你的程式負責執行,再把結果送回對話;第 20–31 堂從自訂工具一路做到多輪工具對話與內建搜尋,核心是用
stop_reason驅動一個可持續的代理迴圈,實際工具行為仍會受模型版本與帳號設定影響。
上一篇 〈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、訊息區塊、多輪對話、串流,以及文字編輯和網路搜尋工具一路接起來。
工具使用的分工可以先記成四步:
使用者問題
↓
Claude 判斷需要工具,提出 tool request
↓
你的伺服器執行函式,取得外部資料
↓
把 tool result 送回 Claude
↓
Claude 根據原問題與新資料產生回應
例如使用者問舊金山的天氣,Claude 不會因為「知道怎麼回答」就突然取得即時天氣。它要先提出需要哪些資料,再由你的伺服器呼叫天氣 API,最後把結果放回對話。
這和一般聊天請求最大的差別,在於工具不是藏在模型裡的一個魔法按鈕。Claude 負責判斷要不要用、要傳哪些參數;實際執行權仍在你的程式。
課程用「設定未來日期的提醒」當作練習專案。表面上只是請 Claude 設定一個看醫生的提醒,實際上會碰到三個模型本身不一定可靠的地方:
| 模型缺口 | 對應工具 |
|---|---|
| 不一定知道精確的現在時間 | 取得目前日期時間 |
| 日期加法不適合完全交給模型猜 | 將時間長度加到日期時間 |
| 沒有內建提醒系統 | 設定提醒 |
這個專案的設計起點不是「我想展示三個工具」,而是「模型缺少哪一個能力」。當缺口被定義清楚後,工具才有明確的責任範圍。
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 看得到這段訊息,下一輪有機會帶著修正後的參數重新呼叫。
函式寫好後,還要用 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 沒有說明何時該用,模型仍可能在錯的情境呼叫工具。
一旦把 tools 傳進 Messages API,回應就不再保證只有一段文字。Claude 的 assistant message 可能同時包含:
| 區塊 | 用途 |
|---|---|
text |
給人看的說明,例如「我來查詢目前時間」 |
tool_use |
告訴程式要呼叫哪個工具,以及要傳哪些參數 |
tool_use 會帶有追蹤用的 ID、工具名稱和輸入字典。對話歷史要把完整的 response.content 存回去:
messages.append({"role": "assistant", "content": response.content})
如果只把文字區塊留下來,下一次請求就失去 Claude 剛才要求執行哪個工具的結構,對話也無法正確接續。
工具執行完之後,結果會放在一則 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 才能理解對話歷史裡提到的工具。
如果問題只需要一個工具,流程是一次請求加一次結果;但「從今天起 103 天後是星期幾」需要先取得現在時間,再把天數加上去。這時一個問題會穿過多輪 API request:
使用者問題
→ get_current_datetime
→ tool result
→ add_duration_to_datetime
→ tool result
→ Claude 的最終回答
這就是為什麼工具使用不只是一個 if。應用程式必須保存 assistant 的完整回應、執行所有 tool_use、建立 tool result,再把更新後的 messages 送回 Claude。
判斷 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 長這樣:
為了方便閱讀,以下將長
id簡化成111、222,方便看出tool_use與tool_result的配對關係。
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 從日期推導出來的結果,工具本身只負責日期加法。
在既有迴圈上增加工具,主要就是四個步驟:建立函式、定義 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。
串流工具參數時,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 契約。
文字編輯工具的特別之處,在於 Claude 已經知道一份內建 schema;但實際檢視、建立、替換、插入和撤銷檔案的程式碼,仍然要由你的應用程式負責。
目前 source repo 的實作依模型版本選擇工具版本字串:Claude 4 以後使用 text_editor_20250728 和 str_replace_based_edit_tool,較早模型則使用其他版本。這裡正好有一個課程教材和實作版本不同的例子:教材示範仍列出 text_editor_20250124、text_editor_20241022,不能直接把舊範例當成所有模型的通用設定。
換句話說,內建 schema 減少的是「你要怎麼描述工具」的工作,檔案沙盒、路徑檢查和每個 command 的實作責任仍在你的程式。
網路搜尋工具和文字編輯工具剛好相反:搜尋本身由 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 和結果內容印出來,所以這段只保留流程證據:搜尋在伺服器端完成,應用程式收到工具結果後直接進入最終回應。若要在產品裡顯示完整搜尋過程,就要像前面的自訂工具一樣,把區塊內容和引用一併記錄下來。
有 7 題。官方頁面本身是繁中,以下保留實際題目與正確答案。
答案:告訴 Claude 您的函式預期需要哪些參數以及如何使用它
答案:Claude 提供 schema,但您可能仍需要實作一些功能
答案:使用工具存取外部資訊
答案:初始請求 → 工具請求 → 資料擷取 → 最終回應
答案:包含文字和工具使用區塊的多區塊訊息
答案:當需要多個工具時,它減少了來回通訊的次數
答案:查看 stop_reason 欄位是否為「tool_use」
完整程式碼放在 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 重現。
Building with the Claude API|GitHub Source Code
我是 Jasper,從事軟體開發,目前專注打造 AI 工作流程。
官方圖解與完整表格在 Blog 版,和我一起探討更多 AI 議題 🚀