前一篇文章裡,我們教模型閱讀文件。它可以在一堆檔案中搜尋,並根據內容回答問題,這非常有用。

但我還是沒辦法問它明天會不會下雨、查即時價格,甚至今天是幾號。

因為我們前面也談過,基礎模型本身是被凍結在某個時間點的。它的知識只到訓練截止日為止,而且被鎖在盒子裡,沒有通往外界的窗戶。

這篇文章要講的,就是那扇窗:工具呼叫。我們先給模型一個工具,看它如何連出去取得即時資料;再加第二個工具;接著看看應用程式把模型不知道的事實交給它時,兩種非常不同的方式。這兩種方式中,有一種就是你用過的某個 AI 助理能告訴你今天日期的原因。

所有程式碼都放在我的 GitHub 倉庫ep07-tool-calling 資料夾裡。三個小腳本,各講一個概念:一個工具、兩個工具,以及注入技巧。

模型會執行工具嗎?

我第一次聽到「模型呼叫工具」時,腦中想像的是模型自己伸手去執行程式碼。

但其實不是這樣。

模型不會自己執行任何東西,因為它真的做不到。它本質上還是在讀提示詞並產生文字。

它產生的是一個結構化請求,內容像是:「我想要呼叫這個工具,並帶入這些輸入。」

它只是把一張便條交給你,你的程式讀取這張便條,然後執行真正的工具。接著再把結果回傳給模型,讓它繼續後續動作,可能是直接告訴你答案,也可能再呼叫另一個工具。

模型是決策者,你的程式是雙手。

四步驟迴圈

四步驟工具呼叫迴圈:你送出問題加上工具描述,模型回傳結構化的工具請求,你的程式執行真正的函式,然後把結果送回去,讓模型撰寫最終答案

每次都照這個流程跑:

  1. 你把問題,以及它可以使用的工具描述,一起送給模型。
  2. 模型判斷:這題我自己能答嗎?還是需要工具?如果需要,它會回傳一個結構化請求,一個小包裹,像是 call get_weather, city is Toronto
  3. 你的程式看到這個請求後,執行真正的函式,也就是實際去呼叫天氣 API 的那個。
  4. 你把結果送回模型。現在它就能根據它自己原本不可能知道的即時資料,寫出最終答案。

設定:描述一個工具

我還是用 Amazon Bedrock,跟整個系列一樣,透過 Converse API 呼叫 Claude 模型。Converse 內建了放工具的地方,叫做 toolConfig

response = bedrock.converse(
    modelId=MODEL,
    messages=messages,
    toolConfig={"tools": [WEATHER_TOOL]},
    inferenceConfig={"maxTokens": 2048},
    additionalModelRequestFields=THINKING,
)

先從最簡單的工具開始:查天氣。

把工具描述給模型看,分成三部分:名稱、白話描述、以及參數的輸入結構。

WEATHER_TOOL = {
    "toolSpec": {
        "name": "get_weather",
        "description": "Get the current weather for a single city.",
        "inputSchema": {
            "json": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "A plain city name, e.g. Toronto or Paris.",
                    }
                },
                "required": ["city"],
            }
        },
    }
}

這段描述和 schema,就是模型判斷何時、以及如何使用這個工具時唯一會讀到的東西。

你的工具描述本身就是一段提示詞,所以要把它當成提示詞來寫。

另外,真正做事的函式是這個:

import requests

# Open-Meteo 會回傳數字型 weather_code;把我們需要的程式碼映射成白話。
WEATHER_CODES = {0: "clear sky", 2: "partly cloudy", 3: "overcast", 61: "light rain", 63: "moderate rain"}

def get_weather(city: str) -> dict:
    geo = requests.get(
        "https://geocoding-api.open-meteo.com/v1/search",
        params={"name": city, "count": 1},
    ).json()["results"][0]
    now = requests.get(
        "https://api.open-meteo.com/v1/forecast",
        params={
            "latitude": geo["latitude"],
            "longitude": geo["longitude"],
            "current": "temperature_2m,weather_code,wind_speed_10m",
        },
    ).json()["current"]
    return {
        "city": geo["name"],
        "country": geo["country"],
        "temperature_c": now["temperature_2m"],
        "conditions": WEATHER_CODES.get(now["weather_code"], "unknown"),
        "wind_kph": now["wind_speed_10m"],
    }

