Restringir LLMs con salida estructurada: Ollama, Qwen3 y Python o Go

Un par de formas de obtener salida estructurada de Ollama

Índice

Modelos de Lenguaje Grande (LLMs) son potentes, pero en producción rara vez deseamos párrafos de texto libre. En su lugar, queremos datos predecibles: atributos, hechos u objetos estructurados que puedas ingresar en una aplicación. Eso es la Salida Estructurada de LLM.

La aplicación estricta del esquema reduce la frecuencia con la que los logits incorrectos se convierten en JSON inválido, pero la temperatura y las penalizaciones siguen siendo importantes para las tormentas de reintento; consulta los parámetros de inferencia agéntica para Qwen y Gemma cuando combines restricciones de format con agentes.

Hace algún tiempo, Ollama introdujo soporte para salida estructurada (anuncio), lo que hace posible restringir las respuestas de un modelo para que coincidan con un esquema JSON. Esto habilita pipelines de extracción de datos consistentes para tareas como catalogar características de LLM, realizar benchmarks de modelos o automatizar la integración de sistemas.

patos en fila

En esta entrada, cubriremos:

  • Qué es la salida estructurada y por qué es importante
  • Forma sencilla de obtener salida estructurada de los LLMs
  • Cómo funciona la nueva función de Ollama
  • Ejemplos de extracción de capacidades de LLM:

¿Qué es la Salida Estructurada?

Normalmente, los LLMs generan texto libre:

“El modelo X admite razonamiento con cadena de pensamiento, tiene una ventana de contexto de 200K y habla inglés, chino y español.”

Eso es legible, pero difícil de analizar.

En cambio, con la salida estructurada solicitamos un esquema estricto:

{
  "name": "Modelo X",
  "supports_thinking": true,
  "max_context_tokens": 200000,
  "languages": ["Inglés", "Chino", "Español"]
}

Este JSON es fácil de validar, almacenar en una base de datos o enviar a una interfaz de usuario.


Forma sencilla de obtener Salida Estructurada de LLM

Los LLMs a veces entienden cuál es el esquema y podemos pedirle al LLM que devuelva la salida en JSON utilizando un esquema particular. El modelo Qwen3 de Alibaba está optimizado para el razonamiento y las respuestas estructuradas. Puedes indicarle explícitamente que responda en JSON.

Ejemplo 1: Usando Qwen3 con ollama en Python, solicitando JSON con esquema

import json
import ollama

prompt = """
Eres un extractor de datos estructurados.
Devuelve solo JSON.
Texto: "Elon Musk tiene 53 años y vive en Austin."
Esquema: { "name": string, "age": int, "city": string }
"""

response = ollama.chat(model="qwen3", messages=[{"role": "user", "content": prompt}])
output = response['message']['content']

# Analizar JSON
try:
    data = json.loads(output)
    print(data)
except Exception as e:
    print("Error al analizar JSON:", e)

Salida:

{"name": "Elon Musk", "age": 53, "city": "Austin"}

Aplicando Validación de Esquema con Pydantic

Para evitar salidas mal formadas, puedes validar contra un esquema de Pydantic en Python.

from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int
    city: str

# Supongamos que 'output' es la cadena JSON de Qwen3
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)

Esto asegura que la salida se ajuste a la estructura esperada.


Salida Estructurada de Ollama

Ollama ahora te permite pasar un esquema en el parámetro format. El modelo luego está restringido para responder solo en JSON que cumpla con el esquema (documentación).

En Python, típicamente defines tu esquema con Pydantic y dejas que Ollama lo use como un esquema JSON.


Ejemplo 2: Extraer Metadatos de Características de LLM

Supongamos que tienes un fragmento de texto que describe las capacidades de un LLM:

“Qwen3 tiene un fuerte soporte multilingüe (inglés, chino, francés, español, árabe). Permite pasos de razonamiento (cadena de pensamiento). La ventana de contexto es de 128K tokens.”

Quieres datos estructurados:

from pydantic import BaseModel
from typing import List
from ollama import chat

class LLMFeatures(BaseModel):
    name: str
    supports_thinking: bool
    max_context_tokens: int
    languages: List[str]

