工具呼叫與 Function Calling:讓 LLM 連接外部世界
深入 Agent 最重要的基礎設施——工具呼叫與 Function Calling,理解其運作原理、Schema 設計與常見陷阱。
工具呼叫與 Function Calling:讓 LLM 連接外部世界
前兩篇我們談了 Agent 的概念與核心循環:ReAct、Plan-and-Execute 與 Reflection。
但你有沒有發現,這些模式都預設了一個關鍵能力:Agent 能呼叫工具。
沒有工具,Agent 再會推理、再會規劃,也只能「說」,不能「做」。
這一篇,我們要深入 Agent 最重要的基礎設施:工具呼叫(Tool Use) 與 Function Calling,理解它們的運作原理、設計要點與常見陷阱。
下一篇,我們才會用這些知識,從零打造一個最基礎的 Agent。
一、為什麼 LLM 需要工具?
LLM 很強大,但它有幾個根本限制:
1. 知識有截止日期
模型的知識來自訓練資料,它不知道今天的天氣、現在的股價、最新的新聞。
2. 無法精確計算
LLM 是機率模型,不是計算機。
你問它 12345 × 67890,它可能給你一個看起來合理但錯誤的答案。
3. 無法存取私有資料
它不知道你公司的內部文件、你的資料庫、你的 email。
4. 無法執行動作
它不能寄信、不能下單、不能操作你的電腦。
工具呼叫就是為了解決這些問題。
當 LLM 需要即時資訊、精確計算或實際行動時,它可以呼叫外部工具來完成。
二、工具呼叫的兩種實作方式
要讓 LLM 呼叫工具,目前有兩種主流做法:
方式一:文字格式(Prompt-based)
在 Prompt 中定義一套文字格式,要求 LLM 按照格式輸出工具呼叫請求。
例如 ReAct 風格:
Thought: 使用者想知道天氣,我需要查詢台北的天氣。
Action: get_weather
Action Input: {"city": "台北"}
然後開發者用正規表達式去解析 Action 和 Action Input。
優點:
- 不依賴特定 API,所有 LLM 都能用
- 格式自由,可自訂
缺點:
- 格式容易出錯,解析容易失敗
- 不支援平行呼叫
- 參數驗證困難
- 需要自己處理各種邊界情況
方式二:Function Calling(API-based)
現代 LLM API 直接支援結構化的工具呼叫。
開發者定義好工具 Schema,LLM 回傳結構化的 tool_calls 物件。
{
"role": "assistant",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"台北\"}"
}
}
]
}
優點:
- 結構化輸出,可靠性高
- 支援平行呼叫
- Schema 自動驗證參數
- 不需要寫解析器
缺點:
- 需要 LLM API 支援
- 格式受限於 API 規範
實務建議:
如果你的 LLM API 支援 Function Calling,優先使用它。
如果模型不支援(如某些開源模型),才退回文字格式。
三、Function Calling 的運作原理
Function Calling 的核心概念是:
把工具的定義當作額外的上下文,一起送給 LLM,讓模型在生成時「知道」有哪些工具可用。
具體流程如下:
步驟 1:開發者定義工具 Schema
每個工具包含:
- 名稱:工具的唯一識別碼
- 描述:這個工具做什麼、何時該用
- 參數結構:每個參數的名稱、型別、描述、是否必填
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查詢指定城市與日期的天氣資訊",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名稱,例如:台北、台中、高雄"
},
"day": {
"type": "string",
"enum": ["今天", "明天", "後天"],
"description": "日期"
}
},
"required": ["city", "day"]
}
}
}
]
步驟 2:把工具定義與使用者問題一起送給 LLM
messages = [
{"role": "user", "content": "台北今天適合爬山嗎?"}
]
response = client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools,
tool_choice="auto" # 讓模型自行決定是否呼叫工具
)
步驟 3:LLM 決定是否呼叫工具
LLM 會根據使用者的問題與工具描述,判斷:
- 需不需要呼叫工具?
- 需要呼叫哪個工具?
- 工具的參數是什麼?
如果需要,LLM 回傳結構化的 tool_calls:
{
"role": "assistant",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"台北\", \"day\": \"今天\"}"
}
}
]
}
如果不需要,LLM 直接生成最終回答。
步驟 4:開發者執行工具
開發者解析 arguments(是一個 JSON 字串),執行對應的函式:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
result = TOOL_FUNCTIONS[function_name](**arguments)
步驟 5:把結果回傳給 LLM
把工具結果包成 tool 訊息,送回給 LLM:
messages.append(response.choices[0].message) # LLM 的工具呼叫請求
messages.append({
"role": "tool",
"tool_call_id": "call_abc123",
"content": "多雲,降雨機率 60%,氣溫 22-28°C"
})
步驟 6:LLM 生成最終回答
LLM 根據工具結果,生成最終回答:
今天台北降雨機率 60%,氣溫 22-28°C,山區午後有陣雨機率。
整體來說,今天不是最適合爬山的時機。
這就是完整的 Function Calling 流程。
四、Tool Schema 的設計要點
工具定義的品質,直接影響 LLM 能否正確使用工具。以下是幾個設計要點:
1. 名稱要清楚
- 好:
get_weather、send_email、query_database - 差:
tool1、func_a、do_stuff
2. 描述要具體
描述是 LLM 判斷「何時該用這個工具」的關鍵。
# 好
"description": "查詢指定城市與日期的天氣資訊,回傳溫度、降雨機率與天氣狀況"
# 差
"description": "查天氣"
3. 參數要明確
每個參數都應該有型別、描述,以及是否必填。
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名稱,例如:台北、台中、高雄"
},
"day": {
"type": "string",
"enum": ["今天", "明天", "後天"],
"description": "日期"
}
},
"required": ["city", "day"]
}
4. 用 enum 限制選項
如果參數只有幾種合法值,用 enum 限制,能大幅降低 LLM 出錯的機率。
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "溫度單位"
}
5. 參數數量要適中
參數太多會增加 LLM 出錯的機率。
如果一個工具需要超過 5 個參數,可以考慮拆成多個工具,或用巢狀結構。
6. 提供範例
在描述中加入範例,能幫助 LLM 理解使用時機。
"description": "查詢天氣。例如:get_weather(city='台北', day='今天')"
7. 工具要單一職責
一個工具只做一件事。
get_weather 只查天氣,不要同時查天氣和匯率。
五、平行工具呼叫
現代 LLM 支援一次輸出多個工具呼叫,稱為 平行工具呼叫(Parallel Tool Calling)。
例如,使用者問:「比較台北和台中今天的天氣。」
LLM 可能一次回傳:
{
"tool_calls": [
{
"id": "call_1",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"台北\", \"day\": \"今天\"}"
}
},
{
"id": "call_2",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"台中\", \"day\": \"今天\"}"
}
}
]
}
開發者可以平行執行這兩個工具,再把結果一起回傳給 LLM。
這能大幅減少延遲,提升效率。
from concurrent.futures import ThreadPoolExecutor
def execute_tools_parallel(tool_calls):
"""平行執行多個工具呼叫"""
def run_one(tool_call):
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
try:
result = TOOL_FUNCTIONS[function_name](**arguments)
except Exception as e:
result = f"工具執行錯誤:{e}"
return tool_call.id, result
with ThreadPoolExecutor() as executor:
results = list(executor.map(run_one, tool_calls))
return results
然後在主迴圈中,把每個結果分別包成 tool 訊息回傳:
if message.tool_calls:
results = execute_tools_parallel(message.tool_calls)
for tool_call_id, result in results:
messages.append({
"role": "tool",
"tool_call_id": tool_call_id,
"content": str(result),
})
六、常見的錯誤與陷阱
1. 幻覺工具
LLM 可能呼叫一個不存在的工具。
解法:在執行前檢查工具名稱是否存在。
if function_name not in TOOL_FUNCTIONS:
result = f"錯誤:找不到工具 {function_name}"
2. 參數錯誤
LLM 可能傳入錯誤的參數型別或缺少必填參數。
解法:用 Schema 驗證,並在執行前檢查必填參數。
import inspect
def safe_execute(function_name: str, arguments: dict):
if function_name not in TOOL_FUNCTIONS:
return f"錯誤:找不到工具 {function_name}"
func = TOOL_FUNCTIONS[function_name]
sig = inspect.signature(func)
required_params = [
name for name, param in sig.parameters.items()
if param.default is inspect.Parameter.empty
]
missing = [p for p in required_params if p not in arguments]
if missing:
return f"錯誤:缺少必填參數 {missing}"
try:
return func(**arguments)
except Exception as e:
return f"執行錯誤:{e}"
3. 工具執行失敗
工具本身可能因為網路、權限、資源等問題失敗。
解法:把錯誤訊息回傳給 LLM,讓它決定要重試、換工具,還是放棄。
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": "錯誤:天氣 API 暫時無法連線,請稍後再試。"
})
4. 無限迴圈
如果 LLM 一直重複呼叫同一個工具卻無法成功,可能會陷入無限迴圈。
解法:設定最大迭代次數。
for i in range(max_iterations):
# ...
if i == max_iterations - 1:
return "已達到最大迭代次數,任務未完成。"
5. 工具呼叫超時
如果某個工具執行時間過長,應該設定超時機制,避免整個 Agent 卡住。
import signal
def timeout_handler(signum, frame):
raise TimeoutError("工具執行超時")
def execute_with_timeout(func, args, timeout_seconds=10):
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout_seconds)
try:
result = func(**args)
finally:
signal.alarm(0)
return result
6. 敏感資訊洩漏
工具回傳的內容可能包含敏感資訊(如密碼、個資)。
解法:在回傳給 LLM 之前,先過濾或脫敏。
import re
def sanitize_output(text: str) -> str:
text = re.sub(r'\S+@\S+\.\S+', '[EMAIL]', text)
text = re.sub(r'\d{4}-\d{3}-\d{3}', '[PHONE]', text)
return text
七、工具呼叫的最佳實踐
1. 回傳結果要結構化
盡量回傳結構化的資料(如 JSON),而不是一大段文字。
這讓 LLM 更容易解析與使用。
# 好
return {"city": "台北", "temp": "22-28°C", "rain": "60%"}
# 差
return "台北今天多雲,降雨機率 60%,氣溫 22 到 28 度,風向東北風..."
2. 限制工具權限
不是所有工具都該讓 LLM 自由呼叫。
高風險操作(如刪除資料、發送郵件、轉帳)應該加入人類審核或權限控制。
HIGH_RISK_TOOLS = {"delete_file", "send_email", "transfer_money"}
def execute_tool_safely(function_name, arguments):
if function_name in HIGH_RISK_TOOLS:
if not human_approval(function_name, arguments):
return "操作已被使用者拒絕"
return TOOL_FUNCTIONS[function_name](**arguments)
3. 記錄所有工具呼叫
保留完整的工具呼叫日誌,方便除錯與審計。
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("agent")
def logged_execute(function_name: str, arguments: dict):
logger.info(f"Tool call: {function_name}({arguments})")
result = safe_execute(function_name, arguments)
logger.info(f"Tool result: {result}")
return result
4. 工具數量要控制
一次提供太多工具,會讓 LLM 難以選擇。
如果工具超過 20 個,可以考慮:
- 分組,讓 LLM 先選類別再選工具
- 用 RAG 動態檢索相關工具
- 用多 Agent 分工,每個 Agent 只負責一組工具
八、Function Calling vs 文字格式:該用哪個?
| 面向 | 文字格式(ReAct) | Function Calling |
|---|---|---|
| 格式可靠性 | 低,容易解析失敗 | 高,結構化輸出 |
| 平行呼叫 | 不支援 | 支援 |
| 參數驗證 | 手動檢查 | Schema 自動驗證 |
| 模型支援 | 所有 LLM | 需支援的 API |
| 實作複雜度 | 需寫解析器 | 直接使用 API |
| 靈活性 | 高,可自訂格式 | 受限於 API 規範 |
結論:
- 如果你的 LLM API 支援 Function Calling,優先使用它。
- 如果模型不支援(如某些開源模型),才退回文字格式。
- 兩者可以混用:主要工具用 Function Calling,特殊需求用文字格式補充。
九、總結:工具是 Agent 的手腳
讓我們回顧這一篇的核心:
- LLM 的限制:知識有截止日期、無法精確計算、無法存取私有資料、無法執行動作。
- 兩種實作方式:文字格式(Prompt-based)與 Function Calling(API-based)。
- Function Calling 流程:定義工具 → 送給 LLM → LLM 決定呼叫 → 執行工具 → 回傳結果 → LLM 生成回答。
- Tool Schema 設計:名稱清楚、描述具體、參數明確、用 enum 限制選項、單一職責。
- 平行工具呼叫:一次執行多個工具,減少延遲。
- 常見陷阱:幻覺工具、參數錯誤、執行失敗、無限迴圈、超時、敏感資訊洩漏。
- 最佳實踐:結構化回傳、權限控制、日誌記錄、工具數量控制。
工具呼叫是 Agent 從「會說」到「會做」的關鍵一步。
理解了 Function Calling 的原理與設計要點,你就具備了打造實用 Agent 的基礎。
下一篇,我們要把這些知識付諸實踐:從零打造一個最基礎的 Agent。
我們會用 LLM + 工具 + 一個迴圈,寫出一個真正能運作的 Agent。
下一篇預告
《從零打造一個最基礎的 Agent:LLM + 工具 + 迴圈》
我們會用最少的程式碼,從零實作一個能接收問題、決定是否呼叫工具、執行工具、並生成最終回答的 Agent。你會看到它的核心其實非常簡單:LLM + 工具 + 一個迴圈。