Asistente personal de IA para tus datos. Parte 1: Vector ChromaDB + DeepSeek | GPT

¡Hola a todos! Hoy me gustaría abordar un tema de interés para muchos: la conexión de un modelo de lenguaje extenso como DeepSeek o ChatGPT con su base de conocimiento. En este artículo, les explicaré detalladamente los principios de las bases de datos vectoriales y por qué pueden utilizarse para conectar su base de conocimiento con redes neuronales extensas ya preparadas. Como ejemplo, consideremos consultar la documentación de Amverum Cloud. Amverum Cloud es una alternativa a Heroku más económica y fácil de implementar, con dominios gratuitos, almacenamiento persistente incluido y la posibilidad de actualizar proyectos mediante Git Push Amverum Master. Actualmente estamos desarrollando activamente un agente de IA que ayudará a los usuarios a implementar proyectos en la nube, eliminando errores de código y configuración, y simplificando el trabajo con la documentación. La búsqueda en la documentación es un excelente ejemplo de cómo usar una base de datos vectorial, como Chroma. Analicemos inmediatamente un determinado bloque limitante Las redes neuronales preconfiguradas, como DeepSeek, ChatGPT o Claude, están diseñadas inicialmente para recibir datos de texto con un contexto específico (indicación) como entrada y, según sus parámetros y entrenamiento, procesarán la solicitud, realizando la tarea necesaria. Es decir, dado que trabajaremos con modelos de lenguaje preconfigurados, no podremos desviarnos del proceso de preparar indicaciones con un contexto específico y enviar solicitudes a las redes neuronales preconfiguradas. Esto nos lleva a una tarea lógica: cómo vincular nuestra base de datos con el proceso de comunicación con la red neuronal. Imaginemos miles de páginas de literatura profesional. Es improbable que convirtamos cientos de megabytes de datos en una sola indicación gigante y, al mismo tiempo, esperemos que la red neuronal acepte trabajar con ella. Como mínimo, recibiremos un error indicando que se ha excedido el contexto transmitido. Es lógico que la solución sugiera formular la solicitud, completándola únicamente con el contexto necesario para esta solicitud específica, pero ¿cómo hacerlo? La opción más sencilla es organizar algún tipo de motor de búsqueda SEO. Enviamos, por ejemplo, "entrenamiento de redes neuronales" y se realiza una búsqueda en nuestros gigabytes de datos, pero aquí surge un problema lógico. Incluso si encontramos información, digamos 1000 veces la frase "entrenamiento de redes neuronales", ¿cómo podemos destacar este contexto? ¿Cómo decidiremos qué es importante transmitir a la red neuronal para la comunicación y qué no? Claro que se puede usar este motor de búsqueda, pero, como comprenderá, este enfoque no es del todo óptimo, lo que significa que es necesario buscar otra solución más flexible y eficaz, que analizaremos en detalle hoy. Bases de datos vectoriales Sé que probablemente no hayas oído hablar de estas bases de datos o solo las conozcas superficialmente. Por lo tanto, a continuación, explicaré en detalle y de forma práctica qué tipo de bases de datos son y, lo más importante, entenderemos cómo nos ayudarán en nuestra tarea. En resumen, una base de datos vectorial es una representación de datos: texto o byte en forma numérica o, más correctamente, en forma de vectores. Por ejemplo, la palabra "hola" en representación vectorial se vería así: -0.012, 0.124, -0.056, 0.203, …, 0.078. En el contexto actual, no importa mucho cómo se obtienen estos números matemáticamente. Ahora solo es importante una comprensión general de la representación digital de los datos. Beneficios prácticos Llegados a este punto, es natural que digas: "Todo esto es pura diversión, pero ¿cuál es la utilidad práctica de todo esto y cómo se obtienen estos números tan extraños?". Vamos a averiguarlo. Lo primero que hay que entender es que las redes neuronales no pueden leer tus textos ni ver tus fotos de forma humana. Por ejemplo, al enviar una solicitud ChatGPT, ocurre lo siguiente: La red neuronal recibe texto. Transforma este texto en vectores o, más correctamente, en incrustaciones. Utiliza estas incrustaciones para generar una respuesta basada en los parámetros entrenados del modelo. Realiza los cálculos. Recibe una respuesta en forma de incrustación. Transforma la incrustación en texto humano (lenguaje natural). Te envía texto humano. Es decir, ya en esta etapa está claro que en la comunicación con redes neuronales aparece una representación numérica de la información entrante (representación en forma de incrustaciones), y ahora veamos por qué es necesaria. Incrustaciones y por qué son necesarias Las incrustaciones son "huellas digitales del significado" de un texto. Analicemos esta afirmación con un ejemplo real. Imagina que tienes una gran cantidad de documentos (libros, artículos, notas) y quieres encontrar información en ellos no por las palabras exactas, sino por su significado. Cómo implementar esto: La red neuronal

May 6, 2025 - 07:10
 0
Asistente personal de IA para tus datos. Parte 1: Vector ChromaDB + DeepSeek | GPT

Semantic Search with Open-Source ChromaDB!

¡Hola a todos! Hoy me gustaría abordar un tema de interés para muchos: la conexión de un modelo de lenguaje extenso como DeepSeek o ChatGPT con su base de conocimiento.

En este artículo, les explicaré detalladamente los principios de las bases de datos vectoriales y por qué pueden utilizarse para conectar su base de conocimiento con redes neuronales extensas ya preparadas.

Como ejemplo, consideremos consultar la documentación de Amverum Cloud. Amverum Cloud es una alternativa a Heroku más económica y fácil de implementar, con dominios gratuitos, almacenamiento persistente incluido y la posibilidad de actualizar proyectos mediante Git Push Amverum Master.

Actualmente estamos desarrollando activamente un agente de IA que ayudará a los usuarios a implementar proyectos en la nube, eliminando errores de código y configuración, y simplificando el trabajo con la documentación. La búsqueda en la documentación es un excelente ejemplo de cómo usar una base de datos vectorial, como Chroma.

Analicemos inmediatamente un determinado bloque limitante

Las redes neuronales preconfiguradas, como DeepSeek, ChatGPT o Claude, están diseñadas inicialmente para recibir datos de texto con un contexto específico (indicación) como entrada y, según sus parámetros y entrenamiento, procesarán la solicitud, realizando la tarea necesaria.

Es decir, dado que trabajaremos con modelos de lenguaje preconfigurados, no podremos desviarnos del proceso de preparar indicaciones con un contexto específico y enviar solicitudes a las redes neuronales preconfiguradas.

Esto nos lleva a una tarea lógica: cómo vincular nuestra base de datos con el proceso de comunicación con la red neuronal. Imaginemos miles de páginas de literatura profesional. Es improbable que convirtamos cientos de megabytes de datos en una sola indicación gigante y, al mismo tiempo, esperemos que la red neuronal acepte trabajar con ella. Como mínimo, recibiremos un error indicando que se ha excedido el contexto transmitido.

