博客

生產環境中的 Tool Calling:Schema 設計、tool_choice、並行呼叫與迴圈

Tool calling 演示起來容易,難的是讓它一直波瀾不驚:schema 需要可強制執行,一輪對話要有明確規定好的呼叫次數,迴圈要對失敗有預案。這是生產視角。

生產環境中的 Tool Calling:Schema 設計、tool_choice、並行呼叫與迴圈

Tool calling 上手並不難;難的是讓它一直保持“無聊”。demo 一個下午就能跑通,然後生產環境會提出更難的問題:schema 到底會不會被強制執行、一輪對話裡可以包含多少次呼叫、並行呼叫到來時怎麼辦、結果如何縫合回去、工具失敗時由誰處理。兩家主要廠商都把這些答案寫得很精確,而它們之間的差異正是整合變得有意思的地方。本指南遵循那些在 demo 之外依然站得住的 function calling 最佳實踐:迴圈的結構、schema 要求、tool_choice 引數、並行呼叫、結果處理,以及決定你如何編寫編排程式碼的跨廠商語義。它與本週早些時候的結構化輸出指南一文配套閱讀:那篇文章講的是模型被允許說什麼,這一篇講的是它被允許做什麼。

工具呼叫的往返流程,分三層

OpenAI 的 function calling 文件以三層結構確立了讓本文餘下部分得以清晰展開的術語體系。工具(tools)是我們交給模型的功能:一個用 JSON schema 定義的函式;文件明確指出,函式只是工具的一種,與之並列的還有 custom tools(自由格式文本進、自由格式文本出)和平臺內建工具。工具呼叫(tool calls)是模型請求使用其中某個工具,它以一類特殊響應的形式返回。工具呼叫輸出(tool call outputs)是我們生成的內容:真正執行工具後的結果,並回指到某次具體呼叫,這樣對話才能保持連貫。

文件給出的流程是五步,讀一遍是對抗義大利麵式編排最好的防禦:帶上可用工具發起請求;收到一次工具呼叫;用提供的引數執行你的程式碼;再發起第二次請求,攜帶工具輸出;收到最終響應,或者如果模型還需要更多步驟就繼續。Anthropic 的概覽從另一側描述了同樣的形狀:Claude 依據請求和工具描述決定呼叫某個工具,返回一個由你的應用執行的結構化呼叫,然後你的結果被送回以生成最終答案。整個設計目標就寫在 OpenAI 指南的一行裡:把工具定義、原始 prompt、模型的呼叫和輸出一起發回去,才是讓模型完成任務的關鍵。

function calling 最佳實踐從 schema 開始

OpenAI 文件裡最有價值的一句話是:設定strict: true可確保函式呼叫可靠地遵循函式 schema,而不是“盡力而為”,並且建議始終開啟它。相比過去只能“好好請求”的糟糕年代,這是一次升級,但這種 strict 模式的函式 schema 有硬性要求,因為它在底層借用了結構化輸出的機制:

  1. additionalProperties必須設定為false,作用範圍是parameters中的每個物件。
  2. 其中的所有欄位properties都必須標記為required。

可選欄位要用結構化輸出的方式表達:加上null作為一個型別選項,而不是把這個鍵從required裡省略。不滿足這些要求的 schema 會被拒絕,並給出缺失約束的詳細資訊,這是一種良好的失敗方式。

陷阱在於預設值。如果你省略strict,行為就取決於用哪個 API:在 Responses API 上,服務會盡可能把你的 schema 規範化成 strict 模式,做不到就退回非 strict 的盡力而為式 function calling;這種回退是可見的,因為響應裡的 tool 會顯示strict: false。Chat Completions 的請求預設仍然是非 strict 的。運營層面的解讀,這裡沿用文件的結構:不要以為“沒寫 strict”就是盡力而為,也不要以為“寫了 strict”就一定有保證,因為 API 表面和靜默回退都會改變答案。把你響應中各工具實際生效的 strict 狀態記進日誌,當它與預期不一致時告警。

schema 和模型變更帶來同樣的教訓:切換之前,先用 golden set 與 shadow run 流程。

tool_choice 與並行工具呼叫

預設情況下,由模型決定何時使用工具、用多少個;tool_choice 引數可以覆蓋這一點。文件列出的選項:"auto"(預設值,零個、一個或多個函式)、"required"(一個或多個函式)、強制呼叫某一個指定名稱的函式、allowed-tools 列表(把模型限制在一個子集裡,同時保留完整工具列表,這樣 prompt caching 仍能看到穩定的字首),以及"none",它相當於完全不傳函式。子集這個變體比聽起來更有用:鏈路上的工具相同,每輪的決策空間更小。

在並行工具呼叫上,事實比感覺更重要。在以 GPT-5 開頭的受支援模型上,當內建工具也可用時,函式可以被並行呼叫,但有一個前提:內建工具不能包含在並行的函式呼叫批次裡。如果你希望每輪精確地只有零次或一次呼叫,把parallel_tool_calls設為false。有兩條文件腳註應當寫進你的設計評審。第一,如果你用的是微調模型,而它在一輪裡呼叫了多個函式,這些呼叫的 strict 模式會被關閉。第二,gpt-4.1-nano-2025-04-14快照在啟用並行呼叫時有時會對同一個工具發出多次呼叫,文件建議在那裡關掉這個特性。通用原則:並行換來吞吐、付出確定性;任何有副作用的工具要麼做成冪等,要麼停用並行呼叫。

結果往返與錯誤處理

