精華筆記

· @aihub.tw

Claude API 開發實戰

Tool Use:讓 Claude 呼叫你的函式

Tool Use:讓 Claude 呼叫你的函式

你花了整個下午讓 Claude 幫你建對話機器人。一切運作良好,直到用戶問了一句:「現在台北氣溫幾度?」

Claude 沉默了一秒,然後禮貌地回答:「我的訓練資料有截止日期,無法提供即時天氣。」

對話就此斷掉。你的機器人變成了一個知識淵博但兩手空空的學者:能說天氣原理,卻不能開窗看看外面下不下雨。

Tool Use 就是那扇窗。它讓 Claude 不只是「說」,還能「做」:呼叫你寫的函式、查資料庫、呼叫外部 API,然後把結果整合進回答裡。這是從「聊天機器人」升級到「AI agent」的關鍵一步。

這堂課適合誰 適合:想讓 Claude 能採取行動、串接外部功能的開發者。需要基礎:第 1 課的 Claude API 基礎呼叫、Python 函式與字典操作、基本的 JSON 概念(不需要前端知識)。前置課:第 4 課(結構化輸出)。

這堂學什麼

  • 工具定義三元素:name、description、input_schema 各自的職責與設計要領
  • tool use 完整迴圈:請求 → 偵測 tool_use → 執行函式 → 回傳 tool_result → Claude 繼續
  • tool_choice 控制行為:auto / any / 指定工具 / 停用工具
  • 多工具協作:同一個請求提供複數工具,讓 Claude 自己選擇或同時呼叫
  • 錯誤處理:工具執行失敗時怎麼告知 Claude
  • 實戰:天氣+計算機雙工具 agent 雛形,完整可跑

核心觀念:Claude 決策,你執行

Tool Use 最容易搞錯的心智模型是「Claude 會幫你跑程式」。不是的。正確理解是:

  • Claude 讀取你給的工具說明,決定要不要呼叫、呼叫哪個、傳入什麼參數
  • :接收 Claude 的決策,在自己的程式裡執行真正的函式,把結果回傳給 Claude
  • Claude:拿到結果後繼續生成最終回答

Claude 是大腦,你的程式是手腳。

這個設計有個重要含意:工具的安全性、效能、副作用全都是你負責的。Claude 只是說「我想呼叫 delete_user(id=42)」,你的程式決定要不要真的刪、要不要先請用戶確認、要不要做權限檢查。這讓你對整個系統有完整的控制權。

Tool Use 什麼時候用?什麼時候不用?

適合用 Tool Use 不需要 Tool Use
需要即時資料(天氣、股價、資料庫) 純文字生成、摘要、翻譯
需要精確計算(數字運算) 知識問答(Claude 本身知道)
需要寫入/更新外部系統 簡單的格式轉換
需要查詢用戶自己的資料 不需要外部資訊的對話

過度使用工具反而讓 Claude 慢、貴、複雜。先確認 Claude 本身知不知道,再決定要不要接工具。

Tool Use 心智模型:Claude 決策,你執行

工具定義的三個欄位

在開始寫程式之前,需要先理解 Claude 怎麼讀工具定義。你給 Claude 一份清單,說明「你可以使用這些工具,以下是每個工具的名稱、用途和需要的參數」。Claude 讀完之後,在回答用戶時會自己判斷:這個問題需不需要呼叫工具?要呼叫哪個?要傳什麼參數進去?這個判斷完全在 Claude 內部發生,你看不到思考過程,只看到最後的決策。

每個工具是一個字典,有三個欄位:

tool = {
    "name": "get_weather",                    # 工具識別名
    "description": "取得指定城市的即時天氣資訊,包含溫度與天氣狀況。"
                   "當用戶詢問天氣相關問題時呼叫此工具。",  # 讓 Claude 知道什麼時候用它
    "input_schema": {                          # JSON Schema 格式
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "城市名稱,例如 '台北' 或 'Tokyo'"
            },
            "unit": {
                "type": "string",
                "enum": ["celsius", "fahrenheit"],
                "description": "溫度單位,預設 celsius"
            }
        },
        "required": ["city"]  # 一定要傳入的參數
    }
}

三個欄位各有職責:

欄位 誰在看 設計重點
name 你的程式 用來識別要呼叫哪個函式,用底線命名
description Claude 說明何時使用這個工具,比「做什麼」更重要
input_schema Claude JSON Schema,Claude 用來填入正確參數