這就是一般程式碼,裡面沒有 AI。它會呼叫 Open-Meteo,一個不需要 API 金鑰的免費天氣 API。

示範:模型呼叫工具

問題: 「我今天在多倫多需要帶傘嗎?」

我把這個問題連同 get_weather 的定義一起送給模型。

模型停在 stopReasontool_use,並回傳一個請求:

{
  "toolUse": {
    "toolUseId": "tooluse_abc123",
    "name": "get_weather",
    "input": { "city": "Toronto" }
  }
}

終端機執行示範:「我今天在多倫多需要帶傘嗎?」模型回傳 get_weather 的 tool_use 請求,輸入 city 為 Toronto,推理內容表示它需要查多倫多目前天氣

我從頭到尾都沒有告訴它該用哪個工具,也沒有告訴它參數。它只看一個問題,就自己推得出來。但此時還沒有真正執行任何東西。

於是我的程式執行 get_weather("Toronto"),呼叫 API,拿到真正的天氣狀況。接著我把結果包裝成 toolResult 再送回模型:

messages.append({
    "role": "user",
    "content": [{
        "toolResult": {
            "toolUseId": "tooluse_abc123",
            "content": [{"json": {
                "city": "Toronto",
                "country": "Canada",
                "temperature_c": 23.8,
                "conditions": "overcast",
                "wind_kph": 3.9,
            }}],
        }
    }],
})

只有一個工具時,整個流程就是一條直線。送出、拿到請求、執行、送回結果、拿答案。從頭到尾,不需要迴圈:

messages = [{"role": "user", "content": [{"text": QUESTION}]}]

# 1. 送出問題 + 工具。
response = bedrock.converse(
    modelId=MODEL,
    messages=messages,
    toolConfig={"tools": [WEATHER_TOOL]},
)
messages.append(response["output"]["message"])

# 2. 模型要求工具。3. 執行它。4. 把結果送回去。
tool_request = next(
    b["toolUse"] for b in response["output"]["message"]["content"] if "toolUse" in b
)
result = get_weather(tool_request["input"]["city"])
messages.append({
    "role": "user",
    "content": [{
        "toolResult": {
            "toolUseId": tool_request["toolUseId"],
            "content": [{"json": result}],
        }
    }],
})

# 模型根據真實資料寫出最終答案。
final = bedrock.converse(modelId=MODEL, messages=messages, toolConfig={"tools": [WEATHER_TOOL]})

一個工具,一次來回。我完全知道接下來會發生什麼,所以可以直接把流程寫死。

有了真實資料,模型就能回答:「根據多倫多目前的天氣,你現在大概不需要帶傘。」

這個答案原本不存在於模型裡。它透過一次工具呼叫,從凍結狀態變成即時狀態。

一個工具的執行結果:程式呼叫 get_weather 並回傳真實天氣 JSON(Toronto、overcast、23.8C、風速 3.9 kph),接著模型的最終回答說,因為天氣多雲且沒有下雨,你現在大概不需要帶傘

再給它第二個工具

現在來一個感覺上應該很簡單的問題。

問題: 「今天是幾號?」

模型不知道日期

沒有工具呼叫回來。模型只是很直接地說,它無法取得目前日期。

它唯一能用的工具只有天氣,所以這裡沒有任何工具能提供日期。它答不出來,而且這點我很喜歡:它沒有硬裝懂。它只是說自己不知道,這跟幻覺那篇文章裡的情況形成很大的對比。

如果問題是「沒有日期工具」,解法就很直覺:給它一個。

DATETIME_TOOL = {
    "toolSpec": {
        "name": "get_current_datetime",
        "description": "Get the current date and time.",
        "inputSchema": {"json": {"type": "object", "properties": {}}},
    }
}

def get_current_datetime() -> dict:
    from datetime import datetime
    now = datetime.now()
    return {
        "date": now.strftime("%Y-%m-%d"),
        "day_of_week": now.strftime("%A"),
        "time": now.strftime("%H:%M"),
    }

沒有參數,沒有 AI,它只會回傳今天的日期和時間。我把它加入模型可使用的工具清單。現在模型有兩個工具:天氣和日期。

