初學者最大的誤解:以為給了工具,Claude 就會自己去跑。不會。Claude 只會告訴你「我想呼叫哪支、參數是什麼」,實際執行是你的程式的事。
1. 你 → API messages(使用者問題)+ tools(工具說明書)
2. API → 你 「我要呼叫 get_weather,參數 {city:"台北"}」
← 這是 tool_use block,stop_reason: "tool_use"
3. 你的程式 自己去跑 get_weather("台北"),拿到「28度晴」
← Anthropic 完全沒有參與這一步
4. 你 → API 上面全部歷史 + tool_result(「28度晴」)再送一次
5. API → 你 「台北現在 28 度晴天」
← stop_reason: "end_turn",迴圈結束
這個來回叫 agentic loop(代理迴圈)。模組 2 的 2.1–2.3、模組 5 的 5.5,講的全都是這個迴圈的規則。
stop_reason 是迴圈的方向盤你的程式靠這個欄位決定下一步做什麼,不是靠猜:
| 值 | 意思 | 你該做什麼 |
|---|---|---|
end_turn | 講完了 | 把文字給使用者,迴圈結束 |
tool_use | 要你去執行工具 | 執行 → 回 tool_result → 再呼叫一次 |
max_tokens | 撞到輸出上限被截斷 | 見 模組 2 §2.4 的 truncation 處理 |
pause_turn | server 端工具跑到迭代上限 | 把 assistant 回應接回歷史、原樣再送一次(不要加「請繼續」) |
refusal | 被安全機制擋下 | 看 stop_details 分類,不要用同一個 prompt 重試 |
end_turn 和 tool_use,其他三個沒接。refusal 沒接會讓程式在讀 content[0] 時直接爆掉(refusal 時 content 可能是空陣列)。tools + 完整對話歷史。第 1 個 request:tools[全部] + system + [user:"台北天氣?"]
第 2 個 request:tools[全部] + system + [user:"台北天氣?",
assistant:[tool_use],
user:[tool_result]]
第 3 個 request:tools[全部] + system + [上面全部 + assistant:"28度晴",
user:"那高雄呢?"]
↑ tools 那包從頭到尾都在,每一次都在
CLAUDE.md、記得整段對話——那是應用層在你本機保管歷史、每輪重送給 API。API 端永遠是失憶的。「設定一次就好」這件事在 Messages API 不存在。必填只有三個:model、max_tokens、messages。其他全是選填。
POST https://api.anthropic.com/v1/messages
{
── 必填三兄弟 ──────────────────────────────
"model": "claude-opus-5",
"max_tokens": 16000,
"messages": [ {"role":"user", "content":"台北天氣?"} ],
── 常用選填 ────────────────────────────────
"system": "You are a helpful assistant.",
"tools": [ {name, description, input_schema} ],
"tool_choice": {"type":"auto"}, ← 模組 2 §2.1
"thinking": {"type":"adaptive", "display":"summarized"},
"output_config": {
"effort": "high",
"format": {"type":"json_schema", "schema":{…}} ← 模組 2 §2.4
},
"stream": true,
"cache_control": {"type":"ephemeral"},
"stop_sequences": ["END"],
"metadata": {"user_id":"…"}
}
system 是獨立參數,不是 messages 的一員system 就是俗稱的 system prompt——角色設定、行為規則、格式要求、背景資料。它不屬於任何一輪對話,整段對話從頭到尾都掛著。
messages 陣列。messages:[{role:"system",…},{role:"user",…}];Claude 的 system 是跟 messages 平行的獨立欄位,messages 陣列裡基本上只有 user 和 assistant 兩種 role。兩種寫法——要掛快取就必須用陣列形式:
// 簡單版:字串就好
"system": "You are a helpful assistant."
// 要快取就必須用 text block 陣列
"system": [
{ "type": "text",
"text": "…很長的背景文件…",
"cache_control": {"type":"ephemeral"} }
]
實務上 system 最值錢的用法是塞大量但穩定的背景資料(產品文件、API spec、公司規範)——因為整段對話都不變,可以整包快取。Claude Code 讀完 CLAUDE.md,就是把內容組進這個欄位,然後每輪重送(§2)。至於它為什麼排在 tools 之後、messages 之前,見 §5。
max_tokens 是輸出上限,不是總量它管的是模型這次能吐多少,跟你送進去多少無關。給太小會被截斷(stop_reason:"max_tokens",處理方式見 模組 2 §2.4)。實務預設:非串流 ~16000、串流 ~64000——超過約 16K 又不開串流,SDK 會撞 HTTP timeout。
| Header | 值 | 備註 |
|---|---|---|
x-api-key | sk-ant-… | 必填 |
anthropic-version | 2023-06-01 | 必填,這個值長年沒變 |
content-type | application/json | 必填 |
anthropic-beta | beta flag | 只有用到 beta 功能才要 |
用 SDK 時你會寫成 betas=["…"] 參數,SDK 幫你翻成 header——所以看到「某功能需要 beta header」和「SDK 的 betas 參數」講的是同一件事。
{
"id": "msg_01XYZ…",
"type": "message",
"role": "assistant",
"model": "claude-opus-5",
"content": [ ← 永遠是陣列,元素是不同 type 的 block
{"type":"thinking", "thinking":"…"},
{"type":"text", "text":"讓我查一下"},
{"type":"tool_use",
"id":"toolu_01ABC", ← 記住這個 id
"name":"get_weather",
"input":{"city":"台北"}}
],
"stop_reason": "tool_use", ← 方向盤(§1 那張表)
"stop_details": null, ← 只有 refusal 時才有內容
"usage": {
"input_tokens": 1200, ← 沒快取到、全額計費
"output_tokens": 85,
"cache_creation_input_tokens": 0, ← 寫入快取(~1.25x 價)
"cache_read_input_tokens": 3400 ← 快取命中(~0.1x 價)
}
}
response.content[0].text。開 thinking 時第一個 block 是 thinking,沒有 .text,直接爆。永遠先檢查 .type 再取值——這在每一種語言的 SDK 都一樣。input_tokens 不是輸入總量,只是其中一塊。三個欄位加起來才是這次 prompt 的實際大小:
| 欄位 | 是什麼 | 計價 |
|---|---|---|
cache_read_input_tokens | 以前寫過快取、這次讀到的部分 | ~0.1x(賺到) |
cache_creation_input_tokens | 這次第一次寫進快取的部分(存起來給下次用) | 1.25x(5 分鐘 TTL)/2x(1 小時 TTL) |
input_tokens | 沒進快取的剩餘部分 | 1x 全額 |
input_tokens 只有 4K,不是統計壞掉——是其他都命中快取了。看總和,不要看單一欄位。那個 toolu_01ABC 就是下一輪 tool_result 要用 tool_use_id 對回去的值——見 模組 2 §2.2。
API 內部會把 payload 拼成一段給模型看的文字,順序固定:
tools → system → messages
這個順序不是隨便排的,是按穩定程度排——最不會變的放最前面:
| 位置 | 內容 | 多常變 |
|---|---|---|
1. tools | 工具說明書 | 幾乎不變(整個 app 生命週期) |
2. system | 角色、規則、背景文件 | 很少變(整段對話不變) |
3. messages | 對話歷史 | 每一輪都在長 |
三層是巢狀的,每一層的快取鍵 = 這層以前的所有內容 bytes + 被歸類到這層的請求參數:
| 快取層 | 它的 key 包含什麼 |
|---|---|
| tools | 工具定義 + model |
| system | ↑ 全部 + system 內容 |
| messages | ↑ 全部 + 對話內容 + tool_choice、圖片、thinking 開關 |
所以 tool_choice 明明是 top-level 參數、不在 messages 裡面,改它卻會打掉 messages 層——因為它被歸類為 messages 層參數(它改變的是模型生成 assistant 回應當下的行為,完全不影響 tools 和 system 怎麼被編碼)。
| 你改了什麼 | tools 快取 | system 快取 | messages 快取 |
|---|---|---|---|
| 工具定義(新增/刪除/換順序) | ❌ | ❌ | ❌ |
| 換模型 | ❌ | ❌ | ❌ |
| system prompt 內容 | ✅ | ❌ | ❌ |
tool_choice/圖片/thinking 開關 | ✅ | ✅ | ❌ |
| 對話內容(正常往下講) | ✅ | ✅ | ❌ |
(✅ = 還活著可重用,❌ = 失效要重算)
tool_choice 不會炸掉 tools/system——但長對話裡 messages 段才是 token 大頭,跑了 20 輪的 agentic loop 每輪換 tool_choice,一樣會讓成本暴增。對照 模組 2 §2.1 的加碼事實。datetime.now() 或 UUID——前綴每次都不一樣,永遠 miss。動態內容要往後放。json.dumps(tools) 沒加 sort_keys=True——序列化順序不固定,bytes 就不同。怎麼驗證有沒有命中:看 usage.cache_read_input_tokens。重複送相同前綴卻一直是 0 → 上面三個裡面中了一個。
另外:一個 request 最多 4 個 cache_control 斷點;快取讀取約 0.1x 價、寫入 1.25x(5 分鐘 TTL)或 2x(1 小時 TTL)——所以 5 分鐘 TTL 送兩次就回本。
讀舊文章、舊教材、或問到舊版模型時會看到的寫法,在現行模型上直接 400。考題很愛拿這些當誘答:
| 舊寫法 | 現在怎麼寫 | 從哪個模型開始 |
|---|---|---|
temperature / top_p / top_k | 全部移除,改用 prompt 引導行為 | Opus 4.7 之後的 Opus、Sonnet 5、Fable 5 |
thinking:{type:"enabled", budget_tokens:N} | thinking:{type:"adaptive"} + output_config.effort | 同上(4.6 家族僅為過渡保留) |
| 最後一則 assistant 的 prefill | output_config.format(結構化輸出) | 4.6 家族起(見 §2.4) |
top-level output_format | output_config.format | 全模型(API 層級改名) |
budget_tokens、temperature=0 求「決定性輸出」、或「用 prefill 逼出 JSON」——這三個在現行模型上都是錯的。prefill 那條的官方遷移路徑永遠是 Structured Outputs。不用寫字,心裡答完再展開對答案。三題都答得出來就可以去讀模組 2 了。
有,而且是完整一包。API 是 stateless,第 10 輪跟第 1 輪一樣要傳整包 tools + 前面九輪的完整歷史。
延伸:這正是為什麼「工具數量」是每輪都在付的固定成本,也是為什麼 prompt caching 對 agentic loop 特別重要。
三層全滅。tools 排在渲染順序最前面(position 0),前綴一變,system 和 messages 全部陪葬,整份重算。
對照組:中途改 tool_choice 只掉 messages 層,tools/system 保得住。只有「改工具定義」和「換模型」是全滅。
usage.input_tokens 顯示 4200,這次呼叫的輸入總共是 4200 tokens 嗎?不是。三個輸入欄位加起來才是總量,input_tokens 只是其中「沒進快取」的那一塊。
一個多輪 agent 跑到第 3 輪時,usage 通常長這樣——三個欄位同時都不是 0:
"usage": {
"input_tokens": 4200, ← 這輪新的、沒進快取 ×1
"cache_creation_input_tokens": 6800, ← 這輪剛寫進快取的 ×1.25
"cache_read_input_tokens": 73200 ← 前面幾輪命中的 ×0.1
}
實際輸入總量 = 4200 + 6800 + 73200 = 84,200 tokens
三個欄位的關係就是快取的一生:這一輪的 cache_creation,就是下一輪的 cache_read。第 4 輪再送時,這 6800 會併進 cache_read,而第 4 輪新增的內容成為新的 cache_creation。
拿它當診斷儀:
cache_read 一直是 0 → 快取從來沒命中,去查 §5 摺疊區那三個無聲失效寫法cache_creation 每輪都很大、cache_read 卻一直是 0 → 最糟的狀態:每輪都在重寫快取卻從沒讀回來,等於用 1.25x 的價格做 1x 的事。通常是前綴每次都在變(system prompt 塞了時間戳、或 tools 一直在換)input_tokens → 你根本沒開快取,或 prompt 短到沒達最小門檻