在前一篇文章裡,我們教模型閱讀文件。它可以在一堆檔案中搜尋,並根據內容回答問題,這非常有用。
但我還是沒辦法問它明天會不會下雨、查即時價格,甚至今天是幾號。
因為我們前面也談過,基礎模型本身是被凍結在某個時間點的。它的知識只到訓練截止日為止,而且被鎖在盒子裡,沒有通往外界的窗戶。
這篇文章要講的,就是那扇窗:工具呼叫。我們先給模型一個工具,看它如何連出去取得即時資料;再加第二個工具;接著看看應用程式把模型不知道的事實交給它時,兩種非常不同的方式。這兩種方式中,有一種就是你用過的某個 AI 助理能告訴你今天日期的原因。
所有程式碼都放在我的 GitHub 倉庫的
ep07-tool-calling資料夾裡。三個小腳本,各講一個概念:一個工具、兩個工具,以及注入技巧。
我第一次聽到「模型呼叫工具」時,腦中想像的是模型自己伸手去執行程式碼。
但其實不是這樣。
模型不會自己執行任何東西,因為它真的做不到。它本質上還是在讀提示詞並產生文字。
它產生的是一個結構化請求,內容像是:「我想要呼叫這個工具,並帶入這些輸入。」
它只是把一張便條交給你,你的程式讀取這張便條,然後執行真正的工具。接著再把結果回傳給模型,讓它繼續後續動作,可能是直接告訴你答案,也可能再呼叫另一個工具。
模型是決策者,你的程式是雙手。

每次都照這個流程跑:
call get_weather, city is Toronto。我還是用 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 的定義一起送給模型。
模型停在 stopReason 的 tool_use,並回傳一個請求:
{
"toolUse": {
"toolUseId": "tooluse_abc123",
"name": "get_weather",
"input": { "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]})
一個工具,一次來回。我完全知道接下來會發生什麼,所以可以直接把流程寫死。
有了真實資料,模型就能回答:「根據多倫多目前的天氣,你現在大概不需要帶傘。」
這個答案原本不存在於模型裡。它透過一次工具呼叫,從凍結狀態變成即時狀態。

現在來一個感覺上應該很簡單的問題。
問題: 「今天是幾號?」

沒有工具呼叫回來。模型只是很直接地說,它無法取得目前日期。
它唯一能用的工具只有天氣,所以這裡沒有任何工具能提供日期。它答不出來,而且這點我很喜歡:它沒有硬裝懂。它只是說自己不知道,這跟幻覺那篇文章裡的情況形成很大的對比。
如果問題是「沒有日期工具」,解法就很直覺:給它一個。
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 帶著 {"city": "Toronto"},接著是 get_current_datetime 帶著 {}。我的程式把兩個都執行完,再把結果回傳,模型就用這兩份資料組合出一個答案。

一句話,兩個不同需求,各自配對正確工具。它只是把工作路由好了。
但注意我前面那條漂亮直線的問題。只有一個工具時,我知道一定只會有一次來回。可是有兩個工具時,我不知道模型會選哪個,也不知道它會選幾個,甚至不知道它看完第一個結果後會不會再要求更多。因此四個步驟必須放進迴圈裡。只要模型還在要求工具,就持續執行;一旦它開始直接寫答案,就停止:
# 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 的雛形!
這段是我在學習時最困惑的地方。如果原始模型不知道今天日期,那 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}."
}]
問它「今天是幾號?」它就會正確回答,而且完全不需要工具呼叫。因為你已經把日期交給它了。

所以,把模型不知道的事實交給它,有兩種方式:要嘛呼叫工具,由你去執行;要嘛直接把上下文注入提示詞中。那什麼時候該用哪一種?
而且記得那個 schema 嗎?只有 city,沒有其他欄位。這就是為什麼我不能問它下週的天氣。因為根本沒有日期可傳。如果我要的是預報,那就得是另一個工具。
所以現在我們有兩個能用的工具:天氣和日期。很棒。但真實系統不會只有兩個工具。它們會有幾十個:查行事曆、搜尋 CRM、查資料庫、寄信、讀檔案。
而且照我們現在做法,每一個工具都得自己手動接線:寫 schema、寫函式、註冊工具、工具變更時還要維持描述同步。
兩個工具這樣還行,五十個工具、分散在五個應用程式裡,而且還會持續變動呢?那就是維護災難。而且所有在做 AI 應用的人,都在一再重複寫同樣的膠水程式碼。
這就是 MCP 要解決的問題。MCP 是 Model Context Protocol(模型上下文協定)。它是一個開放標準,由 Anthropic 發起,現在已經廣泛用於業界,定義 AI 應用和工具之間如何溝通。

最簡單的理解方式是: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