LLM Request/Response 與 Tool Use 完整拆解(真實 payload)

🧭 這是系列的一塊 — 先看全景地圖
想先看 LLM / 平台 / 工具 / Agent / 微調 怎麼拼成一張圖 → 從一顆腦到公司 Agent:LLM 全景地圖
重點摘要(TL;DR)
  • LLM API 本質:你 POST 一段 JSON、拿回一段 JSON,伺服器完全無狀態——它不記得你上一句。
  • 兩大 wire format:OpenAI Chat Completions(Groq/Cerebras/DashScope/xAI/OpenRouter 全用)vs Anthropic Messages(Claude 專用)。欄位名不同、tool 往返也不同。
  • 模型不會自己執行工具。它只回一個「請你幫我呼叫 get_weather(Taipei)」,你在本地跑完,把結果塞回去再送一次,它才給最終答案。
  • 你付的是 token:prompt(輸入)+ completion(輸出)。無狀態代表每一輪都要重送整段歷史——這就是長對話越來越貴的原因。
  • reasoning 模型會多燒「看不見的思考 token」:實測算一題燒 61 個 reasoning token,答案只吐 1 個字,你付 72 個
  • 本文所有 OpenAI 格式 JSON 都是用免費 API 真的打出來抓的,不是編的。

我每天用 Claude Code,一直很好奇底層到底在傳什麼:一個 request 長怎樣、response 長怎樣、所謂的「工具呼叫(tool use)」是誰在執行、Claude 跟 Grok/Qwen 的格式差在哪。這篇把它拆到見骨,而且每一段 OpenAI 格式的 JSON 都是我用免費 key(Groq、Cerebras)當場 curl 打出來的真資料,你可以自己複製重跑。

🧭 本文導覽(六部分,一片看完)
Part 一 — 基礎:一個請求到底長怎樣
Part 二 — 格式的派系:是不是只有 Claude 不一樣
Part 三 — Tool Use:模型怎麼用工具、又怎麼做到
Part 四 — 放到真實世界:Claude Code、A2A、可攜性
Part 五 — 副手實測:工具呼叫是另一張成績單
Part 六 — 實戰・速查・附錄
Part 一 — 基礎:一個請求到底長怎樣

一句話總覽:POST 一段 JSON,拿回一段 JSON,而且它沒記憶

所有主流 LLM API 都是一個 HTTP POST:你把「模型名 + 對話訊息 + 參數」打包成 JSON 送過去,伺服器算完把「生成的內容 + 停止原因 + token 用量」打包成 JSON 回來。就這樣。沒有連線狀態、沒有 session、伺服器不記得你是誰、也不記得上一句

最反直覺的一點:無狀態(stateless)
ChatGPT / Claude 的網頁看起來「記得」整段對話,是因為前端每次都把完整歷史重新塞進 request 再送一次,不是伺服器記得。API 本身每一發都是獨立的、失憶的。理解這點,後面的計費、context 上限、tool 往返就全通了。

Request 解剖:一個最小請求逐欄位拆(真 payload)

下面是我真的送給 Groq 的 request(OpenAI 格式)。先看沒有工具的骨架,每個欄位都標了作用:

POST https://api.groq.com/openai/v1/chat/completions
Authorization: Bearer <你的 KEY>
Content-Type: application/json

{
  "model": "llama-3.3-70b-versatile",   // 要用哪個模型
  "messages": [                          // 對話歷史(順序有意義)
    { "role": "system", "content": "你是簡潔的助理" },  // (選)人設/規則
    { "role": "user",   "content": "用一句話說什麼是 API" }
  ],
  "max_tokens": 60,      // 最多生成幾個 token(保護費用/避免暴衝)
  "temperature": 0,      // 0=最確定/可重現,越高越發散
  "stream": false        // true=逐字串流(見後面 SSE 一節)
}
  • model:字串,決定用哪顆腦。同一個端點可切不同模型。
  • messages[]:整段對話。role 只有三種常見值——system(人設)、user(你)、assistant(模型上一輪的回答)。tool 往返會多一種 tool
  • max_tokens:輸出上限。設太小會被硬切斷(finish_reason=length),reasoning 模型尤其容易踩。
  • temperature:亂度。寫程式/要重現設 0;要創意調高。

Response 解剖:content、finish_reason、usage(真回應)

同一個請求,Groq 實際回傳的 JSON(去掉 id 等雜訊欄位):