description 是最影響效果的欄位。要寫清楚觸發條件,例如「當用戶詢問…時呼叫此工具」,不能只寫功能說明。

input_schema 用標準的 JSON Schema 格式。properties 底下每個參數都要寫 typedescription,Claude 靠這些說明來填入正確的值。required 列出一定要有的參數——沒列進去的 Claude 可以省略,你的函式就要處理缺少參數的情況。

多個工具一起傳入時,Claude 會讀整份清單再做決定,所以工具名稱之間不能重複,description 也要寫得讓 Claude 能區分「什麼情況用哪個工具」,否則會產生錯誤的呼叫選擇。

工具定義三欄位分解:name / description / input_schema 各給誰看

tool use 完整迴圈

理解心智模型後,來看完整流程。這個迴圈是 Tool Use 的核心:

Tool Use 完整迴圈:六個步驟,步驟 2-5 可能重複多次

Step 1:安裝與準備

pip install anthropic
import anthropic
import json

client = anthropic.Anthropic()

Step 2:定義工具與模擬函式

先用假資料模擬天氣查詢和計算機功能:

# 工具定義清單
tools = [
    {
        "name": "get_weather",
        "description": "取得指定城市的即時天氣資訊。當用戶詢問天氣、溫度或氣候時呼叫此工具。",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名稱,例如 '台北'、'Tokyo'、'New York'"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "溫度單位"
                }
            },
            "required": ["city"]
        }
    },
    {
        "name": "calculate",
        "description": "執行數學計算。當用戶需要計算數字時呼叫此工具,避免用語言模型直接做算術。",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "數學運算式,例如 '(100 + 200) * 0.05'"
                }
            },
            "required": ["expression"]
        }
    }
]

# 模擬天氣資料(真實場景換成 API 呼叫)
def get_weather(city: str, unit: str = "celsius") -> dict:
    mock_data = {
        "台北": {"temp": 32, "condition": "晴天", "humidity": 75},
        "tokyo": {"temp": 28, "condition": "多雲", "humidity": 60},
        "new york": {"temp": 22, "condition": "小雨", "humidity": 80},
    }
    data = mock_data.get(city.lower(), {"temp": 25, "condition": "晴天", "humidity": 65})
    if unit == "fahrenheit":
        data["temp"] = data["temp"] * 9/5 + 32
    data["city"] = city
    data["unit"] = unit
    return data

# 計算機(用 eval 示範,正式環境請改用安全的計算函式庫)
def calculate(expression: str) -> dict:
    try:
        result = eval(expression, {"__builtins__": {}}, {})
        return {"result": result, "expression": expression}
    except Exception as e:
        return {"error": str(e), "expression": expression}

# 工具分派器:根據工具名稱呼叫對應函式
def execute_tool(tool_name: str, tool_input: dict) -> str:
    if tool_name == "get_weather":
        result = get_weather(**tool_input)
    elif tool_name == "calculate":
        result = calculate(**tool_input)
    else:
        result = {"error": f"未知工具: {tool_name}"}
    return json.dumps(result, ensure_ascii=False)

Step 3:基本單輪 Tool Use

先看最簡單的情況:Claude 呼叫一次工具:

def ask_with_tools(user_message: str) -> str:
    """完整 tool use 迴圈"""
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=tools,
            messages=messages
        )

        # 把 Claude 的回應加入對話歷史(包含 tool_use blocks)
        messages.append({"role": "assistant", "content": response.content})

        # 如果 Claude 決定不用工具,直接回答
        if response.stop_reason == "end_turn":
            # 找出文字回應
            for block in response.content:
                if block.type == "text":
                    return block.text
            return ""

        # Claude 要呼叫工具了
        if response.stop_reason == "tool_use":
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    print(f"[工具呼叫] {block.name}({block.input})")

                    # 執行工具
                    result = execute_tool(block.name, block.input)
                    print(f"[工具結果] {result}")

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,  # 對應 Claude 的呼叫 ID
                        "content": result
                    })

            # 把工具結果回傳給 Claude
            messages.append({"role": "user", "content": tool_results})
            # 繼續迴圈,等 Claude 整合結果

# 測試
answer = ask_with_tools("台北現在天氣怎麼樣?")
print(f"\n最終回答: {answer}")

執行結果大概長這樣:

[工具呼叫] get_weather({'city': '台北', 'unit': 'celsius'})
[工具結果] {"temp": 32, "condition": "晴天", "humidity": 75, "city": "台北", "unit": "celsius"}

