AI Agent 系列 · 第 3 篇 / 全 3 篇

AI Agent - Tool 的定義與設計:讓模型好好用你的工具

讓模型好好用你的工具

上一篇的磁碟追查 Agent 有兩個工具,disk_usagelargest_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_opget_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.serviceNginx、甚至它猜的服務名。有了 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 怎麼收場才不會把使用者一起拖下水。

留言