{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "API 是應用程式之間溝通的介面。"   // ← 你要的答案在這
      },
      "finish_reason": "stop"     // stop=正常講完;length=被 max_tokens 切斷
    }
  ],
  "usage": {
    "prompt_tokens": 23,      // 輸入用了幾個 token(你送進去的)
    "completion_tokens": 14,  // 輸出生成了幾個 token
    "total_tokens": 37        // 計費看這兩個(通常輸出單價比輸入貴)
  }
}
三個你每次都該看的欄位
message.content = 答案本體。② finish_reason = 它是講完還是被切斷(看到 length 就要調大 max_tokens)。③ usage = 帳單,prompt + completion 都要付錢

串流(SSE):打字機效果背後的格式

"stream": true,伺服器就不會等全部生成完才回,而是邊生成邊一小塊一小塊推給你(Server-Sent Events)。這就是 ChatGPT/Claude「一個字一個字冒出來」的原理。下面是我真的開串流抓到的原始封包(精簡了重複的 id 欄位):

data: {"choices":[{"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"choices":[{"delta":{"content":"API"},"finish_reason":null}]}

data: {"choices":[{"delta":{"content":"("},"finish_reason":null}]}

data: {"choices":[{"delta":{"content":"Application"},"finish_reason":null}]}

data: {"choices":[{"delta":{"content":" Programming"},"finish_reason":null}]}

...(每個 token 一包,持續推送)...

data: {"choices":[{"delta":{},"finish_reason":"stop"}]}

data: [DONE]
  • 每一包是一個 chunk,差別在 message 變成 delta(增量)——只帶「這次新增的那幾個字」。
  • 你的前端把所有 delta.content 依序拼起來,就是完整答案。
  • 最後一包 finish_reasonstop,再來一行 data: [DONE] 收尾。
  • 串流只改傳輸方式,不改內容;體感更快(第一個字更早到),但總 token、總費用一樣。

無狀態的代價:為什麼長對話越聊越貴、越慢

因為伺服器失憶,你的 harness(ChatGPT 前端、Claude Code、你的腳本)每一輪都要把「到目前為止的完整對話」整包重送。第 10 輪的 request 裡塞著前 9 輪的所有文字——這些全部重新計為 prompt_tokens 再算一次錢、再讀一次。

對話進行每次 request 送出的東西prompt_tokens
第 1 輪system + 你的第 1 句
第 5 輪system + 前 4 輪全部 + 第 5 句
第 20 輪system + 前 19 輪全部 + 第 20 句大(且逼近 context 上限)

這解釋三件事:(1) 長對話越來越貴——不是錯覺;(2) 越來越慢——每次要重讀全部;(3) context 上限(如 8K / 128K / 200K token)是「一次 request 能塞多少」的天花板,聊爆了最舊的訊息就得被丟掉或摘要。前一篇 GLM 在 8K context 被推理鏈餓死,就是撞到這面牆。

Reasoning 模型的隱藏帳單:看不見的思考 token

reasoning 模型(gpt-oss、GLM-4.7、o 系列)在給答案之前會先「想」一大段——這段思考通常不回傳給你看,但照樣算 token、照樣收費。我丟一個腦筋急轉彎給 Cerebras 的 gpt-oss-120b,真實 usage 長這樣:

// 問題:「農夫有 17 隻羊,除了 9 隻其他都跑走了,剩幾隻?只回數字」
{
  "choices": [
    { "message": { "content": "9" }, "finish_reason": "stop" }   // ← 答案只有 1 個字
  ],
  "usage": {
    "prompt_tokens": 100,
    "completion_tokens": 72,                          // 你付 72 個輸出 token
    "completion_tokens_details": {
      "reasoning_tokens": 61                          // 其中 61 個花在「想」(你看不到內容)
    },
    "total_tokens": 172
  }
}
為什麼這很重要
答案 content 只有「9」1 個字,但 completion_tokens 是 72——61 個花在看不見的思考上,你全付。這也是上一篇 GLM-4.7-Flash 在 8K context「吐不出答案」的真因:它的 reasoning 鏈把 max_tokens 燒光了,還沒輪到寫答案就被截斷(finish_reason=length)。用 reasoning 模型時 max_tokens 一定要開大,不然你付了思考的錢卻拿不到答案。
Part 二 — 格式的派系:是不是只有 Claude 不一樣

兩大陣營:OpenAI Chat Completions vs Anthropic Messages

市面上幾乎所有模型的 API 只分兩種方言。搞懂這兩種,你就看得懂任何一家:

面向OpenAI Chat CompletionsAnthropic Messages
誰在用Groq / Cerebras / DashScope(Qwen)/ xAI(Grok)/ OpenRouter / 本地 Ollama / vLLMClaude 全系列(Anthropic 官方 / Bedrock / Vertex)
端點路徑/v1/chat/completions/v1/messages
認證 headerAuthorization: Bearer <key>x-api-key: <key> + anthropic-version
system 提示放進 messages[0],role=system頂層獨立 system 欄位(不在 messages 裡)
回應主體choices[0].message.content(單一字串)content[] 是 block 陣列(text/tool_use 混排)
停止原因finish_reason:stop / tool_calls / lengthstop_reason:end_turn / tool_use / max_tokens
token 欄位usage.prompt_tokens / completion_tokensusage.input_tokens / output_tokens
為什麼 Claude Code 不能直接換 Grok/Qwen
因為 Claude Code 講的是 Anthropic Messages 方言(content block、tool_use block、stop_reason)。把它的 request 原封送給只懂 OpenAI 方言的 Grok 端點會直接 400——欄位對不上。這不是「授權問題」,是語言不通。想混用得靠中間層(LiteLLM/OpenRouter)翻譯,或用外部 CLI(aider)另開一條線。

延伸一:是不是只有 Claude 長得不一樣?

不是。原生方言其實有三家——但 OpenAI 變成了「事實標準」,所以你會有「只有 Claude 不一樣」的錯覺。

陣營誰在講原生格式有沒有另出 OpenAI 相容層
OpenAI(事實標準)Groq · Cerebras · DashScope · xAI · DeepSeek · Mistral · Together · Ollama · vLLM/v1/chat/completions它就是本尊
AnthropicClaude/v1/messages(積木)有(相容端點,但只吃子集)
Google GeminiGeminigenerateContent(contents/parts)有(相容 shim)

因為 OpenAI 先紅、成了通用語,所有第三方推理商為了讓你一行不改就能接,直接原生講 OpenAI;連 Anthropic、Google 自己都額外出一個「OpenAI 相容端點」。你常用又堅持原生格式的只有 Claude,才會覺得只有它不同(其實 Gemini 也自成一格,你只是沒踩到)。

「相容」≠「一模一樣」
大多第三方是「OpenAI 的子集 + 小差異」——有的不支援 n/logprobs/response_format,串流 tool_calls 的切法、tool_choice 接受的值、stop 行為都可能略有出入。基本款能跑,冷門參數要各家實測。
Part 三 — Tool Use:模型怎麼用工具、又怎麼做到

🔧 Tool Use 完整往返:模型不執行工具,它只「請你幫它跑」

這是最多人搞混的地方。模型沒有手,它不能真的查天氣、讀檔、call API。所謂 tool use,是一套四步的往返協議:你給它一張「可用工具清單」,它決定要用哪個、參數是什麼,然後回一張「請你幫我呼叫」的單子;你在本地執行,把結果送回,它才產生最終人話。下面四步全是真的打 Groq 抓回來的

步驟 1|Request:你在請求裡「宣告」有哪些工具(附 JSON schema)

{
  "model": "llama-3.3-70b-versatile",
  "messages": [ { "role": "user", "content": "台北現在天氣如何?" } ],
  "tools": [                                  // ← 告訴模型「你可以用這些工具」
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {                       // 參數用 JSON Schema 描述
          "type": "object",
          "properties": {
            "location": { "type": "string", "description": "City name, e.g. Taipei" }
          },
          "required": ["location"]
        }
      }
    }
  ],
  "tool_choice": "auto"    // auto=模型自己決定要不要用工具
}

步驟 2|Response:模型不回話,回一張「呼叫單」

注意 contentnull——它沒有直接回答,而是回了一個 tool_calls,並把 finish_reason 設成 tool_calls,意思是「換你了,幫我跑這個」:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,                       // ← 沒有人話
        "tool_calls": [
          {
            "id": "4n06y5qdw",                 // 這次呼叫的識別碼(等下要對回去)
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"location\":\"Taipei\"}"   // 模型填好的參數(字串化 JSON)
            }
          }
        ]
      },
      "finish_reason": "tool_calls"            // ← 「我要用工具,球在你這」
    }
  ],
  "usage": { "prompt_tokens": 231, "completion_tokens": 15, "total_tokens": 246 }
}
注意 prompt_tokens 跳到 231
沒工具時同樣一句話才 20 幾個 token,這裡變 231——因為整張工具 schema(名字、描述、參數定義)都算進輸入 token。工具給越多、描述越長,每一發的固定成本越高。這是設計 agent 時的隱形開銷。