prompt = """
Analiza la siguiente descripción y devuelve las características del modelo solo en JSON.
Descripción del modelo:
'Qwen3 tiene un fuerte soporte multilingüe (inglés, chino, francés, español, árabe).
Permite pasos de razonamiento (cadena de pensamiento).
La ventana de contexto es de 128K tokens.'
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format=LLMFeatures.model_json_schema(),
    options={"temperature": 0},
)

print(resp.message.content)

Salida posible:

{
  "name": "Qwen3",
  "supports_thinking": true,
  "max_context_tokens": 128000,
  "languages": ["Inglés", "Chino", "Francés", "Español", "Árabe"]
}

Ejemplo 3: Comparar Múltiples Modelos

Introduce descripciones de múltiples modelos y extráelas en formato estructurado:

from typing import List

class ModelComparison(BaseModel):
    models: List[LLMFeatures]

prompt = """
Extrae las características de cada modelo a JSON.

1. Llama 3.1 admite razonamiento. La ventana de contexto es de 128K. Idiomas: solo inglés.
2. GPT-4 Turbo admite razonamiento. La ventana de contexto es de 128K. Idiomas: inglés, japonés.
3. Qwen3 admite razonamiento. La ventana de contexto es de 128K. Idiomas: inglés, chino, francés, español, árabe.
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format=ModelComparison.model_json_schema(),
    options={"temperature": 0},
)

print(resp.message.content)

Salida:

{
  "models": [
    {
      "name": "Llama 3.1",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["Inglés"]
    },
    {
      "name": "GPT-4 Turbo",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["Inglés", "Japonés"]
    },
    {
      "name": "Qwen3",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["Inglés", "Chino", "Francés", "Español", "Árabe"]
    }
  ]
}

Esto hace que sea trivial realizar benchmarks, visualizar o filtrar modelos por sus características.


Ejemplo 4: Detectar Fallos Automáticamente

Incluso puedes permitir valores null cuando falta un campo:

from typing import Optional

class FlexibleLLMFeatures(BaseModel):
    name: str
    supports_thinking: Optional[bool]
    max_context_tokens: Optional[int]
    languages: Optional[List[str]]

Esto asegura que tu esquema siga siendo válido incluso si alguna información es desconocida.


Beneficios, Consideraciones y Mejores Prácticas

El uso de salida estructurada a través de Ollama (o cualquier sistema que lo soporte) ofrece muchas ventajas, pero también tiene algunas consideraciones.

Beneficios

  • Garantías más fuertes: Se le pide al modelo que cumpla con un esquema JSON en lugar de texto libre.
  • Análisis más fácil: Puedes usar directamente json.loads o validar con Pydantic / Zod, en lugar de regex o heurísticas.
  • Evolución basada en esquema: Puedes versionar tu esquema, agregar campos (con valores predeterminados) y mantener la compatibilidad hacia atrás.
  • Interoperabilidad: Los sistemas aguas abajo esperan datos estructurados.
  • Determinismo (mejor con baja temperatura): Cuando la temperatura es baja (ej. 0), el modelo es más propenso a adherirse rígidamente al esquema. La documentación de Ollama recomienda esto.

Consideraciones y Problemas

  • Desajuste de esquema: El modelo aún podría desviarse; por ejemplo, omitir una propiedad requerida, reordenar claves o incluir campos extra. Necesitas validación.
  • Esquemas complejos: Los esquemas JSON muy profundos o recursivos podrían confundir al modelo o provocar fallos.
  • Ambigüedad en el prompt: Si tu prompt es vago, el modelo podría adivinar incorrectamente los campos o las unidades.
  • Inconsistencia entre modelos: Algunos modelos pueden ser mejores o peores al cumplir con las restricciones estructuradas.
  • Límites de tokens: El esquema en sí mismo añade costo de tokens al prompt o a la llamada a la API.

Mejores Prácticas y Consejos (basados en el blog de Ollama + experiencia)

  • Usa Pydantic (Python) o Zod (JavaScript) para definir tus esquemas y generar automáticamente esquemas JSON. Esto evita errores manuales.
  • Incluye siempre instrucciones como “responde solo en JSON” o “no incluyas comentarios o texto extra” en tu prompt.
  • Usa temperatura = 0 (o muy baja) para minimizar la aleatoriedad y maximizar la adherencia al esquema. Ollama recomienda el determinismo.
  • Valida y posiblemente ten un plan de contingencia (ej. reintento o limpieza) cuando falle el análisis de JSON o la validación del esquema.
  • Comienza con un esquema más simple y luego extiéndelo gradualmente. No lo compliques demasiado al principio.
  • Incluye instrucciones de error útiles pero restringidas: por ejemplo, si el modelo no puede completar un campo requerido, responde con null en lugar de omitirlo (si tu esquema lo permite).