Es lógico que la solución sugiera formular la solicitud, completándola únicamente con el contexto necesario para esta solicitud específica, pero ¿cómo hacerlo? La opción más sencilla es organizar algún tipo de motor de búsqueda SEO. Enviamos, por ejemplo, "entrenamiento de redes neuronales" y se realiza una búsqueda en nuestros gigabytes de datos, pero aquí surge un problema lógico. Incluso si encontramos información, digamos 1000 veces la frase "entrenamiento de redes neuronales", ¿cómo podemos destacar este contexto? ¿Cómo decidiremos qué es importante transmitir a la red neuronal para la comunicación y qué no? Claro que se puede usar este motor de búsqueda, pero, como comprenderá, este enfoque no es del todo óptimo, lo que significa que es necesario buscar otra solución más flexible y eficaz, que analizaremos en detalle hoy.

Bases de datos vectoriales

Sé que probablemente no hayas oído hablar de estas bases de datos o solo las conozcas superficialmente. Por lo tanto, a continuación, explicaré en detalle y de forma práctica qué tipo de bases de datos son y, lo más importante, entenderemos cómo nos ayudarán en nuestra tarea.

En resumen, una base de datos vectorial es una representación de datos: texto o byte en forma numérica o, más correctamente, en forma de vectores.

Por ejemplo, la palabra "hola" en representación vectorial se vería así: -0.012, 0.124, -0.056, 0.203, …, 0.078.
En el contexto actual, no importa mucho cómo se obtienen estos números matemáticamente. Ahora solo es importante una comprensión general de la representación digital de los datos.

Beneficios prácticos

Llegados a este punto, es natural que digas: "Todo esto es pura diversión, pero ¿cuál es la utilidad práctica de todo esto y cómo se obtienen estos números tan extraños?". Vamos a averiguarlo.

Lo primero que hay que entender es que las redes neuronales no pueden leer tus textos ni ver tus fotos de forma humana. Por ejemplo, al enviar una solicitud ChatGPT, ocurre lo siguiente:

La red neuronal recibe texto.

Transforma este texto en vectores o, más correctamente, en incrustaciones.

Utiliza estas incrustaciones para generar una respuesta basada en los parámetros entrenados del modelo.

Realiza los cálculos.

Recibe una respuesta en forma de incrustación.

Transforma la incrustación en texto humano (lenguaje natural).

Te envía texto humano.

Es decir, ya en esta etapa está claro que en la comunicación con redes neuronales aparece una representación numérica de la información entrante (representación en forma de incrustaciones), y ahora veamos por qué es necesaria.

Incrustaciones y por qué son necesarias

Las incrustaciones son "huellas digitales del significado" de un texto. Analicemos esta afirmación con un ejemplo real.

Imagina que tienes una gran cantidad de documentos (libros, artículos, notas) y quieres encontrar información en ellos no por las palabras exactas, sino por su significado.

Cómo implementar esto:

La red neuronal acepta todas las palabras que has preparado y las convierte en secuencias de números (incrustaciones).

Ejemplo:
"Gato" → [0.2, -0.5, 0.7, …]
"Gatito" → 0.19, -0.48, 0.69, ….

Una base de datos vectorial, como Chroma, almacena estos números y puede:

Buscar vectores con un significado similar (incluso si las palabras son diferentes).

Responder consultas como: "Buscar algo sobre mascotas con bigote" → mostrará documentos sobre gatos y gatitos.

Analogía simple:

Búsqueda normal (como Ctrl+F): busca coincidencias exactas de palabras.

La consulta "coche" no encontrará "coche".

Búsqueda por incrustaciones: busca significados similares.

"Coche", "coche", "transporte" estarán uno al lado del otro en números, y la base de datos los conectará.

¿Por qué es esto necesario?

Los chatbots (como ChatGPT) usan incrustaciones para interpretar las consultas por significado, no por coincidencias exactas.

Puedes preguntar un documento con tus propias palabras y el sistema lo entenderá, incluso si no hay coincidencias exactas.

Resultado: Las incrustaciones son un "traductor" de texto a números, por lo que puedes buscar por significado, no por letras. Las bases de datos vectoriales son su "almacenamiento", donde la búsqueda funciona como un imán para ideas similares.

Existen redes neuronales especiales que pueden transformar tus datos de texto en una representación vectorial (numérica). Para estas tareas, no es necesario recurrir a gigantes como ChatGPT o DeepSeek, sino que bastará con redes neuronales especializadas. Además, en la parte práctica, demostraré la creación de incrustaciones y la implementación de la búsqueda inteligente utilizando el ejemplo de la red neuronal paraphrase-multilingual-MiniLM-L12-v2.

Esta red neuronal pesa solo 500 MB y se ejecuta localmente incluso en ordenadores promedio, por lo que es ideal para nuestras tareas.

Bases de datos vectoriales (continuación)

A estas alturas, ya debería comprender que, al trabajar con datos en representación numérica, se aplica la siguiente conexión:

Los datos de texto se transforman en una representación vectorial (numérica) mediante una red neuronal especial. A continuación, para que la búsqueda inteligente funcione, necesitamos:

Obtener la incrustación de la matriz de datos en representación vectorial.

Escribir una consulta de texto como "encontrar todo lo que se lleva en la mano".

Transformar la consulta en representación vectorial.

Comparar el vector de la consulta con los vectores de la incrustación grande.

Transformar el resultado encontrado de nuevo en lenguaje humano.

Proporcionar el resultado.

Ahora surge una pregunta lógica sobre el almacenamiento de estos datos numéricos. Es evidente que podemos tomar el texto y luego transformarlo en vectores utilizando una paráfrasis multilingüe MiniLM-L12-v2, aún poco clara, pero ¿qué ocurre con el almacenamiento?

Este puede almacenarse en una base de datos especializada, como Chroma, con la que trabajaremos hoy, o en RAM.

El principio del almacenamiento en RAM:

Preparar datos de texto.

Transformarlos en incrustaciones.

Enviar una solicitud de incrustación.

Una vez finalizada la ejecución del script, se borra la memoria y, al reiniciar, es necesario generar la incrustación repetidamente.

El principio del almacenamiento en una base de datos especializada:

Preparar datos de texto.

Transformarlos en incrustaciones.

Guardar la incrustación en el formato de la base de datos.

Al trabajar, conéctese a la base de datos y utilice la incrustación ya creada.

Las bases de datos vectoriales pueden ser locales (soluciones autoalojadas) o en la nube.

La base de datos local más sencilla es ChromaDB, con la que trabajaremos hoy. La elegí por su simplicidad y excelente integración con el lenguaje de programación Python.

Otras bases de datos vectoriales locales: Qdrant, Weaviate, Milvus, entre otras.

Aquí hay algunos ejemplos de bases de datos en la nube: Pinecone, RedisVL, Qdrant Cloud, entre otras.

¿Qué vamos a hacer hoy?

Habrá más teoría más adelante, pero seguro que están esperando la parte práctica. Así que, además de absorber la información teórica, veamos qué haremos en la práctica.

A continuación, les mostraré, con un ejemplo práctico, cómo transformar datos de texto en una base de datos vectorial de ChromaDB. Primero, crearemos una base de datos con un ejemplo sencillo y trabajaremos con búsquedas en ella, y luego trabajaremos con un ejemplo más complejo, sobre cuya base crearemos nuestro futuro asistente inteligente.