步驟 3|你在本地執行工具,把結果當一則新訊息塞回去

模型的 tool_calls 只是「意圖」。真正去查天氣的是你的程式(這裡用假資料示範)。執行完,把結果包成一則 role: "tool" 的訊息,tool_call_id 對回步驟 2 那張單,連同前面歷史一起再送一次:

{
  "model": "llama-3.3-70b-versatile",
  "messages": [
    { "role": "user", "content": "台北現在天氣如何?" },

    {                                          // ← 把模型剛剛那則「呼叫單」原封放回
      "role": "assistant",
      "content": null,
      "tool_calls": [
        { "id": "4n06y5qdw", "type": "function",
          "function": { "name": "get_weather", "arguments": "{\"location\":\"Taipei\"}" } }
      ]
    },
    {                                          // ← 你執行工具後的結果
      "role": "tool",
      "tool_call_id": "4n06y5qdw",             // 對回上面那張單的 id
      "name": "get_weather",
      "content": "{\"temp_c\":28,\"condition\":\"晴時多雲\",\"humidity\":\"70%\"}"
    }
  ]
}

步驟 4|Response:模型拿到結果,產生最終人話

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "台北現在天氣晴時多雲,氣溫28度,濕度70%。"   // ← 真正的答案
      },
      "finish_reason": "stop"                  // 這次是 stop,結束
    }
  ],
  "usage": { "prompt_tokens": 96, "completion_tokens": 22, "total_tokens": 118 }
}
一句話記住 tool use
模型只會做兩件事:①「我想呼叫 X(參數 Y)」,②拿到結果後把它翻成人話。中間「真的去執行」永遠是你的程式的責任。一次工具呼叫 = 至少兩發 API 請求(要它決定 → 給它結果)。Agent 會這樣來回好幾輪,直到 finish_reason 不再是 tool_calls。