Ejemplo en Go 1: Extrayendo Características de LLM

Aquí hay un programa simple en Go que le pide a Qwen3 una salida estructurada sobre las características de un LLM.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"github.com/ollama/ollama/api"
)

type LLMFeatures struct {
	Name             string   `json:"name"`
	SupportsThinking bool     `json:"supports_thinking"`
	MaxContextTokens int      `json:"max_context_tokens"`
	Languages        []string `json:"languages"`
}

func main() {
	client, err := api.ClientFromEnvironment()
	if err != nil {
		log.Fatal(err)
	}

	prompt := `
  Analiza la siguiente descripción y devuelve las características del modelo solo en JSON.
  Descripción:
  "Qwen3 tiene un fuerte soporte multilingüe (inglés, chino, francés, español, árabe).
  Permite pasos de razonamiento (cadena de pensamiento).
  La ventana de contexto es de 128K tokens."
  `

	// Define el esquema JSON para la salida estructurada
	formatSchema := map[string]any{
		"type": "object",
		"properties": map[string]any{
			"name": map[string]string{
				"type": "string",
			},
			"supports_thinking": map[string]string{
				"type": "boolean",
			},
			"max_context_tokens": map[string]string{
				"type": "integer",
			},
			"languages": map[string]any{
				"type": "array",
				"items": map[string]string{
					"type": "string",
				},
			},
		},
		"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
	}

	// Convertir esquema a JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("Error al serializar el esquema de formato:", err)
	}

	req := &api.GenerateRequest{
		Model:   "qwen3:8b",
		Prompt:  prompt,
		Format:  formatJSON,
		Options: map[string]any{"temperature": 0},
	}

	var features LLMFeatures
	var rawResponse string
	err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
		// Acumular contenido mientras se transmite
		rawResponse += response.Response

		// Analizar solo cuando la respuesta esté completa
		if response.Done {
			if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
				return fmt.Errorf("error de análisis JSON: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Estructura analizada: %+v\n", features)
}

Para compilar y ejecutar este ejemplo de programa Go, asumamos que tenemos este archivo main.go en una carpeta ollama-struct, Necesitamos ejecutar dentro de esta carpeta:

# inicializar módulo
go mod init ollama-struct
# obtener todas las dependencias
go mod tidy
# compilar y ejecutar
go build -o ollama-struct main.go
./ollama-struct

Salida de Ejemplo

Estructura analizada: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[Inglés Chino Francés Español Árabe]}

Ejemplo en Go 2: Comparando Múltiples Modelos

Puedes extender esto para extraer una lista de modelos para comparación.

  type ModelComparison struct {
		Models []LLMFeatures `json:"models"`
	}

	prompt = `
	Extrae características de las siguientes descripciones de modelos y devuelve como JSON:

	1. PaLM 2: Este modelo tiene capacidades de razonamiento limitadas y se centra en la comprensión básica del lenguaje. Soporta una ventana de contexto de 8,000 tokens. Soporta principalmente solo el idioma inglés.
	2. LLaMA 2: Este modelo tiene habilidades de razonamiento moderadas y puede manejar algunas tareas lógicas. Puede procesar hasta 4,000 tokens en su contexto. Soporta los idiomas inglés, español e italiano.
	3. Codex: Este modelo tiene fuertes capacidades de razonamiento específicamente para programación y análisis de código. Tiene una ventana de contexto de 16,000 tokens. Soporta los idiomas inglés, Python, JavaScript y Java.

	Devuelve un objeto JSON con un array "models" que contenga todos los modelos.
	`

	// Define el esquema JSON para la comparación de modelos
	comparisonSchema := map[string]any{
		"type": "object",
		"properties": map[string]any{
			"models": map[string]any{
				"type": "array",
				"items": map[string]any{
					"type": "object",
					"properties": map[string]any{
						"name": map[string]string{
							"type": "string",
						},
						"supports_thinking": map[string]string{
							"type": "boolean",
						},
						"max_context_tokens": map[string]string{
							"type": "integer",
						},
						"languages": map[string]any{
							"type": "array",
							"items": map[string]string{
								"type": "string",
							},
						},
					},
					"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
				},
			},
		},
		"required": []string{"models"},
	}

	// Convertir esquema a JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("Error al serializar el esquema de comparación:", err)
	}

	req = &api.GenerateRequest{
		Model:   "qwen3:8b",
		Prompt:  prompt,
		Format:  comparisonFormatJSON,
		Options: map[string]any{"temperature": 0},
	}

	var comp ModelComparison
	var comparisonResponse string
	err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
		// Acumular contenido mientras se transmite
		comparisonResponse += response.Response

		// Analizar solo cuando la respuesta esté completa
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("error de análisis JSON: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, m := range comp.Models {
		fmt.Printf("%s: Contexto=%d, Idiomas=%v\n", m.Name, m.MaxContextTokens, m.Languages)
	}

