Agent Loop 中的 GLM 5.2 工具呼叫:「OpenAI 相容」藏了什麼
把現成的 OpenAI 風格 agent loop 接到 GLM 5.2 上,大部分都能直接跑:你送出 tools,拿回 tool_calls,執行它們,再把結果送回去。接著它會做一件 SDK 範例從來沒演示過的事。助理在送出工具呼叫的同一輪裡,順手回了一行文字:
{
"choices": [{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": "I'll look up both pieces of information for you at the same time!",
"tool_calls": [
{"id": "call_…", "type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}},
{"id": "call_…", "type": "function",
"function": {"name": "get_time", "arguments": "{\"city\":\"Tokyo\"}"}}
]
}
}]
}
主流的慣例有兩套,最好兩套都記在腦子裡。OpenAI 那套:你送出 function schema,拿回 tool_calls,然後針對每個呼叫用一則 tool 訊息回應,以 tool_call_id 對應:
resp = openai.chat.completions.create(model="…", tools=tools, tool_choice="auto", messages=messages)
# assistant.tool_calls → [{"id": "call_…", "function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}}]
messages.append(resp.choices[0].message)
messages.append({"role": "tool", "tool_call_id": "call_…", "content": "18C, clear"})
Anthropic 那套長得不一樣:工具帶的是 input_schema,模型吐出 tool_use 區塊,你用 tool_result 區塊回應:
resp = anthropic.messages.create(model="…", tools=tools, messages=messages)
# resp.content → [{"type": "tool_use", "id": "toolu_…", "name": "get_weather", "input": {"city": "Paris"}}]
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_…", "content": "18C, clear"}]})
GLM 5.2 講的是 OpenAI 這套方言。
在 OpenAI 的約定裡,finish_reason 是 tool_calls 時,message.content 會是 null。很多 agent loop 就靠這點:它們用「有 content 還是有 tool call」來分流,把 content 當成最終答案記下來,或是直接斷言它是空的。GLM 兩個一起給你,這個假設就是第一個會壞掉的地方。
這裡的行為來自對 glm-5.2 的實際工具呼叫請求,並用 gpt-5.5 和 claude-opus-4-8 跑同一個任務當對照組。簡單講:GLM 5.2 用的是 OpenAI 的 API 介面,但在某幾個面向上,它的行為比較像 Claude 而不是 GPT,而會踩雷的正是按 OpenAI 訓練出來的那種 loop。
同一回合,三種樣貌
相同的 prompt、相同的兩個工具,丟給三個模型:
GLM (glm-5.2) | OpenAI (gpt-5.5) | Anthropic (claude-opus-4-8) | |
|---|---|---|---|
| API 介面 | OpenAI chat-completions | OpenAI chat-completions | Anthropic messages |
| 工具呼叫回合中的文字 | content 帶前導文字(非 null) | content 為 null | tool_use 之前有一個 text block |
| 該回合的 reasoning | 直接給出:reasoning_content + reasoning_tokens | 隱藏;usage 裡只有 reasoning_tokens | 只有在啟用時,才會以 thinking block 出現 |
| 平行工具呼叫 | 支援,帶 index | 支援 | 支援,多個 tool_use block |
| 完成訊號 | finish_reason: "tool_calls" | finish_reason: "tool_calls" | stop_reason: "tool_use" |
| 工具呼叫 id 前綴 | call_… | call_… | toolu_… |
會讓迴圈出問題的是其中兩列:工具呼叫回合中的文字,以及該回合冒出來的 reasoning。其餘的部分都平淡無奇,反而讓人安心。
文字跟著工具呼叫一起來
GLM 5.2 經常在 tool_calls 旁邊帶上一段簡短的 assistant content 前導文字,並回傳 finish_reason: "tool_calls"。這不是錯誤,也不是偶發狀況。
下面是同一回合在三個模型上的樣子,只留下有差異的部分:
// OpenAI gpt-5.5: content is null on a tool-call turn
"message": { "content": null,
"tool_calls": [ {/* get_weather */}, {/* get_time */} ] }
// GLM glm-5.2: content carries a preamble
"message": { "content": "I'll look up both pieces of information for you at the same time!",
"tool_calls": [ {/* get_weather */}, {/* get_time */} ] }
// Anthropic claude-opus-4-8: a text block sits before the tool_use blocks
"content": [ { "type": "text", "text": "I'll get both pieces of information for you." },
{ "type": "tool_use", /* get_weather */ },
{ "type": "tool_use", /* get_time */ } ]
OpenAI 把 content 留成 null;GLM 會把它填滿;Anthropic 則一向會在那裡放一個 text block。所以 GLM 用的是 OpenAI 的傳輸格式,卻帶著 Anthropic 那種動手前先說明的習慣。針對 OpenAI 寫的迴圈反而會被打個措手不及。修正很小,但你得刻意去做。別再把工具呼叫回合當成沒有 content:
resp = client.chat.completions.create(model="glm-5.2", messages=msgs, tools=tools)
msg = resp.choices[0].message
# GLM may return assistant text in the same turn as the tool calls.
if msg.content:
log.debug("preamble: %s", msg.content) # keep or drop, but don't assume it's empty
msgs.append(msg)
for call in msg.tool_calls:
result = dispatch(call.function.name, json.loads(call.function.arguments))
msgs.append({"role": "tool", "tool_call_id": call.id, "content": result})
如果你的迴圈會把 content 當成 assistant 的回覆呈現給使用者,那麼每次工具呼叫前都會出現一句「我來查一下」之類的話。你得自己決定要不要保留。重點在於這個決定權在你手上,而不是被模型的沉默替你決定。
它會把思考過程說出來
GLM 5.2 是推理模型,呼叫工具時不會暫停推理。每一次 tool-call 回合都帶著推理過程,而 GLM 5.2 會把它以文字形式直接呈現出來。在非串流的回應裡,從 token 計數就能清楚看到這一點:
"usage": {
"prompt_tokens": 224,
"completion_tokens": 68,
"completion_tokens_details": { "reasoning_tokens": 30 },
"total_tokens": 292
}
這次請求看得見的輸出只是兩個簡短的函式呼叫,但 completion 裡將近一半都是推理。三個模型在這一列上的差異最明顯。GLM 5.2 會把推理過程以 reasoning_content 呈現,並附上 token 計數。OpenAI 會在 usage 裡計費 reasoning_tokens,但從不顯示內容。Anthropic 只在 thinking 區塊裡顯示,而且只有開啟 extended thinking 時才有。三者之中,GLM 5.2 預設曝露得最多。
這帶來兩個後果。第一是成本:tool-call 回合裡的這些推理 token 都得付費,而一個 agent loop 會跑很多回合。reasoning effort 就是調整這個數字的旋鈕,我們在 GLM 5.2:Reasoning Effort 是成本槓桿 裡談過。每一回合都要計算推理 token,不能只算最後的答案。
第二是串流順序。串流請求時,GLM 會先送推理過程,接著是前言文字,最後才是 tool calls:
reasoning_content (many deltas)
content (a few deltas)
tool_calls (id + name, then arguments)
照標準 OpenAI chat completions 寫的 parser 不認得 reasoning_content 欄位,會默默忽略開頭那一段。通常沒問題。但如果你的 UI 用第一個 content delta 來觸發「thinking…」狀態,問題就來了:因為通訊線上最先出現的是推理,不是 content,指示燈永遠不會切換。
GLM 5.2 的一次 tool-call 回合要花多少
行為只說了一半,帳單是另一半,而 agent loop 會把同一個回合反覆跑很多次。固定一段前綴(约 2,000 個 token 的系統提示加上工具定義),每次呼叫只變動使用者訊息,量測十個熱回合的結果如下:
| 每個熱 tool-call 回合 | GLM glm-5.2 | OpenAI gpt-5.5 | Anthropic claude-opus-4-8 |
|---|---|---|---|
| 成本 | $0.0009 | $0.0042 | $0.0051 |
| 延遲(中位數) | 6.6s | 1.9s | 3.1s |
| Prompt 快取 | ≈96% | ≈81% | ≈97% |
| 推理 token | ≈27 | 0 | 0 |
| 冷 → 熱成本 | 3.4× | 2.8× | 4.9× |
GLM 5.2 是最便宜的:每個熱回合大約比 GPT-5.5 便宜 4.5 倍,比 Opus 便宜 5.4 倍。它也是最慢的,延遲是另外兩者的兩到三倍半,因為它每一回合都花推理 token,而另外兩個在這個任務上完全沒花。這就是取捨:GLM 用延遲換成本,而 reasoning effort 就是調整它的旋鈕。
快取是讓這些在 loop 裡都還負擔得起的關鍵。系統提示和工具定義佔了每次 prompt 的絕大部分,而且每回合都一樣,所以前綴一旦進了快取,每回合就便宜 2.8 到 4.9 倍。能不能享受到這個好處,由兩件事決定。GLM 和 OpenAI 會自動快取前綴;Anthropic 只快取你用 cache_control 標記的部分。而且 GLM 的快取會慢一拍才暖起來,所以三步的任務可能整段都付全價,三十步的任務則一路跑在快取上。機制細節在 開放權重 LLM 的快取。
什麼時候該用 GLM 5.2,以及怎麼把它跑好
把前面的線索拼起來。在那張表裡,GLM 5.2 是最便宜的模型,也是最慢的,而且它每一輪都會推理。這個特性本身就指出了它該出現在哪裡。
它適合的場景:成本是主要考量、每輪多花幾秒可以接受的長鏈、多步驟 agent loop。背景執行的 coding agent、CI 與批次自動化,以及無人值守的工作。讓它變慢的那套推理,也正是它在真實的寫程式和規劃任務上撐得住的原因,而不只是做些瑣碎的路由。快取在暖機之後會讓優勢進一步放大:一個三十步的任務能攤平前綴成本,跑起來很便宜;而一個三步的任務可能得付全額,還白白吃下延遲。所以長任務就交給 GLM 5.2,互動式、單次呼叫、每輪六秒會被使用者感受到的場景,則留給更快的模型。
怎麼把 GLM 5.2 跑好。下面五個習慣能讓你的 loop 準備好接 GLM,而且完全不用離開 OpenAI API 的介面:
- 把帶有 tool call 的那一輪當成「可能也帶
content」來處理,別斷定它是空的。 - 預期 wire 上會出現
reasoning_content,usage裡會有reasoning_tokens;兩者都要預留額度,並用 reasoning-effort 這個旋鈕在品質和成本之間做取捨。 - 在 streaming 中,別把 UI 狀態綁在第一個 content delta 上,因為 reasoning 會先到。
- 原封不動地回傳
tool_call_id;把它當成不透明的字串,永遠不要去解析或重新產生它。 - 用
index把 streaming 的arguments累積起來,直到該次呼叫結束;不要假設 chunk 的數量。
有兩件事你不需要特別防範:GLM 發出 parallel tool call 時,會像其他模型一樣帶 index,而且整個 round-trip 會正常收尾。把 assistant 那一輪附加上去,每個呼叫各附加一則帶結果的 tool 訊息,它就會以 finish_reason: "stop" 結束。順手把可快取的前綴在每一輪之間維持 byte 層級的穩定;system prompt 和 tool 定義占了每個 prompt 的絕大部分,而穩定的前綴正是讓 GLM 的快取在暖起來之後能持續攤平成本的關鍵。
這些都不是什麼奇技。它就是「請求成功」和「agent loop 正確」之間的那道縫,而在 GLM 上,這道縫差不多就是兩個假設的寬度:以為帶 tool call 的那一輪是沉默的,以及以為它沒在思考。拿掉這兩個假設,把前綴維持穩定,同一個 loop 就能同時承載 GLM、GPT 和 Claude,而且只要不是在優化延遲的地方,GLM 都能用零頭的成本把活幹完。
免責聲明
上面的成本、延遲與快取數字,是在 2026-06-30 量測的,每個模型跑十次暖過的 tool-call 輪次,使用 glm-5.2、gpt-5.5 和 claude-opus-4-8。成本取自回報的 usage;延遲是 wall-clock 中位數,會隨負載和 reasoning effort 而變動。模型行為和價格都會漂移,所以把這些數字當作參考即可,在依賴它們之前,請用你自己的流量重新量測。