延伸二:模型到底「怎麼做到」tool use?

關鍵認知:模型沒有魔法,它從頭到尾只會預測下一個 token。tool use = 訓練 + 格式約定 + 伺服器解析,三步:

  1. 你的 tools schema 被塞進 prompt,變成純文字。伺服器用模型的 chat template 把工具定義渲染成一段「你有這些工具:{schema}」接在對話前面——這就是前面 prompt_tokens 從 20 幾跳到 231 的真相:工具宣告本身變成了輸入文字
  2. 模型被訓練成:看到工具宣告 + 需要時,吐出一段特定格式的 token(常用特殊控制 token 框起來,如 Llama 的 <|python_tag|> 之類)。對模型而言,吐「工具呼叫」和吐「一句話」沒有本質差別,都是它學到的正確接續。
  3. 伺服器的 parser 認得那格式,拆回結構化的 tool_calls 給你,並把 finish_reason 設成 tool_calls。
加分機制:受限解碼(constrained decoding)
有些平台用 grammar / 狀態機強制模型輸出必須是「符合你 schema 的合法 JSON」(vLLM 叫 guided decoding、llama.cpp 用 GBNF、OpenAI 叫 structured outputs)。這保證參數一定 parse 得動。但不是每家都開——沒開的就賭模型夠聰明,這是弱模型工具呼叫常吐壞 JSON 的原因。

那 MCP 呢?模型根本不知道 MCP 存在

MCP 不是模型的能力,是 host(Claude Code / Desktop / IDE)端的水電工程。它標準化的是「工具從哪來、怎麼被執行」,不是模型怎麼吐呼叫:

  1. host 連上 MCP server,呼叫 tools/list 發現有哪些工具 + JSON schema。
  2. host 把這些 schema 翻成模型看得懂的 tools 格式,塞進上面那個 request。
  3. 模型照延伸二吐一個 tool_call(它以為那只是個普通工具)。
  4. host 收到後路由到對應 MCP server 呼叫 tools/call,拿結果當 tool_result 塞回。
切乾淨兩層
tool-use 格式 = 模型怎麼開口要工具。MCP = host 從哪拿到工具、怎麼真的執行。模型全程只看到普通 tool schema——所以「懂不懂 MCP」不是模型強弱的分界線,這條線不存在。(Anthropic 近期在 API 加 mcp_servers 參數、server-side 工具,是把這層搬到伺服器端,底層仍是同一個骨架。)

同一件事,Anthropic 格式怎麼寫?(清晰對照)

上面是 OpenAI 方言。Claude 的 Anthropic Messages 做一模一樣的事,但欄位長得不同。下面 Anthropic 範例依官方 spec寫(我沒有 Claude 原始 API key 可當場抓,故標明;OpenAI 那側全是實測真 payload):

