Reasoning blocks pre-abiertos: por qué el tool calling se rompe sin avisar
Un agente que corre sobre un modelo open source deja de llamar herramientas de un momento a otro. No hay error, no hay excepción: la respuesta llega con content="" y tool_calls vacío. El loop del agente interpreta ese turno como puro razonamiento y se detiene ahí. El bug no está en el modelo ni en el prompt: está en cómo el harness parsea la salida.
Esta semana llama.cpp documentó un caso concreto de esta familia de bugs, y el propio commit deja ver que no es la primera vez que pasa.
El supuesto que rompe todo
Los parsers genéricos de tool calling asumen que el bloque de razonamiento es simétrico: el modelo emite una etiqueta de apertura (<think>), razona, y emite una etiqueta de cierre (</think>) antes de decidir si llama una herramienta. El parser busca ese cierre para saber dónde termina el razonamiento y dónde empieza el contenido real.
El problema es que varias plantillas de chat ya no generan la apertura: la dejan pre-escrita en el prompt de generación. El modelo entra directamente en modo razonamiento sin emitir <think> porque la plantilla ya lo puso ahí. Y si el modelo decide llamar una herramienta antes de cerrar ese bloque, el tool call llega envuelto en texto que el parser todavía cree que es razonamiento.
El caso Ling 3.0
Según las notas de llama.cpp (build b11057), las plantillas de Ling 3.0 Flash (Bailing V3) hacen exactamente eso: pre-abren el bloque de pensamiento en el prompt de generación, así que el modelo nunca emite el <think> de apertura, y un tool call puede llegar antes de cualquier </think>. El “autoparser” genérico que llama.cpp genera para esta familia de modelos solo terminaba el razonamiento al ver la etiqueta de cierre, así que clasificaba esos tool calls enteros como reasoning_content. El cliente recibía contenido vacío, sin tool calls, y el loop del agente moría interpretando el turno como razonamiento puro.
La solución no fue un parche al parser genérico: fue escribir un parser dedicado para esta familia de modelos, que termina el razonamiento al ver la etiqueta de cierre o al ver el inicio de un <tool_call>, lo que ocurra primero.
No es un caso aislado
Lo que hace que esto valga como tesis y no como anécdota es que las propias notas del commit lo dicen explícitamente: este parser dedicado imita a los parsers ya escritos a mano para Qwen3-Coder y Kimi K3, y sigue el mismo criterio que ya usa la implementación de referencia de vLLM/SGLang para Ling3, que trata <tool_call> como un terminador implícito de razonamiento.
Es decir: tres familias de modelos distintas (Qwen3-Coder, Kimi K3, Ling 3.0) ya necesitaron su propio parser hecho a mano porque el genérico asume simetría de etiquetas que sus plantillas no respetan. Cada vez que llega un modelo nuevo con razonamiento “adaptativo” o “always-on”, hay que volver a preguntarse si su plantilla abre el bloque de pensamiento por su cuenta o si lo pre-abre por el modelo.
La misma semana, llama.cpp también tocó el parser de DeepSeek V3.2/V4 para agregarle delimitadores de mensaje explícitos. No hay evidencia en esas notas de que comparta la misma causa raíz que el caso de Ling 3.0, pero confirma el patrón: los parsers de tool calling para modelos con razonamiento se siguen rompiendo y parchando uno por uno, no de una vez para todos.
Qué revisar si integras un modelo nuevo con tool calling
Antes de confiar en el parser genérico o autogenerado de tu harness para un modelo con razonamiento:
- Prueba explícitamente el caso de un tool call que llega antes de que el modelo cierre su bloque de razonamiento, tanto en modo streaming como no streaming.
- Revisa la plantilla de chat del modelo: si el bloque de pensamiento aparece ya abierto en el prompt de generación, el modelo nunca va a emitir esa apertura y tu parser no puede depender de detectarla.
- Si el content llega vacío y sin tool_calls en un turno donde esperabas una llamada a herramienta, sospecha primero del parser de razonamiento, no del modelo.
- No asumas que un parser genérico sirve para toda una familia de modelos con razonamiento: los casos ya conocidos (Qwen3-Coder, Kimi K3, Ling 3.0) necesitaron cada uno su propio manejo del punto de corte entre razonamiento y tool call.
Fuentes: https://github.com/ggml-org/llama.cpp/releases/tag/b11057, https://github.com/ggml-org/llama.cpp/releases/tag/b11020, https://github.com/ggml-org/llama.cpp/releases/tag/b10936
Seguir leyendo
Esto se demuestra en video, y se sigue a diario en la comunidad
Las demos completas van al canal. El seguimiento release por release, adentro.