前言
這陣子 LLM 應用爆炸性成長,但你可能會發現,雖然 LLM 很會聊天、很會寫文章,但在處理一些「即時性」或「需要外部工具」的任務時,它就顯得力不從心了。比方說,它沒辦法即時知道現在臺北的天氣,也沒辦法幫你直接查詢今天的股價,更別說幫你發一封信了。
難道 LLM 只能當個「嘴砲型」AI 嗎?當然不是!這時候,我們就要請出 LLM 的超能力之一:「Function Calling」或「Tool Use」了。這項技術讓 LLM 不只會說,還能「動手做」!今天,Hikari 我就來跟大家白話拆解一下,Function Calling 到底怎麼運作,以及我們在實作上會遇到哪些眉角。
想像一下,你現在有一個很聰明的助理,他無所不知,但是他沒辦法自己去拿咖啡、沒辦法自己打電話。Function Calling 就是給這個助理一套「工具箱」跟「使用說明書」,讓他知道在什麼情況下,可以「呼叫」哪個工具,並且提供正確的「參數」給工具,讓工具去完成任務。
簡單來說,Function Calling 就是讓 LLM 能夠:
- 判斷:根據使用者的需求,判斷是否需要使用某個外部功能(例如:查詢天氣、發送郵件、查詢資料庫)。
- 決定:如果需要,決定要使用哪個功能,以及需要哪些參數。
- 建議:生成一個「呼叫這個功能」的建議,包含功能名稱和對應的參數。
接下來,就是你寫的程式碼來接收這個建議,實際去執行那個功能,再把執行結果丟回給 LLM,讓 LLM 根據結果來生成最終的回覆。
它怎麼運作的?原理拆解
整個 Function Calling 的流程可以拆成幾個步驟:
- 定義工具(Tools Definition):你必須先告訴 LLM,你的應用程式有哪些「工具」可以用。這些工具通常會被定義成一個 JSON 結構,裡面包含工具的名稱、簡短描述,以及它接受的參數(參數的名稱、類型、描述等等)。這個步驟就像是給 LLM 一份工具使用手冊。
- 使用者提問:使用者提出一個請求,例如:「今天臺北的天氣怎麼樣?」
- LLM 判斷與生成工具呼叫:LLM 收到使用者問題後,會去比對它手上的「工具使用手冊」。它會判斷:「喔,這個問題跟『查詢天氣』這個工具很像耶!而且臺北就是城市參數。」接著,它不會直接回覆答案,而是生成一個「請呼叫查詢天氣工具,參數是臺北」的指令。
- 應用程式執行工具:你的應用程式會收到 LLM 傳回來的這個指令(通常是一個
tool_calls 物件)。這時候,你的程式碼就要負責去解析這個指令,然後真的去執行那個「查詢天氣」的函式,並把「臺北」當作參數傳進去。這個函式會去打天氣 API 或查詢資料。
- 回傳工具結果給 LLM:函式執行完畢後,會得到一個結果(例如:臺北今天多雲,氣溫 25 度)。你的應用程式再把這個結果,連同先前的對話紀錄,一併回傳給 LLM。
- LLM 生成最終答案:LLM 收到工具執行結果後,就有了即時的資訊。它會根據這個資訊,結合它本身的語言能力,生成一個自然、有用的回覆給使用者:「根據查詢結果,臺北今天多雲,氣溫 25 度。」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| graph TD A[使用者提問: "臺北今天天氣?"] B[應用程式: 定義工具給 LLM] C[LLM (接收問題 + 工具定義)] D{LLM 判斷:需要工具嗎?} E[LLM 生成 Tool Call 指令] F[應用程式: 解析 Tool Call 並執行實際函式] G[實際函式執行 (e.g., 呼叫天氣 API)] H[實際函式返回結果 (e.g., "多雲, 25°C")] I[應用程式: 將結果傳回 LLM] J[LLM (接收結果 + 之前對話)] K[LLM 生成最終答案] L[最終答案回傳給使用者]
A --> C B --> C C --> D D -- 是 --> E E --> F F --> G G --> H H --> I I --> J J --> K K --> L D -- 否 --> K
|
實戰範例:用 OpenAI API 玩玩看
我們來用 Python 和 OpenAI API 模擬一下這個流程。假設我們有一個簡單的工具,可以「取得目前的日期和時間」。
首先,定義我們的工具函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import json import datetime
def get_current_datetime(format_string: str = '%Y-%m-%d %H:%M:%S') -> str: """ 取得目前的日期和時間。 可以指定輸出格式,例如 '%Y-%m-%d' 或 '%H:%M:%S'。 預設格式為 '%Y-%m-%d %H:%M:%S'。 """ return datetime.datetime.now().strftime(format_string)
available_functions = { "get_current_datetime": get_current_datetime, }
|
接著,我們要準備工具的描述,這個會傳給 LLM:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| tools = [ { "type": "function", "function": { "name": "get_current_datetime", "description": "取得目前的日期和時間,可以指定輸出格式", "parameters": { "type": "object", "properties": { "format_string": { "type": "string", "description": "日期時間的格式字串,例如 '%Y-%m-%d %H:%M:%S'", "default": "%Y-%m-%d %H:%M:%S" } }, "required": [], }, }, } ]
|
然後是跟 LLM 互動的流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
| from openai import OpenAI
class MockClient: def chat(self): return self def completions(self): return self def create(self, model, messages, tools=None, tool_choice=None): if messages[-1]['content'] == "現在幾點了?": return { "choices": [{ "message": { "role": "assistant", "tool_calls": [{ "id": "call_123", "type": "function", "function": { "name": "get_current_datetime", "arguments": "{}" } }] } }] } elif messages[-1]['content'] == "今天的日期是什麼?": return { "choices": [{ "message": { "role": "assistant", "tool_calls": [{ "id": "call_456", "type": "function", "function": { "name": "get_current_datetime", "arguments": "{\"format_string\": \"%Y-%m-%d\"}" } }] } }] } else: return { "choices": [{ "message": { "role": "assistant", "content": "抱歉,我只能告訴你時間和日期。" } }] }
client = MockClient()
messages = [ {"role": "user", "content": "現在幾點了?"} ]
response = client.chat.completions.create( model="gpt-3.5-turbo-0125", messages=messages, tools=tools, tool_choice="auto", ) response_message = response["choices"][0]["message"]
if response_message.get("tool_calls"): tool_calls = response_message["tool_calls"] messages.append(response_message)
for tool_call in tool_calls: function_name = tool_call["function"]["name"] function_to_call = available_functions[function_name] function_args = json.loads(tool_call["function"]["arguments"]) function_response = function_to_call(**function_args) messages.append( { "tool_call_id": tool_call["id"], "role": "tool", "name": function_name, "content": function_response, } ) final_response = client.chat.completions.create( model="gpt-3.5-turbo-0125", messages=messages, ) print(final_response["choices"][0]["message"]["content"]) else: print(response_message["content"])
messages = [ {"role": "user", "content": "今天的日期是什麼?"} ]
response = client.chat.completions.create( model="gpt-3.5-turbo-0125", messages=messages, tools=tools, tool_choice="auto", ) response_message = response["choices"][0]["message"]
if response_message.get("tool_calls"): tool_calls = response_message["tool_calls"] messages.append(response_message)
for tool_call in tool_calls: function_name = tool_call["function"]["name"] function_to_call = available_functions[function_name] function_args = json.loads(tool_call["function"]["arguments"]) function_response = function_to_call(**function_args) messages.append( { "tool_call_id": tool_call["id"], "role": "tool", "name": function_name, "content": function_response, } ) final_response = client.chat.completions.create( model="gpt-3.5-turbo-0125", messages=messages, ) print(final_response["choices"][0]["message"]["content"]) else: print(response_message["content"])
|
上面這個範例因為使用了 MockClient,所以 print 出來的會是 LLM 收到工具結果後的預設回覆,而不是真的生成式的答案,但在實際使用時,LLM 會根據你提供的 content 來生成一個更自然的句子。你會發現整個流程其實就是一個「LLM 請求 -> 程式執行 -> 程式回報結果給 LLM -> LLM 回覆」的迴圈。
實作 Function Calling 常踩的雷與實用建議
Function Calling 雖然強大,但實作上還是有些坑要注意:
雷點一:工具描述不夠清楚
LLM 是靠你提供的 description 和 parameters 來理解工具的用途和用法。如果你寫得太模糊、太簡潔,LLM 可能就搞不清楚這個工具什麼時候該用、每個參數代表什麼意思。
- 情境:你定義了一個
get_stock_price 函式,但描述只寫「取得股價」。LLM 收到「幫我查蘋果的股價」時,可能會不知道「蘋果」是公司名稱還是水果,甚至不知道該如何辨識股票代號。
💡 建議:
- 寫得像文件一樣詳細:描述要清楚說明工具的功能、適用情境。參數也一樣,說明它們的意義、接受的資料類型、範例值等。
- 提供範例提示:在描述中暗示 LLM 該如何呼叫,甚至直接寫一個範例,例如:「當使用者想查詢股票價格時,請使用
get_stock_price,並提供公司名稱或股票代號。」
雷點二:LLM 生成的參數有問題
即使描述清楚,LLM 有時候還是會「腦補」或生成出不符合預期的參數值,例如傳了非數字給需要數字的參數,或是傳了非法的日期格式。
- 情境:你定義了
get_weather(city: str),LLM 卻可能傳來 city="火星" 或 city="不存在的城市"。
💡 建議:
- 程式端嚴格驗證參數:在你的實際函式被呼叫前,務必對從 LLM 傳來的參數進行完整的驗證。檢查資料類型、範圍、格式等。如果驗證失敗,不要直接執行,而是回傳一個錯誤訊息給 LLM,讓它知道參數有誤,或許能引導它重新生成。
- **善用
enum 或 pattern**:如果參數有固定的選項,可以使用 enum 在 parameters 裡面定義,限制 LLM 生成的範圍。如果是特定格式(例如電話號碼),可以用 pattern 搭配正規表達式。
雷點三:工具數量過多或過於複雜
當你的應用程式擁有很多工具時,LLM 在判斷時的負擔會增加,導致反應變慢,甚至選錯工具。
- 情境:你給了 LLM 幾十個功能,從查詢天氣、股票、新聞、翻譯、寫信到訂餐,結果它在簡單問題上也要想很久,或是搞混了。
💡 建議:
- 少即是多,循序漸進:一開始只提供少數、最核心的功能。確認 LLM 能正確使用後,再逐步增加。
- 聚合相似功能:如果多個功能很相似,考慮將它們整合成一個更通用的功能,用一個參數來區分具體操作。
- 動態載入工具:根據對話的上下文或使用者身份,只提供當下「可能需要」的工具,減少 LLM 的判斷範圍。
雷點四:安全性考量
Function Calling 讓 LLM 有了執行程式碼的能力,這也帶來了潛在的安全性風險。如果 LLM 能夠呼叫危險的函式(例如刪除檔案、修改資料庫),一旦被提示詞注入(Prompt Injection)攻擊,後果不堪設想。
- 情境:惡意使用者透過提示詞,誘使 LLM 呼叫一個
delete_user_data(user_id) 函式。
💡 建議:
- 最小權限原則:你的工具函式應該只擁有完成任務所需的最小權限。避免讓 LLM 有權限執行敏感操作。
- 沙盒環境:對於可能造成影響的工具,考慮將其放在沙盒(Sandbox)環境中執行,限制其對系統的存取能力。
- 人工確認:對於特別敏感的操作(例如發送郵件、付款),在執行前加入人工確認環節,或者讓使用者授權。
小結
Function Calling / Tool Use 是一個超級強大的功能,它把 LLM 從單純的語言模型,提升到了「可以跟現實世界互動」的智能代理。透過這個機制,LLM 不再只會跟你聊天,它能幫你查資料、發通知、控制你的智慧家庭設備,甚至串接各種外部服務,讓你的 AI 應用變得更聰明、更有用。
雖然實作上會有些眉角,但只要掌握好工具定義的清晰度、參數驗證的嚴謹性,以及最重要的安全性考量,你就能打造出功能更豐富、更可靠的 LLM 應用了。趕快動手玩玩看,讓你的 AI 助理從「嘴砲王」進化成「行動派」吧!