
昨天那筆帳:空目錄裡回一句 OK,51,673 個 token,0.335 美元。
今天同一件事,同一個空目錄,換兩個參數:
$ claude -p "回覆 OK" --output-format json \
--tools "Read,Grep,Glob" \
--setting-sources project,local
結果:6,171 個 token,0.035 美元。
| token | 成本 | ||
|---|---|---|---|
| 預設 | 51,673 | $0.3354 | |
| 精簡 | 6,171 | $0.0352 | 省 88% |

一天叫 100 次,一個月省下來的錢是 900 美元。今天講這兩個參數怎麼用,以及其中一個的地雷版本。
--tools:最大的一塊肥肉--tools 指定這次 spawn 要載入哪些內建工具。不給它,就是全部載入。
--tools "Read,Grep,Glob" # 只讀,適合巡檢型 agent
--tools "" # 全部關掉,純文字生成
--tools "default" # 預設全載(等於不寫)
工具 schema 是固定開銷裡最大的一塊,因為每個工具都要把完整的 JSON Schema 送進去。砍掉十個用不到的工具,省下來的量非常可觀。
怎麼決定留哪些?我的做法是照任務類型分成幾組常數,寫死在程式裡,不要每次現想:
# 巡檢型:只讀,不改任何東西
TOOLS_INSPECT = "Read,Grep,Glob"
# 分析型:可以讀、可以查網路,仍然不能寫
TOOLS_RESEARCH = "Read,Grep,Glob,WebFetch,WebSearch"
# 開發型:完整能力
TOOLS_DEV = "Read,Write,Edit,Bash,Grep,Glob,TodoWrite"
# 純生成:不需要任何工具(摘要、翻譯、格式轉換)
TOOLS_NONE = ""
TOOLS_NONE 特別值得一提。如果你的任務只是「把這段文字摘要成三行」,一個工具都不需要。這種任務在自動化流程裡佔比很高,而它們預設卻都揹著整套工具鏈在跑。
--setting-sources:省得漂亮,但有個坑Claude Code 啟動時會載入三個層級的設定:user(你家目錄的全域設定)、project(專案的 .claude/settings.json)、local(本機覆寫)。
--setting-sources 決定載入哪幾層。
我原本是這樣寫的:
--setting-sources "" # ← 這個版本有問題
全部不載入,省最多。跑起來也很正常,token 數字很漂亮。
問題是,專案的 .claude/settings.json 裡有我的 hook。
我在那份設定裡掛了一個 PreToolUse hook,用來擋住 agent 修改自己的身分設定檔(防止它改寫自己的權限宣告)。--setting-sources "" 把設定源全部關掉,那個 hook 就沒有載入,於是這道防線在所有自動化路徑上默默消失了。
而且它消失得無聲無息。沒有警告,沒有錯誤,agent 跑得比以前還順,因為少了一個會攔它的東西。
改成這樣就好:
--setting-sources project,local # 保留專案與本機設定,跳過全域
實測下來,project,local 跟空字串省下的 token 幾乎一樣多(差別在 user 層那份全域設定),但 hook 還在。
這是我在整個優化過程裡學到最重要的一課:省錢的參數和關掉防護的參數,長得很像。你在改成本相關的旗標時,要逐一問「這個旗標還順手關掉了什麼」。
如果你掛了 MCP server,工具 schema 的量會再上一個層級。
我自己的平台有 200 個以上的 MCP 工具。全部宣告出去的話,光工具定義就是好幾萬 token,而任何一個 agent 實際上用得到的可能只有二十個。
處理方式有兩層。
第一層,在 CLI 端隔離。 只掛需要的 MCP server:
claude -p "$PROMPT" \
--mcp-config ./minimal-mcp.json \
--strict-mcp-config # 忽略其他所有 MCP 設定來源
--strict-mcp-config 很關鍵。沒有它,你指定的設定會跟全域設定合併,你以為只掛了一個 server,實際上把家目錄那五個也帶進來了。
第二層,在 server 端過濾。 這是我後來的主要做法:MCP server 回應 tools/list 的時候,依照呼叫端的能力宣告過濾,只回傳這個 agent 有權呼叫的工具。
def handle_tools_list(caller_capabilities: set[str]) -> list[dict]:
"""只宣告這個呼叫端真的能用的工具。"""
return [
tool.schema for tool in ALL_TOOLS
if tool.required_capability in caller_capabilities
]
這裡有個原則要守住:可發現的工具集合,必須是可呼叫的子集合。
換句話說,你可以隱藏一個工具不讓它出現在清單裡,但不能讓一個出現在清單裡的工具在被呼叫時因為權限被拒。前者是省 token,後者是設計缺陷,會讓模型不斷嘗試一個它以為可用的工具,浪費好幾輪。
反過來說也不成立:隱藏不等於禁用。有些協定允許呼叫沒有列在清單裡的工具,所以工具過濾是省錢機制,不是權限機制。真正的權限要在執行的那一刻再檢查一次。這件事 Day 9 會單獨講。
把這幾天的東西合起來:
import subprocess
TOOLS_INSPECT = "Read,Grep,Glob"
def spawn_lean(prompt: str, cwd: str, tools: str = TOOLS_INSPECT) -> str:
cmd = [
which_claude(), "-p", prompt,
"--output-format", "json",
"--tools", tools, # 只帶需要的工具
"--setting-sources", "project,local", # 保留 hook,跳過全域
"--strict-mcp-config", # 不要偷偷合併其他 MCP
"--disallowedTools", "Bash", # 雙重保險:白名單之外再加黑名單
]
proc = subprocess.run(cmd, cwd=cwd, capture_output=True,
text=True, timeout=900)
if proc.returncode != 0:
raise RuntimeError(proc.stderr[:500])
return proc.stdout
--disallowedTools 那行是刻意的冗餘。--tools 已經沒給 Bash 了,再加一次黑名單看起來多餘。我還是留著,因為這兩個參數的解析路徑不同,而我不想賭它們在所有版本裡的交互行為都跟我想的一樣。安全設定寧可重複,不要留給推論。
改完之後一定要量,不然你不知道自己省到還是省壞了。
measure() {
claude -p "OK" --output-format json "$@" \
| jq '[.usage.input_tokens, .usage.cache_creation_input_tokens,
.usage.cache_read_input_tokens] | add'
}
echo "預設: $(measure)"
echo "精簡: $(measure --tools "Read,Grep,Glob" --setting-sources project,local)"
echo "無工具: $(measure --tools "" --setting-sources project,local)"
我的機器上跑出來是 51,673 / 6,171 / 更低。你的環境數字會不一樣,重點是看比例。
砍工具就是砍能力,這件事沒有例外。
我用 TOOLS_INSPECT 跑巡檢任務時,遇過一次 agent 需要看某個 log 的最後 100 行,但它只有 Read(會讀整個檔案)跟 Grep。那個 log 有 40 萬行。它讀了,然後 context 爆掉,任務失敗。
這種失敗的處理方式有兩種。差的做法是把 Bash 加回去,能力恢復了,防線也沒了。好的做法是給它一個專用的小工具,例如一個只能 tail 指定檔案最後 N 行的 MCP 工具。這樣它拿到能力,你保留邊界。
另一個代價比較隱晦:精簡過的 agent 比較不會自己想辦法。工具多的時候,模型會嘗試繞路(讀不到就搜尋,搜不到就執行指令查)。工具少的時候它就直接卡住或放棄。
所以工具清單要跟任務描述一起設計。如果你砍了工具卻還在 prompt 裡寫「必要時可以自行探索」,你是在要求它做不到的事。
第一幕到這裡結束。我們有了一個會自己醒來、記得住上一句、開機不那麼貴的 agent。
它現在還是很笨,因為它的手腳只有那三個內建工具。明天開始第二幕,講怎麼給它一雙自己的手:MCP 到底在解決什麼問題,以及怎麼在四十行內手刻一個。