Tool use 的每一步OpenAI(實測)Anthropic(依 spec)
宣告工具tools[].function.parameterstools[].input_schema
模型要求呼叫assistant 訊息帶 tool_calls[],content=nullcontent 陣列裡一個 {type:"tool_use"} block
停止訊號finish_reason: "tool_calls"stop_reason: "tool_use"
你回傳結果一則 role:"tool" 訊息 + tool_call_id一則 role:"user" 訊息,內含 {type:"tool_result"} block
參數位置arguments(字串化 JSON)input(直接是物件)

Anthropic 版:模型要求呼叫工具(對應 OpenAI 步驟 2)

// POST https://api.anthropic.com/v1/messages
// 回應(依 Anthropic Messages spec):
{
  "role": "assistant",
  "content": [                               // ← content 是「積木陣列」,不是單一字串
    { "type": "text", "text": "我幫你查一下台北的天氣。" },
    {
      "type": "tool_use",                    // ← 一個 tool_use 積木(相當於 OpenAI 的 tool_calls)
      "id": "toolu_01A09q90qw",
      "name": "get_weather",
      "input": { "location": "Taipei" }      // ← 參數是物件,不是字串
    }
  ],
  "stop_reason": "tool_use",                 // ← 對應 OpenAI 的 finish_reason:"tool_calls"
  "usage": { "input_tokens": 420, "output_tokens": 58 }
}

Anthropic 版:你把工具結果送回(對應 OpenAI 步驟 3)

{
  "role": "user",                            // ← 注意:結果用 user 角色送回(OpenAI 是 tool 角色)
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01A09q90qw",     // 對回上面的 tool_use id
      "content": "{\"temp_c\":28,\"condition\":\"晴時多雲\"}"
    }
  ]
}
一句話抓住差異
概念完全相同(宣告→模型要求→你執行→送回→最終答案),差在包裝:OpenAI 用「訊息 + 平行的 tool_calls 陣列 + tool 角色」;Anthropic 把一切都當 content 裡的積木(block)——text、tool_use、tool_result 全是積木,結果還用 user 角色送回。看懂積木模型,就看懂 Claude 為什麼能在一則回應裡「先講一句話再呼叫工具」。
Part 四 — 放到真實世界:Claude Code、A2A、可攜性

這跟 Claude Code 有什麼關係?它就是把這個 loop 自動化

你在 Claude Code 裡看到的「它自己讀檔、跑指令、改程式」,拆開來就是上面那個 tool-use 往返,自動跑很多輪:

  1. Claude Code 在每個 request 的 tools 裡宣告一堆工具:Read、Edit、Bash、Grep……(各自有 input_schema)。
  2. 模型回一個 tool_use:「我要 Read 這個檔」→ Claude Code 本地真的去讀 → 把檔案內容當 tool_result 送回。
  3. 模型看到內容,再回下一個 tool_use:「Edit 這幾行」→ 本地執行 → 送回結果。
  4. 如此往返,直到模型不再要求工具(stop_reason: end_turn),吐出給你看的最終訊息。
所以「Claude Code 只吃 Claude」是格式問題
Claude Code 整個 harness 是照 Anthropic 積木格式寫死的——tool_use block、tool_result block、stop_reason。換一顆只會講 OpenAI 方言的模型,這些欄位對不上就崩。想用 Grok/Qwen 省錢,要嘛靠翻譯層(LiteLLM),要嘛另開一條外部 CLI(如 aider)當副手——這正是上一篇委派實測在做的事。

接上 A2A:Tool Use、MCP、A2A 是同一個骨架的三層

把前面的 tool-use 往返理解透之後,會發現它跟 MCP(模型接外部工具伺服器)、A2A(agent 對 agent 委派)是同一套訊息協議,只是層級不同。四步骨架都一樣:①宣告能力 ②結構化請求 ③結構化結果 ④可串流。差別只在「對方是一個工具,還是一個有自己生命週期的完整 agent」。

