Learning Objectives
- 定義工具:名稱、用途說明與參數的 JSON Schema
- 走完一次完整來回:模型提出呼叫、程式執行、結果以 tool 訊息放回、模型回答
- 說明模型只提出呼叫,真正執行的是你的程式
- 驗證參數,並把工具的錯誤回傳給模型
- 處理模型一次要求多個工具的情況
模型查不到的東西,由程式去查
模型只會產生文字。它不知道你今天的課表,也改不了校務系統裡的資料。要讓它用到這些東西,就要讓它能「請求」你的程式去做:這就是工具呼叫(tool calling,也叫 function calling)。
這一課的情境是校園助手,資料來自虛構的《優學大學學生手冊》,不是任何學校的規定;學號、課表也都是假的,工具全部是範例裡的 Python 函數,不連外。
先記住最重要的一句話:
模型只是提出呼叫,執行的是你的程式。
模型回傳的只是一段「我想呼叫這個函數,參數是這些」的資料。要不要執行、怎麼執行、把什麼結果告訴它,全部由你的程式決定。第 17、18 課談安全時,都建立在這一點上。
逐步看:模型提出一個工具呼叫
程式只呼叫模型一次,還沒有執行任何工具。請看模型這次回傳的和第 7 課有什麼不同。
參數的定義會影響模型怎麼填
上面的 keyword 只寫了「要查的關鍵字」,模型實際填的是 停修 一學期 最多 幾門 課。Schema 只限制型別,內容還是模型依說明猜的。
strict 模式的要求(OpenAI 文件):每個物件都要 additionalProperties: false,properties 裡的欄位全部要列在 required;要讓欄位可省略,就在型別加上 null。Chat Completions 預設不是 strict,要自己打開。
內容只有固定幾種時,用 enum 列出來最可靠。下一個範例把主題寫成 enum,模型就只能從這幾個值裡選:
"properties": {"topic": {"type": "string", "enum": ["停修", "圖書館借閱", "宿舍訪客"]}}
逐步看:完整的一次來回
這次把流程走完:提出呼叫、執行、放回結果、再問一次。注意 messages 怎麼一路變長,模型總共被呼叫了兩次。
一次來回的訊息順序
| 順序 | role |
誰寫的 | 內容 |
|---|---|---|---|
| 1 | user |
使用者 | 一學期最多可以停修幾門課? |
| 2 | assistant |
模型 | content 是空的,tool_calls 提出 search_handbook |
| 3 | tool |
你的程式 | tool_call_id 對應第 2 則,content 是查到的條文 |
| 4 | assistant |
模型 | 一學期最多可以停修 1 門課…… |
模型每一輪都不記得上一輪(第 7 課),所以第 2、3 則都要放進 messages 重新送出。工具結果要多長,也就多花多少輸入 token,所以工具不要回傳用不到的欄位。
逐步看:工具出錯時,把錯誤告訴模型
使用者把學號打成小寫。我們的函數會丟出例外。先猜猜看:模型收到錯誤訊息後,會自己改成大寫重試,還是回頭問使用者?
錯誤要回傳給模型,而且要寫得讓它看得懂
上面錄到的結果是:模型沒有自己把 b1123456 改成大寫,而是回覆使用者「請提供 1 個大寫英文字母加 7 位數字的學號」。你可能以為它會自己改成大寫,但錄到的不是這樣;我們的錯誤訊息也沒有說可以自己改。
工具出錯時有兩種常見寫法:
| 寫法 | 結果 |
|---|---|
| 讓例外往外丟,整支程式中斷 | 使用者只看到系統錯誤 |
| 接住例外,把錯誤內容當成工具結果回傳 | 模型可以改參數重試,或用白話告訴使用者 |
錯誤訊息要寫清楚哪裡錯、正確的格式是什麼。不要把程式內部的資訊(資料庫路徑、堆疊追蹤、金鑰)放進去:回傳給模型的內容可能會出現在回答裡。
另一件要記得的事:模型填的參數不可以當作已驗證的資料。 例如 student_id 是模型從對話裡讀來的,程式應該用登入者的身分去查,不是誰說自己是誰就查誰。第 17 課會再談。
逐步看:一次要求多個工具
一句話問了兩件事。請看 tool_calls 有幾個元素,以及少回一則 tool 訊息時 API 怎麼回應。
實作:派送工具呼叫
完成 dispatch(call):依呼叫的名稱執行對應的函數,回傳一則 tool 訊息。這題不呼叫模型,make_call 做出和 SDK 相同寫法的呼叫物件。
content 一律是 JSON 字串:成功時是 {"result": 結果},失敗時是 {"error": 訊息}。要處理這四種失敗,不可以讓程式中斷:
| 情況 | 怎麼發現 | error 的內容 |
|---|---|---|
| 工具不存在 | 名稱不在 TOOLS 裡 |
未知的工具:名稱 |
| 參數不是 JSON | json.JSONDecodeError |
參數不是合法的 JSON |
| 參數和函數對不上 | TypeError |
參數不符合工具的定義 |
| 工具自己回報錯誤 | ValueError |
例外本身的訊息 |
小測驗:工具呼叫
模型回傳 finish_reason 是 tool_calls 時,那個工具執行了嗎?
call.function.arguments 的型別是什麼?
把工具結果放回 messages 時,下列哪一則是正確的?
模型一次提出 2 個工具呼叫,你只回了第 1 個的結果就送出下一次請求,會怎樣?
工具的函數丟出例外(例如學號格式錯誤),比較好的做法是什麼?
重點整理
- 模型只提出呼叫,執行的是你的程式;要不要執行,由程式決定。
- 流程:送出
tools→ 模型回finish_reason: tool_calls→ 解析arguments(JSON 字串)→ 執行 → 以role: tool和tool_call_id放回 → 再呼叫一次。 - Schema 管得到型別與格式,管不到內容對不對;參數要在工具裡再驗證。內容只有固定幾種時用
enum。 - 工具出錯要回傳清楚的錯誤訊息給模型,不要讓程式中斷,也不要放內部資訊。
- 模型可能一次要求多個工具,每個呼叫都要有一則 tool 訊息。
下一課: 很多任務要查好幾次才做得完,例如先查課表、再查每門課的缺課紀錄。把這個來回放進迴圈,讓模型自己決定下一步,就是 Agent。