問題: 「我今天在多倫多需要帶傘嗎?另外今天是幾號?」

終端機執行兩個工具的結果:模型判斷問題有兩個獨立部分,接著要求 get_weather 查多倫多天氣(回傳晴朗、23.5C)以及 get_current_datetime(回傳 Thursday, 2026-08-20)

回來的是兩個請求,對應兩個工具。get_weather 帶著 {"city": "Toronto"},接著是 get_current_datetime 帶著 {}。我的程式把兩個都執行完,再把結果回傳,模型就用這兩份資料組合出一個答案。

一個問題被路由到兩個工具:模型呼叫 get_current_datetime 和 get_weather 查多倫多,然後把兩者結果整合成一個答案

一句話,兩個不同需求,各自配對正確工具。它只是把工作路由好了。

有什麼改變?現在需要一個迴圈了

但注意我前面那條漂亮直線的問題。只有一個工具時,我知道一定只會有一次來回。可是有兩個工具時,我不知道模型會選哪個,也不知道它會選幾個,甚至不知道它看完第一個結果後會不會再要求更多。因此四個步驟必須放進迴圈裡。只要模型還在要求工具,就持續執行;一旦它開始直接寫答案,就停止:

# name → 當模型要求這個工具時,實際要執行的函式。
TOOLS = {
    "get_weather": get_weather,
    "get_current_datetime": get_current_datetime,
}

messages = [{"role": "user", "content": [{"text": QUESTION}]}]

while True:
    response = bedrock.converse(
        modelId=MODEL,
        messages=messages,
        toolConfig={"tools": [WEATHER_TOOL, DATETIME_TOOL]},
    )
    assistant_message = response["output"]["message"]
    messages.append(assistant_message)

    # 完成了嗎?模型不再要求工具,並且已經寫出答案。
    if response["stopReason"] != "tool_use":
        answer = "".join(b["text"] for b in assistant_message["content"] if "text" in b)
        break

    # 否則:執行模型要求的每個工具,把結果送回去。
    tool_results = []
    for block in assistant_message["content"]:
        if "toolUse" not in block:
            continue
        request = block["toolUse"]
        result = TOOLS[request["name"]](**request["input"])
        tool_results.append({
            "toolResult": {
                "toolUseId": request["toolUseId"],
                "content": [{"json": result}],
            }
        })
    messages.append({"role": "user", "content": tool_results})

這個 while 迴圈,就是全部差異所在。只有一個工具時,我可以把流程硬編成直線;工具一多,我就把控制權交給模型,讓它一路決定到完成為止。

一個工具是一條可以硬編的直線,只有單次來回;多個工具則變成迴圈,模型會持續要求工具直到寫出答案

這一點超級重要,因為這就是 agent 的雛形!

那 AI 助理怎麼知道日期?

這段是我在學習時最困惑的地方。如果原始模型不知道今天日期,那 ChatGPT、Claude 或任何 AI 助理是怎麼知道的?你一問它今天幾號,它就立刻回答。難道每次都會呼叫日期工具嗎?簡單說,不是。

Anthropic 其實有公開 Claude 使用的 system prompt,寫在他們的版本更新說明裡。他們提到,Claude 的網頁版和行動版會在每段對話一開始,就使用一個 system prompt 提供即時資訊,例如目前日期。

就這樣。沒有工具執行。只是文字在你的訊息送進去之前,先被塞進指令裡。模型一開始就被給了日期這個上下文。

你也可以在腳本裡做一樣的事。把日期工具拿掉,然後直接在 system prompt 裡插入今天日期:

system_prompt = [{
    "text": f"Today's date is {datetime.now():%A, %d %B %Y}."
}]

問它「今天是幾號?」它就會正確回答,而且完全不需要工具呼叫。因為你已經把日期交給它了。

工具還是注入?清楚的判斷規則

圖解「把事實交給模型的兩種方式」。左邊是呼叫工具:模型回傳 tool_use,你的程式打真正的 API,結果回來後模型作答,適合即時且持續變動的事實,例如天氣。右邊是注入上下文:system prompt 直接寫著「今天是……」,模型已經有這個事實,因此不用工具呼叫,適合便宜且靜態的事實,例如今天日期