抽象概念LLM Tool Use
模型 ↔ 自己的 harness
MCP
agent ↔ 工具伺服器
A2A
agent ↔ 另一個 agent
宣告「我會什麼」tools[](function schema)tools/listAgentCard.skills + capabilities
發起請求模型回 tool_callstools/callSendMessage → 建 Task
工作單位一次 tool_call(無狀態)一次 callTask(有狀態:SUBMITTED→WORKING→COMPLETED)
回傳結果tool 訊息 / tool_resultcall result contentArtifact(由 Parts 組成)
內容最小單位content blockcontent partPart(text/file/data)
串流SSE deltaSSESendStreamingMessage / SubscribeToTask
非同步通知—(同步往返)push notification webhook
多輪續傳重送 messages 全歷史taskId/contextId 續傳
一句話看穿三者
把 tool use 的「工具」升級成「一個有自己 Task 生命週期的完整 agent」,就是 A2A。
· Tool Use = 模型叫喚自己 harness 裡的手(Read/Bash)。
· MCP = 把工具標準化成外部可插拔的伺服器,任何 agent 都能接。
· A2A = 對方是另一個完整 agent,有 AgentCard(能力清單)、有 Task(狀態機)、能非同步回報。
共同骨架都是 宣告 schema → 結構化請求 → 結構化結果 → 可串流——學會讀一個,另外兩個一眼就懂。
接回委派實驗
上一篇用 aider 把活丟給 Groq/Qwen 副手,本質就是手工版的 A2A:主腦(Claude Code)= client agent、副手 CLI = remote agent、instruction.txt = Message、副手產出的檔 = Artifact。差別只在現在用「跑一個外部程序」當傳輸;A2A 則把它標準化成 AgentCard + Task 的網路協議。

延伸四:這套 harness 能原封給用 OpenAI 的朋友嗎?

能,而且他比你更輕鬆。因為整套「主腦 + 委派副手」的 harness 是建在 OpenAI Chat Completions 格式上,而你朋友用的 OpenAI 就是這格式的本尊——他連相容層都不用。移交時只改三樣,harness 邏輯(tool loop、delegate、providers.conf 結構)一字不動:

改什麼你的(免費副手)朋友的(OpenAI)
base_urlhttps://api.groq.com/openai/v1https://api.openai.com/v1
api_keyGROQ_API_KEYOPENAI_API_KEY
modelllama-3.3-70b-versatilegpt-4o-mini / gpt-5
tools schema · tool loop · 迴圈邏輯完全不動
這正是「OpenAI 是事實標準」的紅利
你的 providers.conf 登錄表加一行 openai 就收編了——同一支 delegate、同一份 tool schema,換個 endpoint/key/model 就跑。反過來才麻煩:若你朋友想把 Claude 當副手,因為 Claude 講的是 Anthropic 積木格式(見前面對照),就得多一層 adapter 或用 LiteLLM 之類的翻譯層。OpenAI ↔ OpenAI 幾乎零成本,牽涉到 Claude 才要翻譯。
Part 五 — 副手實測:工具呼叫是另一張成績單

延伸三:工具呼叫是另一張成績單 — 用真實 agent 任務重評

前面幾篇的副手排名,量的都是「寫程式」(隱形契約、一次過);工具呼叫是另一種要專門訓練的能力,兩者不一定一致。第一版我用「問天氣該不該叫工具」的玩具題測——但玩具題會騙人。真正的 agent 是多步、要串接、要用上一個工具吐回來的結果,所以改成真實 agent-loop(給檔案工具 list/read/write,跑完整迴圈,真的把工具結果餵回去),兩個真任務:

  • T1 讀→篩→寫→回報:讀 users.json → 篩出 role=admin → 把 email 寫成 admins.csv → 回報筆數。考點:讀完要真的用資料再寫。
  • T2 讀兩檔→比對:讀 dev.envprod.env,比對 DATABASE_URL 是否一致並點出差異。考點:多檔讀取 + 跨檔推理。
副手(平台 / 模型)T1 讀→篩→寫→回報T2 讀兩檔→比對真實表現
DashScope / qwen3-coder-plus✅ 兩次都過2/2 穩,agent 首選
Cerebras / gpt-oss-120b✅ 兩次都過2/2 穩(玩具題反而漏叫,真實檔案任務穩)
Groq / qwen3-32b✅ 兩次都過2/2 穩,黑馬
Groq / llama-3.3-70b❌ 兩次都寫出空 CSV1/2:讀得到檔卻帶不過資料
DashScope / qwen3-coder-flash🟡 一次 melt 成寫 5 個腳本、一次乾淨不穩,agent 不可靠
洞見:玩具測跟真實測結論相反,只信真實多步任務
· gpt-oss-120b:玩具題漏叫算式看似不行,真實檔案任務反而 2/2 穩——玩具題誤判了。
· llama-3.3-70b(寫程式 10/10):真實任務兩次都栽——讀得到 users.json,卻寫出空的 / 只有標頭的 CSV,資料帶不過去。這是穩定重現的 agent 缺陷,玩具題看不出來。
· qwen3-coder-flash:一次直接 melt——不寫 CSV,反而寫了 extract_admins.pyrun_script.shprocess_users.py… 五個腳本(coder 反射「我來寫個程式」),另一次卻乾淨。同任務兩種結果 = 不可靠。

