Foundations · Anatomy of One API Call

地基 · 一次 API 呼叫長什麼樣

五個模組共用的先備知識 | 沒有這六塊,模組 2 一開頭的 tool_choice 四個值會看不懂 | 讀完 → 模組 2 · API 機制
這頁不是考點清單,是讀其他模組前的地基。真考不會直接問「payload 有哪些欄位」,但幾乎每一題情境題都預設你知道這六件事。讀不懂某個模組的某一段時,先回來這裡對一下。

1. Claude 不會執行工具 — agentic loop

初學者最大的誤解:以為給了工具,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_turnserver 端工具跑到迭代上限把 assistant 回應接回歷史、原樣再送一次(不要加「請繼續」)
refusal被安全機制擋下stop_details 分類,不要用同一個 prompt 重試
常見 bug:只處理 end_turntool_use,其他三個沒接。refusal 沒接會讓程式在讀 content[0] 時直接爆掉(refusal 時 content 可能是空陣列)。

2. API 是 stateless — 每次都要全傳

Messages API 不記得你上一次講過什麼。沒有 session、沒有連線狀態。每一個 request 都要重傳整包 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 Code 造成的錯覺:你用 Claude Code 時它會讀 CLAUDE.md、記得整段對話——那是應用層在你本機保管歷史、每輪重送給 API。API 端永遠是失憶的。「設定一次就好」這件事在 Messages API 不存在。

三個推論(後面模組會一直用到)

3. Request 長什麼樣

必填只有三個modelmax_tokensmessages。其他全是選填。

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——角色設定、行為規則、格式要求、背景資料。它不屬於任何一輪對話,整段對話從頭到尾都掛著。

從 OpenAI 帶過來的最常見錯誤:把 system 塞進 messages 陣列。
OpenAI 寫 messages:[{role:"system",…},{role:"user",…}]Claude 的 system 是跟 messages 平行的獨立欄位messages 陣列裡基本上只有 userassistant 兩種 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。

認證和 beta 開關在 header,不在 body

Header備註
x-api-keysk-ant-…必填
anthropic-version2023-06-01必填,這個值長年沒變
content-typeapplication/json必填
anthropic-betabeta flag只有用到 beta 功能才要

用 SDK 時你會寫成 betas=["…"] 參數,SDK 幫你翻成 header——所以看到「某功能需要 beta header」和「SDK 的 betas 參數」講的是同一件事。

4. Response 長什麼樣

{
  "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 價)
  }
}
最常見的初學者 bug:直接寫 response.content[0].text。開 thinking 時第一個 block 是 thinking,沒有 .text,直接爆。永遠先檢查 .type 再取值——這在每一種語言的 SDK 都一樣。

usage 的三個輸入欄位

input_tokens 不是輸入總量,只是其中一塊。三個欄位加起來才是這次 prompt 的實際大小:

欄位是什麼計價
cache_read_input_tokens以前寫過快取、這次讀到的部分~0.1x(賺到)
cache_creation_input_tokens這次第一次寫進快取的部分(存起來給下次用)1.25x(5 分鐘 TTL)/2x(1 小時 TTL)
input_tokens沒進快取的剩餘部分1x 全額
總量 = 三個相加。agent 跑了三小時卻看到 input_tokens 只有 4K,不是統計壞掉——是其他都命中快取了。看總和,不要看單一欄位。

那個 toolu_01ABC 就是下一輪 tool_result 要用 tool_use_id 對回去的值——見 模組 2 §2.2

5. 渲染順序與三層快取

API 內部會把 payload 拼成一段給模型看的文字,順序固定

tools  →  system  →  messages

這個順序不是隨便排的,是按穩定程度排——最不會變的放最前面:

位置內容多常變
1. tools工具說明書幾乎不變(整個 app 生命週期)
2. system角色、規則、背景文件很少變(整段對話不變)
3. messages對話歷史每一輪都在長
Prompt caching 是前綴比對(prefix match):任何一個位置的 byte 改變,它後面的全部失效。所以「穩定的東西排前面、會變的東西排後面」是設計 prompt 的第一原則。

快取鍵不是只有 bytes

三層是巢狀的,每一層的快取鍵 = 這層以前的所有內容 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 開關
對話內容(正常往下講)

(✅ = 還活著可重用,❌ = 失效要重算)

一句話結論:只有「換工具定義」和「換模型」是全滅,其他都只掉 messages 層。所以每個 request 換 tool_choice 不會炸掉 tools/system——但長對話裡 messages 段才是 token 大頭,跑了 20 輪的 agentic loop 每輪換 tool_choice,一樣會讓成本暴增。對照 模組 2 §2.1 的加碼事實。
加碼:三個讓快取無聲失效的寫法(考題不考,實務會害死你)
  • system prompt 裡塞 datetime.now() 或 UUID——前綴每次都不一樣,永遠 miss。動態內容要往後放。
  • json.dumps(tools) 沒加 sort_keys=True——序列化順序不固定,bytes 就不同。
  • prompt 太短——低於最小可快取長度會安靜地不快取,不報錯。門檻隨模型不同(Opus 5 是 512 tokens、Opus 4.8/Sonnet 5 是 1024、Opus 4.6/Haiku 4.5 是 4096),而且不是越新越低

怎麼驗證有沒有命中:看 usage.cache_read_input_tokens。重複送相同前綴卻一直是 0 → 上面三個裡面中了一個。

另外:一個 request 最多 4 個 cache_control 斷點;快取讀取約 0.1x 價、寫入 1.25x(5 分鐘 TTL)或 2x(1 小時 TTL)——所以 5 分鐘 TTL 送兩次就回本。

6. 這些參數已經被移除了

讀舊文章、舊教材、或問到舊版模型時會看到的寫法,在現行模型上直接 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 的 prefilloutput_config.format(結構化輸出)4.6 家族起(見 §2.4
top-level output_formatoutput_config.format全模型(API 層級改名)
辨識訊號:選項裡出現 budget_tokenstemperature=0 求「決定性輸出」、或「用 prefill 逼出 JSON」——這三個在現行模型上都是錯的。prefill 那條的官方遷移路徑永遠是 Structured Outputs。

自我檢查

不用寫字,心裡答完再展開對答案。三題都答得出來就可以去讀模組 2 了。

Q1|對話進行到第 10 輪,這一次的 request 裡有 tools 嗎?

有,而且是完整一包。API 是 stateless,第 10 輪跟第 1 輪一樣要傳整包 tools + 前面九輪的完整歷史。

延伸:這正是為什麼「工具數量」是每輪都在付的固定成本,也是為什麼 prompt caching 對 agentic loop 特別重要。

Q2|你在對話中途新增了一支工具,快取會怎樣?

三層全滅。tools 排在渲染順序最前面(position 0),前綴一變,system 和 messages 全部陪葬,整份重算。

對照組:中途改 tool_choice 只掉 messages 層,tools/system 保得住。只有「改工具定義」和「換模型」是全滅。

Q3|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 一直在換)
  • 兩個都是 0、只有 input_tokens → 你根本沒開快取,或 prompt 短到沒達最小門檻