Después de comprender completamente cómo funcionan la generación de incrustaciones y la búsqueda inteligente, conectaremos las grandes redes neuronales DeepSeek y GPT al contexto general, implementando la siguiente lógica:

El usuario envía una solicitud específica.

Mediante la búsqueda inteligente, extraemos contenido adicional de nuestra base de datos vectorial.

Reenviaremos la solicitud del usuario junto con nuestros resultados de búsqueda inteligente a DeepSeek/GPT y recibiremos una respuesta de la red neuronal basada en nuestro contexto personal.

¡Será interesante!

Base de datos vectorial simple

Ahora, para consolidar el bloque teórico general descrito anteriormente, prepararemos una base de datos ChromaDB simple basada en la descripción de los productos de una tienda online ficticia (para el ejemplo más simple, este es un ejemplo visual).

Imaginemos que tenemos un array de productos de este tipo:

SHOP_DATA = [
    {
        "text": 'Portátil Lenovo IdeaPad 5: 16 Gb RAM, SSD 512 Gb, LED 15.6", precio 550 $.',
        "metadata": {
            "id": "1",
            "type": "product",
            "category": "laptops",
            "price": 55000,
            "stock": 3,
        },
    },
    {
        "text": 'Smartphone Xiaomi Redmi Note 12: 128 Gb RAM, SSD 512 Gb, LED 15.6", precio 1550 $.',
        "metadata": {
            "id": "2",
            "type": "product",
            "category": "phones",
            "price": 18000,
            "stock": 10,
        },
    },
    # ...
]

Como puede ver, la información se presenta como una lista de diccionarios, cada uno con dos claves principales: texto y metadatos.

Documentos y metadatos en bases de datos vectoriales

Este es un punto importante que conviene analizar con más detalle. Debe comprender que, en la base de datos ChromaDB, nuestra búsqueda inteligente funcionará de forma predeterminada solo con el valor de la clave de texto (el nombre puede ser cualquiera).

En el contexto de las bases de datos vectoriales, se distinguen conceptos como documentos (en nuestro caso, el texto que describe un producto específico) y metadatos.

Restricciones de longitud de los documentos

Al preparar los datos de texto para su carga en una base de datos vectorial, es fundamental tener en cuenta las restricciones de longitud del texto establecidas por el modelo que los convertirá en una representación vectorial. Estas restricciones dependen de la red neuronal específica y suelen oscilar entre 256 y 512 tokens. Por lo tanto, si, por ejemplo, planea agregar un libro completo de 200 páginas a la base de datos, primero debe descomponerse en fragmentos más pequeños, cada uno con un máximo de 256 a 512 tokens. Esto garantizará que el modelo procese correctamente el texto y preserve la integridad semántica en el espacio vectorial.

El modelo paraphrase-multilingual-MiniLM-L12-v2 con el que trabajaremos hoy tiene un límite de 512 tokens por documento. Esto equivale aproximadamente a 250-350 palabras o 1500-2200 caracteres. Sin embargo, dado que utilizaremos nuestros datos con las potentes redes neuronales Deepseek y ChatGPT, reduciremos el tamaño del documento para evitar la sobrecarga de contexto.

El rol de los metadatos

Los metadatos son nuestro asistente, lo que hace que la búsqueda sea más precisa y local. Aquí tiene un ejemplo sencillo:

Imaginemos que su tienda en línea vende computadoras portátiles, teléfonos inteligentes y otros equipos. Puede limitar la búsqueda inteligente por metadatos de categoría. Independientemente de si utiliza metadatos para la búsqueda explícitamente, es importante comprender que los documentos encontrados (resultados de la búsqueda) siempre contendrán estas metaetiquetas, lo que le permitirá realizar un procesamiento adicional.

Ejemplo con libros:

Imagine que decide cargar toda la serie de libros de Harry Potter en una base de datos vectorial. Divide el texto en fragmentos de 512 tokens y especifica las siguientes metaetiquetas: título del libro, número de página, número de capítulo y otra información.

A continuación, envía una consulta como "Cuando Harry Potter aprendió sobre los Horrocruxes". Su búsqueda inteligente devuelve un número determinado de resultados: documentos con metaetiquetas que le permiten comprender de qué libro proviene y en qué página se encuentra la información.

Otro ejemplo de trabajo con metaetiquetas es limitar la búsqueda a un libro, una serie de libros o un autor específico al realizar una consulta.

Recuerde que la búsqueda inteligente no está relacionada con los metadatos de ninguna manera, a menos que especifique explícitamente la lógica para procesarlos en la consulta.

Uso práctico de los metadatos
En el contexto de los metadatos para nuestros productos:

{
    "id": "1",
    "type": "product",
    "category": "laptops",
    "price": 55000,
    "stock": 3,
}

Pasamos el ID del producto, el tipo, la categoría, el precio y el stock. Por ejemplo, se podría implementar una búsqueda inteligente de productos en el sitio web y solo los productos con el ID encontrado se mostrarían al usuario en la página de resultados.

Por lo tanto, una base de datos vectorial es una herramienta excelente no solo para la integración con redes neuronales, sino también para resolver problemas prácticos, como un motor de búsqueda inteligente en un sitio web o en la API.

Creación de una base de datos vectorial en Python

Ahora escribamos código. El código será sencillo, ya que no necesitamos dividir documentos (descripciones de productos) en fragmentos ni aplicar lógica adicional para analizar y procesar productos.

Preparación del proyecto

Lo primero que hacemos es crear un nuevo proyecto de Python con un entorno virtual dedicado. Añadimos un archivo requirements.txt al proyecto con el siguiente contenido:

langchain-huggingface==0.1.2
torch==2.6.0
loguru==0.7.3
chromadb==0.6.3
sentence-transformers==3.4.1
langchain-chroma==0.2.2
pydantic-settings==2.8.1
langchain-text-splitters==0.3.7
langchain-deepseek==0.1.3
langchain-openai==0.3.11

¿Qué es Langchain?
Quizás hayas notado que he encontrado la palabra Langchain en dependencias hasta 5 veces, lo cual no es casualidad.

Langchain es una potente herramienta de Python que permite integrar una gran cantidad de redes neuronales y otras herramientas útiles en tu proyecto de forma inmediata. Este framework merece ser mencionado en más de un artículo, por lo que te recomiendo encarecidamente que leas la documentación del proyecto (más de 105 000 estrellas en GitHub deberían indicar que el proyecto merece atención).

Build context-aware reasoning applications!

En el proyecto de hoy, utilizaremos las siguientes herramientas de este framework:

langchain-text-splitters==0.3.7

Una herramienta para dividir textos extensos en fragmentos más pequeños (trozos). Contiene diversas estrategias de segmentación: por símbolos, oraciones, tokens y bloques semánticos. Es necesaria para procesar documentos extensos cuando superan la ventana de contexto de los modelos de lenguaje (256-512 tokens en nuestro caso).

langchain-deepseek==0.1.3

Integración con los modelos de IA de DeepSeek. Permite utilizar los potentes modelos de lenguaje de DeepSeek para el análisis y la generación de texto (hoy la demostración se basará en el modelo deepseek-chat).

langchain-openai==0.3.11

