Limitar LLMs con salida estructurada: Ollama, Qwen3 & Python o Go
Un par de formas de obtener salida estructurada de Ollama
Modelos de Lenguaje Grande (LLMs) son poderosos, pero en producción raramente queremos párrafos libres. En su lugar, queremos datos predecibles: atributos, hechos u objetos estructurados que puedas alimentar en una aplicación. Eso es Salida Estructurada de LLM.
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 desbloquea pipelines consistentes de extracción de datos para tareas como catalogar características de LLM, evaluar modelos o automatizar la integración del sistema.
En este post, cubriremos:
- ¿Qué es la salida estructurada y por qué importa?
- Una forma sencilla de obtener salida estructurada de LLM
- Cómo funciona la nueva característica de Ollama
- Ejemplos de extracción de capacidades de LLM:
¿Qué es la Salida Estructurada?
Normalmente, los LLM 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 salida estructurada le pedimos un esquema estricto:
{
"nombre": "Modelo X",
"admite_pensamiento": true,
"max_tokens_contexto": 200000,
"idiomas": ["Inglés", "Chino", "Español"]
}
Este JSON es fácil de validar, almacenar en una base de datos o alimentar a una interfaz de usuario.
Forma sencilla de obtener Salida Estructurada de LLM
A veces los LLM entienden qué es el esquema y podemos pedirle que devuelva la salida en JSON usando un esquema específico. El modelo Qwen3 de Alibaba está optimizado para razonamiento y respuestas estructuradas. Puedes instruirlo explícitamente para 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: { "nombre": string, "edad": int, "ciudad": 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:
{"nombre": "Elon Musk", "edad": 53, "ciudad": "Austin"}
Forzar la Validación del Esquema con Pydantic
Para evitar salidas malformadas, puedes validar contra un esquema Pydantic en Python.
from pydantic import BaseModel
class Persona(BaseModel):
nombre: str
edad: int
ciudad: str
# Supongamos que 'output' es la cadena JSON de Qwen3
data = Persona.model_validate_json(output)
print(data.nombre, data.edad, data.ciudad)
Esto asegura que la salida se ajuste a la estructura esperada.
Salida Estructurada de Ollama
Ahora Ollama te permite pasar un esquema en el parámetro format
. El modelo se ve restringido a responder solo en JSON que se ajuste al esquema (docs).
En Python, normalmente defines tu esquema con Pydantic y dejas que Ollama lo use como un esquema JSON.
Ejemplo 2: Extraer Metadatos de Capacidad 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):
nombre: str
admite_pensamiento: bool
max_tokens_contexto: int
idiomas: List[str]
prompt = """
Analiza la siguiente descripción y devuelve las características del modelo en JSON solo.
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:
{
"nombre": "Qwen3",
"admite_pensamiento": true,
"max_tokens_contexto": 128000,
"idiomas": ["Inglés", "Chino", "Francés", "Español", "Árabe"]
}
Ejemplo 3: Comparar Varios Modelos
Proporciona descripciones de varios modelos y extrae en forma estructurada:
from typing import List
class ComparacionModelos(BaseModel):
modelos: List[LLMFeatures]
prompt = """
Extrae las características de cada modelo en forma de JSON.
1. Llama 3.1 admite razonamiento. Ventana de contexto de 128K. Idiomas: solo inglés.
2. GPT-4 Turbo admite razonamiento. Ventana de contexto de 128K. Idiomas: inglés, japonés.
3. Qwen3 admite razonamiento. Ventana de contexto 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:
{
"modelos": [
{
"nombre": "Llama 3.1",
"admite_pensamiento": true,
"max_tokens_contexto": 128000,
"idiomas": ["Inglés"]
},
{
"nombre": "GPT-4 Turbo",
"admite_pensamiento": true,
"max_tokens_contexto": 128000,
"idiomas": ["Inglés", "Japonés"]
},
{
"nombre": "Qwen3",
"admite_pensamiento": true,
"max_tokens_contexto": 128000,
"idiomas": ["Inglés", "Chino", "Francés", "Español", "Árabe"]
}
]
}
Esto hace trivial evaluar, visualizar o filtrar modelos según sus características.
Ejemplo 4: Detectar Brechas Automáticamente
Incluso puedes permitir valores null
cuando un campo esté ausente:
from typing import Optional
class LLMFeaturesFlexibles(BaseModel):
nombre: str
admite_pensamiento: Optional[bool]
max_tokens_contexto: Optional[int]
idiomas: Optional[List[str]]
Esto asegura que tu esquema siga siendo válido incluso si falta cierta información.
Beneficios, Precauciones y Buenas Prácticas
Usar salida estructurada a través de Ollama (o cualquier sistema que lo admita) ofrece muchos beneficios — pero también tiene algunas precauciones.
Beneficios
- Garantías más fuertes: El modelo se le pide que se ajuste a 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 expresiones regulares 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 downstream esperan datos estructurados.
- Determinismo (mejor con temperatura baja): Cuando la temperatura es baja (por ejemplo, 0), el modelo es más propenso a adherirse estrictamente al esquema. Las documentaciones de Ollama recomiendan esto.
Precauciones y Peligros
- Incongruencia del esquema: El modelo podría aún desviarse — por ejemplo, omitir una propiedad requerida, reordenar claves o incluir campos extraños. Necesitas validación.
- Esquemas complejos: Esquemas JSON muy profundos o recursivos podrían confundir al modelo o causar errores.
- Ambigüedad en el prompt: Si tu prompt es vago, el modelo podría adivinar campos o unidades incorrectamente.
- Inconsistencia entre modelos: Algunos modelos pueden ser mejores o peores en cumplir con las restricciones estructuradas.
- Límites de tokens: El esquema en sí mismo agrega costo de token al prompt o a la llamada de API.
Buenas Prácticas y Consejos (tomados del blog de Ollama + experiencia)
- Usa Pydantic (Python) o Zod (JavaScript) para definir tus esquemas y generar automáticamente esquemas JSON. Esto evita errores manuales.
- Siempre incluye 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 determinismo.
- Valida y potencialmente recupera (por ejemplo, reintenta o limpia) cuando el análisis de JSON falle o la validación del esquema falle.
- Comienza con un esquema más simple, luego extiéndelo gradualmente. No lo compliques al principio.
- Incluye instrucciones de error útiles pero restringidas: por ejemplo, si el modelo no puede llenar un campo requerido, responda con
null
en lugar de omitirlo (si tu esquema lo permite).
Ejemplo 1 en Go: Extrayendo Características de LLM
Aquí hay un programa simple 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 {
Nombre string `json:"nombre"`
AdmitePensamiento bool `json:"admite_pensamiento"`
MaxTokensContexto int `json:"max_tokens_contexto"`
Idiomas []string `json:"idiomas"`
}
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 en JSON solo.
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{
"nombre": map[string]string{
"type": "string",
},
"admite_pensamiento": map[string]string{
"type": "boolean",
},
"max_tokens_contexto": map[string]string{
"type": "integer",
},
"idiomas": map[string]any{
"type": "array",
"items": map[string]string{
"type": "string",
},
},
},
"required": []string{"nombre", "admite_pensamiento", "max_tokens_contexto", "idiomas"},
}
// Convierte el esquema a JSON
formatJSON, err := json.Marshal(formatSchema)
if err != nil {
log.Fatal("Fallo al convertir 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 {
// Acumula contenido mientras se transmite
rawResponse += response.Response
// Solo analiza 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 — supongamos 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: {Nombre:Qwen3 AdmitePensamiento:true MaxTokensContexto:128000 Idiomas:[Inglés Chino Francés Español Árabe]}
Ejemplo 2 en Go: Comparando Varios Modelos
Puedes extender esto para extraer una lista de modelos para comparación.
type ComparacionModelos struct {
Modelos []LLMFeatures `json:"modelos"`
}
prompt = `
Extrae las características de las siguientes descripciones de modelos y devuélvelas como JSON:
1. PaLM 2: Este modelo tiene capacidades de razonamiento limitadas y se centra en el entendimiento básico del lenguaje. Soporta una ventana de contexto de 8,000 tokens. Principalmente soporta solo el idioma inglés.
2. LLaMA 2: Este modelo tiene capacidades 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 una "lista de modelos" 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{
"modelos": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"nombre": map[string]string{
"type": "string",
},
"admite_pensamiento": map[string]string{
"type": "boolean",
},
"max_tokens_contexto": map[string]string{
"type": "integer",
},
"idiomas": map[string]any{
"type": "array",
"items": map[string]string{
"type": "string",
},
},
},
"required": []string{"nombre", "admite_pensamiento", "max_tokens_context意图", "idiomas"},
},
},
},
"required": []string{"modelos"},
}
// Convierte el esquema a JSON
comparisonFormatJSON, err := json.Marshal(comparisonSchema)
if err != nil {
log.Fatal("Fallo al convertir el esquema de comparación:", err)
}
req = &api.GenerateRequest{
Model: "qwen3:8b",
Prompt: prompt,
Format: comparisonFormatJSON,
Options: map[string]any{"temperature": 0},
}
var comp ComparacionModelos
var comparisonResponse string
err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
// Acumula contenido mientras se transmite
comparisonResponse += response.Response
// Solo analiza 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.Modelos {
fmt.Printf("%s: Contexto=%d, Idiomas=%v\n", m.Nombre, m.MaxTokensContexto, m.Idiomas)
}
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 en estos ejemplos funciona bien, igual que qwen3:8b.
Buenas Prácticas para Desarrolladores en Go
- Establece la temperatura a 0 para la máxima adherencia al esquema.
- Valida con
json.Unmarshal
y recupera si falla el análisis. - Mantén los esquemas simples — estructuras JSON profundamente anidadas o recursivas pueden causar problemas.
- Permite campos opcionales (usa
omitempty
en las etiquetas de structs de Go) si esperas datos faltantes. - Agrega reintentos si el modelo emite ocasionalmente JSON inválido.
Ejemplo completo - Dibujar un gráfico con especificaciones de LLM (paso a paso: desde JSON estructurado a tablas de comparación)
- Define un esquema para los datos que deseas
Usa Pydantic para poder (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):
nombre: str
admite_pensamiento: bool
max_tokens_contexto: int
idiomas: List[str]
- Pide a Ollama que devuelva solo JSON en ese formato
Pasa el esquema en format=
y reduce la temperatura para determinismo.
from ollama import chat
prompt = """
Extrae las características de cada modelo. Devuelve solo JSON que coincida con el esquema.
1) Qwen3 admite cadena de pensamiento; 128K contexto; Inglés, Chino, Francés, Español, Árabe.
2) Llama 3.1 admite cadena de pensamiento; 128K contexto; Inglés.
3) GPT-4 Turbo admite cadena de pensamiento; 128K contexto; 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
- Valida y normaliza
Siempre valida antes de usar en producción.
from pydantic import TypeAdapter
adapter = TypeAdapter(list[LLMFeatures])
modelos = adapter.validate_json(raw_json) # -> lista[LLMFeatures]
- 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 modelos])
df["idiomas_count"] = df["idiomas"].apply(len)
df["idiomas"] = df["idiomas"].apply(lambda xs: ", ".join(xs))
# Reordenar columnas para mejor legibilidad
df = df[["nombre", "admite_pensamiento", "max_tokens_contexto", "idiomas_count", "idiomas"]]
# Guardar como CSV para uso posterior
df.to_csv("comparacion_caracteristicas_llm.csv", index=False)
- (Opcional) Gráficos rápidos
Gráficos simples te ayudan a ver rápidamente las diferencias entre modelos.
import matplotlib.pyplot as plt
plt.figure()
plt.bar(df["nombre"], df["max_tokens_contexto"])
plt.title("Ventana de contexto máxima por modelo (tokens)")
plt.xlabel("Modelo")
plt.ylabel("Tokens máximos de contexto")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("ventana_contexto_maxima.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 ventana de contexto y idiomas admitidos — tareas que de otro modo requerirían análisis frágil.
Ya sea que estés construyendo un catálogo de modelos LLM, un tablero de evaluación o un asistente de investigación impulsado por IA, las salidas estructuradas hacen la integración suave, confiable y lista para producción.
Enlaces útiles
- https://ollama.com/blog/structured-outputs
- Hoja de trucos de Ollama
- Hoja de trucos de Python
- AWS SAM + AWS SQS + Python PowerTools
- Hoja de trucos de Golang
- Comparando ORMs de Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Reclasificación de documentos de texto con Ollama y modelo de embedding Qwen3 - en Go
- Rendimiento de AWS lambda: JavaScript vs Python vs Golang