結論:『會寫程式』保證不了『會當 agent』。玩具測會騙你,只有真實多步任務算數。
誠實的但書
樣本仍小(每題實測 2 次),是方向性訊號;正式評工具呼叫要看 Berkeley Function-Calling Leaderboard(BFCL)那種數百題 benchmark。過程中 Groq/Cerebras 撞到免費層限流(429),已用退避重試繞開,最終數字裡沒有限流雜訊

重評後的副手分工(plus 與 flash 分開)

用途選誰為什麼
委派寫程式(單檔)qwen3-coder-plus flash兩者寫程式都一次過、省 token;但僅限單檔委派
當 agent(多步、串工具)qwen3-coder-plus / gpt-oss-120b / qwen3-32b真實 agent 任務 2/2;寫程式 + 工具串接都穩
別當 agent 主力llama-70b(寫空檔)、coder-flash(會 melt 成寫腳本、不穩)flash 寫程式行、當 agent 不行——這就是 plus 和 flash 不能綁一起的原因
一句話帶走
選副手要拆兩張成績單:「會不會寫」「會不會當 agent 穩穩把多步任務串完」。coder-flash 是活生生的例子:寫單檔沒問題,一丟進多步 agent 迴圈就 melt 成寫一堆腳本。要當 agent,就得用真實多步任務測,別只看寫碼或聊天。
Part 六 — 實戰・速查・附錄

實戰:複製這段,親手打一次 tool-use 往返

拿一把免費 Groq key(console.groq.com),貼上就能跑,看模型回你一張 tool_calls 單:

# 第 1 發:給它工具,看它要不要呼叫
curl -s https://api.groq.com/openai/v1/chat/completions \
  -H "Authorization: Bearer $GROQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama-3.3-70b-versatile",
    "messages": [{"role":"user","content":"台北現在天氣如何?"}],
    "tools": [{
      "type":"function",
      "function":{
        "name":"get_weather",
        "description":"Get the current weather for a city",
        "parameters":{"type":"object",
          "properties":{"location":{"type":"string"}},
          "required":["location"]}
      }
    }],
    "tool_choice":"auto"
  }' | python3 -m json.tool

# 你會看到回應裡 finish_reason=tool_calls、tool_calls[0].function.arguments={"location":"Taipei"}
# 接著你的程式去查天氣,把結果用 role:"tool" 訊息送第 2 發,它才回最終人話。
一個排查坑(本文實測踩到)
Groq / Cerebras / DashScope 都在 Cloudflare 後面。用 Python urllib 直接打會被 403 擋(它的 User-Agent 被擋),用 curl,或給 urllib 加一個 User-Agent: curl/8.5.0 header 就過。這跟 API 本身無關,是防爬蟲。

一頁速查:欄位對照 + 心法

你想知道OpenAI 格式看這欄Anthropic 格式看這欄
答案本體choices[0].message.contentcontent[] 裡 type=text 的 block
它是講完還是被切斷finish_reasonstop_reason
它想呼叫工具嗎finish_reason=tool_calls + tool_calls[]stop_reason=tool_use + tool_use block
花了多少錢(輸入)usage.prompt_tokensusage.input_tokens
花了多少錢(輸出)usage.completion_tokensusage.output_tokens
有沒有偷燒思考completion_tokens_details.reasoning_tokens(思考另計,見各家文件)
五句話帶走
① API 無狀態,歷史每次重送——長對話越來越貴。
② 你付 prompt+completion,reasoning 的思考也算。
③ 模型不執行工具,只回「請你幫我呼叫」,執行是你家的事。
④ 一次工具呼叫 = 至少兩發請求;agent 是這個 loop 跑很多輪。
⑤ OpenAI 用 tool_calls+tool 角色,Anthropic 用 tool_use/tool_result 積木——概念一樣,包裝不同。

延伸閱讀:主腦配 CLI 副手:AI coding 委派省 token 實測——同一套 tool-use 往返,怎麼用免費模型當副手把重複的活外包出去。

附錄 A:OpenAI Chat Completions 全參數(用到沒用到都列)

依功能分組,每個都標型別、必填/選填、功用,以及本文實測有沒有用到()。max_tokens 新版被 max_completion_tokens 取代,但多數相容端點(Groq/Cerebras/DashScope)仍吃 max_tokens