最終回答: 台北目前天氣晴朗,氣溫 32°C,濕度 75%。今天是個好天氣!

注意幾個關鍵點:

  1. while True 迴圈是必要的。Claude 可能連續呼叫多個工具才結束
  2. messages.append assistant 的完整 content,不能只存文字,必須保留 tool_use blocks
  3. tool_use_id 必須和 Claude 給的 block.id 完全一致,否則 API 會報錯

**回傳的 messages 結構要注意:**工具結果要作為 role: "user" 的訊息送回,這是規定的對話格式——Claude 用 role: "assistant" 表達「我要呼叫工具」,你用 role: "user" 把工具結果送回,所以對話歷史永遠是 user → assistant → user → assistant 交替。

Step 4:JavaScript 版本(簡化核心迴圈)

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

async function askWithTools(userMessage) {
  const messages = [{ role: "user", content: userMessage }];

  while (true) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      tools: tools,  // 同 Python 定義的工具清單
      messages: messages,
    });

    messages.push({ role: "assistant", content: response.content });

    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find((b) => b.type === "text");
      return textBlock?.text ?? "";
    }

    if (response.stop_reason === "tool_use") {
      const toolResults = [];

      for (const block of response.content) {
        if (block.type === "tool_use") {
          const result = await executeTool(block.name, block.input);
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: result,
          });
        }
      }

      messages.push({ role: "user", content: toolResults });
    }
  }
}

Step 5:tool_choice 控制策略

預設是 auto,Claude 自己判斷要不要用工具。你可以強制指定:

# 強制一定要用工具(不能純文字回答)
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "any"},   # 必須用至少一個工具
    messages=messages
)

# 強制用特定工具
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "get_weather"},  # 一定要呼叫 get_weather
    messages=messages
)

# 禁用所有工具(有時候需要純文字回答)
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "none"},
    messages=messages
)

Step 6:多工具平行呼叫

Claude 不只能選一個工具,它可以在同一個 API 回應裡同時呼叫多個工具。這個能力很重要——如果你問「比較台北和東京的天氣」,Claude 不需要先查台北、等你回傳,再查東京,而是一次回傳兩個 tool_use block,你執行完兩個函式後把結果一起回傳,速度大幅提升。

預設行為是允許平行呼叫的。如果你希望 Claude 一次只呼叫一個工具(例如你需要嚴格控制執行順序),可以在 tool_choice 加上 disable_parallel_tool_use: true

Claude 可以在同一個回應裡呼叫多個工具。以下問法會觸發:

answer = ask_with_tools("台北和東京的天氣差多少度?差距是多少?")

Claude 可能一次回傳兩個 tool_use blocks:先查台北天氣、再查東京天氣,你拿到兩個結果後一起回傳,Claude 再算差值或請你計算。

[工具呼叫] get_weather({'city': '台北'})
[工具呼叫] get_weather({'city': 'tokyo'})
[工具呼叫] calculate({'expression': '32 - 28'})

最終回答: 台北 32°C,東京 28°C,相差 4 度,台北比東京熱。

這就是為什麼要對所有 block.type == "tool_use" 都處理、收集成 tool_results 清單一起回傳。

Step 7:工具執行失敗的處理

工具不一定成功執行。用 is_error 告知 Claude:

for block in response.content:
    if block.type == "tool_use":
        try:
            result = execute_tool(block.name, block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
                "is_error": False  # 成功(可省略,預設 False)
            })
        except Exception as e:
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": f"工具執行失敗:{str(e)}",
                "is_error": True  # 告訴 Claude 這次失敗了
            })

Claude 收到 is_error: true 後,通常會調整策略(換個參數重試,或告訴用戶無法完成)。

工具錯誤處理流程:成功與失敗都要回傳 tool_result,差別在 is_error

完整實戰:天氣+計算機雙工具 agent

把前面所有東西組合成一個可以直接跑的 agent:

import anthropic
import json

client = anthropic.Anthropic()

# 工具定義
tools = [
    {
        "name": "get_weather",
        "description": "取得指定城市的即時天氣資訊,包含溫度、天氣狀況和濕度。"
                       "當用戶詢問天氣、氣溫、是否下雨等問題時呼叫此工具。",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "城市名稱"},
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "溫度單位,預設 celsius"
                }
            },
            "required": ["city"]
        }
    },
    {
        "name": "calculate",
        "description": "執行精確的數學計算。當需要計算數字、換算單位或做統計時使用。"
                       "不要自己猜測計算結果,一律呼叫此工具。",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "合法的 Python 數學運算式,例如 '(32 - 0) * 5/9'"
                }
            },
            "required": ["expression"]
        }
    }
]