Adaptador para trabajar con la API de OpenAI. Proporciona acceso a la familia de modelos de lenguaje GPT y sus funciones. Incluye compatibilidad con modelos de chat, modelos de incrustación y funciones para trabajar con imágenes mediante DALL-E (consideremos un ejemplo con el modelo gpt-3.5-turbo).

langchain-chroma==0.2.2

Conector a la base de datos vectorial ChromaDB. Permite almacenar y recuperar eficientemente representaciones vectoriales de textos para implementar funciones de búsqueda semántica y crear bases de conocimiento con capacidad de búsqueda por similitud.

langchain-huggingface==0.1.2

La integración con el ecosistema Hugging Face brinda acceso a miles de modelos abiertos desarrollados por la comunidad. Estos incluyen modelos de lenguaje, modelos para incrustaciones, clasificación y otras tareas de PLN. Esta herramienta es especialmente útil cuando se necesita ejecutar modelos localmente o utilizar modelos especializados. En nuestro caso, podremos utilizar la red neuronal local paraphrase-multilingual-MiniLM-L12-v2.

Otros paquetes

Torch: Una biblioteca de aprendizaje automático que impulsa muchos modelos de lenguaje

Loguru: Un práctico registrador para seguir el progreso de nuestro código

ChromaDB: La base de datos vectorial que usaremos

Sentence-Transformers: Una biblioteca para convertir texto en vectores (incrustaciones)

Instalando dependencias

Ahora, instalemos:

pip install -r requirements.txt

La instalación puede tardar bastante debido al peso total de los paquetes que se instalan (en mi caso, el proceso de instalación tardó unos 20 minutos).

Creación de una base de datos

Ahora, creemos un archivo llamado create_chromadb.py (puedes darle cualquier nombre).

Realizar las importaciones:

import time
from langchain_huggingface import HuggingFaceEmbeddings
import torch
from loguru import logger
from langchain_chroma import Chroma

Definamos las variables principales:

CHROMA_PATH = "./shop_chroma_db"
COLLECTION_NAME = "shop_data"

Las colecciones en bases de datos vectoriales se pueden comparar con las tablas en bases de datos clásicas. Si no se especifica un nombre de colección, ChromaDB proporcionará un valor predeterminado.

Ahora, agreguemos una matriz de productos (cuantos más, mejor):

SHOP_DATA = [
    {
        "text": 'Lenovo IdeaPad 5: 16 RAM, SSD 512, 15.6", prise 55000 $.',
        "metadata": {
            "id": "1",
            "type": "product",
            "category": "laptops",
            "price": 55000,
            "stock": 3,
        },
    },
    # add other products
]

Y finalmente, escribamos una función para crear la base de datos:

def generate_chroma_db():
    try:
        start_time = time.time()

        logger.info("Cargando el modelo de incrustación...")
        embeddings = HuggingFaceEmbeddings(
            model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
            model_kwargs={"device": "cuda" if torch.cuda.is_available() else "cpu"},
            encode_kwargs={"normalize_embeddings": True},
        )
        logger.info(f"Modelo subido para {time.time() - start_time:.2f} segundo")

        logger.info("Creación de una base de datos de croma...")
        chroma_db = Chroma.from_texts(
            texts=[item["text"] for item in SHOP_DATA],
            embedding=embeddings,
            ids=[str(item["metadata"]["id"]) for item in SHOP_DATA],
            metadatas=[item["metadata"] for item in SHOP_DATA],
            persist_directory=CHROMA_PATH,
            collection_name=COLLECTION_NAME,
        )
        logger.info(f"Chroma DB fue creado para {time.time() - start_time:.2f} segundo")

        return chroma_db
    except Exception as e:
        logger.error(f"Error: {e}")
        raise

En este código:

Creamos un modelo para convertir texto a vectores (incrustaciones).

Inicializamos la base de datos de Chroma con nuestros productos.

Transferimos textos, IDs, metadatos y la configuración del espacio de búsqueda.

Registramos la creación correcta o los errores de la base de datos.

Es importante destacar que utilizamos el modelo multilingüe paraphrase-multilingual-MiniLM-L12-v2, compatible con el español. Esto nos permitirá crear correctamente representaciones vectoriales para nuestros productos en ruso. El modelo se cargará automáticamente desde el repositorio de Hugging Face en la primera ejecución. No será necesario cargarlo en ejecuciones posteriores si el modelo ya está instalado en su equipo.

La peculiaridad del modelo elegido es su equilibrio en tamaño y calidad: ofrece buenos resultados de vectorización de texto incluso en la CPU, pero con una tarjeta gráfica compatible con CUDA funciona mucho más rápido. Además, gracias al parámetro normalize_embeddings=True, todas las incrustaciones creadas se normalizarán, lo que mejorará la precisión de la búsqueda de productos similares.

Ejecute el código:

if __name__ == "__main__":
   generate_chroma_db()

Chroma DB!

¡Listo! Ahora tenemos la base para crear una base de datos vectorial con una descripción de los documentos. Tras el lanzamiento, en la raíz del proyecto debería aparecer una carpeta llamada "shop_chroma_db", donde se ubicarán los archivos de la base de datos. En el futuro, podremos usar esta base de datos para la búsqueda semántica de productos según las solicitudes del usuario: la búsqueda encontrará no solo coincidencias exactas, sino también productos semánticamente similares.

Creación de un motor de búsqueda de bases de datos

Ahora que la base de datos está lista, podemos describir un sistema de búsqueda simple dentro de ella. El principio será el siguiente:.

Conectarse a una base de datos existente, recibiendo incrustaciones

Recibir una consulta de búsqueda del usuario

Transformar la consulta de búsqueda en una representación vectorial

Comparar el vector de la consulta recibida con los datos de la base de datos

Devolver al usuario la cantidad de documentos que solicitó
En cuanto a la emisión de documentos al usuario, existe una función: siempre indicamos la cantidad de documentos que queremos recibir. Por ejemplo, pueden ser 5. Si no se vincula estrictamente a los metadatos mediante filtros, siempre recibirá como respuesta de la base de datos exactamente la cantidad de documentos que especificó.

Es decir, incluso si el resultado no coincide en absoluto con su solicitud, recibirá sus 5 documentos como respuesta. Es importante tener esto en cuenta al desarrollar una interfaz de usuario.

La respuesta en sí consta de: un documento, los metadatos vinculados a él y un índice de clasificación (cuanto más bajo sea el índice, mayor será la similitud en significado del documento que recibirá). La emisión se basa en el principio: cuanto más bajo sea el índice, mayor será la posición en la emisión.

Volvamos al código.

Importaciones:

from langchain_huggingface import HuggingFaceEmbeddings
import torch
from loguru import logger
from langchain_chroma import Chroma

Variables:

CHROMA_PATH = "./shop_chroma_db"
COLLECTION_NAME = "shop_data"

Ahora describamos la función para conectarse a una base de datos existente:

def connect_to_chroma():
    """Conectarse a Chroma."""
    try:
        logger.info("Descarga de incrustación...")
        embeddings = HuggingFaceEmbeddings(
            model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
            model_kwargs={"device": "cuda" if torch.cuda.is_available() else "cpu"},
            encode_kwargs={"normalize_embeddings": True},
        )

        chroma_db = Chroma(
            persist_directory=CHROMA_PATH,
            embedding_function=embeddings,
            collection_name=COLLECTION_NAME,
        )

        logger.success("Conectarse a Chroma")
        return chroma_db
    except Exception as e:
        logger.error(f"Error al conectar Chroma: {e}")
        raise

Esta función devolverá un objeto de base de datos listo para usar, dentro del cual se realizará una búsqueda vectorial.

A continuación, describiremos una función que permitirá buscar tanto por documento (páginas de documentación) como por metadatos:

def search_products(query: str, metadata_filter: dict = None, k: int = 4):
    """
    Búsqueda de páginas
    """
    try:
        chroma_db = connect_to_chroma()
        results = chroma_db.similarity_search_with_score(
            query, k=k, filter=metadata_filter
        )

        logger.info(f"Found {len(results)} query result: {query}")
        formatted_results = []
        for doc, score in results:
            formatted_results.append(
                {
                    "text": doc.page_content,
                    "metadata": doc.metadata,
                    "similarity_score": score,
                }
            )
        return formatted_results
    except Exception as e:
        logger.error(f"Error: {e}")
        raise

Y ahora llamémoslo:

for i in search_products(query="how deploy app in Amverum, with git push?"):
    print(i)

El parámetro metadata_filter nos permite especificar las condiciones de filtrado de metadatos. El ejemplo de búsqueda de productos de una tienda online es más adecuado que nuestra documentación. Por ejemplo, si queremos encontrar solo portátiles con un precio inferior a 600 $, podemos usar un filtro como este:

filter_condition = {
    "category": "laptops",
    "price": {"$lt": 600}
}

Quiero destacar que este ejemplo es educativo. En un sistema de producción, se suele crear una clase para gestionar una sesión con una base de datos, de modo que se pueda conectar una vez y mantener la conexión. En la implementación actual, cada llamada a la función reiniciará la conexión y cargará el modelo de incrustación, lo cual consume muchos recursos. En el próximo artículo, si veo que este tema les interesa, les mostraré cómo implementar dicha clase y cómo crear una aplicación web completa basada en una base de datos vectorial.

Por cierto, el código fuente del proyecto de hoy, así como la clase ya escrita para gestionar la conexión a la base de datos vectorial, se pueden encontrar en mi canal de Telegram "Easy Way to Python". El código fuente completo del proyecto lleva ahí aproximadamente una semana.

Realicen la búsqueda y vean los resultados.

result!

Presta atención al valor de similarity_score. Cuanto menor sea este valor, más nos acercamos a la consulta en cuanto a significado.

¿No está mal, verdad? Aunque no especificamos explícitamente un modelo de aspiradora específico ni sus características en la consulta, la búsqueda vectorial logró comprender la semántica de la consulta y devolver los resultados más relevantes de nuestra base de datos. Esto demuestra el poder de las bases de datos vectoriales combinadas con modelos de lenguaje modernos.

¡A practicar!

Ahora que tienes una comprensión básica de los principios del trabajo con bases de datos vectoriales, podemos empezar a crear una lógica más compleja.

Hablaremos del servicio Amverum Cloud, que ya mencioné en mis artículos anteriores sobre Habr. En resumen, es una plataforma para la implementación sencilla (lanzamiento remoto) de proyectos en prácticamente cualquier lenguaje de programación.
Una de las grandes ventajas de Amverum Cloud es la posibilidad de implementar rápidamente un proyecto en diversas tecnologías mediante Git Push (o arrastrando archivos en la interfaz), así como obtener un nombre de dominio gratuito. Además, el servicio proporciona proxy integrado gratuito para las API de OpenAI, Antropic, Cloude y Grok, lo cual facilita la interacción con LLM. Gracias a estas capacidades técnicas, el servicio cuenta con documentación extensa y detallada para todo tipo de ocasiones: desde guías básicas, como la migración desde Heroku, hasta temas más específicos, como el lanzamiento de proyectos en FastAPI o Django, o la resolución de problemas de facturación.

Esta documentación (con aproximadamente 70 documentos diferentes) es perfecta para una demostración práctica: podemos tomar un conjunto de textos, transformarlo en una base de datos vectorial y mostrar cómo integrarla eficazmente con modelos de redes neuronales para obtener respuestas significativas y útiles.

Datos de entrada

Ya tenía en mis manos unos 70 documentos en Markdown de Amverum en este formato:

Compatibilidad con sondas de Kubernetes

¿Cómo configurar?

El formulario requiere completar la configuración en formato YAML, nativo del formato utilizado por k8s (ver aquí).
El archivo de configuración se sustituye en tu implementación, que se carga junto con él en el clúster.
Esto significa que si has cargado una configuración incorrecta o en un formato incorrecto, tu proyecto se bloqueará durante la compilación.

Estos documentos me fueron proporcionados como un proyecto que contiene archivos MD y otros archivos técnicos. Pero lo más probable es que, en tu caso, no exista tal regalo.

Por lo tanto, es importante recordar que para recopilar información para la base de datos, puedes usar cualquier herramienta que te convenga: analizar sitios, cargar datos desde tus propias bases de datos SQL, leer documentos en Excel, Word, PDF, etc. Lo principal es que, al final, la información se convierta a un formato compatible con bases de datos vectoriales y con una estructura clara.

Lo primero que implementé en el formato de una tarea con preparación fue transformar todos estos archivos con una estructura anidada a un formato JSON de este tipo:

{
   "text": "some text",
   "metadata": {
       "section_count": 2,
       "section_1": "backups",
       "section_2": "backups /data"
   }
}

Es decir, se trata de información de texto completo en la clave de texto sin signos de puntuación, caracteres especiales ni mayúsculas, ni información técnica incluida en los metadatos, como el número de secciones, nombres, etc.

Es importante entender que los metadatos son nuestra ayuda, pero nada impediría cargar solo textos. Sin embargo, con los metadatos obtenemos oportunidades adicionales para filtrar y analizar los resultados de búsqueda.

Describamos la lógica para crear una carpeta con archivos JSON:

import json
import os
import re
import string
import sys
from typing import Any, Dict, List
from config import settings
from loguru import logger

Aquí utilicé las bibliotecas integradas de Python, con la excepción de loguru, para facilitar el registro de resultados. La extracción se basará en expresiones regulares.

A continuación, se generarán las constantes:

HEADER_PATTERN = re.compile(r"^(#+)\s(.+)")
PUNCTUATION_PATTERN = re.compile(f"[{re.escape(string.punctuation)}]")
WHITESPACE_PATTERN = re.compile(r"\s+")

Describamos la lógica para normalizar textos:

def normalize_text(text: str) -> str:
  """Normalización de texto: eliminación de puntuación y caracteres especiales."""
  if not isinstance(text, str):
  raise ValueError("El texto de entrada debe ser una cadena")

  # Eliminación de la puntuación
  text = PUNCTUATION_PATTERN.sub(" ", text)
  # Eliminar saltos de línea y espacios adicionales
  text = WHITESPACE_PATTERN.sub(" ", text)
  # Convirtiendo a minúsculas
  return text.lower().strip()