所以,把模型不知道的事實交給它,有兩種方式:要嘛呼叫工具,由你去執行;要嘛直接把上下文注入提示詞中。那什麼時候該用哪一種?

  • 便宜又靜態,例如今天日期?直接注入。 一行就好,不需要工具。
  • 即時而且一直變動,例如天氣?用工具。 你不可能先把天氣注入進去,因為那等於你得提前知道,這就失去意義了。工具會在模型需要時去抓最新資料。

而且記得那個 schema 嗎?只有 city,沒有其他欄位。這就是為什麼我不能問它下週的天氣。因為根本沒有日期可傳。如果我要的是預報,那就得是另一個工具。

手寫整合的問題,以及 MCP

所以現在我們有兩個能用的工具:天氣和日期。很棒。但真實系統不會只有兩個工具。它們會有幾十個:查行事曆、搜尋 CRM、查資料庫、寄信、讀檔案。

而且照我們現在做法,每一個工具都得自己手動接線:寫 schema、寫函式、註冊工具、工具變更時還要維持描述同步。

兩個工具這樣還行,五十個工具、分散在五個應用程式裡,而且還會持續變動呢?那就是維護災難。而且所有在做 AI 應用的人,都在一再重複寫同樣的膠水程式碼。

這就是 MCP 要解決的問題。MCP 是 Model Context Protocol(模型上下文協定)。它是一個開放標準,由 Anthropic 發起,現在已經廣泛用於業界,定義 AI 應用和工具之間如何溝通。

MCP 像 AI 工具的 USB-C:MCP client 連上 MCP server,由 server 描述它提供的工具,因此應用程式可以在執行時發現工具,而不是一個一個手動接線

最簡單的理解方式是:MCP 之於 AI 工具,就像 USB-C 之於裝置。USB-C 出現之前,每個裝置都有自己的線和接頭,線材一團亂。USB-C 是一個統一規格的接頭。MCP 也是一樣,只不過它是用來連接模型、工具與資料。

工具存在於 MCP server 後面,而那個 server 會描述自己:我提供哪些工具、每個工具做什麼、我需要哪些輸入。你的應用程式則是 MCP client。它只要問「你有什麼工具?」server 就會告訴它。工具是在執行時才被發現的。

所以如果有人為 GitHub、你的資料庫或 Slack 建好了 MCP server,你就不需要自己寫整合。你只要把應用程式指向那個 server,工具就會自動出現。

今天我們不會真的建立 MCP server,這本身就是另一個大主題。不過現在只要掌握這個心智模型就夠了:工具呼叫是單一模型怎麼使用工具;MCP 則是任何模型怎麼發現並使用工具。

重點整理

如果你剛開始接觸: 工具呼叫就是 AI 如何跳出封閉盒子。給它工具,它就能抓取即時資訊、執行動作,而不只是聊天。最重要的一點是:模型是大腦,你的程式是雙手。

如果你比較偏開發者: 模型會自己挑工具並填入參數,而它做決定時唯一會讀的,就是你的描述和 schema。所以要像寫提示詞一樣來寫它們,而且要清楚具體地描述工具能做什麼、不能做什麼。接著記住:靜態資料直接注入,即時資料用工具。等你超過幾個工具之後,就別再手寫接線了,去看看 MCP。

接下來呢

今天模型呼叫了一個工具,或兩個工具,而且各只做一次,然後就回答了。但如果一個問題需要好幾個工具,而且要照正確順序來呢?先看我的行事曆,再查那天的天氣,然後草擬一封 email。模型必須先規劃、執行、看結果,再決定下一步。這個流程會一遍又一遍地在迴圈裡跑,直到完成。

其實,那個迴圈就叫做 agent。下一篇,我們會用 Strands Agents SDK 來做一個。

一起走下去吧。

這篇文章是「Learning AI Out Loud」系列的一部分,一位雲端架構師用第一原理學習 AI。

https://dev.to/rohini_gaonkar 追蹤這個系列


原文出處:https://dev.to/aws/how-ai-actually-calls-an-api-tool-calling-explained-from-scratch-4lf8


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝64   💬1  
854
🥈
NewsData
2
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登