想先看 LLM / 平台 / 工具 / Agent / 微調 怎麼拼成一張圖 → 從一顆腦到公司 Agent:LLM 全景地圖
- 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 打出來的真資料,你可以自己複製重跑。
一句話總覽:POST 一段 JSON,拿回一段 JSON,而且它沒記憶
所有主流 LLM API 都是一個 HTTP POST:你把「模型名 + 對話訊息 + 參數」打包成 JSON 送過去,伺服器算完把「生成的內容 + 停止原因 + token 用量」打包成 JSON 回來。就這樣。沒有連線狀態、沒有 session、伺服器不記得你是誰、也不記得上一句。
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_reason變stop,再來一行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 一定要開大,不然你付了思考的錢卻拿不到答案。
兩大陣營:OpenAI Chat Completions vs Anthropic Messages
市面上幾乎所有模型的 API 只分兩種方言。搞懂這兩種,你就看得懂任何一家:
| 面向 | OpenAI Chat Completions | Anthropic Messages |
|---|---|---|
| 誰在用 | Groq / Cerebras / DashScope(Qwen)/ xAI(Grok)/ OpenRouter / 本地 Ollama / vLLM | Claude 全系列(Anthropic 官方 / Bedrock / Vertex) |
| 端點路徑 | /v1/chat/completions | /v1/messages |
| 認證 header | Authorization: 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 / length | stop_reason:end_turn / tool_use / max_tokens |
| token 欄位 | usage.prompt_tokens / completion_tokens | usage.input_tokens / output_tokens |
因為 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 | 它就是本尊 |
| Anthropic | Claude | /v1/messages(積木) | 有(相容端點,但只吃子集) |
| Google Gemini | Gemini | generateContent(contents/parts) | 有(相容 shim) |
因為 OpenAI 先紅、成了通用語,所有第三方推理商為了讓你一行不改就能接,直接原生講 OpenAI;連 Anthropic、Google 自己都額外出一個「OpenAI 相容端點」。你常用又堅持原生格式的只有 Claude,才會覺得只有它不同(其實 Gemini 也自成一格,你只是沒踩到)。
大多第三方是「OpenAI 的子集 + 小差異」——有的不支援
n/logprobs/response_format,串流 tool_calls 的切法、tool_choice 接受的值、stop 行為都可能略有出入。基本款能跑,冷門參數要各家實測。
🔧 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:模型不回話,回一張「呼叫單」
注意 content 是 null——它沒有直接回答,而是回了一個 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 }
}
沒工具時同樣一句話才 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 }
}
模型只會做兩件事:①「我想呼叫 X(參數 Y)」,②拿到結果後把它翻成人話。中間「真的去執行」永遠是你的程式的責任。一次工具呼叫 = 至少兩發 API 請求(要它決定 → 給它結果)。Agent 會這樣來回好幾輪,直到
finish_reason 不再是 tool_calls。
延伸二:模型到底「怎麼做到」tool use?
關鍵認知:模型沒有魔法,它從頭到尾只會預測下一個 token。tool use = 訓練 + 格式約定 + 伺服器解析,三步:
- 你的 tools schema 被塞進 prompt,變成純文字。伺服器用模型的 chat template 把工具定義渲染成一段「你有這些工具:{schema}」接在對話前面——這就是前面 prompt_tokens 從 20 幾跳到 231 的真相:工具宣告本身變成了輸入文字。
- 模型被訓練成:看到工具宣告 + 需要時,吐出一段特定格式的 token(常用特殊控制 token 框起來,如 Llama 的
<|python_tag|>之類)。對模型而言,吐「工具呼叫」和吐「一句話」沒有本質差別,都是它學到的正確接續。 - 伺服器的 parser 認得那格式,拆回結構化的 tool_calls 給你,並把 finish_reason 設成 tool_calls。
有些平台用 grammar / 狀態機強制模型輸出必須是「符合你 schema 的合法 JSON」(vLLM 叫 guided decoding、llama.cpp 用 GBNF、OpenAI 叫 structured outputs)。這保證參數一定 parse 得動。但不是每家都開——沒開的就賭模型夠聰明,這是弱模型工具呼叫常吐壞 JSON 的原因。
那 MCP 呢?模型根本不知道 MCP 存在
MCP 不是模型的能力,是 host(Claude Code / Desktop / IDE)端的水電工程。它標準化的是「工具從哪來、怎麼被執行」,不是模型怎麼吐呼叫:
- host 連上 MCP server,呼叫
tools/list發現有哪些工具 + JSON schema。 - host 把這些 schema 翻成模型看得懂的 tools 格式,塞進上面那個 request。
- 模型照延伸二吐一個 tool_call(它以為那只是個普通工具)。
- 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.parameters | tools[].input_schema |
| 模型要求呼叫 | assistant 訊息帶 tool_calls[],content=null | content 陣列裡一個 {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 為什麼能在一則回應裡「先講一句話再呼叫工具」。
這跟 Claude Code 有什麼關係?它就是把這個 loop 自動化
你在 Claude Code 裡看到的「它自己讀檔、跑指令、改程式」,拆開來就是上面那個 tool-use 往返,自動跑很多輪:
- Claude Code 在每個 request 的
tools裡宣告一堆工具:Read、Edit、Bash、Grep……(各自有 input_schema)。 - 模型回一個
tool_use:「我要 Read 這個檔」→ Claude Code 本地真的去讀 → 把檔案內容當tool_result送回。 - 模型看到內容,再回下一個
tool_use:「Edit 這幾行」→ 本地執行 → 送回結果。 - 如此往返,直到模型不再要求工具(
stop_reason: end_turn),吐出給你看的最終訊息。
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/list | AgentCard.skills + capabilities |
| 發起請求 | 模型回 tool_calls | tools/call | SendMessage → 建 Task |
| 工作單位 | 一次 tool_call(無狀態) | 一次 call | Task(有狀態:SUBMITTED→WORKING→COMPLETED) |
| 回傳結果 | tool 訊息 / tool_result | call result content | Artifact(由 Parts 組成) |
| 內容最小單位 | content block | content part | Part(text/file/data) |
| 串流 | SSE delta | SSE | SendStreamingMessage / 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_url | https://api.groq.com/openai/v1 | https://api.openai.com/v1 |
| api_key | GROQ_API_KEY | OPENAI_API_KEY |
| model | llama-3.3-70b-versatile | gpt-4o-mini / gpt-5 等 |
| tools schema · tool loop · 迴圈邏輯 | — | 完全不動 |
你的
providers.conf 登錄表加一行 openai 就收編了——同一支 delegate、同一份 tool schema,換個 endpoint/key/model 就跑。反過來才麻煩:若你朋友想把 Claude 當副手,因為 Claude 講的是 Anthropic 積木格式(見前面對照),就得多一層 adapter 或用 LiteLLM 之類的翻譯層。OpenAI ↔ OpenAI 幾乎零成本,牽涉到 Claude 才要翻譯。
延伸三:工具呼叫是另一張成績單 — 用真實 agent 任務重評
前面幾篇的副手排名,量的都是「寫程式」(隱形契約、一次過);工具呼叫是另一種要專門訓練的能力,兩者不一定一致。第一版我用「問天氣該不該叫工具」的玩具題測——但玩具題會騙人。真正的 agent 是多步、要串接、要用上一個工具吐回來的結果,所以改成真實 agent-loop(給檔案工具 list/read/write,跑完整迴圈,真的把工具結果餵回去),兩個真任務:
- T1 讀→篩→寫→回報:讀
users.json→ 篩出 role=admin → 把 email 寫成admins.csv→ 回報筆數。考點:讀完要真的用資料再寫。 - T2 讀兩檔→比對:讀
dev.env和prod.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 | ❌ 兩次都寫出空 CSV | ✅ | 1/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.py、run_script.sh、process_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,就得用真實多步任務測,別只看寫碼或聊天。
實戰:複製這段,親手打一次 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.content | content[] 裡 type=text 的 block |
| 它是講完還是被切斷 | finish_reason | stop_reason |
| 它想呼叫工具嗎 | finish_reason=tool_calls + tool_calls[] | stop_reason=tool_use + tool_use block |
| 花了多少錢(輸入) | usage.prompt_tokens | usage.input_tokens |
| 花了多少錢(輸出) | usage.completion_tokens | usage.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。
| 參數 | 型別 | 必/選 | 功用 | 用過 |
|---|---|---|---|---|
| 核心 | ||||
model | string | 必填 | 用哪個模型 | ✓ |
messages | array | 必填 | 對話歷史;角色 system/user/assistant/tool | ✓ |
| 生成控制 | ||||
max_completion_tokens | int | 選填 | 輸出上限(新版,reasoning 模型用這個) | – |
max_tokens | int | 選填 | 輸出上限(舊名,多數相容端點仍吃) | ✓ |
temperature | 0–2 | 選填 | 亂度;0=可重現,高=發散 | ✓ |
top_p | 0–1 | 選填 | nucleus 取樣;與 temperature 擇一 | – |
n | int | 選填 | 一次生成幾個候選(choices 數) | – |
stop | string|array | 選填 | 遇到就停的字串(最多 4 組) | – |
presence_penalty | -2–2 | 選填 | 鼓勵講新主題 | – |
frequency_penalty | -2–2 | 選填 | 抑制重複用詞 | – |
logit_bias | map | 選填 | 手動加減特定 token 機率 | – |
logprobs | bool | 選填 | 回傳每個 token 機率 | – |
top_logprobs | 0–20 | 選填 | 每步回前 N 名候選機率(需 logprobs) | – |
seed | int | 選填 | 盡量可重現(best-effort) | – |
| 串流 | ||||
stream | bool | 選填 | 逐 token 串流(SSE) | ✓ |
stream_options | object | 選填 | 串流細節,如 include_usage | – |
| 工具 / function calling | ||||
tools | array | 選填 | 宣告可用工具(function schema) | ✓ |
tool_choice | auto|none|required | 選填 | 要不要 / 強制 / 指定用工具 | ✓ |
parallel_tool_calls | bool | 選填 | 允許一次回多個工具呼叫 | – |
functions · function_call | — | 棄用 | 舊版工具寫法,已被 tools 取代 | – |
| 輸出格式 / 推理 | ||||
response_format | object | 選填 | 強制輸出格式(json_object / json_schema) | – |
reasoning_effort | low|med|high | 選填 | reasoning 模型思考力度 | – |
prediction | object | 選填 | predicted outputs:已知大半內容時加速 | – |
| 多模態 | ||||
modalities | array | 選填 | 要哪些輸出模態 | – |
audio | object | 選填 | 語音輸出設定(voice / format) | – |
web_search_options | object | 選填 | 內建網路搜尋(支援的模型) | – |
| 帳務 / 基礎設施 | ||||
user | string | 選填 | 終端使用者識別(濫用偵測) | – |
store | bool | 選填 | 是否保存對話(eval / 蒸餾) | – |
metadata | map | 選填 | 自訂標籤 key-value | – |
service_tier | auto|default|flex | 選填 | 運算優先級 / 成本檔位 | – |
附錄 B:Anthropic Messages 全參數
依官方文件核對。兩個「陷阱差異」先記住:max_tokens 在這裡是必填(OpenAI 選填)、temperature 上限是 1(OpenAI 是 2)。
| 參數 | 型別 | 必/選 | 功用 | 用過 |
|---|---|---|---|---|
| 核心 | ||||
model | string | 必填 | 用哪個 Claude 模型 | – |
messages | array | 必填 | user/assistant 輪替;content 是積木陣列 | – |
max_tokens | int | 必填 | 輸出上限(這裡是必填!設 0 可做快取預熱) | – |
| 內容 / 停止 | ||||
system | string|block[] | 選填 | 系統提示,頂層獨立欄位(不塞在 messages) | – |
stop_sequences | string[] | 選填 | 遇到就停的自訂字串 | – |
| 生成控制 | ||||
temperature | 0–1 | 選填 | 亂度(上限 1,不是 2) | – |
top_p | 0–1 | 選填 | nucleus 取樣 | – |
top_k | int | 選填 | 只從前 K 個 token 取樣(OpenAI 無此參數) | – |
| 工具 | ||||
tools | array | 選填 | 工具定義(參數用 input_schema);含 bash / code_execution / web_search 等官方工具 | – |
tool_choice | auto|any|tool|none | 選填 | 要不要 / 任選 / 指定 / 禁用(any = OpenAI 的 required) | – |
| 延伸思考 / 輸出 | ||||
thinking | object | 選填 | 延伸思考:enabled / adaptive + budget_tokens(≥1024) | – |
stream | bool | 選填 | SSE 串流 | – |
output_config | object | 選填 | 結構化輸出設定(JSON schema) | – |
| 帳務 / 快取 / 基礎設施 | ||||
metadata | object | 選填 | 附 user_id(濫用偵測) | – |
service_tier | auto|standard_only | 選填 | 優先 / 標準容量 | – |
cache_control | object | 選填 | 手動標記 prompt caching(OpenAI 是自動) | – |
container | string | 選填 | code execution 容器 id,跨請求重用 | – |
inference_geo | string | 選填 | 指定推論地區 | – |
| HTTP Headers | ||||
x-api-key | header | 必填 | API 金鑰 | – |
anthropic-version | header | 必填 | API 版本(如 2023-06-01) | – |
anthropic-beta | header | 選填 | 開啟 beta 功能 | – |
Anthropic 有、OpenAI 沒有:
top_k、頂層 system、thinking.budget_tokens(精確設思考預算,比 OpenAI 的三檔 reasoning_effort 細)、手動 cache_control。OpenAI 有、Anthropic 沒有:
n、presence/frequency_penalty、logit_bias、seed。
附錄 C:同功能,兩派參數對映速查
| 你想做的事 | OpenAI | Anthropic |
|---|---|---|
| 系統提示 | messages role=system | 頂層 system |
| 輸出上限 | max_completion_tokens(選填) | max_tokens(必填) |
| 亂度 | temperature 0–2 | temperature 0–1 |
| Top-K 取樣 | —(無) | top_k |
| 停止字串 | stop | stop_sequences |
| 工具參數 schema | function.parameters | input_schema |
| 強制用工具 | tool_choice:"required" | tool_choice:"any" |
| 思考預算 | reasoning_effort(三檔) | thinking.budget_tokens(精確) |
| 結構化輸出 | response_format | output_config |
| prompt 快取 | 自動(回 cached_tokens) | 手動 cache_control |
| 停止原因 | finish_reason | stop_reason |
| 輸入 / 輸出 token | prompt_tokens / completion_tokens | input_tokens / output_tokens |
發佈留言