Escribamos la lógica para analizar un documento en formato md:

def parse_markdown(md_path: str) -> Dict[str, Any]:
    """Análisis de datos"""
    if not os.path.exists(md_path):
        raise FileNotFoundError(f"Archivo {md_path} no encontrado")

    try:
        with open(md_path, "r", encoding="utf-8") as file:
            content = file.read()
    except Exception as e:
        logger.error(f"Error de lectura {md_path}: {e}")
        raise

    sections: List[str] = []
    section_titles: List[str] = []
    current_section: str | None = None
    current_content: List[str] = []

    for line in content.splitlines():
        section_match = HEADER_PATTERN.match(line)

        if section_match:
            if current_section:
                sections.append("\n".join(current_content).strip())
                section_titles.append(current_section)
                current_content = []
            current_section = section_match.group(2)
            current_content.append(current_section)
        else:
            current_content.append(line)

    if current_section:
        sections.append("\n".join(current_content).strip())
        section_titles.append(current_section)

    # Text normalization
    normalized_sections = [normalize_text(section) for section in sections]
    full_text = " ".join(normalized_sections)

    # Metadata structure
    metadata = {
        "file_name": os.path.basename(md_path),
        "section_count": len(section_titles),
    }

    # Header add
    for i, title in enumerate(section_titles):
        metadata[f"section_{i+1}"] = title

    return {"text": full_text, "metadata": metadata}

Y describiremos la función para analizar todos los documentos:

def process_all_markdown(input_folder: str, output_folder: str) -> None:
    """procesamiento de rebajas"""
    if not os.path.exists(input_folder):
        raise FileNotFoundError(f"{input_folder} extraviado")

    try:
        os.makedirs(output_folder, exist_ok=True)
    except Exception as e:
        logger.error(f"Error de creación del directorio de salida: {e}")
        raise

    for root, _, files in os.walk(input_folder):
        for file_name in files:
            if file_name.endswith(".md"):
                try:
                    md_path = os.path.join(root, file_name)
                    output_path = os.path.join(
                        output_folder, file_name.replace(".md", ".json")
                    )
                    parsed_data = parse_markdown(md_path)

                    with open(output_path, "w", encoding="utf-8") as file:
                        json.dump(parsed_data, file, ensure_ascii=False, indent=4)
                    logger.info(f"Resultado en ahorro {output_path}")
                except Exception as e:
                    logger.error(f"Error de procesamiento de archivo {file_name}: {e}")

La función toma como entrada la carpeta donde se encuentran todos los documentos y la carpeta donde se escribirán los archivos JSON recibidos.

La lógica de detección de archivos md funciona buscando directorios con documentos en todas las carpetas anidadas. Esto es especialmente útil si la documentación tiene una estructura compleja de archivos y directorios.

Ahora llamamos:

if __name__ == "__main__":
    try:
        process_all_markdown(
            input_folder="ruta a la carpeta con documentos",
            output_folder="Ruta a la carpeta para guardar JSON",
        )
    except Exception as e:
        logger.error(f"Error crítico: {e}")
        sys.exit(1)

Vamos a lanzarnos.

Se recibieron 63 documentos (un ejemplo de todos los archivos está en el código fuente del proyecto)!

Se recibieron 63 documentos (un ejemplo de todos los archivos está en el código fuente del proyecto)

¡Los archivos están listos! Ahora solo necesitamos leer todos los archivos y crear una base de datos vectorial basada en ellos.

Tenga en cuenta que nuestro analizador extrae no solo el texto de Markdown, sino también la estructura del documento. En los metadatos, guardamos el nombre del archivo y los encabezados de todas las secciones. Esto nos permite no solo buscar por contenido, sino también indicar con precisión de qué sección de la documentación se extrajo un fragmento en particular, lo cual es muy útil para generar respuestas significativas del asistente.

Crear una base de datos
El proceso de creación de una base de datos para Amverum no difiere mucho del enfoque descrito anteriormente, con la diferencia de que tomaremos los datos de archivos JSON existentes.

Antes de continuar, para un código más estructurado, crearemos un archivo .env en la raíz del proyecto y le asignaremos las siguientes variables:

DEEPSEEK_API_KEY=sk-1234
OPENAI_API_KEY=sk-proj-1234

Ambas claves API deben estar aquí. Si no hay claves, no es necesario crear el archivo.

A continuación, cree el archivo config.py y complételo como se indica a continuación:

import os
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict

class Config(BaseSettings):
    DEEPSEEK_API_KEY: SecretStr
    BASE_DIR: str = os.path.abspath(os.path.join(os.path.dirname(__file__)))
    DOCS_AMVERA_PATH: str = os.path.join(BASE_DIR, "amverum_data", "docs_amvera")
    PARSED_JSON_PATH: str = os.path.join(BASE_DIR, "amverum_data", "parsed_json")
    AMVERA_CHROMA_PATH: str = os.path.join(BASE_DIR, "amverum_data", "chroma_db")
    AMVERA_COLLECTION_NAME: str = "amvera_docs"
    MAX_CHUNK_SIZE: int = 512
    CHUNK_OVERLAP: int = 50
    LM_MODEL_NAME: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
    DEEPSEEK_MODEL_NAME: str = "deepseek-chat"
    OPENAI_MODEL_NAME: str = "gpt-3.5-turbo"
    OPENAI_API_KEY: SecretStr
    model_config = SettingsConfigDict(env_file=f"{BASE_DIR}/.env")

settings = Config()  # type: ignore

En esta clase, recopilamos todas las variables necesarias para el funcionamiento del proyecto. Como puede ver, aquí se indican las rutas a las carpetas importantes (su estructura puede variar), los nombres de las colecciones, el tamaño máximo de fragmentos y los nombres de los modelos de red neuronal.

Preste especial atención a la declaración de las claves API:

OPENAI_API_KEY: SecretStr
DEEPSEEK_API_KEY: SecretStr

Es importante describirlas no como cadenas regulares, sino mediante SecretStr. Este es un requisito de la biblioteca LangChain para el manejo seguro de datos confidenciales.

Ahora podemos importar la variable de configuración en cualquier parte del código y acceder a la configuración que necesitamos mediante un punto.

Creación de una base de datos
Volvamos a la creación de una base de datos. Comencemos con las importaciones:

import json
import os
import sys
from typing import Any, Dict, List, Optional
import torch
from langchain_chroma import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from loguru import logger
from config import settings

Aquí, entre otras cosas, se ve la importación de RecursiveCharacterTextSplitter. Este componente nos permitirá dividir documentos grandes en fragmentos semánticos (fragmentos de texto relacionados entre sí por significado).

Escribamos una función para cargar todos los archivos JSON existentes en el formato de lista de diccionario:

def load_json_files(directory: str) -> List[Dict[str, Any]]:
    """Descarga de JSON"""
    documents = []

    try:
        if not os.path.exists(directory):
            logger.error(f"El directorio {directorio} no existe")
            return documents

        for filename in os.listdir(directory):
            if filename.endswith(".json"):
                file_path = os.path.join(directory, filename)
                try:
                    with open(file_path, "r", encoding="utf-8") as file:
                        data = json.load(file)
                        documents.append(
                            {"text": data["text"], "metadata": data["metadata"]}
                        )
                        logger.info(f"Archivo cargado: {filename}")
                except Exception as e:
                    logger.error(f"Error al leer el archivo{filename}: {e}")

        logger.success(f"Se cargaron {len(documentos)} archivos JSON")
        return documents
    except Exception as e:
        logger.error(f"Error al cargar archivos JSON: {e}")
        return documents

