Agent 循环里的 GLM 5.2 工具调用:「兼容 OpenAI」背后藏了什么
把一个现成的 OpenAI 风格 agent 循环指向 GLM 5.2,大部分都能直接跑通:你发 tools,拿回 tool_calls,执行它们,再把结果发回去。但接下来它会做一件 SDK 示例里从没演示过的事——assistant 在和工具调用同一轮里返回了一行文本:
{
"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 这边:你发函数 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 循环都靠这一点:它们在「content 还是 tool calls」上分支,把 content 当成最终答案记下来,或者断言它是空的。GLM 把两个一起塞给你,于是这个假设第一个崩。
这里的行为是从对 glm-5.2 的真实工具调用请求里抓出来的,同一个任务也跑了 gpt-5.5 和 claude-opus-4-8 作为参照。一句话总结:GLM 5.2 用的是 OpenAI 的 API 表面,但在几个维度上它的行为更像 Claude 而不是 GPT,真正栽跟头的恰恰是按 OpenAI 习惯写出来的循环。
同一轮对话,三种写法
同样的 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 块 |
| 该轮的推理内容 | 暴露出来:reasoning_content + reasoning_tokens | 隐藏;只在 usage 里给 reasoning_tokens | 只在你启用后以 thinking 块出现 |
| 并行工具调用 | 支持,带 index | 支持 | 支持,多个 tool_use 块 |
| 完成信号 | finish_reason: "tool_calls" | finish_reason: "tool_calls" | stop_reason: "tool_use" |
| 工具调用 id 前缀 | call_… | call_… | toolu_… |
会让循环出问题的是两行:工具调用轮里出现文本,以及推理内容出现在该轮。其余部分都老老实实,没什么花样。
文本和工具调用一起返回
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 块。所以 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 是推理模型,调用工具时也不会停下推理。每个工具调用回合都带着推理过程,而 GLM 5.2 会把它以文本形式暴露出来。在非流式响应里,token 统计把这一点说得很清楚:
"usage": {
"prompt_tokens": 224,
"completion_tokens": 68,
"completion_tokens_details": { "reasoning_tokens": 30 },
"total_tokens": 292
}
这次请求的可见输出只是两个简短的函数调用,但补全里差不多一半都是推理。三个模型正是在这一行上分道扬镳。GLM 5.2 把推理内容作为 reasoning_content 返回,并附上 token 数。OpenAI 在 usage 里对 reasoning_tokens 计费,但从不展示文本。Anthropic 只在你打开 extended thinking 时,以 thinking 块的形式展示。三者之中,GLM 5.2 默认暴露得最多。
由此带来两个后果。第一是成本:工具调用回合里这些推理 token 都要付费,而一个 agent 循环往往要跑很多回合。控制这个数字的旋钮是 reasoning effort,我们在 GLM 5.2:Reasoning Effort 是成本杠杆 里讲过。每个回合都要统计推理 token,而不是只看最终答案。
第二是流式输出的顺序。流式请求时,GLM 先发推理,再发前导文本,最后才是工具调用:
reasoning_content (many deltas)
content (a few deltas)
tool_calls (id + name, then arguments)
按原版 OpenAI chat completions 写的解析器不认识 reasoning_content 字段,会悄悄忽略开头这一串。通常没问题。但如果你的 UI 用第一个 content delta 来触发”thinking…”状态,问题就来了:线上最先到的是推理而不是 content,指示器永远不会切换。
一个 GLM 5.2 工具调用回合的成本
行为只是一半,账单是另一半,而 agent 循环会把同样的回合跑上许多遍。固定前缀(约 2000 token 的 system prompt 加上工具定义),每次调用只改 user message,在十个热回合上测量:
| 每个热工具调用回合 | 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 是调节它的旋钮。
在循环里,靠缓存才能让这些花费变得可承受。system prompt 和工具定义占了每个 prompt 的大部分,而且每个回合都一样,所以前缀一旦缓存,回合成本就便宜 2.8 到 4.9 倍。能不能用上缓存取决于两点。GLM 和 OpenAI 会自动缓存前缀;Anthropic 只缓存你用 cache_control 标记的部分。另外 GLM 的缓存预热慢半拍,所以三步的任务可能全程按原价付费,而三十步的任务则跑在缓存上。具体机制见 开放权重 LLM 的缓存。
什么时候该用 GLM 5.2,以及怎么用好它
把前面的信息拼起来看:那张表里 GLM 5.2 是最便宜的、也是最慢的,而且每一轮都会推理。这个特点决定了它适合用在哪。
它的位置:成本占主导、每轮多花几秒钟也能接受的长链路、多步骤 agent 循环。比如后台编码 agent、CI 和批量自动化、无人值守跑的任务。让它变慢的那部分推理,恰恰也是它在真实编码和规划任务上稳得住、而不只是做简单路由的原因。预热之后缓存的优势会叠加:一个三十步的任务把前缀成本摊薄,跑起来很便宜;而三步的任务可能要付全价,还白白吃下延迟。所以长任务交给 GLM 5.2,交互式、单次调用、那种每轮六秒会被明显感知的场景,留给更快的模型。
怎么用好 GLM 5.2。五个习惯能让你的循环在不离开 OpenAI API 接口的前提下适配 GLM:
- 把一个带 tool-call 的轮次当成可能也带
content,别断定它一定是空的。 - 在传输流里预期会有
reasoning_content,在usage里预期会有reasoning_tokens;两者都要留出预算,并用 reasoning-effort 这个旋钮在质量和成本之间做取舍。 - 在 streaming 中,不要用第一个内容 delta 来触发 UI 状态,因为推理内容会先到。
- 原样回传
tool_call_id,把它当成不透明的字符串,不要解析、也不要重新生成。 - 按
index累积 streaming 的arguments,直到这次调用结束;不要假定分片的数量。
有两件事你不用专门去防:GLM 发出的并行 tool call 跟其他模型一样带 index,而且整个往返会正常收尾。追加 assistant 轮次,每个调用追加一条带结果的 tool 消息,它就会以 finish_reason: "stop" 结束。顺便在每一轮都保持可缓存前缀的字节稳定;system prompt 和工具定义占了每个 prompt 的大头,正是稳定的前缀让 GLM 的缓存在预热后把成本扛下来。
这些都不复杂。它只是「请求成功」和「agent 循环正确」之间的差距,而在 GLM 上这个差距主要就是两个假设:以为带 tool-call 的轮次是沉默的,以及以为它没在思考。去掉这两个假设,保持前缀稳定,同一套循环就能同时跑 GLM、GPT 和 Claude,而在延迟不是优化目标的场景里,GLM 能用零头的成本完成同样的事。
免责声明
上文的成本、延迟和缓存数据是在 2026-06-30 测的,用 glm-5.2、gpt-5.5 和 claude-opus-4-8,每个模型跑十轮预热后的 tool-call 轮次。成本取自上报的 usage;延迟是 wall-clock 中位数,会随负载和 reasoning effort 变化。模型行为和价格都会变,所以把这些数字当作参考,在依赖它们之前先用你自己的流量重新测一遍。