Salida de Ejemplo

PaLM 2: Contexto=8000, Idiomas=[Inglés]
LLaMA 2: Contexto=4000, Idiomas=[Inglés Español Italiano]
Codex: Contexto=16000, Idiomas=[Inglés Python JavaScript Java]

Por cierto, qwen3:4b funciona bien en estos ejemplos, igual que qwen3:8b.

Mejores Prácticas para Desarrolladores de Go

  • Establece la temperatura en 0 para una máxima adherencia al esquema.
  • Valida con json.Unmarshal y ten un plan de contingencia si el análisis falla.
  • Mantén los esquemas simples: las estructuras JSON profundamente anidadas o recursivas pueden causar problemas.
  • Permite campos opcionales (usa omitempty en las etiquetas de la estructura Go) si esperas datos faltantes.
  • Agrega reintentos si el modelo ocasionalmente emite JSON inválido.

Ejemplo Completo: Dibujando un Gráfico con Especificaciones de LLM (Paso a paso: desde JSON estructurado hasta tablas de comparación)

llm-chart

  1. Define un esquema para los datos que deseas

Usa Pydantic para que puedas (a) generar un Esquema JSON para Ollama y (b) validar la respuesta del modelo.

from pydantic import BaseModel
from typing import List, Optional

class LLMFeatures(BaseModel):
    name: str
    supports_thinking: bool
    max_context_tokens: int
    languages: List[str]
  1. Pide a Ollama que devuelva solo JSON en esa forma

Pasa el esquema en format= y baja la temperatura para el determinismo.

from ollama import chat

prompt = """
Extrae características para cada modelo. Devuelve solo JSON que coincida con el esquema.
1) Qwen3 soporta cadena de pensamiento; contexto de 128K; inglés, chino, francés, español, árabe.
2) Llama 3.1 soporta cadena de pensamiento; contexto de 128K; inglés.
3) GPT-4 Turbo soporta cadena de pensamiento; contexto de 128K; inglés, japonés.
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format={"type": "array", "items": LLMFeatures.model_json_schema()},
    options={"temperature": 0}
)

raw_json = resp.message.content  # Lista JSON de LLMFeatures
  1. Valida y normaliza

Siempre valida antes de usar en producción.

from pydantic import TypeAdapter

adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json)  # -> list[LLMFeatures]
  1. Construye una tabla de comparación (pandas)

Convierte tus objetos validados en un DataFrame que puedas ordenar/filtrar y exportar.

import pandas as pd

df = pd.DataFrame([m.model_dump() for m in models])
df["languages_count"] = df["languages"].apply(len)
df["languages"] = df["languages"].apply(lambda xs: ", ".join(xs))

# Reordenar columnas para legibilidad
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# Guardar como CSV para uso posterior
df.to_csv("llm_feature_comparison.csv", index=False)
  1. (Opcional) Visualizaciones rápidas

Los gráficos simples te ayudan a identificar visualmente las diferencias entre modelos rápidamente.

import matplotlib.pyplot as plt

plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Ventana de Contexto Máxima por Modelo (tokens)")
plt.xlabel("Modelo")
plt.ylabel("Tokens de Contexto Máximo")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")

TL;DR

Con el nuevo soporte para salida estructurada de Ollama, puedes tratar a los LLMs no solo como chatbots, sino como motores de extracción de datos.

Los ejemplos anteriores mostraron cómo extraer automáticamente metadatos estructurados sobre características de LLM como soporte de pensamiento, tamaño de la ventana de contexto e idiomas soportados — tareas que de otra manera requerirían análisis frágil.

Ya sea que estés construyendo un catálogo de modelos LLM, un panel de evaluación o un asistente de investigación impulsado por IA, las salidas estructuradas hacen que la integración sea fluida, confiable y lista para producción.

Enlaces Útiles

Suscribirse

Recibe nuevas publicaciones sobre sistemas, infraestructura e ingeniería de IA.