返回路徑的規定比請求路徑隨意,團隊糟糕的即興發揮正出在這裡。你在 call-output 訊息裡回傳的結果通常應該是字串,格式隨你選:JSON、錯誤碼、純文本;由模型來解釋。唯一的結構性要求是回指到具體某次呼叫,這樣並行或序列的呼叫就不會被搞混。對於返回圖片或檔案的工具,可以接受相應物件的陣列來代替字串。

兩個做法能把它從 demo 變成可運營的系統。第一,把工具失敗當作可讀的結果:當工具背後呼叫的 API 返回錯誤時,把簡明的失敗描述作為工具輸出喂回去,而不是讓迴圈崩潰或靜默重試;模型往往能繞開被清楚描述的失敗,而對話記錄裡也能看到真實經過。第二,像工程師而不是旁觀者那樣記錄往返過程:工具名、呼叫引用、耗時、重試次數和 request ID;這份日誌該記哪些欄位,包括值得儲存的廠商請求標識,本站的日誌指南里有講。當迴圈本身被 HTTP 錯誤碼絆住時,錯誤碼指南就是分診路徑;agent 迴圈正是 401 和 429 藏在重試裡的地方,這也是為什麼 CI 指南裡的護欄對 Web 後端和對流水線一樣適用。

非 JSON 輸出:custom tools 與文法

並非每個工具都想要 JSON。OpenAI 記錄了接受並返回自由格式文本的 custom tools;當輸出必須遵循特定形狀、不能套 JSON 外殼時,可以給 custom tool 附加一個文法,支援兩種語法:Lark 和 regex。文法機制是一個上下文無關文法,文件對它的代價說得很直接:把文法限制在工具真正需要的規則和模式上,因為過於複雜的文法可能導致 API 錯誤,而複雜的文法往往需要在文法、prompt 和工具描述三者之間一起迭代。有兩個文件細節值得內化:錨點之間的自由文本要用單個有界終結符,因為詞法分析器是貪婪匹配的,把自由文本拆到多條規則裡就失去了控制;還要記住終結符和規則是不同層次,regex 語法遵循 Rust regex crate 的語法,而不是 Python 的re模組。文法很強大,而像大多數強大的東西一樣,它的維護賬單隨複雜度一起增長。

Token 與成本核算

工具定義不是免費的,文件給出了原因:函式會被注入到 system message,所以可呼叫的函式定義會佔用上下文上限,並按 input tokens 計費。文件給出的緩解辦法都很實際:限制一開始載入的函式數量、儘可能縮短描述,並使用 tool search 讓延遲載入的工具只在需要時載入。這與你整合中其他部分的成本紀律是同一回事,而讓這件事可控的歸屬習慣——按呼叫方和功能給用量打標籤——在用量歸屬指南裡有講。只增不減的工具列表就是悄無聲息的成本上漲;像修剪依賴那樣修剪它。

跨廠商:同樣的迴圈,不同的語義

Anthropic 的 tool use 概覽讀起來像是同一批概念的譯文,有兩處差異值得提前規劃。在請求側,你傳入的工具帶一個input_schema;在響應側,Claude 返回一個或多個tool_use塊,指明工具名稱及其引數,並帶有一個stop_reason為"tool_use";你的程式碼執行後,第二次請求攜帶tool_result塊,這些塊通過tool_use_id一一配對回來。配對用的識別符號,而不是位置順序,才是契約。

第二處差異是執行位置。Anthropic 把 client tools(包含你的 custom tools,以及 Anthropic 定義的 schema,如bash和text_editor,它們在你的應用裡執行)與 server tools(如web_search、web_fetch、code_execution和tool_search)區分開來:它們執行在 Anthropic 的基礎設施上,並直接返回你能看到的結果。server tools 不需要執行 harness,但文件提到一條互動規則:當 server tool 與 client tool 共享同一個並行批次時需注意,所以在一輪裡混用兩者之前,先讀 stop-reason 相關文件。在並行控制上,同一個tool_choice塊接受一個停用並行工具呼叫的標誌,文件表述是要求每輪最多一次工具呼叫。

跨廠商的編排教訓是:OpenAI 的迴圈用顯式呼叫引用把結果與呼叫配對,Anthropic 的迴圈用tool_use_id;兩者都是基於 ID 的契約,都不容忍假設順序的迴圈。按 ID 來寫編排層,同一套邏輯就能在廠商之間移植。如果你通過一個相容端點訪問模型,相容性清單裡的形狀檢查對工具呼叫和對文本一樣適用,而你服務的端點背景資料在它的文件裡。

生產環境清單

  1. 讓每個 schema 都符合 strict 的要求:additionalProperties: false處處都要有,每個屬性都要進required。
  2. 按 API 表面逐一核實實際生效的 strict 狀態,並在回退產生strict: false時告警。
  3. 按端點決定一輪裡允許多少次工具呼叫;不要稀裡糊塗地繼承預設值。
  4. 讓有副作用的工具冪等,或者對它們停用並行呼叫。
  5. 把失敗作為可讀結果返回,並記錄工具名、call ID、耗時、重試次數和 request ID。
  6. 保持工具定義精簡,目錄變大時用 tool search 延遲載入。
  7. 按 call ID 而不是按順序來寫迴圈,這樣換廠商的成本才低。
  8. 模型升級時重新跑一遍工具呼叫迴歸,模型輪換時按棄用應對手冊裡的遷移習慣來做。

Tool calling 獎勵那些把它當作有明確邊界的協議、而不是當作魔法的團隊。讀這兩家的指南,尊重基於 ID 的契約,迴圈就會以最好的方式無聊下去:無人值守、可觀測、無聊。