AI Agent - Tool 的定義與設計:讓模型好好用你的工具
讓模型好好用你的工具
上一篇的磁碟追查 Agent 有兩個工具,disk_usage 和 largest_entries,都是我手工挑過的。當時跳過了一個問題:為什麼是這兩個?
明明給一個 run_shell 就好了。模型自己會下 df、自己會 du,什麼指令都會,一個工具打天下。
這篇就從這個問題開始,講工具設計。
版本聲明
- Go 1.24、anthropic-sdk-go v1.61.0、模型 claude-opus-4-8
- 寫於 2026 年 7 月
為什麼不乾脆給一個 shell
run_shell 確實是能力上限最高的工具,模型透過它幾乎什麼都做得到。問題不在模型端,在你這端:你的程式只會看到一個不透明的指令字串。
上一篇說過,工具的執行方是你的程式。收到 tool_use 之後、真正執行之前,這中間是你唯一的介入點。工具的形狀決定了你在這個點上能做什麼:
拿到 restart_service{"service": "nginx"},你可以在執行前跳確認、可以記 audit log、可以檢查這個服務是否在允許清單裡。
拿到 run_shell{"cmd": "systemctl restart nginx"},你只有一個字串。要攔截就得自己 parse shell 語法——然後模型哪天改用 sh -c '...' 包一層,你的檢查就穿了。
所以判斷標準不是「模型需要什麼能力」,而是「你需不需要在執行前後做事」:
- 需要人工確認的(重啟、刪除、對外發送)→ 專用工具,參數結構化才攔得住
- 需要判斷可否平行執行的(唯讀查詢可以平行,
git push不行)→ 專用工具,程式才分得出來 - 純唯讀、低風險的探索(查狀態、看 log)→ shell 沒問題
我的做法:從 shell 起步把流程跑通,觀察模型實際用了哪些指令,再把高風險、高頻的那幾個提升成專用工具。上一篇的兩個工具就是這樣來的。
名字和描述是 prompt 的一部分
模型看不到你的實作,它認識一個工具的全部資訊就是名字、描述、參數 schema。這三樣東西實際上是 prompt,值得用寫 prompt 的力氣去寫。
名字要具體:restart_service 好過 service_op,get_slow_queries 好過 db_tool。模型靠名字做第一輪篩選。
描述要寫「什麼時候用」,不只是「做什麼」。這是上一篇踩過的坑的完整版:
// 不好:只描述功能
Description: anthropic.String("列出目錄大小")
// 好:功能 + 使用時機
Description: anthropic.String(
"列出指定目錄下佔用空間最大的子目錄,依大小遞減排序。" +
"用於逐層追查磁碟空間被誰吃掉。")
這不是玄學。新一代的模型呼叫工具比以前保守,不確定該不該用的時候傾向不用。描述裡有明確的觸發條件(「當使用者問到⋯⋯時呼叫」),實測會直接反映在該呼叫時有沒有呼叫上。
參數 schema:能拴的就拴
自由填空的參數,模型就會自由發揮。能列舉的就用 enum 拴死:
"service": map[string]any{
"type": "string",
"enum": []string{"nginx", "mysql", "php-fpm"},
"description": "服務名稱,只允許清單內的值",
},
沒有 enum 的話,模型可能會傳 nginx.service、Nginx、甚至它猜的服務名。有了 enum,選項之外的值根本不會出現。
另外兩條:
選填參數在你這端補預設值,不要指望模型每次都給。上一篇 top 沒給就補 10,就是這個。
錯誤訊息是寫給模型看的。工具失敗時回傳的字串,模型會讀、會據此修正下一步。「invalid input」它只能瞎猜;「path 必須是絕對路徑,收到的是 log/nginx」它下一次就會改對。
strict mode:讓 schema 變成保證
一般情況下,schema 對模型是「強烈建議」,極少數情況下它仍可能給出不合法的參數——多一個沒定義的欄位、型別不對。參數要直接進資料庫或敏感操作的話,開 strict 把建議變成保證:
strictTool := anthropic.ToolParam{
Name: "restart_service",
Description: anthropic.String("重啟指定的 systemd 服務。只在使用者明確要求重啟時呼叫。"),
Strict: anthropic.Bool(true),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"service": map[string]any{
"type": "string",
"enum": []string{"nginx", "mysql", "php-fpm"},
},
},
Required: []string{"service"},
ExtraFields: map[string]any{
"additionalProperties": false, // strict 模式必填
},
},
}
開了 strict,API 保證 tool_use.input 完全符合 schema,你的 unmarshal 不會再遇到意外。代價是 schema 有些限制:不能遞迴、不支援 minimum/maximum 這類數值約束、物件都要宣告 additionalProperties: false。範圍檢查還是得在自己程式裡做。
tool_choice:控制用不用
預設模型自己決定要不要用工具(auto),但可以強制:
| 寫法 | 行為 | 用在哪 |
|---|---|---|
OfAuto | 模型自己決定(預設) | 一般 Agent |
OfAny | 一定要用某個工具 | 不准直接回文字的流程 |
OfTool + Name | 一定要用指定工具 | 拿工具當結構化輸出用 |
OfNone | 不准用工具 | 只要它總結、不要它再動手 |
// 強制呼叫指定工具
choice := anthropic.ToolChoiceUnionParam{
OfTool: &anthropic.ToolChoiceToolParam{Name: "restart_service"},
}
OfTool 有個正經用途以外的妙用:定義一個參數就是你要的輸出格式的工具,強制模型呼叫它,就得到了保證合法的結構化 JSON——分類器、資料萃取這類場景很好用。
另外,模型預設可以在一則回應裡同時呼叫多個工具(上一篇講過,結果要放同一則訊息回去)。如果你的工具之間有順序依賴,可以關掉平行呼叫:
serial := anthropic.ToolChoiceUnionParam{OfAuto: &anthropic.ToolChoiceAutoParam{
DisableParallelToolUse: anthropic.Bool(true),
}}
不過先想清楚再關——平行呼叫是省 token 也省時間的,大部分「順序問題」其實是工具設計問題。
工具給幾個?
比你直覺的少。
工具清單每個都佔 context、每個都參與模型的決策。給了二十個工具,模型在相近的幾個之間猶豫、選錯的機率也跟著上升。我的經驗是一個 Agent 十個工具以內都還算好管,超過就該懷疑是不是把兩個不同職責的 Agent 揉在一起了。
真的有大量工具的場景(比如包 MCP server、上百個 API),API 有 tool search 機制讓模型按需載入工具定義,那是之後的話題。
收尾檢查清單
設計一個工具時過一遍:
- 名字夠具體嗎?看名字就知道跟誰有關、做什麼事?
- 描述有寫「什麼時候用」嗎?
- 能 enum 的參數都 enum 了嗎?
- 選填參數有預設值嗎?
- 錯誤訊息模型讀得懂、知道怎麼修嗎?
- 這個工具需要執行前攔截嗎?需要的話,參數夠結構化嗎?
下一篇講錯誤處理:工具壞掉的時候,Agent 怎麼收場才不會把使用者一起拖下水。
留言