讓模型做事:
Tool calling、結構化輸出與看圖
上一課模型只是回話。要把它放進程式裡做事,關鍵是這個動作:模型不直接回答, 而是回一個「請幫我呼叫 get_weather(city="台北")」的結構化請求, 你執行完把結果餵回去,它再寫最終答案。這就是所有 Agent 的地基。先看一次完整的兩回合:
這三種能力都走同一個 gateway、同一把 key,但各家支援程度差很多—— notebook 的每一節除了示範,都會掃一遍可用模型產出「誰能用」的結論表。
說明書、兩回合、然後把它包成迴圈
工具對模型來說是一份說明書:名字、做什麼、參數長什麼樣(JSON Schema)。 這是 OpenAI 訂的格式,gateway 會翻譯給各家。第一回合帶著 tools= 送出, 回應的 message.content 是空的、tool_calls 裡有一筆 get_weather,arguments 是 JSON 字串(要 json.loads)。
第二回合最容易搞錯:對話紀錄要按順序塞三樣東西——
為什麼手組 dict 而不是直接塞 SDK 物件?序列化後 content 是 null、 欄位齊全度依供應商而異,有的相容層會 400。手組最可攜。真實程式把兩回合包成一個迴圈: 只要模型還在要求工具就執行、餵回、再問;問「1+1」它會直接回答——用不用工具是模型自己判斷的。
到 notebook 的 1️⃣–3️⃣ 節:說明書、兩回合、run_with_tools誰能可靠地呼叫工具?
同一段程式、同一個問題,掃遍教學 key 能用的對話模型。實測(2026-08-20):
| 模型 | 結果 | 備註 |
|---|---|---|
| nemotron-3.5-lightning | ✅ | 本系列預設模型;會把 city 翻成 "Taipei",假天氣函式要接受英文別名 |
| nemotron-3-ultra | ✅ | tool call 回合的 content 是 None(不是空字串) |
| gpt-oss-120b(Groq) | ✅ | 約 1 秒 |
| gemini-3.5-flash | ✅ | 約 1.3 秒 |
| cf-gpt-oss-120b | ✅ | 參數裡的中文被 \u 跳脫,json.loads 後一樣 |
| deepseek-v4-flash | ❌ | HTTP 402:上游(HuggingFace)月額度用完 |
這張表在 notebook 裡是活的——你執行時會重跑一次,結果可能跟今天不同。 這正是重點:供應商行為會變,pipeline 要依賴某個能力之前,自己掃一遍。
到 notebook 的 4️⃣ 節:掃描所有模型逼模型吐合法 JSON,以及一個「默默失效」的陷阱
沒有約束時,問「抽取人物資料」模型會回一段漂亮的 markdown、欄位名還自己取(「姓名」「興趣」)。 加上 response_format={"type": "json_schema", ...} 並設 strict: True, 輸出就是一行嚴格照 schema 的 JSON:{"name":"小明","age":12,"city":"台北","hobbies":["籃球","圍棋"]}, age 是真的 int。
陷阱:gateway 開了 drop_params——某家不認得 response_format, 這個參數會被悄悄拿掉,呼叫不報錯、輸出看似正常、實則毫無約束。更陰險的是 nemotron-3-ultra 這種同一個模型名背後三個上游的情況:實測這一發嚴格照 schema、 下一發回一張 markdown 表格(json.loads 當場炸掉),取決於落到哪家; nemotron-3.5-lightning 的兩個來源實測都遵守,但別把這當保證。 所以驗收標準不是「有沒有報錯」,而是逐欄驗證輸出型別——notebook 的 validate() 就在做這件事, 示範格也寫成「解析失敗就解釋原因」而不是直接崩潰。
到 notebook 的 5️⃣ 節:schema、逐欄驗證、掃描把圖片塞進訊息——用已知答案驗證
多模態訊息 = content 變成一個陣列:文字塊 + 圖片塊(data URL 或公開網址)。 為了確認模型「真的看到圖」而不是瞎掰,notebook 當場用 Pillow 畫一張白底紅色圓形—— 答案已知,答對才算。實測只有 gemini-3.5-flash 答「圖片中是一個紅色的圓形」; 其他模型不是 400 就是 404(本系列預設的 nemotron-3.5-lightning 也不吃圖)。 結論:圖片任務要指名看得懂圖的模型,別丟給多來源的群組名,它會輪到不吃圖的家。
到 notebook 的 6️⃣ 節:畫圖、問圖、掃描換你動手
加第二個工具 convert_currency(amount, from, to)(匯率寫死),問「100 美元換台幣多少?」——模型會挑對工具嗎?
在 schema 加一個原文沒有的 email 欄位,看嚴格模式下模型被迫填什麼——這就是 schema 要留 optional/null 的理由。
讓 run_with_tools 在工具失敗時把錯誤訊息當 tool 結果餵回去(查一個不存在的城市),觀察模型會不會自我修正或誠實說查不到。
卡住了?每一題在 notebook 末節都有折疊解答——先自己做,再打開對照。
情境測驗
離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。
Q1 情境題
第一回合模型回了 tool_calls,你執行 get_weather 拿到 {"weather": "晴", "temp_c": 31}。要讓模型寫出最終答案,下一步應該怎麼組 messages?
對話紀錄要按順序塞三樣:user 原話、模型的 tool call 回合(手組 dict)、你的 role: "tool" 結果(id 要對上)。A 有副作用——SDK 物件序列化後 content 是 null、欄位齊全度依供應商而異,有的相容層會 400,手組最可攜;B 能動,但丟掉了 tool 協定——多工具、多回合時對不回哪次呼叫,迴圈就寫不下去;D 少了 assistant 的 tool_calls 回合,tool 訊息的 tool_call_id 沒有對象,整段紀錄不成立。
Q2 錯誤診斷
用同一段程式、同一份 TOOLS 掃遍所有模型,某家的結果是 ⚠️:沒報錯、tool_calls 是空的,content 直接是一段編出來的天氣描述。最可能的原因是?
掃描的三種結果裡最陰險的就是 ⚠️:不報錯、也不呼叫工具,直接瞎掰天氣——「沒報錯」不等於「支援」,這正是 pipeline 依賴某個能力前要自己掃一遍的理由。A 截斷會留下 finish_reason='length' 的痕跡(上一課踩過);B 額度問題是直接報 HTTP 錯——實測 deepseek-v4-flash 回 402,是 ❌ 不是 ⚠️;C 用別的模型就能排除——同一份說明書在其他家都正常發出 get_weather。
Q3 錯誤診斷
你用 nemotron-3-ultra 配 response_format 的 json_schema 模式抽資料。上一發輸出嚴格照 schema,這一發卻拿到這樣的結果。最可能的原因是?
nemotron-3-ultra 背後三個上游,遵不遵守 schema 取決於這發落到哪家;不支援的那家經 drop_params 把 response_format 悄悄拿掉——呼叫不報錯、實則毫無約束。修法:驗收改成逐欄驗證型別(notebook 的 validate()),或改用實測來源都遵守的模型(lightning 兩個來源實測都遵守,但別當保證)。B 若是寫法錯,每一發都會壞,不會這發好、下發壞;C 治標不治本——沒有 schema 約束時模型愛怎麼包就怎麼包;D——供應商不會因為忙就丟掉你的參數,會丟參數的是 drop_params。
Q4 情境題
你要做「使用者上傳圖片,模型描述內容」的功能,走這個 gateway。應該怎麼做?
多模態訊息=content 從字串變成陣列(文字塊+圖片塊),而且模型要指名:用「白底紅圓」這種已知答案實測,整排掃下來只有 gemini-3.5-flash 答對。A 不行——本系列預設的 lightning 不吃圖,丟過去不是 400 就是 404;C 是誤解——文字通道進去的 base64 對模型只是一串亂碼,它沒有把字串還原成影像的能力;D 正是本課點名的陷阱:gateway 的輪替不看能力,會輪到不吃圖的家。
實作在 molab 跑(免費)
molab 的登入狀態進不了內嵌框架(瀏覽器的跨站 cookie 保護), 所以 notebook 要在新分頁執行——把它跟本頁並排開,左邊教學照樣對照。
- 登入 molab(GitHub / Google)
- 開啟課程 notebook,Fork 成自己的副本即可編輯
- 從第一格往下全部執行(首次安裝套件約 1 分鐘)——免費 CPU 環境即可,不需要 GPU
不想用 molab?下載 litellm-tools_ext.py 後在自己電腦
uvx marimo edit --sandbox litellm-tools_ext.py,依賴會自動安裝。