Tras ejecutar esta función, recibiremos todos los datos de los archivos JSON como diccionarios almacenados en RAM. La información de estos diccionarios puede utilizarse para crear una base de datos vectorial.

A continuación, describiremos la lógica para dividir documentos grandes en fragmentos significativos:

def split_text_into_chunks(text: str, metadata: Dict[str, Any]) -> List[Any]:
    """Dividir el texto en fragmentos conservando los metadatos."""
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=settings.MAX_CHUNK_SIZE,
        chunk_overlap=settings.CHUNK_OVERLAP,
        length_function=len,
        is_separator_regex=False,
    )

    chunks = text_splitter.create_documents(texts=[text], metadatas=[metadata])
    return chunks

Gracias a esta función, seguimos teniendo una lista de diccionarios, pero ahora los textos extensos se dividen en fragmentos semánticos. Cada fragmento contiene los metadatos necesarios. Esto es importante para mantener el contexto general incluso al dividir un texto extenso en varias partes.

Finalmente, describiremos la lógica para crear una base de datos vectorial:

def generate_chroma_db() -> Optional[Chroma]:
    """Inicializando ChromaDB con datos de archivos JSON."""
    try:
        # Crea un directorio para almacenar la base de datos si no existe
        os.makedirs(settings.AMVERA_CHROMA_PATH, exist_ok=True)

        # Cargando archivos JSON
        documents = load_json_files(settings.PARSED_JSON_PATH)

        if not documents:
            logger.warning("No hay documentos para agregar a la base de datos.")
            return None

        # Inicializar el modelo de incrustación
        embeddings = HuggingFaceEmbeddings(
            model_name=settings.LM_MODEL_NAME,
            model_kwargs={"device": "cuda" if torch.cuda.is_available() else "cpu"},
            encode_kwargs={"normalize_embeddings": True},
        )

        # Preparación de datos para Chroma
        all_chunks = []
        for i, doc in enumerate(documents):
            chunks = split_text_into_chunks(doc["text"], doc["metadata"])
            all_chunks.extend(chunks)
            logger.info(
                f"Document {i+1}/{len(documents)} is divided into {len(chunks)} chunks"
            )

        # Crear un almacenamiento vectorial
        texts = [chunk.page_content for chunk in all_chunks]
        metadatas = [chunk.metadata for chunk in all_chunks]
        ids = [f"doc_{i}" for i in range(len(all_chunks))]

        chroma_db = Chroma.from_texts(
            texts=texts,
            embedding=embeddings,
            ids=ids,
            metadatas=metadatas,
            persist_directory=settings.AMVERA_CHROMA_PATH,
            collection_name=settings.AMVERA_COLLECTION_NAME,
            collection_metadata={
                "hnsw:space": "cosine",
            },
        )

        logger.success(
            f"Se inicializó Chroma, se agregaron {len(all_chunks)} fragmentos de {len(documents)} documentos"
        )
        return chroma_db
    except Exception as e:
        logger.error(f"Error de inicialización de Chroma: {e}")
        raise

La lógica de creación de una base de datos no difiere mucho de los ejemplos que vimos anteriormente. La principal diferencia radica en la división de textos extensos en fragmentos significativos (trozos).

En mi modesto ordenador, el proceso de creación de una base de datos tardó menos de 10 minutos. En dispositivos con GPU, este proceso solo tardará unos minutos gracias a la computación paralela.

Optimización del rendimiento

Cabe destacar que, al trabajar con grandes volúmenes de datos, se puede optimizar aún más el proceso:

Utilizar el procesamiento por lotes al crear incrustaciones (procesamiento de documentos en grupos).

Configurar los parámetros MAX_CHUNK_SIZE y CHUNK_OVERLAP según la estructura de los datos.

Elegir un modelo de incrustación adecuado, buscando un equilibrio entre calidad y velocidad.

ChromaDB ofrece un buen rendimiento para la mayoría de las tareas RAG, pero para volúmenes de datos muy grandes (millones de documentos), conviene considerar otras soluciones, como FAISS o Pinecone.

Integración de un motor de búsqueda con redes neuronales

Anteriormente, vimos cómo describir un motor de búsqueda sin utilizar grandes redes neuronales. Para consolidar este material, te sugiero que crees tú mismo un motor de búsqueda basado en tu base de datos.

Pasaré a la parte más interesante: explicaré cómo combinar la salida de una base de datos vectorial con redes neuronales para crear un agente de IA inteligente. Para ello, utilizaremos datos de la documentación de Amverum Cloud, que ya he transformado en una base de datos vectorial.

Configuración inicial del proyecto

Comencemos creando un archivo llamado chat_with_ai.py.

Realicemos las importaciones necesarias:

from typing import Any, Dict, List, Literal, Optional
import torch
from config import settings
from langchain_chroma import Chroma
from langchain_deepseek import ChatDeepSeek
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from loguru import logger

Aquí incluimos las nuevas herramientas langchain_deepseek y langchain_openai, que nos permitirán integrar fácilmente los modelos DeepSeek y ChatGPT (así como otros modelos de OpenAI) en nuestro proyecto.

Estructura de la clase principal

Para un código más estructurado, decidí formatearlo como una clase. Declaremos nuestra clase y describamos la lógica de inicialización:

class ChatWithAI:
    def __init__(self, provider: Literal["deepseek", "openai"] = "deepseek"):
        self.provider = provider
        self.embeddings = HuggingFaceEmbeddings(
            model_name=settings.LM_MODEL_NAME,
            model_kwargs={"device": "cuda" if torch.cuda.is_available() else "cpu"},
            encode_kwargs={"normalize_embeddings": True},
        )

        if provider == "deepseek":
            self.llm = ChatDeepSeek(
                api_key=settings.DEEPSEEK_API_KEY,
                model=settings.DEEPSEEK_MODEL_NAME,
                temperature=0.7,
            )
        elif provider == "openai":
            self.llm = ChatOpenAI(
                api_key=settings.OPENAI_API_KEY,
                model=settings.OPENAI_MODEL_NAME,
                temperature=0.7,
            )
        else:
            raise ValueError(f"Proveedor no compatible: {provider}")

        self.chroma_db = Chroma(
            persist_directory=settings.AMVERA_CHROMA_PATH,
            embedding_function=self.embeddings,
            collection_name=settings.AMVERA_COLLECTION_NAME,
        )

Al crear un objeto de clase, pasamos el parámetro proveedor con el valor "deepseek" o "openai" para indicar la red neuronal con la que trabajaremos en el proyecto.

Observe la construcción:

ChatDeepSeek(
    api_key=settings.DEEPSEEK_API_KEY,
    model=settings.DEEPSEEK_MODEL_NAME,
    temperature=0.7,
)