參數型別必/選功用用過
核心
modelstring必填用哪個模型
messagesarray必填對話歷史;角色 system/user/assistant/tool
生成控制
max_completion_tokensint選填輸出上限(新版,reasoning 模型用這個)
max_tokensint選填輸出上限(舊名,多數相容端點仍吃)
temperature0–2選填亂度;0=可重現,高=發散
top_p0–1選填nucleus 取樣;與 temperature 擇一
nint選填一次生成幾個候選(choices 數)
stopstring|array選填遇到就停的字串(最多 4 組)
presence_penalty-2–2選填鼓勵講新主題
frequency_penalty-2–2選填抑制重複用詞
logit_biasmap選填手動加減特定 token 機率
logprobsbool選填回傳每個 token 機率
top_logprobs0–20選填每步回前 N 名候選機率(需 logprobs)
seedint選填盡量可重現(best-effort)
串流
streambool選填逐 token 串流(SSE)
stream_optionsobject選填串流細節,如 include_usage
工具 / function calling
toolsarray選填宣告可用工具(function schema)
tool_choiceauto|none|required選填要不要 / 強制 / 指定用工具
parallel_tool_callsbool選填允許一次回多個工具呼叫
functions · function_call棄用舊版工具寫法,已被 tools 取代
輸出格式 / 推理
response_formatobject選填強制輸出格式(json_object / json_schema)
reasoning_effortlow|med|high選填reasoning 模型思考力度
predictionobject選填predicted outputs:已知大半內容時加速
多模態
modalitiesarray選填要哪些輸出模態
audioobject選填語音輸出設定(voice / format)
web_search_optionsobject選填內建網路搜尋(支援的模型)
帳務 / 基礎設施
userstring選填終端使用者識別(濫用偵測)
storebool選填是否保存對話(eval / 蒸餾)
metadatamap選填自訂標籤 key-value
service_tierauto|default|flex選填運算優先級 / 成本檔位

附錄 B:Anthropic Messages 全參數

依官方文件核對。兩個「陷阱差異」先記住:max_tokens 在這裡是必填(OpenAI 選填)、temperature 上限是 1(OpenAI 是 2)。

參數型別必/選功用用過
核心
modelstring必填用哪個 Claude 模型
messagesarray必填user/assistant 輪替;content 是積木陣列
max_tokensint必填輸出上限(這裡是必填!設 0 可做快取預熱)
內容 / 停止
systemstring|block[]選填系統提示,頂層獨立欄位(不塞在 messages)
stop_sequencesstring[]選填遇到就停的自訂字串
生成控制
temperature0–1選填亂度(上限 1,不是 2)
top_p0–1選填nucleus 取樣
top_kint選填只從前 K 個 token 取樣(OpenAI 無此參數)
工具
toolsarray選填工具定義(參數用 input_schema);含 bash / code_execution / web_search 等官方工具
tool_choiceauto|any|tool|none選填要不要 / 任選 / 指定 / 禁用(any = OpenAI 的 required)
延伸思考 / 輸出
thinkingobject選填延伸思考:enabled / adaptive + budget_tokens(≥1024)
streambool選填SSE 串流
output_configobject選填結構化輸出設定(JSON schema)
帳務 / 快取 / 基礎設施
metadataobject選填附 user_id(濫用偵測)
service_tierauto|standard_only選填優先 / 標準容量
cache_controlobject選填手動標記 prompt caching(OpenAI 是自動)
containerstring選填code execution 容器 id,跨請求重用
inference_geostring選填指定推論地區
HTTP Headers
x-api-keyheader必填API 金鑰
anthropic-versionheader必填API 版本(如 2023-06-01)
anthropic-betaheader選填開啟 beta 功能
兩派各自獨有的參數
Anthropic 有、OpenAI 沒有:top_k、頂層 systemthinking.budget_tokens(精確設思考預算,比 OpenAI 的三檔 reasoning_effort 細)、手動 cache_control
OpenAI 有、Anthropic 沒有:npresence/frequency_penaltylogit_biasseed

附錄 C:同功能,兩派參數對映速查

你想做的事OpenAIAnthropic
系統提示messages role=system頂層 system
輸出上限max_completion_tokens(選填)max_tokens(必填)
亂度temperature 0–2temperature 0–1
Top-K 取樣—(無)top_k
停止字串stopstop_sequences
工具參數 schemafunction.parametersinput_schema
強制用工具tool_choice:"required"tool_choice:"any"
思考預算reasoning_effort(三檔)thinking.budget_tokens(精確)
結構化輸出response_formatoutput_config
prompt 快取自動(回 cached_tokens)手動 cache_control
停止原因finish_reasonstop_reason
輸入 / 輸出 tokenprompt_tokens / completion_tokensinput_tokens / output_tokens

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *