Llamadas a herramientas en GLM 5.2 dentro de bucles de agentes: lo que oculta 'compatible con OpenAI'
Contenido
Apunta un bucle de agente al estilo de OpenAI hacia GLM 5.2 y la mayor parte funciona sin más: envías tools, recibes tool_calls, las ejecutas y mandas los resultados. Pero entonces hace algo que los ejemplos de los SDK nunca muestran. El asistente devuelve una línea de texto en el mismo turno que las tool calls:
{
"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\"}"}}
]
}
}]
}
Hay dos convenciones dominantes y conviene tener presentes ambas. En la de OpenAI envías esquemas de funciones, recibes tool_calls de vuelta y respondes con un mensaje tool por cada llamada, identificado con 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"})
La de Anthropic tiene otra forma: las herramientas llevan un input_schema, el modelo emite bloques tool_use y tú respondes con un bloque 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 habla el dialecto de OpenAI.
En el contrato de OpenAI, message.content es null cuando finish_reason vale tool_calls. Muchos bucles de agentes se apoyan en eso: ramifican según “contenido o tool calls”, registran content como respuesta final o dan por hecho que está vacío. GLM te entrega ambos a la vez, y esa suposición es lo primero que se rompe.
Este comportamiento se capturó a partir de peticiones reales de tool calling a glm-5.2, con gpt-5.5 y claude-opus-4-8 ejecutados sobre la misma tarea como referencia. En resumen: GLM 5.2 usa la superficie de la API de OpenAI, pero en un par de aspectos se parece más a Claude que a GPT, y es el bucle entrenado para OpenAI el que tropieza.
El mismo turno, de tres formas
Mismo prompt, las mismas dos tools, tres modelos:
GLM (glm-5.2) | OpenAI (gpt-5.5) | Anthropic (claude-opus-4-8) | |
|---|---|---|---|
| Superficie de API | OpenAI chat-completions | OpenAI chat-completions | Anthropic messages |
| Texto en el turno de tool-call | preámbulo en content (no nulo) | content es null | un bloque text antes de tool_use |
| Reasoning en ese turno | expuesto: reasoning_content + reasoning_tokens | oculto; solo reasoning_tokens en usage | solo como bloque thinking, si lo activas |
| Tool calls en paralelo | sí, con index | sí | sí, varios bloques tool_use |
| Señal de fin | finish_reason: "tool_calls" | finish_reason: "tool_calls" | stop_reason: "tool_use" |
| Prefijo del id de tool-call | call_… | call_… | toolu_… |
Dos filas son las que rompen los loops: el texto en el turno de tool-call y el reasoning que aparece en ese mismo turno. El resto es tranquilizadoramente aburrido.
El texto viaja junto al tool call
GLM 5.2 emite con frecuencia un breve preámbulo en content del asistente junto a tool_calls, con finish_reason: "tool_calls". No es un error ni algo ocasional.
Aquí está el mismo turno en los tres, recortado a la parte que difiere:
// 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 deja content en null; GLM lo rellena; Anthropic siempre ha puesto un bloque text ahí. Así que GLM toma el formato de wire de OpenAI con la costumbre de Anthropic de narrar antes de actuar, y un loop escrito contra OpenAI es el que queda desprevenido. El arreglo es pequeño, pero hay que hacerlo a propósito. Deja de tratar el turno de tool-call como algo sin contenido:
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})
Si tu loop muestra content al usuario como la respuesta del asistente, ahora verás una línea del tipo “déjame revisar eso” antes de cada tool call. Decide si la quieres. Lo importante es que la decisión es tuya, no algo que el silencio del modelo tome por ti.
Piensa en voz alta
GLM 5.2 es un modelo de razonamiento, y eso no se detiene cuando hay uso de herramientas. Un turno con llamada a herramienta arrastra el razonamiento consigo, y GLM 5.2 lo expone como texto. En una respuesta sin streaming, el conteo de tokens lo deja explícito:
"usage": {
"prompt_tokens": 224,
"completion_tokens": 68,
"completion_tokens_details": { "reasoning_tokens": 30 },
"total_tokens": 292
}
Casi la mitad de la completion fue razonamiento, en una petición cuya salida visible son dos llamadas a función cortas. Esta es la fila donde los tres modelos se separan. GLM 5.2 te da el razonamiento como reasoning_content y el conteo de tokens. OpenAI factura reasoning_tokens en usage pero nunca muestra el texto. Anthropic lo muestra solo como bloques thinking, y solo si activas el extended thinking. GLM 5.2 es, por defecto, el más expuesto de los tres.
Dos consecuencias. Primero, el coste: pagas esos reasoning tokens en los turnos con llamada a herramienta, y un agent loop son muchos turnos. El reasoning effort es la palanca que mueve el número, algo que cubrimos en GLM 5.2: el reasoning effort es la palanca de coste. Cuenta los reasoning tokens en cada turno, no solo en la respuesta final.
Segundo, el orden del streaming. Cuando haces streaming de la petición, GLM envía primero el razonamiento, luego el texto del preámbulo y al final las tool calls:
reasoning_content (many deltas)
content (a few deltas)
tool_calls (id + name, then arguments)
Un parser escrito contra el chat completions estándar de OpenAI no conoce el campo reasoning_content y va a ignorar sin avisar esa ráfaga inicial. Normalmente no pasa nada. Empieza a ser un problema si tu UI muestra un estado “thinking…” disparado por el primer content delta, porque lo primero que llega por el cable es razonamiento, no content, y el indicador nunca cambia.
Cuánto cuesta un turno con llamada a herramienta en GLM 5.2
El comportamiento es la mitad de la historia; la factura es la otra mitad, y un agent loop ejecuta el mismo turno muchas veces. Con un prefijo fijo (un system prompt de unos 2.000 tokens más las definiciones de herramientas) y el mensaje del usuario variando en cada llamada, medido sobre diez turnos en caliente:
| por turno en caliente con llamada a herramienta | GLM glm-5.2 | OpenAI gpt-5.5 | Anthropic claude-opus-4-8 |
|---|---|---|---|
| Coste | $0.0009 | $0.0042 | $0.0051 |
| Latencia (mediana) | 6.6s | 1.9s | 3.1s |
| Prompt en caché | ≈96% | ≈81% | ≈97% |
| Reasoning tokens | ≈27 | 0 | 0 |
| Coste frío → caliente | 3.4× | 2.8× | 4.9× |
GLM 5.2 es el barato: unas 4,5× más barato que GPT-5.5 y 5,4× más barato que Opus por turno en caliente. También es el lento, entre dos y tres veces y media su latencia, porque gasta reasoning tokens en cada turno mientras que los otros dos no gastaron ninguno en esta tarea. Ese es el trato: GLM compra coste con latencia, y el reasoning effort es la palanca que lo mueve.
El caching es lo que hace que cualquiera de estos sea viable en un loop. El system prompt y las definiciones de herramientas son la mayor parte de cada prompt y son idénticos en cada turno, así que una vez cacheado el prefijo el turno sale entre 2,8× y 4,9× más barato. Dos cosas deciden si lo aprovechas. GLM y OpenAI cachean el prefijo automáticamente; Anthropic solo cachea lo que marcas con cache_control. Y la caché de GLM calienta con un poco de retraso, así que una tarea de tres pasos puede pagar precio completo mientras que una de treinta pasos corre cacheada. Los detalles están en Caching de LLMs de pesos abiertos.
Cuándo usar GLM 5.2 y cómo sacarle provecho
Junta las piezas. GLM 5.2 es el modelo barato de esa tabla, también el lento, y razona en cada turno. Ese perfil indica dónde aporta valor.
Dónde encaja: bucles de agente largos y de varios pasos, donde el coste manda y unos segundos por turno no son problema. Agentes de coding en segundo plano, CI y automatización por lotes, trabajos que corren sin supervisión. El razonamiento que lo vuelve lento es también lo que le permite aguantar tareas reales de coding y planificación, no solo enrutado trivial. El caching refuerza el argumento una vez superado el arranque: una tarea de treinta pasos amortiza el prefijo y sale barata, mientras que una de tres pasos puede pagar el precio completo y comerse la latencia sin obtener nada a cambio. Así que usa GLM 5.2 en los trabajos largos y reserva un modelo más rápido para las llamadas interactivas de un solo turno, donde sí se notan los seis segundos por turno.
Cómo sacarle provecho a GLM 5.2. Cinco hábitos dejan un bucle listo para GLM sin salir de la superficie de la API de OpenAI:
- Asume que un turno con tool call puede traer
content. No des por hecho que está vacío. - Espera
reasoning_contenten el cable yreasoning_tokensenusage; presupuesta ambos y usa el dial de reasoning-effort para cambiar calidad por coste. - En streaming, no enganches el estado de la UI al primer delta de content, porque el razonamiento llega antes.
- Devuelve
tool_call_idtal cual; trátalo como opaco, no lo parsees ni lo regeneres. - Acumula los
argumentsdel streaming porindexhasta que la llamada se cierre; no supongas un número de chunks.
Dos cosas contra las que no hace falta protegerse: GLM emite tool calls en paralelo con un index como los demás, y el round-trip se cierra con normalidad. Añade el turno del assistant, añade un mensaje tool por llamada con su resultado, y termina con finish_reason: "stop". De paso, mantén byte a byte estable el prefijo cacheable entre turnos; el system prompt y las definiciones de tools son la mayor parte de cada prompt, y un prefijo estable es lo que permite que la cache de GLM cargue con el coste una vez que se calienta.
Nada de esto es exótico. Es la diferencia entre “la petición tiene éxito” y “el bucle del agente es correcto”, y en GLM esa diferencia se reduce básicamente a dos suposiciones: que un turno con tool call es silencioso, y que no está pensando. Descarta esas dos, mantén el prefijo estable, y un mismo bucle sirve para GLM, GPT y Claude por igual, con GLM haciéndolo por una fracción del coste allí donde la latencia no es lo que estás optimizando.
Aviso
Las cifras de coste, latencia y cache de arriba se midieron el 2026-06-30 sobre diez turnos con tool call en caliente por modelo, con glm-5.2, gpt-5.5 y claude-opus-4-8. El coste sale del usage reportado; la latencia es la mediana de wall-clock y varía con la carga y el reasoning effort. El comportamiento de los modelos y los precios cambian, así que toma las cifras como orientativas y vuelve a medirlas contra tu propio tráfico antes de depender de ellas.