De esta forma sencilla, podemos declarar la red neuronal con la que interactuaremos. Aquí, basta con pasar el token de la API y el nombre del modelo (estos son parámetros obligatorios). También se pueden pasar parámetros adicionales para un ajuste más preciso, por ejemplo, la temperatura para controlar la creatividad de las respuestas.

Obtención del contexto relevante
Describamos el método para obtener una respuesta de la base de datos:

Obtención del contexto relevante
Describamos el método para obtener una respuesta de la base de datos:

def get_relevant_context(self, query: str, k: int = 3) -> List[Dict[str, Any]]:
    """Obtener contexto relevante de la base de datos."""
    try:
        results = self.chroma_db.similarity_search(query, k=k)
        return [
            {
                "text": doc.page_content,
                "metadata": doc.metadata,
            }
            for doc in results
        ]
    except Exception as e:
        logger.error(f"Error al obtener el contexto: {e}")
        return []

El método toma como entrada la solicitud del usuario y el número de documentos que esperamos recibir (por defecto, 3).
Para la búsqueda, utilicé el método similarity_search. Cabe recordar que la esencia de este método es transformar la solicitud de búsqueda en una representación vectorial y, con base en este vector, buscar en nuestra base de datos.

Formatear el contexto
A continuación, describiremos el método que transformará el resultado obtenido en un formato adecuado para redes neuronales:

def format_context(self, context: List[Dict[str, Any]]) -> str:
    """Dar formato al contexto de un mensaje."""
    formatted_context = []
    for item in context:
        metadata_str = "\n".join(f"{k}: {v}" for k, v in item["metadata"].items())
        formatted_context.append(
            f"Текст: {item['text']}\nMetadata:\n{metadata_str}\n"
        )
    return "\n---\n".join(formatted_context)

Este método transforma el paquete de documentos y metadatos de todos los documentos recibidos en un mensaje estructurado, que luego pasamos a la red neuronal.

Generación de una respuesta
Ahora describiremos el método que vinculará nuestro contexto con la solicitud a la red neuronal:

def generate_response(self, query: str) -> Optional[str]:
    """Generar una respuesta basada en una solicitud y contexto."""
    try:
        context = self.get_relevant_context(query)
        if not context:
            return "Lo siento, no pude encontrar el contexto relevante para la respuesta."

        formatted_context = self.format_context(context)

        messages = [
            {
"role": "system",
"content": """Eres el administrador interno de la empresa. Amverum Cloud (https://amverum.com/). Respondes concisamente, sin preámbulos.
Reglas:
1. Vaya directo al grano, sin frases como "Basado en el contexto".
2. Use solo datos. Si no hay datos exactos, responda con frases generales sobre Amverum Cloud, pero no entre en detalles.
3. Use texto plano sin formato.
4. Incluya enlaces solo si están en contexto.
5. Hable en primera persona del plural: "Proporcionamos", "Tenemos".
6. Al mencionar archivos, hágalo con naturalidad, por ejemplo: "Adjuntaré instrucciones donde se detallan los pasos".
7. Responda a los saludos con amabilidad, a las negativas con un poco de humor.
8. Al responder, puede usar información general de fuentes abiertas sobre Amverum Cloud, pero apóyese en el contexto.
9. Si el usuario pregunta sobre precios, planes o especificaciones técnicas, proporcione respuestas específicas del contexto.
10. Para preguntas técnicas, ofrezca soluciones prácticas.

Personalice las respuestas mencionando el nombre del cliente si está en el contexto. Sea breve, informativo y útil.""",
 },
            {
                "role": "user",
                "content": f"Pregunta: {query}\nContext: {formatted_context}",
            },
        ]
        response = self.llm.invoke(messages)
        if hasattr(response, "content"):
            return str(response.content)
        return str(response).strip()
    except Exception as e:
        logger.error(f"Error al generar respuesta: {e}")
        return "Se produjo un error al generar la respuesta."

Tenga en cuenta que aquí se utiliza un mensaje de texto, que actúa como contenedor de nuestro contexto. El mensaje en sí (consulta) puede ser cualquier cosa, y solo he dado un ejemplo posible.

Conviene experimentar con el contenido del mensaje según los requisitos específicos de su proyecto. Las instrucciones pueden ser más o menos directivas y orientar el modelo a diferentes estilos de respuesta o formatos de presentación de la información.

El método acepta una consulta de búsqueda del usuario como entrada. Esta consulta se procesa primero mediante una base de datos vectorial y, a partir de ella, obtenemos el texto estructurado de los documentos relevantes. A continuación, este texto se combina con el mensaje que hemos preparado y, al llamar a self.llm.invoke(messages), obtenemos una respuesta de la red neuronal.

A continuación, transformamos la respuesta y la devolvemos al usuario.

Lanzamiento y pruebas
Ahora solo queda inicializar correctamente la clase y llamar a sus métodos:

if __name__ == "__main__":
    chat = ChatWithAI(provider="deepseek")
    print("\n=== Chat with AI===\n")

    while True:
        query = input("Tú: ")
        if query.lower() == "output":
            print("\nVeo tu tarde!")
            break

        print("\nImpresión con IA...", end="\r")
        response = chat.generate_response(query)
        print(" " * 20, end="\r")  # Borrar "Impresiones AI""..."
        print(f"AI: {response}\n")

Tenga en cuenta que estamos ejecutando un bucle que solo saldrá si el usuario pulsa "exit". Esto demuestra cómo podemos mantener la conexión con la base de datos vectorial de Chroma sin tener que reconectarnos con cada solicitud.

Nos conectamos a la base de datos una vez y solo nos desconectamos al completar el bucle infinito. Este enfoque ahorra el tiempo de inicializar la conexión con cada solicitud del usuario.

Conclusión

En esta etapa, solo tenemos un prototipo de motor de búsqueda para la documentación de Amverum Cloud; es difícil considerarlo un servicio completo con un asistente de IA.

Si este material genera interés en forma de visualizaciones, "me gusta" y comentarios, en el próximo artículo les detallaré qué bloques del código actual se pueden mejorar. Basándonos en la versión modificada, crearemos un servicio web listo para usar similar al sitio web chat.openai.com, donde implementaremos un chat completo y práctico con nuestro asistente de IA. Espero haberles explicado cómo funcionan las bases de datos vectoriales, cómo recopilar y estructurar información para ellas, y cómo organizar la generación y la búsqueda en dichas bases de datos.

Debido a la limitación de recursos, no pude abordar el tema a fondo. Por lo tanto, recomiendo encarecidamente estudiar la representación vectorial de datos con más detalle. Esto es importante, sobre todo porque gigantes como ChatGPT, Claude, DeepSeek y otras redes neuronales modernas trabajan con la representación vectorial de la información.
Además, recomiendo prestar atención a una herramienta tan potente como LangChain. Lo que hemos analizado en este artículo es solo una pequeña parte de sus capacidades.

Eso es todo. ¡Nos vemos en el próximo artículo!

Alex Yacovenko

Amverum Cloud

Amverum Cloud es una alternativa a Heroku más económica y fácil de implementar, con dominios gratuitos, almacenamiento persistente incluido y la posibilidad de actualizar proyectos mediante "git push amverum master".