Ir al contenido

Construyendo aplicaciones con IA generativa localmente

·1278 palabras·6 mins
Bases De Datos IA
Alejandro Duarte
Autor
Alejandro Duarte
Alejandro Duarte es Ingeniero de Software, escritor, Ingeniero de Relaciones con Desarrolladores en MariaDB y consultor en desarrollo de software. Ha programado computadores desde mediados de los 90s. Comenzando con BASIC, Alejandro transitó a C, C++ y Java durante sus años académicos en la Universidad Nacional de Colombia. Se mudó primero al Reino Unido y luego a Finlandia para profundizar su participación en la industria del código abierto. Alejandro es reconocido en círculos de Java y MariaDB.
Tabla de contenido

Como desarrolladores de aplicaciones, a menudo queremos ejecutar todo localmente. En nuestras máquinas de desarrollo. Incluyendo bases de datos y, más recientemente, modelos de IA como embedders y LLMs. Sabemos que podemos ejecutar fácilmente una base de datos como PostgreSQL o MariaDB de forma local incluso en un diminuto Raspberry Pi Zero. Sin embargo, un modelo de IA consume sustancialmente más recursos. Aun así, queremos ejecutarlo en nuestra máquina, incluso si es particularmente lento. Aún así, es simplemente satisfactorio.

Además, cuando ejecutamos algo localmente, obtenemos experiencia de primera mano en el uso de la tecnología y descubrimos algunos de sus aspectos más oscuros. Luego llega ese momento “¡ajá!” cuando finalmente nos damos cuenta de que no era tan oscuro como parecía cuando lo consumíamos desde un servicio en la nube. También están los beneficios obvios en ahorro de costos, seguridad y privacidad de los datos cuando ejecutamos servicios localmente.

En este artículo exploraremos una configuración básica para ejecutar incrustadores (embedders) localmente y almacenar los vectores generados en MariaDB. También cubriremos algunos términos básicos para aquellos que quieran ponerse al día con el desarrollo de aplicaciones de IA generativa.

¿Qué es un incrustador?
#

Un modelo de incrustación (o incrustador) es un tipo de modelo de IA. Convierte texto, imágenes o sonido en números. Más precisamente, convierte datos en vectores. Un vector es una secuencia de números. Por ejemplo:

[0.134, -0.527, 0.762, -0.243, ... más números ... , 0.418]

Un vector captura el significado semántico de un determinado dato. En este contexto, lo llamamos incrustación vectorial o simplemente incrustación. Siempre está asociado a un dato. Tal vez el dato original utilizado para calcular el vector. O quizás a un ID que apunta a datos relacionados en una base de datos. Cuando calculamos una incrustación (usando un incrustador), podemos almacenarla en una base de datos para mantener una representación numérica del dato. Esto es útil más adelante para recomendaciones personalizadas, búsqueda mejorada con IA y, en general, aplicaciones de RAG (Retrieval-Augmented Generation, más sobre esto más adelante).

MariaDB incluye el tipo de datos VECTOR para almacenar y manipular incrustaciones vectoriales. Para ilustrarlo, supongamos que tenemos una tabla productos como la siguiente:

CREATE TABLE productos(
	id INT PRIMARY KEY AUTO_INCREMENT,
	nombre VARCHAR(200) UNIQUE NOT NULL,
	descripcion VARCHAR(500)
);

Supongamos también que queremos sugerir productos relacionados en la página de cada producto. Primero, necesitamos almacenar los vectores de cada producto. Podemos usar la misma tabla o una diferente. Es una buena práctica crear una nueva tabla para este propósito, por ejemplo:

CREATE TABLE productos_incrustaciones(
	producto_id INT,
	incrustacion_llama VECTOR(2048) NOT NULL,
	FOREIGN KEY (producto_id) REFERENCES productos(id)
);

Con esto, un producto puede tener una incrustación asociada (incrustacion_llama). Usamos el nombre del incrustador en el nombre de la tabla o de la columna, en este artículo usaremos Llama. Esta también es una buena práctica, ya que las incrustaciones producidas por diferentes incrustadores no son compatibles y no podemos compararlas, algo que queremos hacer más adelante. 2048 es el número de dimensiones o números que tiene la incrustación o vector. Debemos especificar el número exacto de dimensiones que genera nuestro incrustador.

Generando incrustaciones
#

Antes de configurar nuestros servicios locales, veamos cómo generamos y utilizamos estas incrustaciones vectoriales. Un poco de pseudocódigo debería aclararlo:

detalles_del_producto = "Estos son los datos sobre algún producto. Tal vez el nombre + la descripción en una cadena de texto."
incrustacion_a_guardar = llamar_incrustador(data)
guardar_vector_en_bd(incrustacion_a_guardar)

Haremos esto para cada producto. Luego, podemos sugerir productos similares con algo como lo siguiente:

datos = "Nombre y descripción del producto para el cual queremos encontrar productos similares"
incrustacion_a_comparar = call_embedder(data)
productos = obtener_vectores_mas_cercanos_en_bd(incrustacion_a_comparar)
mostrar_productos_similares(products)

Aquí, obtener_vectores_mas_cercanos_en_bd(incrustacion_a_comparar) ejecutaría una consulta SQL como la siguiente:

SELECT p.id , p.nombre
FROM productos p
ORDER BY VEC_DISTANCE_COSINE(p.incrustacion_llama, ?)
LIMIT 5

Normalmente, nuestro framework de persistencia reemplaza ? con un valor real. Por ejemplo, podemos pasar el valor almacenado en la variable incrustacion_a_comparar a nuestro framework de persistencia. Podemos pasar el valor directamente en formato binario (por ejemplo, byte[] en Java) o en texto plano (en formato de arreglo JSON), en cuyo caso deberíamos usar la función VEC_FromText de MariaDB en el SELECT anterior.

VEC_DISTANCE_COSINE es la función de distancia. MariaDB tiene varias funciones vectoriales. Estamos utilizando la versión coseno, que es buena para búsquedas de similitud en texto. Observa que estamos ordenando los resultados según el resultado de esta función, lo que significa que obtendremos un conjunto de resultados donde las primeras filas contienen los vectores más cercanos al que acabamos de calcular (incrustacion_a_comparar). Y solo nos interesan los 5 primeros. Dado que “más cercano” significa “más similar”, estamos obteniendo los 5 productos más similares.

Ten en cuenta que podríamos tener millones o incluso miles de millones de productos, en cuyo caso necesitaremos un índice vectorial. Con un índice, la búsqueda es aproximada. Existen técnicas como la búsqueda híbrida para incluir resultados de coincidencia exacta, pero eso queda como tarea para otro día.

¡Muéstrame la configuración!
#

Ahora que hemos cubierto lo más básico sobre incrustaciones y búsqueda vectorial, configuremos nuestro entorno local para probar esto. Aquí tienes un archivo Docker Compose que inicia tanto una base de datos como un incrustador para que puedas probarlo en tu computadora sin necesidad de generar claves de API ni configurar variables de entorno del sistema:

services:
  mariadb:
    image: mariadb:11.7-rc
    container_name: mariadb
    environment:
      MARIADB_ROOT_PASSWORD: password
      MARIADB_DATABASE: demo
    ports:
      - "3306:3306"

  local-ai:
      image: localai/localai:master-ffmpeg-core
      container_name: local-ai
      command: bert-embeddings
      ports:
        - "8080:8080"
      volumes:
        - ./models:/build/models:cached

MariaDB estará escuchando en el puerto 3306 y el incrustador en el puerto 8080. Estamos utilizando algo llamado LocalAI. Existen alternativas como Ollama o vLLM, solo por mencionar dos. LocalAI es interesante porque es un reemplazo directo de la API REST ofrecida por OpenAI. Así que, cuando pasemos a producción, solo necesitaremos cambiar el endpoint (URL) y configurar las credenciales necesarias (clave de API). También podríamos implementar una configuración basada en LocalAI en la nube o en cualquier entorno con mayor capacidad de cómputo.

Si queremos agregar un LLM (o más bien un SLM si lo estamos ejecutando en una máquina de desarrollo), podemos añadir modelos adicionales de los cientos disponibles en la Galería de LocalAI. Por ejemplo, podemos agregar el modelo Phi-4 de Microsoft en nuestro archivo de Docker Compose de la siguiente manera:

...
      command: bert-embeddings phi-4
...

LocalAI descargará y ejecutará automáticamente los modelos de IA especificados.

Invocando el incrustador
#

Después de esto, podemos utilizar un framework de IA, como LangChain4j (Java), LangChain.js (JavaScript), LangChain (Python) o llama.cpp (C++). Alternativamente, podemos invocar directamente los endpoints de embeddings y chat. Por ejemplo, si queremos generar una incrustación para la cadena "tenis de correr para mujer ligeros", podemos usar este comando curl:

curl http://127.0.0.1:8080/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bert-embeddings",
    "input": "tenis de correr para mujer ligeros"
  }'

Después de esto, obtendremos una respuesta similar a esta:

{
  "object": "embedding",
  "model": "bert-embeddings",
  "data": [
    {
      "embedding": [
        0.1234, -0.5678, 0.9101, -0.1121, 0.3141, -0.5161, 0.7182, -0.9202,
        0.2233, -0.4252, 0.6273, -0.8293, 1.0313, -1.2333, 1.4353, -1.6373,
        0.8393, -0.0412, 0.2432, -0.4452, 0.6472, -0.8492, 1.0512, -1.2532,
        0.9552, -0.1572, 0.3592, -0.5612, 0.7632, -0.9652, 1.1672, -1.3692
		... más números ...
      ],
      "index": 0
    }
  ],
  "usage": {
    "prompt_tokens": 7,
    "total_tokens": 7
  }
}

Podemos extraer la parte que nos interesa usando una expresión JSON Path como $.data[0].embedding. Ese es el valor que queremos almacenar en la base de datos o enviar a la consulta SELECT para realizar la búsqueda de similitud. Los frameworks de IA hacen esto y mucho más por ti. ¡Úsalos!

Aprende más
#

Las herramientas y técnicas cubiertas aquí son útiles para desarrollar aplicaciones de Retrieval-Augmented Generation (RAG). Pero eso queda como tarea para otro día. Mientras tanto, aquí tienes un video (en Inglés) que lo demuestra:

¿Te gustó este artículo? Puedo ayudar a tu equipo a implementar soluciones similares. Contáctame para saber más.