# 工具實作
def get_weather(city: str, unit: str = "celsius") -> dict:
    mock = {
        "台北": {"temp_c": 32, "condition": "晴天", "humidity": 75},
        "tokyo": {"temp_c": 28, "condition": "多雲", "humidity": 60},
        "london": {"temp_c": 15, "condition": "陰天", "humidity": 85},
    }
    data = mock.get(city.lower(), {"temp_c": 25, "condition": "晴天", "humidity": 65})
    temp = data["temp_c"] if unit == "celsius" else data["temp_c"] * 9/5 + 32
    return {
        "city": city,
        "temperature": round(temp, 1),
        "unit": unit,
        "condition": data["condition"],
        "humidity": data["humidity"]
    }

def calculate(expression: str) -> dict:
    allowed = {k: v for k, v in __builtins__.items()
               if k in ("abs", "round", "min", "max", "sum", "pow")} \
              if isinstance(__builtins__, dict) else {}
    try:
        result = eval(expression, {"__builtins__": allowed})
        return {"expression": expression, "result": result}
    except Exception as e:
        raise ValueError(f"無效運算式: {e}")

def execute_tool(name: str, tool_input: dict) -> tuple[str, bool]:
    """回傳 (結果字串, 是否出錯)"""
    try:
        if name == "get_weather":
            result = get_weather(**tool_input)
        elif name == "calculate":
            result = calculate(**tool_input)
        else:
            return f"未知工具:{name}", True
        return json.dumps(result, ensure_ascii=False), False
    except Exception as e:
        return str(e), True

def weather_agent(user_message: str, verbose: bool = True) -> str:
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=tools,
            messages=messages
        )

        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text
            return ""

        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    if verbose:
                        print(f"  [工具] {block.name} <- {json.dumps(block.input, ensure_ascii=False)}")
                    content, is_error = execute_tool(block.name, block.input)
                    if verbose:
                        print(f"  [結果] {content}")
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": content,
                        "is_error": is_error
                    })
            messages.append({"role": "user", "content": tool_results})


# 測試幾個情境
if __name__ == "__main__":
    questions = [
        "台北現在天氣如何?適合出門嗎?",
        "台北和東京的氣溫差幾度?如果我要把台北的溫度換成華氏是幾度?",
        "12345 * 6789 等於多少?"
    ]

    for q in questions:
        print(f"\n問: {q}")
        answer = weather_agent(q)
        print(f"答: {answer}")

執行後你會看到 Claude 根據問題自動選工具、組合結果,最後生成完整的自然語言回答。

常見坑

坑 1:stop_reason 不是 tool_use 卻去找 tool_use blocks

症狀:程式在 response.content 裡找 tool_use block 卻找不到,或是程式提前結束只拿到空字串。

原因:當問題很簡單時,Claude 會直接回答,stop_reasonend_turn 而非 tool_use。很多初學者的程式忘記判斷 stop_reason,直接跑迴圈找工具就會出錯。

# 錯誤:沒判斷 stop_reason,直接假設 Claude 一定會用工具
for block in response.content:
    if block.type == "tool_use":  # Claude 沒用工具時這裡永遠跳過
        ...

# 正確:先判斷 stop_reason
if response.stop_reason == "end_turn":
    return get_text_from_response(response)
elif response.stop_reason == "tool_use":
    # 處理工具呼叫
    ...

坑 2:沒有 while 迴圈,只跑一輪就結束

症狀:Claude 回傳了 tool_use,你執行工具、回傳結果,但程式就結束了。最終回答是空的,或是 Claude 的中間過程而不是最終整合後的答案。

原因:Tool Use 的過程是「請求 → 工具 → 再請求 → 再工具 → …→ 最終回答」,有時需要多輪。沒有 while True 就只能跑一輪。

# 錯誤:只呼叫一次 API
response = client.messages.create(...)
# 就算 Claude 還沒說完,程式就結束了

# 正確:用迴圈,直到 stop_reason == "end_turn"
while True:
    response = client.messages.create(...)
    if response.stop_reason == "end_turn":
        break
    # 處理工具呼叫並繼續

坑 3:description 寫得太模糊,Claude 不知道什麼時候用

症狀:明明問了天氣問題,Claude 卻直接回答「我不知道即時天氣」,完全沒呼叫工具。

原因:description 寫的是「功能」而不是「觸發條件」。Claude 靠 description 決定什麼時候用工具,如果你只寫「取得天氣資料」而沒說「當用戶詢問天氣時」,Claude 可能不知道要觸發。

# 效果差:只說功能
"description": "天氣查詢工具。"

# 效果好:明確說觸發條件
"description": "取得指定城市的即時天氣資訊。當用戶詢問天氣、溫度、是否會下雨、"
               "出門要不要帶傘等問題時,必須呼叫此工具,不要憑記憶回答。"

description 設計對照:只寫功能 vs 寫觸發條件

坑 4:tool_use_id 對不上,API 直接噴錯

症狀:回傳 tool_result 時收到 API 錯誤,訊息大概是 invalid tool_use_idtool_use_id does not match any tool_use in the conversation

原因:你在收集 tool_results 時,沒有用 Claude 回傳的 block.id,而是自己生了一個 ID,或是複製貼上時打錯。

# 錯誤:自己造 ID
tool_results.append({
    "type": "tool_result",
    "tool_use_id": "my_custom_id_001",  # 這個不存在於對話歷史中
    "content": result
})

# 正確:用 block.id
for block in response.content:
    if block.type == "tool_use":
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": block.id,  # 直接用 Claude 給的 ID
            "content": result
        })

坑 5:回傳的 content 型別不對

症狀:工具結果送出去後,Claude 的回答出現奇怪的解讀,或是 API 回傳 400 Bad Request

原因:tool_resultcontent 欄位必須是字串。你如果把 Python dict 或 list 直接丟進去,會出問題。

# 錯誤:把 dict 直接放進 content
tool_results.append({
    "type": "tool_result",
    "tool_use_id": block.id,
    "content": {"temp": 32, "city": "台北"}  # 這會出錯
})

# 正確:先序列化成 JSON 字串
tool_results.append({
    "type": "tool_result",
    "tool_use_id": block.id,
    "content": json.dumps({"temp": 32, "city": "台北"}, ensure_ascii=False)
})

模型選擇與成本考量

Tool Use 不一定要用最貴的模型。以 2026 年 7 月的定價為例:

模型 每百萬 Input Token 每百萬 Output Token 適合 Tool Use 場景
Claude Opus 4.8 (claude-opus-4-8) $5.00 $25.00 複雜多步驟 agent、需要高品質判斷
Claude Sonnet 4.6 (claude-sonnet-4-6) $3.00 $15.00 一般 agent、CP 值高
Claude Haiku 4.5 (claude-haiku-4-5) $1.00 $5.00 簡單工具選擇、高流量低複雜度

Tool Use 的 token 消耗比一般對話多,因為每次請求要附上所有工具的定義。如果你有 5 個工具,每個定義約 200 token,光是工具定義就先扣了 1000 token。工具多的時候,建議評估是否把不常用的工具拆成不同端點或按需載入。

建議開發階段用 Sonnet 4.6,功能確定後再依複雜度決定是否升 Opus 或降 Haiku。

作業

  1. 基礎:把本課的天氣+計算機 agent 跑起來,測試至少五個問題,包含需要同時查兩個城市天氣的問句。觀察 Claude 的 stop_reason 和它呼叫工具的順序。

  2. 進階:再新增一個工具,例如 search_news(topic: str)convert_currency(amount, from_currency, to_currency),寫模擬資料,接入 agent 迴圈。試著讓 Claude 在一個問題裡同時用到三個工具。

  3. 挑戰:把 get_weather 的模擬資料換成真正的天氣 API,例如 Open-Meteo(免費,不需要 API Key)。測試真實資料和模擬資料的回答差異。

  4. 觀察題:把 calculate 的 description 改成只說「執行計算」(不寫觸發條件),問 Claude「1234 × 5678 等於多少?」,觀察它有沒有用工具還是自己猜。

下一課預告

你現在有了一個能呼叫工具的 agent,但它有兩個限制:每次回應都得等完整結果才能繼續(用戶要乾等),而且只能處理文字。

第 6 課教你解決這兩件事:用 Streaming 讓回答一邊生成一邊輸出,用戶不用等到底就能看到進度;再加入多模態輸入讓 Claude 能讀圖——用戶傳一張餐廳菜單或截圖,Claude 直接理解。結合 Tool Use,你的 agent 就能看圖、說話、採取行動,一氣呵成。

#Claude API#Tool Use#Agent#Python#JavaScript

← 回所有文章