Este proyecto documenta el diseño e implementación de una plataforma de datos full-stack y de código abierto, desarrollada para reemplazar una infraestructura analítica comercial costosa y rígida. A través de la migración de la base de datos relacional de AdventureWorks desde SQL Server hacia PostgreSQL, el desarrollo de pipelines ETL modulares en Python y la integración de modelos predictivos de Machine Learning, se transformó un sistema de reportes tradicional en un motor independiente y escalable, centralizado en una aplicación web interactiva con Flask y Docker.
El Desafío del Negocio (The Problem Statement)
AdventureWorks Inc. gestionaba su inteligencia de negocio y reportería a través de una suite de analítica comercial propietaria y tradicional basada en licencias de software cerrado. Al escalar la organización, el equipo se enfrentó a tres bloqueos críticos:
Escala Exponencial de Costos por Licenciamiento: El modelo de cobro por usuario o por núcleo se volvió financieramente insostenible ante la creciente demanda de acceso concurrente por parte de Ventas, RRHH, Producción y Compras.
Limitaciones de Integración con Data Science y ML: El ecosistema cerrado dificultaba acoplar modelos avanzados de Machine Learning (como forecasting de series temporales con Prophet o clasificación con XGBoost) en los flujos de datos sin recurrir a costosos módulos de IA adicionales.
Dependencia del Proveedor (Vendor Lock-in): La falta de control sobre el procesamiento impedía personalizar la interfaz a nivel corporativo y flexibilizar el despliegue en infraestructura propia.
Nuestra Solución:
Arquitectura Full-Stack Data de Código Abierto. Diseñamos y desarrollamos una plataforma web propietaria, independiente y end-to-end que migró la infraestructura y transformó los reportes rígidos del pasado en un motor analítico y predictivo centralizado:
Pipeline del proyecto
Fase 1: Ingesta, Auditoría y Exploración Profunda (OLTP)
Objetivo: Comprender las reglas y la estructura exacta del negocio para mitigar sesgos en las etapas de transformación y modelado.
Aprovisionamiento: Extracción y restauración del backup relacional nativo (.bak) de AdventureWorks 2022 en SQL Server.
Data Quality & Perfilado Inicial: Auditoría técnica mediante scripts SQL de exploración individual para cada una de las 18 tablas relevantes, documentando rangos de fechas, valores únicos y patrones de columnas clave.
Garantía de Integridad: Identificación exhaustiva de claves primarias y foráneas reales, consolidando las dinámicas operacionales en un Diccionario de Datos maestro y un diagrama relacional completo.
Fase 2: Diseño del Modelo Analítico Relacional y Requerimientos
Objetivo: Traducir los objetivos corporativos en una arquitectura de datos técnica limpia y estructurada.
Definición de Granularidad: Establecimiento preciso del nivel de detalle de las entidades operativas (ej. Ventas por línea de pedido, Inventarios por snapshots de producto/almacén y Finanzas por periodo contable).
Mapeo de KPIs: Selección de indicadores estratégicos para una visión de 360° (Sales Amount, Total Product Cost, Profit, Margen %, Tendencias YoY/MoM, Ticket Promedio y Alertas de Stock).
Gobernanza Técnica: Generación de un Documento de Requerimientos Funcionales como base para validar formalmente las necesidades lógicas con cada área antes de iniciar el código.
Fase 3: Extracción y Preparación mediante Vistas SQL
Objetivo: Proveer una capa de datos purificada, segura y optimizada desde la base de origen.
Desacoplamiento Estructural: Creación de vistas en SQL Server diseñadas según los requerimientos funcionales para pre-estructurar el set de datos.
Seguridad y Anonimización: Selección rigurosa de atributos descriptivos y demográficos para segmentación, aplicando políticas de privacidad al anonimizar o remover datos sensibles de clientes.
Cómputo en Base de Datos: Implementación de transformaciones básicas directamente en el motor de origen (como cálculo dinámico de edad y unificación de jerarquías de productos) para aligerar las fases posteriores de procesamiento en Python.
Fase 4: Migración Heterogénea de Bases de Datos
Objetivo: Romper la dependencia con el proveedor tradicional migrando el núcleo de la compañía a un motor Open-Source (PostgreSQL) sin alterar su estructura relacional original.
Schema Mapping: Traducción de tipos de datos, restricciones e incompatibilidades entre SQL Server y PostgreSQL mediante el uso de Python.
Pipelines de Ingesta: Diseño de scripts de migración progresiva y por lotes para mover de manera controlada el modelo relacional puro, asegurando consistencia matemática absoluta entre ambos motores.
Stack Principal: SQL Server, PostgreSQL, SQLAlchemy, PyODBC, Psycopg2 y Pandas.
Fase 5: Pipeline ETL y Automatización con Python
Objetivo: Construir un pipeline reproducible con un solo comando que extraiga, limpie y guarde datos curados listos para consumo concurrente.
Pipeline Modular (Jupyter/Scripts): Desarrollo de notebooks de experimentación consolidados posteriormente en un script ejecutable (etl_pipeline.py) alimentado por un archivo central de configuración (config.py).
Feature Engineering & Limpieza: Tratamiento automatizado de valores nulos mediante reglas específicas según la columna, conversión estructurada de tipos/fechas y eliminación de duplicados mediante funciones reutilizables (CheckData).
Persistencia Eficiente (Formatos de Alto Rendimiento): Exportación de datasets limpios y validados a disco en formato Parquet (para optimizar espacio y velocidad de lectura).
Trazabilidad y Auditoría: Integración de logs detallados de seguimiento en consola para registrar el éxito/fallo del pipeline y mantenimiento de un registro maestro de control (datasets_control.xlsx) con métricas de filas y columnas procesadas.
Fase 6: Optimización de la Capa Analítica en PostgreSQL
Objetivo: Centralizar la lógica analítica pesada dentro de PostgreSQL para mitigar la carga computacional en el backend de la aplicación.
Modelado Lógico: Configuración de Vistas (Views) enriquecidas y almacenamiento indexado en PostgreSQL directamente sobre las tablas migradas.
Agregación Avanzada: Construcción de consultas complejas diseñadas para unificar la información dispersa de las áreas de Sales, HR, Product Performance, Supply Chain y Customer Analytics en estructuras consolidadas.
Fase 7: Backend Analítico y Arquitectura de Software
Objetivo: Construir la capa lógica del servidor encargada de procesar peticiones y unificar los datos analíticos de la empresa.
Backend Engineering: Desarrollo de una aplicación web robusta en Python utilizando Flask, estructurada de forma modular mediante Blueprints para segmentar los servicios de cada departamento de la compañía.
Persistencia Dinámica y APIs: Conexión interactiva a PostgreSQL para leer de forma nativa los archivos estructurados y las vistas optimizadas, garantizando tiempos mínimos de respuesta del servidor (latencia en milisegundos).
Fase 8: Pipeline de Machine Learning y Analítica Predictiva
Objetivo: Escalar la plataforma hacia la analítica predictiva, dotando al sistema de la capacidad de anticipar eventos críticos de negocio.
Modelado de Inteligencia Artificial: Diseño e implementación de modelos supervisados y de análisis temporal utilizando Scikit-Learn, XGBoost y Prophet.
Casos de Uso Operacionales:
Forecasting de Ventas: Modelos de series de tiempo para proyectar la demanda mensual e ingresos.
Clasificación de Churn: Modelos predictivos para calcular la probabilidad de abandono de clientes recurrentes.
Optimización de Cadena de Suministro: Predicción de quiebres de stock basándose en flujos históricos de inventario.
Inferencia en Producción: Creación de un script automatizado para la extracción de features, reentrenamiento de modelos y serialización de artefactos (.pkl / ONNX), consumidos directamente por la API de Flask para generar predicciones en tiempo real. [1]
Fase 9: Visualización de Datos e Interfaces de Usuario (Frontend)
Objetivo: Democratizar los insights corporativos permitiendo a la mesa directiva evaluar el rendimiento histórico y forecast predictivos en una sola pantalla unificada.
Frontend Interactivo: Creación de una interfaz web multipágina y responsive codificada con Plotly y Flask, reemplazando por completo los visualizadores de la suite comercial rígida.
Capa Semántica Unificada: Ocultamiento de columnas técnicas y exposición directa al usuario de medidas avanzadas de KPIs históricos calculados en base de datos junto a las proyecciones dinámicas estimadas por los modelos de Machine Learning.
Fase 10: Despliegue en Producción y DevOps (MLOps)
Objetivo: Publicar el sistema bajo una infraestructura escalable, segura y con costos de mantenimiento fijos, logrando la independencia total del negocio.
Infraestructura Cloud: Aprovisionamiento, hardening y configuración de políticas de seguridad en un Servidor Virtual Privado (VPS) con Ubuntu Server.
Contenerización Completa: Aislamiento de entornos y servicios (PostgreSQL, la aplicación Flask con sus pipelines de ML, las dependencias de Python y los servidores web) mediante Docker y Docker Compose.
Arquitectura de Producción: Implementación del servidor WSGI Gunicorn acoplado a Nginx como proxy inverso, garantizando la gestión eficiente de peticiones, compresión de assets, balanceo de carga básico y cifrado SSL.
Construyendo aplicaciones analíticas portables con DashForge
Las aplicaciones desarrolladas con Flask son extremadamente flexibles para crear dashboards, plataformas analíticas y sistemas de machine learning interactivos. Sin embargo, cuando el proyecto crece y empieza a desplegarse en servidores reales, surge un problema importante: el entorno.
Distintas versiones de Python, conflictos entre librerías, dependencias del sistema operativo o diferencias entre equipos pueden hacer que una aplicación funcione correctamente en desarrollo pero falle en producción.
Docker resuelve este problema encapsulando toda la aplicación dentro de un contenedor reproducible. El resultado es un entorno completamente aislado que puede ejecutarse exactamente igual en cualquier servidor compatible con Docker.
En este tutorial vamos a dockerizar una aplicación Flask desarrollada con DashForge utilizando una arquitectura preparada para producción.
Docker permite empaquetar una aplicación junto con todas sus dependencias. Todo queda integrado en una única imagen portable. Esto aporta ventajas muy importantes para aplicaciones analíticas:
despliegues reproducibles
aislamiento entre proyectos
facilidad para escalar aplicaciones
despliegues automáticos
ejecución bajo demanda
compatibilidad entre servidores
simplificación del entorno de producción
En el caso de DashForge, Docker permite convertir cada dashboard o aplicación analítica en una unidad independiente que puede iniciarse o detenerse dinámicamente desde un portal central.
Instalar docker
Linux
Para instalar en sistemas Debian sigue las siguientes instrucciones :
# Descarga la lista actualizada de los paquetes disponibles en los servidores de Linuxsudoaptupdate# Intalar las dependenciassudoaptinstall-yca-certificatescurlgnupg# Añadir clave oficial de Dockersudoinstall-m0755-d/etc/apt/keyrings# Descargar y registrar de forma segura la clave pública oficial de Docker en tu sistemacurl-fsSLhttps://download.docker.com/linux/ubuntu/gpg|\sudogpg--dearmor-o/etc/apt/keyrings/docker.gpg# Dar permisos de lectura a todos los usuarios sobre la clave de seguridad de Dockersudochmoda+r/etc/apt/keyrings/docker.gpg
Una vez instalado, hay que reiniciar el sistema. Docker instala servicios, WSL, variables y el daemon. Sigue las instrucciones de la aplicación; quizás sea necesario actualizar WSL, Docker Desktop normalmente guía todo automáticamente.
Verifica la instalación de Docker y su daemon, PowerShell ejecuta:
# Verifica la instalacion de dockerdocker --version# Verifica que el daemon este funcionandodocker ps
Preparar el proyecto Flask para Docker
Antes de dockerizar la aplicación, es recomendable mantener una estructura organizada. Una aplicación Flask típica preparada para Docker podría tener esta estructura:
Docker necesita conocer todas las librerías utilizadas por la aplicación para poder instalarlas dentro del contenedor. La forma estándar de hacerlo es mediante un archivo requirements.txt. Este archivo incluirá todas las librerías necesarias para ejecutar la aplicación.
Al usar este comando, ten en cuenta que exporta todo, incluyendo paquetes del sistema Windows que pueden impedir crear la imagen Docker al tratar de instalar paquetes de Windows en WLS (Subsistema de Linux para Windows). Una vez ejecutado el código revisa el fichero y elimina archivos como pywin32, pywinpty o winshell.
pipfreeze>requirements.txt
Excluyendo archivos innecesarios con .dockerignore
Cuando Docker construye una imagen, copia el contenido del proyecto al contenedor. Sin embargo, muchos archivos no deben incluirse:
entornos virtuales
cachés
configuraciones del editor
repositorios Git
logs
Para evitarlo se utiliza un archivo /.dockerignore.
Instalamos Gunicorn dentro del entorno virtual y lo agregamos a las dependencias:
pip install gunicornpip show gunicorn
Configurar Gunicorn creando el archivo: /gunicorn.conf.py. Esta configuración es suficiente para la mayoría de los dashboards analíticos pequeños y medianos.
FROM python:3.13-slim: La aplicación se construirá sobre una imagen ligera de Python. La versión slim reduce considerablemente el tamaño final del contenedor.
ENV PYTHONDONTWRITEBYTECODE=1: Evitan generar archivos .pyc, mejoran el comportamiento del contenedor en producción.
ENV PYTHONUNBUFFERED=1: fuerzan salida inmediata de logs.
WORKDIR /app: Define el directorio interno donde vivirá la aplicación.
RUN pip install : instala las librerías listadas en requirements.txt.
RUN apt-get ... : instalar curl para healthcheck.
COPY ... : Copia el resto del proyecto al contenedor.
EXPOSE 5000: Indica que Flask/Gunicorn utilizará el puerto 5000.
HEALTHCHECK CMD curl: añadir comprobaciones automáticas de salud de los contenedores.
CMD : inicia la aplicación utilizando Gunicorn.
Construyendo la imagen Docker
Una vez preparados todos los archivos, ya podemos construir la imagen. En Powershell y desde la raíz del proyecto ejecutamos:
docker build -t dashforge-spacex .
Docker empezará a:
descargar la imagen base
instalar dependencias
copiar el proyecto
construir la imagen final
docker build: Es el comando principal que le ordena a Docker empaquetar tu aplicación, sus dependencias (como Python, Pandas, Scikit-Learn) y el sistema operativo base en una sola imagen aislada.
-t dashforge-spacex(Tag): Asigna un nombre y una etiqueta personalizada a la imagen que estás creando. El nombre que elijas aquí (dashforge-spacex) es la referencia exacta que usarás después para arrancar el contenedor con el comando docker run.
.(Punto final): Indica el contexto de construcción, diciéndole a Docker que busque el archivo llamado Dockerfile en el directorio actual.
Ejecutar el contenedor
Cuando la imagen termina de construirse, podemos iniciar la aplicación:
docker run -d -p 5010:5000--name dashforge-spacex dashforge-spacex
docker run -d : Esto crea un contenedor en segundo plano -d (Detached).
-p 5000:5000 (Publish / Ports): Conecta un puerto de tu computadora real (Anfitrión) con un puerto dentro del contenedor (Contenedor) siguiendo la estructura -p puerto_externo:puerto_interno.
Primer 5010 (Externo): El puerto de tu máquina real. Podrás abrir tu navegador web e ingresar a http://localhost:5000 para ver tu app.
Segundo 5000 (Interno): El puerto donde tu servidor (como Flask, Dash o FastAPI) está escuchando dentro del entorno cerrado del contenedor.
--name dashforge-spacex: Asigna un nombre personalizado e identificable a este contenedor específico.
dashforge-spacex(Al final): Es el nombre de la imagen de Docker de origen que vas a utilizar como plantilla para construir este contenedor. Debe coincidir exactamente con el nombre de la imagen que creaste previamente con el comando docker build.
Gestionando contenedores Docker
Docker incluye comandos para administrar los contenedores.
# Ver dontenedores activosdocker ps# Detener un contendordocker stop dashforge-spacex# Eliminar un contenedordocker rm dashforge-spacex# Eliminar una imagen de dockerdocker rmi dashforge-spacex
Preparando DashForge para despliegues dinámicos
Una de las ventajas más potentes de Docker es que cada dashboard puede ejecutarse como un contenedor independiente. Esto permite construir una arquitectura bajo demanda:
El portal principal permanece activo
Las aplicaciones solo se inician cuando un usuario las solicita
Los contenedores pueden apagarse automáticamente tras un periodo de inactividad
Cada proyecto puede ejecutarse aislado, con sus propias dependencias, sus modelos ML y su configuración independiente. Esta arquitectura escala muchísimo mejor que mantener decenas de aplicaciones Flask activas permanentemente.
Se crea el objeto de la aplicación como una instancia de una clase Flask importada del paquete flask.
La variable __name__ que se pasa a la clase Flask es una variable predefinida de Python, cuyo nombre corresponde al del módulo en el que se utiliza.
En la práctica, pasar esta variable __name__ casi siempre configurará Flask correctamente. A continuación, la aplicación importa el módulo routes, que aún no existe.
Un aspecto que puede resultar confuso al principio es que existen dos entidades con el mismo nombre app.
El paquete app que se define mediante el directorio de la aplicación y el script __init__.py , y se hace referencia a él en la declaración from app import routes.
La variable app que se define como una instancia de la clase Flask en el script __init__.py , lo que la convierte en miembro del paquete app.
Otra particularidad es que el módulo routes se importa al final del script y no al principio, como se hace habitualmente. La importación al final es una solución conocida que evita las importaciones circulares , un problema común en las aplicaciones Flask. Verás que el módulo routes necesita importar la variable app definida en este script, por lo que colocar una de las importaciones recíprocas al final evita el error que resulta de las referencias mutuas entre estos dos archivos.
Modulo routes
Las rutas gestionan las diferentes URL que admite la aplicación. En Flask, los manejadores de las rutas de la aplicación se escriben como funciones de Python, llamadas view funtions. Estas funciones se asocian a una o más URL de ruta para que Flask sepa qué lógica ejecutar cuando un cliente solicita una URL determinada.
Aquí está la primera función de vista para esta aplicación, que debes escribir en un nuevo módulo llamado routes.py
Las dos líneas @app.route son decoradores , una característica única del lenguaje Python. Un decorador modifica la función que le sigue. Un patrón común con los decoradores es usarlos para registrar funciones como devoluciones de llamada para ciertos eventos.
En este caso, el decorador @app.route crea una asociación entre la URL proporcionada como argumento y la función. Esto significa que cuando un navegador web solicita cualquiera de estas dos URL, Flask invocará esta función y devolverá su valor de retorno al navegador como respuesta.
Para completar la aplicación, necesitas tener un script de Python en el nivel superior que defina la instancia de la aplicación Flask. Llamemos a este script microblog.py y definámoslo como una sola línea que importe la instancia de la aplicación:
fromappimportapp
¿Recuerdas las dos entidades app? Aquí puedes verlas juntas en la misma oración.
La instancia de la aplicación Flask se llama app y pertenece al paquete app.
La instrucción from app import app importa la variable app que pertenece al paquete app. Si te resulta confuso, puedes cambiar el nombre del paquete o de la variable.
Para asegurarnos de que todo se está haciendo correctamente, a continuación se muestra un diagrama de la estructura del proyecto hasta el momento:
La aplicación está lista, solo hace falta llamarla:
flask--appmicroblog.py--debugrun
Templates
Las plantillas ayudan a lograr la separación entre la presentación y la lógica de negocio. En Flask, las plantillas se escriben como archivos separados, almacenados en una carpeta llamada templates dentro del paquete de la aplicación.
Los marcadores de posición {{ … }} representan las partes de la página que son variables y se cargan en el momento de la ejecución. Ahora que usamos las plantillas, podemos eliminar el texto de la función index() y agregar la plantilla.
from flask import render_templatefrom app import app@app.route('/')@app.route('/index')defindex(): user = {'username': 'Miguel'}return render_template('index.html', title='Home', user=user)
La operación que convierte una plantilla en una página HTML completa se llama renderizado . Para renderizar la plantilla, importa una función Flask llamada render_template(). Esta función realiza esta acción:
Busca el archivo templates/index.html
Sustituye las variables Jinja2
Devuelve el HTML generado al navegador
La función render_template() invoca el motor de plantillas Jinja que viene incluido con el framework Flask. Jinja sustituye los bloques {{ ... }} con los valores correspondientes, proporcionados por los argumentos en la llamada render_template().
Declaraciones condicionales
Ya has visto cómo Jinja reemplaza los marcadores de posición con valores reales durante la renderización, pero esta es solo una de las muchas operaciones potentes que Jinja admite en los archivos de plantilla. Por ejemplo, las plantillas también admiten instrucciones de control, que se encuentran dentro {% ... %}de bloques. La siguiente versión de la plantilla index.html añade una instrucción condicional:
<!doctypehtml><html> <head> {% if title %} <title>{{ title }} - Microblog</title> {% else %} <title>Welcome to Microblog!</title> {% endif %} </head> <body> <h1>Hello, {{ user.username }}!</h1> </body></html>
Bucles
Hasta este punto, la plantilla index.html mostraba únicamente información estática o variables individuales, como el nombre del usuario conectado. Sin embargo, en una aplicación real es habitual trabajar con colecciones de datos, por ejemplo:
Publicaciones de un blog.
Comentarios.
Productos.
Resultados de consultas.
Registros de una base de datos.
Para representar este tipo de estructuras, Jinja2 incorpora instrucciones de control similares a las de Python, entre ellas el bucle for.
En app/routes.py, debajo del usuario autenticado, crea una lista llamada posts. Cada elemento de la lista es un diccionario que representa una publicación con autor:body . A la función añade posts=posts
posts = [ {'author': {'username': 'John'},'body': 'Beautiful day in Portland!' }, {'author': {'username': 'Susan'},'body': 'The Avengers movie was so cool!' }]
Uso del bucle for en Jinja2
En templates/index.html, la colección se recorre mediante la instrucción:
{%for post in posts %}...{% endfor %}
Durante cada iteración, post representa un elemento individual de la lista al que se puede acceder a sus atributos o claves.
Acceso a estructuras anidadas
Jinja2 permite navegar por diccionarios anidados usando la notación de punto, lo que hace el código más legible.
{{ post.author.username }}{{ post.body }}
Herencia de plantillas en Jinja2
La idea consiste en crear una plantilla base que contenga toda la estructura común del sitio y dejar definidos ciertos espacios vacíos, llamados bloques, donde cada página insertará su contenido específico.
La plantilla base
La plantilla base.html define la estructura general de todas las páginas.
El bloque content
La instrucción define una zona reemplazable. La plantilla hija podrá proporcionar el contenido que se insertará exactamente en ese punto.
En este proyecto desarrollaremos una aplicación web con Flask que funcionará como el portal principal de nuestro VPS. Su propósito es servir como punto central de acceso a todos los proyectos, aplicaciones y servicios que vayamos desplegando a lo largo del tiempo.
La página estará disponible en el subdominio projects.fernandorioseco.es y actuará como un auténtico escaparate técnico y profesional. Desde ella será posible entrar a las aplicaciones de proyectos realizados. También cada proyecto contará con su propio subdominio dentro del VPS.
En ese subdominio se desplegará la aplicación funcional del proyecto, es decir, la herramienta o servicio que el usuario podrá utilizar directamente, ya sea una aplicación desarrollada con Flask, Streamlit, FastAPI u otra tecnología.
La explicación detallada del proyecto —objetivos, arquitectura, tecnologías empleadas, proceso de desarrollo y aprendizajes obtenidos— no se mostrará dentro de la aplicación, sino en el sitio principal del portfolio, donde cada proyecto tendrá su propia ficha o artículo descriptivo.
De esta forma se separan claramente dos elementos:
La documentación del proyecto, alojada en el sitio principal del portfolio.
La aplicación desplegada, accesible mediante su subdominio correspondiente.
Este enfoque permite mantener el portfolio organizado y profesional, ofreciendo por un lado la explicación técnica del trabajo realizado y, por otro, el acceso directo a la aplicación en funcionamiento.
Estructura general del proyecto
El propósito del proyecto es construir una aplicación web en Flask desplegada en projects.fernandorioseco.es que actuará como portal central del VPS.
Objetivos
Centralizar el acceso a proyectos.
Mostrar información sobre el VPS.
Presentar el stack tecnológico.
Servir como portfolio profesional.
Para desarrollar el Projects Portal seguiremos una metodología organizada que abarcará desde el diseño de la aplicación en local hasta su despliegue definitivo en el VPS. El proyecto no solo consistirá en construir una aplicación web con Flask, sino también en integrarla dentro de la infraestructura del servidor y conectarla con el resto de aplicaciones del portfolio.
La estructura general del proyecto se dividirá en las siguientes fases:
1 – Planificación y Diseño: En primer lugar definiremos el objetivo del portal, las secciones que incluirá y la experiencia de usuario que queremos ofrecer. En esta fase estableceremos la arquitectura de la aplicación, el diseño de la interfaz y la información que se mostrará en cada sección.
2 – Setup de la Aplicación yControl de Versiones con Git y GitHub: Preparar el setup del proyecto con una app mínima, inicializar un repositorio Git y publicar el código en GitHub para mantener un historial de cambios y facilitar el despliegue en el servidor.
3 – Desarrollo Local: Crearemos el proyecto Flask en nuestro entorno local, organizaremos la estructura de carpetas, diseñaremos las plantillas HTML y desarrollaremos los estilos CSS y la lógica necesaria para mostrar los proyectos de forma dinámica.
4 – Configuración del Dominio: Crearemos el subdominio projects.fernandorioseco.es y configuraremos el registro DNS para que apunte a la dirección IP pública del VPS.
5 – Preparación del VPS: Crearemos el directorio del proyecto en el servidor, clonaremos el repositorio desde GitHub, configuraremos el entorno virtual de Python e instalaremos todas las dependencias necesarias.
6 – Configuración del Servicio con Gunicorn y systemd: Configuraremos Gunicorn como servidor WSGI y crearemos un servicio de systemd para que la aplicación se ejecute automáticamente y permanezca activa tras reinicios del sistema.
7 – Configuración de Nginx y HTTPS: Configuraremos Nginx como proxy inverso para publicar la aplicación en Internet y habilitaremos HTTPS mediante certificados SSL de Let’s Encrypt.
8 – Actualización y Mantenimiento: Definiremos el procedimiento para desplegar nuevas versiones del portal y añadir proyectos al catálogo mediante archivos JSON individuales.
9 –Mejoras y Evolución: Una vez desplegado el portal, podremos incorporar nuevas funcionalidades, como filtros por tecnología, estadísticas del VPS en tiempo real o un panel de administración para gestionar los proyectos desde una interfaz web.
1.Planificación y Diseño
Arquitectura de la Aplicación
La aplicación Projects Portal se desarrollará con Flask siguiendo una arquitectura sencilla, modular y escalable. El objetivo es separar claramente la lógica de negocio, la presentación y los datos para facilitar el mantenimiento y la incorporación de nuevos proyectos.
La arquitectura se basará en los siguientes componentes:
Cliente web (navegador).
Servidor web Nginx.
Servidor de aplicaciones Gunicorn.
Aplicación Flask.
Plantillas HTML con Jinja2.
Archivos estáticos (CSS, JavaScript e imágenes).
Archivo de configuración con los datos de los proyectos.
app/__init__.py: Crea y configura la aplicación Flask, registra las rutas y devuelve la instancia principal de la aplicación.
app/routes.py: Define las rutas de la aplicación y establece que la página principal renderice la plantilla index.html.
run.py: Ejecuta la aplicación en modo desarrollo para realizar pruebas en el entorno local.
wsgi.py: Expone la aplicación para que pueda ser ejecutada por un servidor WSGI como Gunicorn en producción.
app/templates/index.html: Crea la estructura HTML básica de la página principal con el nombre del portal y una breve descripción del laboratorio.
app/data/projects.json: Almacenará la información de los proyectos que se mostrarán dinámicamente en el portal.
requirements.txt: Guarda el listado de dependencias Python necesarias para ejecutar el proyecto.
config.py: Centraliza la configuración general de la aplicación.
app/static/css/: Contendrá las hojas de estilo CSS de la aplicación.
app/static/js/: Contendrá los archivos JavaScript utilizados en la interfaz.
app/static/img/: Almacenará imágenes, iconos y recursos gráficos.
app/templates/base.html: Servirá como plantilla base común para todas las páginas del sitio.
app/templates/components/: Almacenará componentes reutilizables de la interfaz, como la barra de navegación o el footer.
Patrón Arquitectónico
El portal Projects Portal seguirá una versión simplificada del patrón arquitectónico MVC (Model-View-Controller), uno de los modelos de diseño más utilizados en el desarrollo de aplicaciones web. El objetivo es separar claramente los datos, la lógica de la aplicación, y la presentación visual.
Componente MVC
Implementación
Model
app/data/projects.json
View
app/templates/*.html
Controller
app/routes.py
Escalabilidad
La arquitectura permite evolucionar fácilmente hacia:
Base de datos relacional.
Panel de administración.
API REST.
Autenticación.
Caché con Redis.
Contenerización con Docker.
Estructura de la Interfaz de Usuario (UI)
La aplicación Projects Portal estará compuesta por una única página principal diseñada para presentar el laboratorio técnico y ofrecer acceso directo a todos los proyectos desplegados en el VPS. La interfaz se organizará en varias secciones claramente diferenciadas, cada una con una función específica dentro de la experiencia de usuario.
Boceto de referencia aproximado generado por IA:
Barra de Navegación (Navbar): La página comenzará con una barra de navegación fija en la parte superior. Elementos de la navbar:
El visitante accede a projects.fernandorioseco.es.
Visualiza la presentación del laboratorio.
Consulta la infraestructura y tecnologías utilizadas.
Explora las tarjetas de proyectos.
Accede a:
La aplicación en producción.
La documentación del proyecto.
El código fuente.
La documentación de la API, si existe.
Navega a GitHub, blog o LinkedIn.
Objetivo de la interfaz
La UI debe transmitir una imagen:
Profesional.
Moderna.
Tecnológica.
Clara y fácil de navegar.
Al mismo tiempo, debe funcionar como un punto central desde el que cualquier visitante pueda comprender tu infraestructura y acceder rápidamente a todos los proyectos de tu portfolio.
2. Setup de la Aplicación yControl de Versiones
Setup del Proyecto
En esta fase realizaremos la configuración inicial del proyecto en nuestro entorno local. El objetivo es preparar la estructura base de la aplicación, crear un entorno virtual, instalar las dependencias necesarias y verificar que Flask funciona correctamente antes de comenzar con el desarrollo de la interfaz. En esta fase desarrollamos:
La interfaz del Projects Portal se construye siguiendo una arquitectura de plantillas modular basada en el motor de plantillas Jinja, integrado de forma nativa en Flask. Este enfoque permite dividir la interfaz en componentes independientes y reutilizables, facilitando el mantenimiento y la escalabilidad de la aplicación.
La finalidad de esta arquitectura es separar la estructura visual del sitio en piezas con responsabilidades bien definidas:
Una plantilla base común.
Una página principal.
Componentes reutilizables.
Gracias a esta organización, cada sección de la interfaz puede desarrollarse y modificarse de forma aislada. Las plantillas HTML se almacenan en el directorio: app/templates/. La estructura adoptada es la siguiente:
Plantilla base de la aplicación. Define la estructura HTML común del sitio, incluyendo la cabecera del documento, la carga de estilos, la barra de navegación, el bloque de contenido principal y el footer.
index.html
Página principal del portal. Ensambla secuencialmente los distintos componentes visuales mediante instrucciones {% include %}. Contiene muy poca lógica y actúa como orquestador de la interfaz.
components/
Directorio que contiene las secciones independientes que conforman la página principal. Cada archivo representa un bloque funcional concreto.
navbar.html
Barra de navegación superior.
hero.html
Sección principal de presentación del portal.
vps_info.html
Información técnica del servidor VPS.
tech_stack.html
Tecnologías y herramientas utilizadas en el laboratorio.
projects.html
Listado dinámico de proyectos desplegados.
resources.html
Enlaces externos a documentación, GitHub, blog y LinkedIn.
Jinja2 se encarga automáticamente de resolver la herencia e inclusiones.
base.html – plantilla base de la aplicación
El archivo base.html constituye la plantilla base del Projects Portal. Su función es definir la estructura HTML común que compartirán todas las páginas del sitio. Este enfoque permite centralizar los elementos globales de la interfaz y evitar la duplicación de código. Ubicación del archivo: app/templates/base.html
Propósito
La plantilla base se encarga de definir la estructura general del documento HTML e incluir aquellos elementos que deben estar presentes en todas las páginas de la aplicación. En concreto, base.html incorpora:
Definir la estructura HTML general del documento.
Configurar las etiquetas meta necesarias.
Establecer un título por defecto.
Cargar la hoja de estilos.
Incluir la barra de navegación.
Definir un bloque de contenido que heredarán las páginas hijas.
Incluir el footer.
Estructura conceptual
[ DOCUMENTO HTML ] (Template Maestro) │ ├── [ HEAD ] (Configuración Invisible) │ ├── Metadatos (Viewport, Charset) │ ├── Título Dinámico {% block title %} │ └── Estilos Propios (styles.css) │ └── [ BODY ] (Estructura Visible) │ ├── [ HEADER ] ─── {% include "navbar.html" %} ── (Componente Reutilizable) │ ├── [ MAIN ] ───── {% block content %} ──────── (Espacio Inyectable/Variable) │ *Aquí se carga el contenido* │ *específico de cada ruta* │ └── [ FOOTER ] ─── {% include "footer.html" %} ── (Componente Reutilizable)
Elementos de Jinja2 utilizados
title: este permite definir un título por defecto y sobrescribirlo desde plantillas hijas.
content: reserva el espacio donde se insertará el contenido específico de cada página.
include: inserta componentes reutilizables como la barra de navegación y el footer.
url_for(): genera automáticamente la URL correcta para los archivos estáticos.
Relación con el resto de plantillas
base.html es utilizada por index.html mediante la instrucción: {% extends "base.html" %}. A partir de ese momento, index.html solo necesita definir el contenido correspondiente al bloque content.
La barra de navegación es el componente situado en la parte superior de la página y proporciona acceso a recursos externos relevantes. Al tratarse de un elemento común a todas las páginas del sitio, se implementa como un componente independiente que es incluido desde base.html. Ubicación del archivo: app/templates/components/navbar.html
Propósito del componente
Identificar visualmente el sitio mediante el nombre del portal.
Proporcionar acceso a recursos externos como GitHub, el blog y LinkedIn.
Los enlaces externos se abren en una nueva pestaña mediante target="_blank".
Relación con base.html
La plantilla base incorpora este componente mediante: {% include "components/navbar.html" %}. De esta forma, cualquier modificación realizada en navbar.html se reflejará automáticamente en todas las páginas del sitio.
Diseño visual
La navbar se diseñará con las siguientes características:
Posición fija o sticky en la parte superior.
Fondo blanco o semitransparente.
Sombra ligera.
Distribución horizontal.
Adaptación responsive para dispositivos móviles.
hero.html – sección principal de presentación
Define la sección superior de la página principal y constituye el primer elemento visual que verá el visitante al acceder al portal. Su objetivo es comunicar de forma inmediata qué es Projects Portal, cuál es su propósito y qué tipo de proyectos alberga. Ubicación del archivo: app/templates/components/hero.html
Elementos: La sección estará compuesta por dos columnas.
La sección presentará el portal como: Un laboratorio personal desplegado en un VPS, orientado a DevOps, Ciencia de Datos, MLOps e Ingeniería de Inteligencia Artificial.
Diseño visual
El Hero Section tendrá las siguientes características:
Fondo azul oscuro.
Texto en color blanco.
Diseño en dos columnas.
Botones de llamada a la acción.
Imagen ilustrativa a la derecha.
Amplio espaciado vertical.
Relación con index.html
La página principal incluirá este componente mediante: {% include "components/hero.html" %}
vps_info.html – infraestructura del VPS
El componente vps_info.html muestra un resumen de las principales características técnicas del servidor donde se alojan el Projects Portal y el resto de aplicaciones del portfolio. Esta sección aporta contexto técnico y refuerza la credibilidad del portal al evidenciar que los proyectos se ejecutan sobre una infraestructura real administrada por el propio autor. Ubicación del archivo: app/templates/components/vps_info.html
Propósito
La sección tiene como objetivo presentar, de forma visual y estructurada, la configuración básica del VPS utilizado como entorno de despliegue. Permite al visitante conocer:
Los iconos utilizados en esta sección se almacenarán en: app/static/img/icons/
Relación con index.html
La página principal incluirá esta sección mediante: {% include "components/vps_info.html" %}
projects.html – catálogo dinámico de proyectos
El componente projects.html es la sección más importante del portal, ya que muestra las aplicaciones desplegadas en el VPS y proporciona acceso directo a cada una de ellas.
A diferencia del resto de secciones, su contenido se genera dinámicamente a partir de los datos almacenados en projects.json. El archivo se ubica en: app/templates/components/projects.html.
La sección incorpora un buscador dinámico que permite filtrar proyectos por cualquier término relevante, como tecnologías, categorías, nombre o palabras incluidas en la descripción. El buscador permite al usuario encontrar proyectos escribiendo uno o varios términos de búsqueda.
Cada tarjeta de proyecto almacena en el atributo data-search el contenido que será evaluado por JavaScript. Este atributo incluye: nombre del proyecto, descripción y tecnologías utilizadas.
Lógica de filtrado
El archivo main.js:
Captura el contenido del input.
Divide el texto por comas.
Normaliza los términos.
Recorre todas las tarjetas.
Muestra solo las que contienen todos los términos.
Propósito
Esta sección tiene como objetivo presentar cada proyecto del portfolio mediante una tarjeta con información resumida y enlaces relevantes. Cada tarjeta permitirá al visitante acceder a:
Los datos de los proyectos se almacenarán en el archivo: app/data/projects.json. Cada registro del archivo contendrá toda la información necesaria para construir una tarjeta.
Información mostrada en cada tarjeta
Nombre del proyecto.
Descripción breve.
Tecnologías utilizadas.
Enlaces de acción.
Estructura conceptual de una tarjeta
[ DATOS: Lista de Proyectos ] │ ▼ (Ciclo for project in projects)[ ARTICLE: .project-card ] <──────────────────┐ │ │ ├── [ .project-title ] ─── {{ project.name }} │ │ │ ├── [ .project-description ] ─ {{ desc }} │ (Repite por cada │ │ proyecto en la lista) ├── [ .project-tech ] ────────────────────────┤ │ └── [ .tech-badge ] ─ {{ tech }} (Bucle) │ │ │ └── [ .project-links ] ───────────────────────┘ ├── App URL ├── Documentation ├── GitHub └── [ IF: API URL ] ── (Carga condicional)
Generación dinámica con Jinja2
La plantilla recorrerá la lista projects enviada desde Flask mediante un bucle for. Por cada elemento del JSON se generará automáticamente una nueva tarjeta.
Relación con Flask
La ruta principal cargará los datos y los enviará a la plantilla: return render_template("index.html", projects=projects)
Archivos involucrados
La página principal incluirá esta sección mediante: {% include "components/projects.html" %}, en la acción de búsqueda se involucran :
Archivo
Función
projects.html
Campo de búsqueda y atributo data-search.
main.js
Lógica de filtrado.
styles.css
Estilos del buscador.
base.html
Carga del archivo JavaScript.
resources.html – recursos y enlaces externos
El componente resources.html agrupa los enlaces externos más relevantes relacionados con el Projects Portal y con el perfil profesional del autor. Su objetivo es ofrecer al visitante un acceso rápido a la documentación técnica, al código fuente y a los perfiles profesionales asociados al proyecto. Ubicación del archivo: app/templates/components/resources.html
Propósito
Esta sección actúa como un bloque complementario al catálogo de proyectos. Mientras que cada tarjeta de proyecto contiene enlaces específicos, resources.html reúne los recursos generales del portal y del autor.
La página principal incluirá esta sección mediante: {% include "components/resources.html" %}
footer.html – pie de página
El componente footer.html define la sección final del sitio y contiene información general sobre el portal y enlaces complementarios. Aunque es un elemento sencillo, cumple una función importante al cerrar visualmente la página y ofrecer referencias adicionales al visitante. Ubicación del archivo: app/templates/components/footer.html
Propósito
Mostrar información de copyright.
Identificar al autor del portal.
Incluir enlaces a recursos relevantes.
Cerrar visualmente la página de forma consistente.
La plantilla base lo incluye mediante: {% include "components/footer.html" %}
index.html – ensamblado de la página principal
El archivo index.html es la plantilla principal del Projects Portal. Su función no es definir el contenido detallado de cada sección, sino ensamblar todos los componentes que conforman la página de inicio. Actúa como un punto de integración donde se combinan la plantilla base y los distintos componentes HTML desarrollados previamente. Ubicación del archivo: app/templates/index.html
Propósito
index.html tiene tres responsabilidades principales:
Heredar la estructura general definida en base.html.
Definir el contenido del bloque principal.
Incluir, en el orden correcto, todos los componentes de la interfaz.
Componentes incluidos
La página principal incorpora las siguientes secciones:
hero.html
vps_info.html
tech_stack.html
projects.html
resources.html
La barra de navegación y el footer no se incluyen directamente aquí, ya que son incorporados automáticamente por base.html.
Relación con base.html
La plantilla comienza con: {% extends "base.html" %}. Esto indica que utilizará la estructura general definida en la plantilla base.
Definición del contenido principal
Todo el contenido de la página se inserta dentro del bloque:
Una vez definida la estructura HTML de la aplicación, el siguiente paso consiste en desarrollar la capa de presentación mediante hojas de estilo CSS. En el Projects Portal se ha optado por utilizar CSS puro, sin frameworks externos como Bootstrap, con el objetivo de mantener un control total sobre el diseño y comprender en detalle el comportamiento visual de cada componente. Todos los estilos del proyecto se almacenan inicialmente en: app/static/css/styles.css
La arquitectura CSS tiene como finalidad:
Centralizar todos los estilos de la aplicación.
Mantener una organización clara y escalable.
Reutilizar reglas comunes.
Facilitar el mantenimiento.
Garantizar consistencia visual.
El archivo styles.css se estructurará en bloques claramente diferenciados.
styles.css├── Variables CSS├── Reset básico├── Estilos globales├── Componentes reutilizables├── Layout├── Estilos por sección└── Media queries
La arquitectura CSS del Projects Portal se basa en un único archivo styles.css, organizado en bloques lógicos que incluyen variables, estilos globales, componentes reutilizables y reglas específicas para cada sección de la interfaz.
El Projects Portal ha sido diseñado para adaptarse correctamente a distintos tamaños de pantalla, garantizando una experiencia de usuario adecuada tanto en ordenadores de escritorio como en tablets y teléfonos móviles.
El objetivo del diseño responsive es que todos los elementos de la interfaz se ajusten automáticamente al espacio disponible sin necesidad de crear versiones separadas de la página.
Breakpoints
La adaptación a diferentes resoluciones se realiza mediante media queries. Los puntos de ruptura utilizados se corresponden con los tamaños habituales de dispositivos:
Móviles pequeños.
Tablets.
Portátiles.
Monitores de escritorio.
Técnicas utilizadas
Para lograr el comportamiento responsive se emplean:
CSS Grid.
Flexbox.
Media queries.
Unidades relativas (rem, %, fr).
Imágenes fluidas (max-width: 100%).
Resultado preliminar de la estructura + css
Haz clic en la imagen para ver.
Incorporación de Recursos Visuales y Ajustes Finales de UI
El propósito es transformar una interfaz funcional en una interfaz visualmente pulida.
Descarga de iconos
Optimización de imágenes
Integración de logos tecnológicos
Ilustración del Hero Section
Ajustes de CSS
Refinamiento de tipografía y colores
Corrección de detalles responsive
Revisión visual final
Resultado final de la aplicación web
4. Configuración del dominio
Una vez completado el desarrollo local de la aplicación, el siguiente paso consiste en asociarla a un dominio público para que pueda ser accesible desde Internet. En este proyecto, la aplicación Flask se publicará en el subdominio: projects.fernandorioseco.es
En el proveedor donde gestionas tu dominio, debes crear un registro de tipo A.
Campo
Valor
Tipo
A
Nombre
projects
Valor
IP pública del VPS
TTL
Auto
Verificar la propagación DNS
Una vez creado el registro, el subdominio debe resolver hacia la IP del servidor.
nslookup projects.fernandorioseco.es
5. Preparación del VPS
Una vez que la aplicación ha sido desarrollada localmente y el subdominio ya apunta al servidor, el siguiente paso consiste en preparar el VPS para alojar el proyecto.
En esta fase se crea la estructura de directorios en el servidor, se descarga el código fuente desde GitHub, se configura un entorno virtual de Python y se instalan las dependencias necesarias para ejecutar la aplicación.
La preparación del VPS tiene como finalidad:
Crear el directorio donde residirá el proyecto: /var/www.
Descargar el código fuente desde el repositorio remoto.
Configurar un entorno virtual aislado.
Instalar las librerías definidas en requirements.txt.
Verificar que la aplicación puede ejecutarse en el servidor.
# Crear la carpeta del proyectosudomkdir-p/var/www/projects-portal# Cambiar propietario y grupo de los archivos del proyectosudochown-R $USER:$USER /var/www/projects-portal# clonar repositorio desde githubgitclonehttps://github.com/fer78/projects-portal.git/var/www/projects-portal
Crear entorno virtual
# en sistemas Debian/Ubuntu no viene instalado por defectosudoaptinstallpython3.12-venv# Accede al directoriocd/var/www/projects-portal# Crea el entorno virtual y activalopython3-mvenvvenvsourcevenv/bin/activate# Instalar dependenciaspipinstall--upgradepippipinstall-rrequirements.txt# comprobar que las dependencias se instalaron correctamentepiplist
Probar la aplicación Flask.
python run.py
Si todo es correcto, Flask iniciará el servidor de desarrollo.
Probar Gunicorn
gunicorn --bind 127.0.0.1:8000 wsgi:app
Este comando confirma que la aplicación está lista para ejecutarse en producción.
Archivos implicados
Archivo
Función
requirements.txt
Lista de dependencias Python.
wsgi.py
Punto de entrada para Gunicorn.
venv/
Entorno virtual del proyecto.
run.py
Script de desarrollo local.
6. Configuración del servicio con Gunicorn y systemd
Una vez que la aplicación Flask funciona correctamente en el VPS, el siguiente paso consiste en ejecutarla como un servicio del sistema. Para ello utilizaremos:
Gunicorn como servidor WSGI.
systemd como gestor de servicios de Linux.
Esta configuración permite que la aplicación se inicie automáticamente al arrancar el servidor y se reinicie en caso de fallo.
La configuración del servicio persigue los siguientes objetivos:
Ejecutar la aplicación Flask en segundo plano.
Iniciar el servicio automáticamente en cada reinicio.
Reiniciar el proceso si se detiene inesperadamente.
[Unit]Description=Gunicorn service for Projects PortalAfter=network.target[Service]User=tu usuarioGroup=www-dataWorkingDirectory=/var/www/projects-portalEnvironment="PATH=/var/www/projects-portal/venv/bin"ExecStart=/var/www/projects-portal/venv/bin/gunicorn \ --workers 3 \ --bind 127.0.0.1:8000 \ wsgi:appRestart=always[Install]WantedBy=multi-user.target
Poner a punto el servicio
# reiniciar systemdsudosystemctldaemon-reload# habilitar el serviciosudosystemctlstartprojects-portal# verificar el estadosudosystemctlstatusprojects-portal
La aplicación arrancará automáticamente tras cada reinicio.
Los logs estarán disponibles mediante journalctl.
El proceso se reiniciará automáticamente ante fallos.
7. Configuración de Nginx y HTTPS
Una vez que la aplicación Flask se ejecuta correctamente mediante Gunicorn y systemd, el siguiente paso consiste en publicarla en Internet a través de un servidor web. Para ello utilizaremos:
Nginx como proxy inverso.
Certbot para obtener certificados SSL.
Let’s Encrypt como autoridad certificadora.
Esta etapa permite:
Asociar el subdominio projects.fernandorioseco.es a la aplicación.
El siguiente paso es crear un archivo de configuración de Nginx para la aplicación Flask. Ese archivo define cómo Nginx debe responder cuando alguien acceda a projects.fernandorioseco.es.
sudoln-s/etc/nginx/sites-available/projects-portal\/etc/nginx/sites-enabled/# Validar la configuraciónsudonginx-t# Recargar Nginxsudosystemctlreloadnginx
Configurar HTTPS con Let’s Encrypt
Generar el certificado SSL con Let’s Encrypt y Certbot.
sudocertbot--nginx-dprojects.fernandorioseco.es
Verificar acceso seguro
La aplicación deberá quedar disponible en:
https://projects.fernandorioseco.es
Flujo de resolución del dominio
El usuario accede al subdominio.
DNS devuelve la IP del VPS.
Nginx recibe la petición.
Nginx reenvía la petición a Gunicorn.
Gunicorn ejecuta Flask.
Flask devuelve la página renderizada.
8. Actualización y Mantenimiento
Una vez desplegado el portal, es importante definir un procedimiento claro para actualizar el código, incorporar nuevos proyectos y mantener la aplicación operativa en el tiempo. La arquitectura adoptada, basada en archivos JSON individuales y control de versiones con Git, simplifica enormemente estas tareas de establecer un flujo de trabajo para:
Desplegar nuevas versiones del código.
Añadir proyectos al catálogo.
Corregir errores.
Reiniciar servicios.
Supervisar el funcionamiento del portal.
Flujo de actualización del código
El proceso habitual consiste en:
Realizar cambios en el entorno local (añadir nuevos proyectos o funcionalidades)
Confirmar los cambios en Git.
Enviar los cambios al repositorio remoto.
Actualizar el VPS mediante git pull.
Reiniciar el servicio.
# ve a la carpeta del proyectocd/var/www/projects-portal# Actualiza los ficheros desde githubgitpulloriginmain# reinicia el servicio sudosystemctlrestartprojects-portal# verifica el funcionamientosudosystemctlstatusprojects-portal
El mantenimiento del portal se reduce a actualizar el repositorio Git, añadir nuevos archivos JSON y reiniciar el servicio Gunicorn cuando sea necesario.
Script de Actualización del Portal
En vez de estar haciendo secuencias de comandos en cada actualización, creamos un script en Bash que agilice el proceso. Este script descargará los últimos cambios desde GitHub, reiniciará el servicio de Gunicorn y mostrará el estado final de la aplicación.
Crear el Script
nano/var/www/projects-portal/update.sh
#!/bin/bashset-eecho"=========================================="echo" Updating Projects Portal"echo"=========================================="cd/var/www/projects-portalecho"Pulling latest changes from GitHub..."gitpulloriginmainecho"Restarting Gunicorn service..."sudosystemctlrestartprojects-portalecho"Checking service status..."sudosystemctlstatusprojects-portal--no-pagerecho"=========================================="echo" Update completed successfully"echo"=========================================="
Permisos de ejecución y añadir al PATH.
# permisos de ejecucionchmod+x/var/www/projects-portal/update.sh# añadir al path para ejecutar desde cualquier ubicacionsudoln-s/var/www/projects-portal/update.sh/usr/local/bin/update-portal
Ahora el script de actualización es un comando que puede ejecutarse desde cualquier ubicación
update-portal
9. Mejoras y Evolución
El Projects Portal se ha diseñado con una arquitectura modular y escalable que facilita la incorporación de nuevas funcionalidades. A medida que el catálogo crezca, el portal puede evolucionar desde una página estática hacia una plataforma más avanzada de gestión y visualización de proyectos.
Posibles mejoras
Buscador avanzado: Filtros por tecnología, por tipo de proyecto u ordenación.
Métricas del VPS: CPU, memoria, uso de disco, uptime, etc.
Panel de administración: Crear y editar proyectos desde una interfaz web, subir imágenes y recursos.
Base de datos: migración desde archivos JSON a PostgreSQL.
API REST: Exponer los proyectos mediante una API desarrollada con FastAPI.
Integración con GitHub: mostrar automáticamente estrellas, forks y fecha de última actualización.
Dashboards embebidos: integrar aplicaciones desarrolladas con Streamlit, Dash o Gradio.
Conclusiones
El desarrollo de Projects Portal ha permitido construir una aplicación web completa utilizando una arquitectura moderna basada en Python y Flask, con despliegue profesional sobre un VPS Linux.
Aunque funcionalmente se trata de un portal sencillo, el proyecto integra todos los componentes fundamentales que intervienen en el ciclo de vida de una aplicación web en producción: desarrollo local, control de versiones, despliegue, automatización de servicios, configuración del servidor web y publicación segura mediante HTTPS.
Conocimientos aplicados
A lo largo del proyecto se han puesto en práctica competencias técnicas de distintas áreas.
Desarrollo Backend
Programación en Python.
Desarrollo web con Flask.
Uso de plantillas con Jinja2.
Lectura dinámica de archivos JSON.
Diseño modular de componentes.
Frontend
HTML5 semántico.
CSS3 con diseño responsive.
JavaScript para búsqueda y filtrado.
Integración de recursos gráficos e iconografía.
DevOps y Linux
Administración de Ubuntu.
Gestión de permisos y estructura de directorios.
Uso de entornos virtuales.
Automatización con systemd.
Despliegue con Gunicorn.
Configuración de Nginx.
Redes e Infraestructura
Configuración de DNS.
Gestión de subdominios.
Resolución de nombres.
Configuración de HTTPS.
Certificados SSL con Let’s Encrypt.
Control de versiones
Uso de Git.
Publicación en GitHub.
Flujo de despliegue basado en git pull.
Diseño arquitectónico
El proyecto ha sido diseñado siguiendo principios de modularidad y mantenibilidad:
Plantillas HTML desacopladas.
Archivos JSON individuales por proyecto.
Separación entre contenido, lógica y presentación.
Servicio persistente gestionado por systemd.
Proxy inverso con Nginx.
Valor del proyecto
Projects Portal cumple una doble función.
Portfolio técnico: Actúa como punto central para acceder a todos los proyectos desplegados.
Laboratorio práctico: Sirve como entorno real para aplicar conocimientos.
Reflexión final
Projects Portal demuestra la capacidad de diseñar, desarrollar y desplegar una aplicación web completa siguiendo prácticas profesionales de ingeniería de software e infraestructura.
El proyecto integra desarrollo backend, frontend, administración de sistemas, redes y automatización, constituyendo una evidencia sólida de competencias en programación, análisis de datos, DevOps y tecnologías modernas de Inteligencia Artificial.
Tutorial paso a paso para dejar un servidor virtual listo para trabajar con Python, Docker, GitHub, Jupyter, FastAPI y herramientas de MLOps. Un VPS (Virtual Private Server) es un servidor virtual que alquilas en la nube y al que accedes como si fuera una máquina Linux real. Disponer de un VPS propio es una de las mejores formas de practicar:
Administración Linux
DevOps
Docker y Kubernetes
CI/CD
APIs con FastAPI
Aplicaciones de IA agéntica
MLOps
Despliegue de modelos de Machine Learning
En esta guía vamos a configurar un VPS desde cero para convertirlo en un entorno profesional de laboratorio.
Características Recomendadas para un VPS de prácticas
Antes de contratar un VPS, conviene elegir una configuración equilibrada que permita trabajar con herramientas de desarrollo, contenedores y cargas moderadas de machine learning.
Configuración mínima recomendada
Para un laboratorio personal de DevOps, Data Science y MLOps, estas especificaciones son más que suficientes:
Recurso
Recomendación mínima
Recomendación ideal
CPU
2 vCPU
4 vCPU
Memoria RAM
4 GB
8 GB o más
Almacenamiento
40 GB SSD/NVMe
80 GB NVMe
Transferencia
1 TB/mes
2 TB/mes o más
Sistema operativo
Ubuntu 24.04 LTS
Ubuntu 24.04 LTS
Acceso root
Sí
Sí
Snapshots
Deseable
Muy recomendable
Backup automático
Opcional
Recomendable
Configuración recomendada según el uso
Caracteristica
Laboratorio básico
Laboratorio profesional
Laboratorio Avanzado
CPU
2
4
8
RAM
4 GB
8 GB
16 GB
NVMe
40 GB
80 GB
160 GBNVMe
Adecuado para:
Linux, SSH, Python, GitHub Actions, Docker Básico
Docker Compose, FastAPI, MLflow, PostgreSQL, DVC, Entrenamiento de modelos moderados.
Una vez completada la suscripción del servicio, nos llegan al correo las credenciales y dirección IP en aproximadamente 30 min. En la consola ya podemos realizar la primera conexión con ssh:
ssh root@IP_DEL_SERVIDOR
ssh: El programa que establece la conexión segura y cifrada.
root: El nombre de usuario con el que quieres entrar. En este caso, es el superusuario (el que tiene control total sobre el servidor).
@203.0.113.10: La dirección IP del servidor remoto al que te quieres conectar.
Primeras acciones después de activar tu VPS
Actualizar el sistema
sudo apt update && sudo apt upgrade -y
sudo: te da permisos de administrador para poder realizar cambios en el sistema.
apt update: descarga la información más reciente sobre qué programas tienen versiones nuevas disponibles. No instala nada, solo actualiza la lista.
&&: Es un conector lógico. Le dice a la terminal: “si el primer comando termina con éxito, ejecuta el siguiente inmediatamente”.
apt upgrade: Compara tus programas instalados con la lista nueva y descarga e instala las actualizaciones.
-y: Significa “Yes” (Sí). Responde automáticamente que sí a la pregunta de confirmación, así no tienes que presionar ninguna tecla para que la instalación continúe.
Crear un usuario administrativo
adduser usuariousermod -aG sudo usuario
adduser usuario: Crea una cuenta de usuario nueva llamada “usuario”.
Te pedirá que establezcas una contraseña.
Creará automáticamente su carpeta personal (/home/usuario) y configurará los archivos básicos del entorno.
usermod -aG sudo usuario: Le otorga permisos de administrador.
-aG sudo: Añade (-a de append) al usuario al grupo (-G) llamado sudo.
Esto permite que el nuevo usuario pueda ejecutar comandos con sudo (como el de actualización que vimos antes) usando su propia contraseña.
Después de ejecutar estos comandos, el nuevo usuario debe cerrar sesión y volver a entrar para que los permisos de administrador se activen.
Configurar la autenticación basada en llaves (en lugar de usar contraseñas).
Este comando se usa para crear un nuevo par de llaves SSH, que es una forma mucho más segura (y cómoda) de entrar a tu servidor que usar una contraseña tradicional.
ssh-keygen: La herramienta para generar las llaves.
-t ed25519: Especifica el tipo de algoritmo. Ed25519 es el estándar moderno más recomendado porque es increíblemente seguro, rápido y genera llaves cortas.
-C "[email protected]": Añade una etiqueta o comentario (normalmente tu email) al final de la llave pública para que sepas a quién pertenece cuando la veas en el servidor.
Cuando se ejecute el comando, te pregunta si guarda la clave en la ubicación por defecto: ~/.ssh/id_ed25519 (presiona Enter para aceptar). Luego te pedirá una “frase de paso” (opcional). Es una contraseña extra para proteger tu llave privada si alguien robara tu ordenador. Como resultado se crearán dos archivos en tu carpeta .ssh:
id_ed25519.pub (Llave Pública): Es como el candado. Esta es la que debes copiar al servidor para poder entrar sin contraseña.
id_ed25519 (Llave Privada): Es como tu llave física. Nunca la compartas ni la subas a ningún sitio.
Instalación de la llave pública en el servidor (o SSH Key Deployment).
Si usas Linux: ssh-copy-id user@IP_DEL_SERVIDORSi usas Windows:type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh user@IP_DEL_SERVIDOR "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
Este código se conecta al servidor remoto usando tu contraseña por última vez.
Copia el contenido de tu llave pública (~/.ssh/id_ed25519.pub) al servidor.
Lo agrega automáticamente en un archivo especial llamado ~/.ssh/authorized_keys dentro de la carpeta del usuario remoto.
Configura los permisos correctos de las carpetas para que el servidor acepte la llave.
A partir de este momento, cuando escribas ssh user@IP_DEL_SERVIDOR, el servidor reconocerá tu “candado” y te dejará entrar sin pedirte contraseña, o si configuras una palabra de paso (passphrase) te la pedirá al loguearte.
Copiar tu llave privada a un pendrive
Puedes copiar tu llave privada a un pendrive y luego usarla en otro ordenador desde la consola de Windows (PowerShell). Supongamos que tu pendrive es la unidad D: Ejecuta esto en PowerShell:
# Crear una carpeta en el pendrive para la llavemkdir D:\mykeys# Copiar la llave privadacopy $env:USERPROFILE\.ssh\id_ed25519 D:\mykeys\
Cómo usarla desde otro ordenador
Cuando estés en el otro ordenador y quieras conectarte a tu servidor usando la llave del pendrive puedes apuntar a ella directamente al conectarte:
El SSH Hardening es un conjunto de configuraciones de seguridad aplicadas al servicio de acceso remoto para evitar ataques. Por defecto, SSH es como una puerta con una cerradura estándar; el hardening es como poner una puerta blindada, cambiar la cerradura por un lector de huellas y ocultar la ubicación de la casa.
El objetivo principal es pasar de una autenticación basada en “algo que sabes” (contraseñas, que pueden ser adivinadas o robadas) a “algo que tienes” (una llave criptográfica única).
Los 3 Pilares del Fortalecimiento
Eliminación de contraseñas: Se desactiva la posibilidad de loguearse con clave, obligando al uso de llaves SSH (Ed25519).
Restricción de privilegios: Se prohíbe el acceso directo al usuario root. Debes entrar con un usuario normal y escalar privilegios solo cuando sea necesario.
Reducción de exposición: Se configuran parámetros para que el servidor ignore intentos de conexión malintencionados.
Riesgos y Cómo evitarlos:
Riesgo
Explicación
Cómo evitarlo
Bloqueo Total (Lockout)
Si tu ordenador se rompe o pierdes tu llave privada, no podrás entrar al servidor porque las contraseñas están desactivadas.
Guarda una copia de tu llave privada en un gestor de contraseñas o en un USB cifrado.
Robo de Llave Privada
Si alguien copia tu archivo id_ed25519, puede entrar a tu servidor sin esfuerzo.
Ponle una passphrase (contraseña) a tu llave al generarla. Así, aunque la roben, no podrán usarla sin la clave.
Cambio de IP del Servidor
Si el servidor cambia de IP y no tienes acceso físico, la llave SSH no servirá de nada si no sabes dónde conectar.
Usa una IP estática o un nombre de dominio (DNS) vinculado a tu servidor.
Visto lo anterior, si necesitas tener una buena seguridad en tu VPS, realiza las siguientes tareas:
Ejecuta el siguiente comando en una terminal para editar el fichero sshd_config. (sustituye ? por i).
sudo nano /etc/ssh/sshd_conf?g
Si deseas eliminar la entrada por contraseñas, realiza los siguientes cambios:
PermitRootLogin no: Prohíbe que alguien intente entrar directamente como root. Ahora es obligatorio entrar con tu usuario normal (user) y luego usar sudo. Esto frena miles de ataques automatizados que prueban contraseñas contra el usuario root.
PasswordAuthentication no: Esta es la medida más importante. Desactiva las contraseñas por completo para SSH. A partir de ahora, si alguien no tiene tu archivo de llave privada, no podrá entrar aunque adivine tu contraseña.
PubkeyAuthentication yes: Asegura que el sistema permita el acceso mediante el par de llaves (pública/privada) que configuraste antes.
sudo systemctl restart ssh: Aplica los cambios inmediatamente.
¡Aviso muy importante! No toques esa ventana de la terminal después de reiniciar el servidor ssh. Úsala como “salvavidas”. Si algo falla, desde ahí puedes volver a abrir el archivo de configuración y arreglarlo.
Reinicia el servidor ssh.
sudo systemctl restart ssh
Abre una ventana de PowerShell (en Windows) o una terminal nueva (en Linux/Mac) totalmente independiente de la primera. Intenta conectar desde la nueva ventana:
ssh user@IP_DEL_SERVIDOR<br>
Analiza el resultado:
Si entras directamente (o te pide la contraseña de tu llave): ¡Éxito! El hardening funciona. Ahora sí puedes cerrar todas las sesiones.
Si te pide la contraseña del usuario “fernan” (la de texto): algo falló. El servidor sigue aceptando contraseñas. Revisa que pusiste PasswordAuthentication no.
Si te dice “Permission denied (publickey)”: ¡Cuidado! El servidor ha bloqueado las contraseñas, pero no reconoce tu llave.
¿Qué hacer si falla?: Como tienes la primera ventana todavía abierta, vuelve a ella y:
Revisa el archivo: sudo nano /etc/ssh/sshd_con*
Asegúrate de que tu llave pública esté en ~/.ssh/authorized_keys.
Reinicia de nuevo: sudo systemctl restart ssh.
Vuelve a probar en la segunda ventana.
Configurar Firewall UFW
Configurar y activar el firewall (cortafuegos) de tu servidor, asegurándote de que no te bloquee el acceso SSH que acabas de configurar.
sudo ufw allow OpenSSH: Crea una regla que permite el tráfico para el perfil “OpenSSH”. Es la forma más recomendada porque el sistema ya sabe qué puertos usa SSH por defecto.
sudo ufw allow 22/tcp: Hace casi lo mismo que el anterior, pero de forma manual especificando el puerto 22 y el protocolo TCP. Es redundante si ya hiciste el anterior, pero sirve como refuerzo.
sudo ufw enable: Activa el firewall.
Al ejecutar el comando, te lanzará un aviso: “Command may disrupt existing ssh connections. Proceed with y/n?”. Escribe y y presiona Enter. Como ya permitiste el puerto 22 en los pasos anteriores, no perderás la conexión.
sudo ufw status: Te muestra la lista de reglas activas para confirmar que el puerto 22 (SSH) está en modo ALLOW (Permitido).
Si en el futuro decides cambiar el puerto de SSH (por ejemplo al 2222) para evitar ataques automáticos, primero deberás hacer un sudo ufw allow 2222/tcp antes de cambiar la configuración del servidor, o te quedarás fuera.
Instalar Fail2Ban
Fail2Ban es un Framework de Prevención de Intrusiones (IPS) ligero y modular, escrito en Python. Su función técnica no es solo vigilar, sino actuar como una capa de automatización de políticas de red. En lugar de depender de la intervención manual de un administrador para bloquear ataques, Fail2Ban establece un puente entre el análisis de logs (aplicación) y la gestión de paquetes (red).
Características Técnicas
Persistencia: Permite mantener bases de datos de atacantes recurrentes, lo que ayuda a identificar patrones de ataque persistentes y a establecer prohibiciones a largo plazo para IPs con comportamiento malicioso sistemático.
Arquitectura Basada en Jails (Jaulas): Permite configurar entornos aislados para cada servicio (SSH, Nginx, MySQL). Cada “jail” combina un filtro (la expresión regular que busca el error) con una acción (la respuesta del firewall).
Gestión Dinámica de Reglas Netfilter: A diferencia de las reglas estáticas de un firewall tradicional, Fail2Ban inyecta y remueve reglas en las tablas de iptables o nftables dinámicamente, minimizando el impacto en el rendimiento del sistema.
Multiservicio: Aunque su uso más común es SSH, es estándar en la industria para mitigar ataques de denegación de servicio (DDoS) a nivel de aplicación en servidores web, intentos de inyección en bases de datos y ataques de fuerza bruta en servidores de correo.
Instala el paquete desde los repositorios oficiales:
sudo apt updatesudo apt install fail2ban -y
Configuración (Crear tu “Jail”):
Para esto lo correcto es crear una copia local llamada jail.local. Nunca edites el archivo jail.conf directamente, ya que se sobrescribe al actualizar.
Si te equivocas de contraseña 3 veces, tú también serás bloqueado. Para evitarlo, busca la línea ignoreip en tu archivo jail.local y añade tu IP local.
ignoreip = 127.0.0.1/8 ::1 TU_IP_AQUÍ
Si te quedas fuera por error o bloqueas a un compañero, puedes liberar la IP inmediatamente con este comando:
sudo fail2ban-client set sshd unbanip TU_IP_AQUÍ
Si no estás seguro de qué IPs están baneadas en este momento, primero listalas con:
sudo fail2ban-client status sshd
Configurar zona horaria
Comprobar la zona horaria del servidor
timedatectl
Establecer tu zona horaria, en mi caso Islas Canarias
sudo timedatectl set-timezone Atlantic/Canary
Herramientas útiles para administrar un VPS
Estas utilidades son ampliamente utilizadas en administración de sistemas, DevOps y operaciones.
tmux — Permite mantener sesiones de terminal persistentes y ejecutar procesos largos aunque se cierre la conexión SSH.
htop — Monitor interactivo de procesos para visualizar CPU, memoria y procesos en tiempo real.
btop — Monitor avanzado del sistema con métricas de CPU, RAM, disco y red.
tree — Muestra la estructura de directorios en formato de árbol.
jq — Procesa y filtra datos JSON desde la línea de comandos.
ncdu — Analiza el uso del disco y muestra qué directorios ocupan más espacio.
rsync — Sincroniza archivos y directorios de forma eficiente, ideal para backups y despliegues.
lsof — Muestra qué procesos tienen abiertos archivos, sockets y puertos.
dnsutils — Incluye herramientas como dig y nslookup para diagnosticar DNS.
mtr — Analiza conectividad de red combinando ping y traceroute.
sysstat — Proporciona herramientas como iostat, sar y mpstat para análisis de rendimiento.
ufw — Firewall simplificado para gestionar reglas de red.
fail2ban — Bloquea automáticamente IPs con intentos repetidos de acceso fallido.
unattended-upgrades — Instala automáticamente actualizaciones de seguridad del sistema.
tcpdump — Captura y analiza tráfico de red a bajo nivel.
iotop — Muestra qué procesos están generando actividad de lectura/escritura en disco.
bash-completion — Añade autocompletado avanzado para comandos y opciones en Bash.
git — Sistema de control de versiones para clonar y gestionar repositorios.
curl — Herramienta para realizar peticiones HTTP y descargar contenido.
wget — Utilidad para descargar archivos desde Internet.
Portal de proyectos para el VPS
Agrega un sitio web alojado en el vps donde tengas los proyectos que vayas realizando. Puedes ver este proyecto aquí.
Los embedding models son el componente que transforma el contenido obtenido de los document en representaciones vectoriales densas que capturan significado semántico. En sistemas modernos de RAG dentro de LangChain, los embeddings son la base sobre la que se construye todo el sistema de recuperación.
En términos operativos, un embedding model proyecta texto en un espacio vectorial donde la distancia entre vectores refleja similitud semántica. Esto permite ejecutar búsquedas por similitud (normalmente usando cosine similarity o dot product) en bases de datos vectoriales. La calidad de esta proyección determina directamente la capacidad del sistema para recuperar contexto relevante.
Relación entre embeddings y organización en el vector store
Un aspecto crítico que suele pasarse por alto es que los embeddings no se almacenan como “un vector por documento”, sino como un vector por fragmento (chunk). Esto significa que un mismo documento genera múltiples embeddings que se indexan de forma independiente en el vector store.
En sistemas como Chroma, todos estos vectores se almacenan dentro de una misma colección (índice), independientemente del documento de origen. No existen “tablas separadas por documento”; en su lugar, cada entrada del índice contiene:
el embedding
el contenido (page_content)
la metadata asociada
un identificador (ID)
De forma conceptual:
[vector, texto, metadata, id]
Cuando se indexan múltiples documentos, todos sus chunks se mezclan dentro del mismo espacio vectorial:
Esto implica que la “separación” entre documentos no es física, sino lógica, y se realiza mediante metadata.
Tipos de embedding models
En producción, no todos los embeddings son equivalentes; se diferencian por arquitectura, objetivo y rendimiento.
General-purpose embeddings: Modelos optimizados para tareas generales de similitud semántica como OpenAI embeddings (text-embedding models), se usan en RAG estándar y búsqueda semántica.
Instruction-tuned embeddings: Modelos ajustados con instrucciones para mejorar tareas específicas (query vs document alignment). Tiene la ventaja de hacer mejor matching entre pregunta y contexto
Domain-specific embeddings: Entrenados en dominios concretos: legal, médico, código. Esto hace que mejoren el recall en casos especializados
Multilingual embeddings: permiten búsqueda cruzada entre idiomas.
Parámetros clave
Dimensionalidad: Más dimensiones → más capacidad semántica, pero mayor coste.
Normalización: Muchos modelos requieren normalización del vector para usar cosine similarity correctamente.
Tokenización: El embedding depende del tokenizer del modelo → afecta cómo se representa el texto.
Decisiones críticas en producción
Query vs Document embeddings: En sistemas avanzados: embeddings distintos para queries y documentos
Chunking alignment: El rendimiento depende directamente de cómo has hecho el text splitting un chunk mal definido lleva a un embedding pobre y la consecuencia es un retrieval malo
Batch vs real-time:
Indexing → los documentos se procesan en batch para generar embeddings de forma eficiente y reducir coste computacional.
Queries → las consultas del usuario se embeben en tiempo real para realizar búsqueda semántica inmediata sobre la base vectorial.
Errores comunes
Usar embeddings genéricos en dominios especializados → reduce la precisión semántica porque el modelo no captura bien el vocabulario ni las relaciones propias del dominio.
No normalizar vectores → provoca que métricas como cosine similarity den resultados incorrectos o inconsistentes en la búsqueda. (encode_kwargs={"normalize_embeddings": True})
Usar chunks demasiado grandes o pequeños → los grandes introducen ruido y los pequeños pierden contexto, degradando el retrieval.
No evaluar recall@k → impide medir si el sistema realmente recupera información relevante, ocultando fallos críticos en producción. Ver en retrievers.
Vector stores
Los vector stores son el componente encargado de almacenar y consultar embeddings de forma eficiente. En un sistema RAG dentro de LangChain, no basta con generar vectores; es necesario indexarlos en una estructura que permita búsquedas por similitud a gran escala con baja latencia.
En términos operativos, un vector store gestiona tres elementos: el vector (embedding), el contenido original (texto) y la metadata asociada. A partir de ahí, permite ejecutar consultas semánticas donde una query se transforma en embedding y se compara contra el índice para recuperar los elementos más cercanos.
Tipos, elección y uso en producción
La elección del vector store no es trivial; afecta a latencia, escalabilidad, coste y operativa. En la práctica, la decisión se reduce a dónde se ejecuta (local vs gestionado) y qué volumen/latencia necesitas.
Tipos de vector stores
Local (on-device): Se ejecutan en la propia máquina, sin dependencias externas. Ideales para prototipos, entornos offline o setups local-first.
Ejemplos: Chroma, FAISS
Ventajas: cero coste de API, baja latencia local, control total
Limitaciones: escalabilidad y concurrencia limitadas
Gestionados (API / SaaS): Servicios externos optimizados para indexación y búsqueda vectorial a gran escala.
Ventajas: alta disponibilidad, escalado automático, operaciones gestionadas
Limitaciones: coste y dependencia de red
Híbridos (self-hosted / cloud opcional): Permiten ejecutar localmente o desplegar en servidor propio con capacidades de escalado.
Ejemplos: Qdrant, Weaviate (self-hosted)
Ventajas: equilibrio entre control y escalabilidad
Limitaciones: necesitas gestionar infraestructura
Comparativa rápida
Tipo
Ejemplo
Latencia
Escalabilidad
Tipo
Uso recomendado
Local
Chroma, FAISS
Muy baja
Baja
gratis
desarrollo, local RAG
API
Pinecone, Weaviate Cloud
Media
Alta
pago
producción a escala
Híbrido
Qdrant, Weaviate
Baja–Media
Alta
variable
producción self-hosted
Criterios de elección
Usa local (Chroma / FAISS) si: trabajas en desarrollo o laboratorio, necesitas privacidad total y los dataset son pequeños/medio
Usa API (Pinecone / Weaviate Cloud) si: necesitas alta disponibilidad, tienes mucho volumen de datos y múltiples usuarios concurrentes
Usa híbrido (Qdrant / Weaviate self-hosted) si: quieres control + escalabilidad, despliegas en servidor propio y buscas evitar costes SaaS
Insight importante
El vector store no mejora embeddings ni corrige errores de chunking solo acelera la búsqueda; la calidad depende del pipeline previo.
Ingesta / Indexación (escritura en el vector store)
La fase de ingesta es donde se construye el índice vectorial a partir de los datos ya preprocesados (cleaning + splitting). En este punto, los textos o documentos se transforman en embeddings y se almacenan junto con su metadata en el vector store.
Esta operación suele ejecutarse en batch durante el indexing, no en tiempo real, y es crítica porque define la calidad y consistencia del sistema de retrieval: cualquier error aquí (mal chunking, embeddings inconsistentes, falta de metadata) se propagará al resto del pipeline.
Métodos de ingesta
Método
Descripción
add_texts
Convierte textos en embeddings y los añade al índice existente.
aadd_texts
Versión asíncrona de add_texts.
add_documents
Añade o actualiza documentos (incluyendo metadata) en el vector store.
aadd_documents
Versión asíncrona de add_documents.
from_texts
Crea un vector store directamente a partir de una lista de textos.
afrom_texts
Versión asíncrona de from_texts.
from_documents
Inicializa un vector store desde documentos estructurados.
En producción, estos métodos se utilizan en pipelines de indexing offline, donde grandes volúmenes de datos se procesan en batch para evitar latencia y optimizar costes.
La gestión de datos en un vector store es fundamental para mantener la coherencia del índice a lo largo del tiempo. A diferencia de bases de datos tradicionales, los embeddings no suelen actualizarse “en sitio”; en la práctica, cualquier cambio en el contenido implica eliminar los vectores antiguos y reindexar los nuevos. Por ello, las operaciones de eliminación no son solo tareas de limpieza, sino una parte crítica del ciclo de vida del dato en sistemas RAG.
Método
Descripción
delete
Elimina vectores del índice por ID o mediante condiciones sobre metadata.
adelete
Versión asíncrona de delete.
Uso típico: eliminar chunks específicos de un documento.
vectorstore.delete(ids=["docA_1", "docA_2"])
Elimina todos los vectores asociados a un documento completo.
vectorstore.delete(where={"source": "docA"})
Patrón de actualización (delete + reindex)
# eliminar versión antiguavectorstore.delete(where={"source": "docA"})# volver a indexar contenido actualizadovectorstore.add_texts(texts=["nuevo contenido actualizado"],metadatas=[{"source": "docA"}])
Acceso directo por ID
El acceso por ID permite recuperar documentos de forma determinista sin pasar por el proceso de búsqueda semántica. En lugar de calcular similitudes vectoriales, se accede directamente a los elementos almacenados en el índice usando sus identificadores únicos. Esta operación es clave para tareas de trazabilidad y control, ya que permite verificar exactamente qué contenido fue indexado o recuperado en etapas anteriores del pipeline.
Métodos de acceso
Método
Descripción
get_by_ids
Recupera documentos directamente por sus IDs.
aget_by_ids
Versión asíncrona de get_by_ids.
Recuperar documentos por ID
docs = vectorstore.get_by_ids(["docA_1", "docB_2"])for doc in docs:print(doc.page_content, doc.metadata)
Uso en debugging: Permite verificar el origen del documento, metadatos asociados y contenido extacto indexado.
Reconstrucción de contexto: reconstruir un documento original a partir de chunks, auditar resultados del retrieval.
ids = ["docA_1", "docA_2"]chunks = vectorstore.get_by_ids(ids)full_text =" ".join([c.page_content for c in chunks])
Búsqueda semántica (core del RAG)
La búsqueda semántica es el núcleo de cualquier sistema RAG: es el mecanismo que permite recuperar información relevante a partir de una consulta, no por coincidencia exacta de palabras, sino por similitud en el espacio vectorial. En esta fase, la query se transforma en embedding y se compara contra el índice para encontrar los fragmentos más cercanos. Este proceso implementa, en la práctica, algoritmos de k-nearest neighbors (k-NN) o sus variantes aproximadas (ANN) para escalar a grandes volúmenes de datos.
Búsqueda básica
La forma más directa de retrieval es recuperar los documentos más similares a una query.
Método
Descripción
similarity_search
Devuelve los documentos más similares a una query.
asimilarity_search
Versión asíncrona de similarity_search.
similarity_search_by_vector
Realiza la búsqueda usando un embedding ya calculado.
asimilarity_search_by_vector
Versión asíncrona de búsqueda por vector.
results = vectorstore.similarity_search("What is a text splitter?",k=3)# k muy bajo → pierdes información relevante# k muy alto → el LLM recibe ruidofor doc in results:print(doc.page_content)
k
Uso
1
muy preciso, poco contexto
3
estándar
5–10
más contexto, más ruido
Usarby_vector cuando ya tienes el embedding calculado y quieres optimizar rendimiento.
query_vector = embeddings.embed_query("What is LCEL?")results = vectorstore.similarity_search_by_vector(query_vector)
Búsqueda con scoring
Estas variantes añaden información cuantitativa sobre la similitud, lo que permite evaluar y depurar el comportamiento del retrieval.
Método
Descripción
similarity_search_with_score
Devuelve documentos junto con su distancia o score técnico.
asimilarity_search_with_score
Versión asíncrona.
similarity_search_with_relevance_scores
Devuelve scores normalizados entre 0 y 1.
asimilarity_search_with_relevance_scores
Versión asíncrona.
Se usa para evaluación de calidad (recall, ranking), análisis de relevancia y tuning del sistema
results = vectorstore.similarity_search_with_relevance_scores("What are embeddings?",k=3)for doc, score in results:print(score, doc.page_content)
MMR (Max Marginal Relevance)
MMR introduce un criterio adicional: no solo busca relevancia respecto a la query, sino también diversidad entre los resultados.
Método
Descripción
max_marginal_relevance_search
Selecciona documentos optimizando relevancia y diversidad.
amax_marginal_relevance_search
Versión asíncrona.
max_marginal_relevance_search_by_vector
MMR usando embeddings en lugar de texto.
amax_marginal_relevance_search_by_vector
Versión asíncrona.
Ejemplo con: max_marginal_relevance_search. Evita resultados redundantes, mejora la cobertura del contexto y es útil cuando los documentos son similares entre sí.
El método search permite unificar distintas estrategias bajo una sola interfaz.
Método
Descripción
search
Permite elegir el tipo de búsqueda (similarity, MMR, etc.).
asearch
Versión asíncrona.
search() permite seleccionar dinámicamente entre similarity, MMR, scoring.
results = vectorstore.search("What is RAG?",search_type="mmr")
Filtering (restricción por metadata)
El filtering permite restringir el espacio de búsqueda en el vector store utilizando metadata asociada a cada chunk. A diferencia de la búsqueda semántica, que opera sobre embeddings, los filtros actúan como una condición lógica que limita qué subconjunto de datos se considera antes (o durante) el cálculo de similitud.
Cómo funciona: Cada chunk indexado incluye metadata:
retriever = vectorstore.as_retriever(search_kwargs={"k": 3,"filter": {"source": "docA"} })docs = retriever.invoke("What is a text splitter?")
Casos de uso
separación entre documentos
sistemas multi-usuario (multi-tenant)
filtrado por secciones (capítulos, páginas)
control de contexto en RAG
Consideraciones
Los filtros dependen de la metadata → si no la defines bien, no funcionan
No sustituyen la similitud → la complementan
Su implementación puede variar según el vector store
Insight clave
embeddings → determinan relevancia
filtering → determina contexto
Ambos son necesarios para un retrieval correcto.
Atributos
Los vector stores no solo almacenan datos, sino que también mantienen información sobre los componentes con los que fueron construidos. El atributo más relevante es embeddings, que hace referencia al modelo de embeddings asociado al índice.
Este atributo permite garantizar que las operaciones de retrieval se realicen con el mismo espacio vectorial con el que se indexaron los datos, evitando inconsistencias difíciles de detectar.
Atributo
Descripción
embeddings
Modelo de embeddings asociado al vector store.
print(vectorstore.embeddings)
Para qué se utiliza
Consistencia → asegura que las queries se embeben con el mismo modelo usado en la indexación
Debugging → permite verificar qué modelo está realmente en uso
Auditoría → ayuda a rastrear configuraciones en sistemas complejos
Caso práctico
query_vector = vectorstore.embeddings.embed_query("What is RAG?")
Error común
Cambiar el modelo de embeddings sin reindexar da como resultado búsquedas incorrectas o incoherentes.
index → creado con modelo A query → generada con modelo B
Retriever
El método as_retriever transforma el vector store en un componente de alto nivel que encapsula la lógica de búsqueda y lo hace directamente integrable en pipelines de RAG. En lugar de llamar manualmente a métodos como similarity_search, el retriever actúa como una interfaz estándar que recibe una query y devuelve los documentos relevantes, desacoplando la capa de almacenamiento de la lógica del sistema.
Este patrón es clave en LangChain, ya que permite componer fácilmente pipelines donde el retrieval se integra como una pieza más dentro del flujo de datos hacia el LLM.
Método
Descripción
as_retriever
Convierte el vector store en un retriever configurable para pipelines RAG.
Ejemplo básico
retriever = vectorstore.as_retriever(search_type="mmr",search_kwargs={"k": 3})docs = retriever.invoke("What is a text splitter?")
Qué está haciendo internamente el retriever: (Es un wrapper sobre similarity_search.)
Recibe la query
La convierte en embedding
Ejecuta búsqueda en el vector store
Devuelve los documentos más relevantes
Insight estructural
Aunque la interfaz de VectorStore en LangChain puede parecer extensa, en la práctica se reduce a tres operaciones fundamentales que reflejan el ciclo de vida del dato en un sistema RAG.
Write (indexing)
Corresponde a la fase de ingesta, donde los datos se transforman en embeddings y se insertan en el índice vectorial. Se ejecuta normalmente en batch durante el indexing offline.
add_* → inserción incremental
from_* → construcción inicial del índice
Read (retrieval)
Es la fase de consulta, donde se recupera información relevante a partir de una query. Es el núcleo del sistema RAG.
similarity_* → búsqueda por similitud
mmr_* → búsqueda con diversidad
search → interfaz general
Manage (mantenimiento)
Permite modificar o inspeccionar el índice. Es clave para actualización, limpieza y debugging.
Los text splitters en LangChain son un componente estructural dentro de cualquier pipeline de RAG: determinan cómo se segmenta el conocimiento antes de ser embebido, indexado y recuperado. En sistemas en producción, esta decisión no es neutra; el splitting define directamente la calidad del retrieval, la densidad semántica de los embeddings y, en última instancia, la precisión del sistema.
pero lo relevante es que el splitter introduce la primera transformación irreversible del dato. Cualquier pérdida de coherencia en esta etapa se propaga aguas abajo.
El punto de abstracción es la interfaz TextSplitter, que define un contrato simple: transformar texto en una lista de fragmentos. Sin embargo, en entornos reales, la elección de implementación no es intercambiable. El uso de CharacterTextSplitter, aunque históricamente común, ha quedado relegado a casos triviales debido a su naturaleza puramente mecánica: corta por longitud fija sin respetar unidades semánticas, lo que degrada la calidad de los embeddings y reduce la efectividad del retrieval.
El estándar de facto en producción es RecursiveCharacterTextSplitter, precisamente porque introduce una heurística jerárquica que preserva la estructura del lenguaje el mayor tiempo posible. En lugar de imponer cortes arbitrarios, aplica una estrategia de degradación progresiva: intenta dividir por párrafos, luego por saltos de línea, después por frases y, solo cuando no hay alternativa, por caracteres. Este comportamiento minimiza la fragmentación semántica sin renunciar al control sobre el tamaño del chunk.
Un aspecto clave en todos los splitters es la configuración de chunk_size y chunk_overlap, donde el primero define el tamaño del fragmento y el segundo preserva contexto entre fragmentos; en práctica profesional, valores típicos oscilan entre 500–1500 tokens con un solapamiento del 10–30%.
Los separators definen la jerarquía de cortes que el algoritmo intenta aplicar en orden, no son simplemente delimitadores; son una estrategia de degradación progresiva. En este caso, conceptualmente, le estamos diciendo a la función: “intenta dividir primero por unidades semánticas grandes; si no puedes cumplir el chunk_size, baja de nivel progresivamente hasta poder hacerlo”
El algoritmo sigue esta lógica:
Intenta dividir usando "\n\n" (párrafos)
Si los chunks siguen siendo demasiado grandes → usa "\n" (líneas)
Si aún no cabe → usa "." (frases)
Luego " " (palabras)
Finalmente "" (caracteres individuales, último recurso)
Splitters
TokenTextSplitter
Cuando el control fino sobre tokens es crítico (coste o límites del modelo), se utiliza TokenTextSplitter, que trabaja directamente con tokenizadores:
from langchain_text_splitters import TokenTextSplittersplitter = TokenTextSplitter(chunk_size=512,chunk_overlap=50)
Internamente se apoya en una abstracción de Tokenizer, y también puedes usar funciones como:
from langchain_text_splitters import split_text_on_tokens
Esto garantiza que nunca excedas el contexto real del modelo.
MarkdownHeaderTextSplitter
Más allá de longitud, LangChain introduce splitters estructurales que respetan formato del documento; por ejemplo, MarkdownHeaderTextSplitter divide en función de headers, manteniendo jerarquía lógica:
Esto permite que cada chunk incluya metadata semántica (sección, subsección, etc.).
HTMLHeaderTextSplitter
En HTML, el equivalente es HTMLHeaderTextSplitter, que detecta etiquetas <h1>, <h2>, etc., y genera documentos jerárquicos con metadata asociada; si no encuentra headers, devuelve el documento completo, lo cual lo hace robusto ante inputs inconsistentes.
Para casos más avanzados, HTMLSemanticPreservingSplitter mantiene estructura completa (incluyendo links, imágenes o multimedia), y solo recurre a splitting recursivo si se supera el tamaño máximo, priorizando integridad semántica.
RecursiveJsonSplitter
Cuando trabajas con datos estructurados, RecursiveJsonSplitter permite dividir JSON preservando jerarquía, lo cual es crítico en agentes que dependen de contexto estructurado:
from langchain_text_splitters import RecursiveJsonSplittersplitter = RecursiveJsonSplitter(max_chunk_size=500)chunks = splitter.split_json(json_data)
Aquí el objetivo no es solo dividir, sino mantener relaciones entre claves.
Splitters específicos por lenguaje o dominio
LangChain también incluye splitters específicos por lenguaje o dominio, lo cual es clave en sistemas profesionales; por ejemplo, PythonCodeTextSplitter divide respetando sintaxis de Python (funciones, clases), mientras que JSFrameworkTextSplitter extiende el splitting recursivo para entender JSX, Vue o Svelte, detectando componentes como separadores naturales:
from langchain_text_splitters import PythonCodeTextSplittersplitter = PythonCodeTextSplitter()chunks = splitter.split_text(code)
Esto evita romper bloques de código de forma incorrecta.
Splitters para procesamiento de texto
En procesamiento lingüístico más avanzado, existen integraciones con NLP clásico como SpacyTextSplitter o NLTKTextSplitter, que segmentan por oraciones usando modelos lingüísticos:
from langchain_text_splitters import SpacyTextSplittersplitter = SpacyTextSplitter(pipeline="sentencizer")chunks = splitter.split_text(text)
Para casos específicos, también existen splitters especializados como:
Divide texto en función de tokens usando un tokenizador; útil para controlar límites de modelos.
SentenceTransformersTokenTextSplitter
Variante basada en tokenizadores de modelos de sentence-transformers; optimizada para embeddings.
SpacyTextSplitter
Usa Spacy para segmentar texto en oraciones; más preciso lingüísticamente.
NLTKTextSplitter
Segmenta texto utilizando NLTK; útil para procesamiento basado en frases.
MarkdownTextSplitter
Divide texto siguiendo la estructura Markdown (headers, secciones).
MarkdownHeaderTextSplitter
Divide Markdown basado en headers específicos, generando chunks con metadata jerárquica.
ExperimentalMarkdownSyntaxTextSplitter
Splitter avanzado que preserva formato original, whitespace y extrae metadata (headers, código, reglas).
HTMLHeaderTextSplitter
Divide HTML según etiquetas de encabezado (<h1>, <h2>, etc.), generando estructura jerárquica.
HTMLSectionSplitter
Divide HTML basado en tags y tamaños de fuente; requiere lxml.
HTMLSemanticPreservingSplitter
Mantiene estructura semántica HTML completa (links, imágenes, etc.) y solo divide si es necesario.
RecursiveJsonSplitter
Divide JSON en fragmentos manteniendo su estructura jerárquica; útil para datos estructurados.
PythonCodeTextSplitter
Divide código Python respetando su sintaxis (funciones, clases).
JSFrameworkTextSplitter
Divide código de frameworks JS (React, Vue, Svelte) detectando componentes y sintaxis.
LatexTextSplitter
Divide texto respetando estructura de documentos LaTeX.
KonlpyTextSplitter
Splitter especializado para texto en coreano usando la librería KoNLPy.
Finalmente, en sistemas complejos de agentes construidos con LangGraph, los text splitters no solo afectan al retrieval, sino a la memoria externa del agente; elegir un splitter adecuado implica decidir cómo el agente “percibe” el conocimiento disponible.
En cualquier sistema de RAG, la calidad del resultado final depende directamente de la calidad del dato de entrada. Antes de aplicar técnicas de segmentación, generación de embeddings o recuperación de información, es imprescindible garantizar que el contenido extraído esté correctamente estructurado y libre de ruido.
En la práctica, los datos rara vez llegan en un formato listo para ser utilizados. Ya provengan de documentos PDF, páginas web u otras fuentes, el contenido suele presentar problemas como fragmentación del texto, elementos irrelevantes o estructuras inconsistentes. Estos problemas no son visibles a simple vista en todos los casos, pero tienen un impacto directo en el rendimiento del sistema.
Esto significa que, aunque el loader funcione correctamente, el resultado puede ser un texto difícil de utilizar en un sistema RAG como en este ejemplo:
print(document[6].page_content[:300])
zonas premium: cada metro valía (y vale) mucho más en Madrid, Baleares y País Vascoqueencualquierotraregión.
Por tanto, antes de avanzar hacia fases más avanzadas del sistema, es necesario validar y procesar adecuadamente el contenido extraído. Un pipeline sólido no comienza con el modelo, sino con datos bien preparados.
Antes de generar embeddings, es fundamental aplicar una fase de limpieza del texto. El objetivo es reconstruir, en la medida de lo posible, una estructura natural del lenguaje.
Una estrategia básica de limpieza incluye:
Eliminación de saltos de línea innecesarios
Normalización de espacios
Unificación del texto en una secuencia continua
Este enfoque elimina múltiples espacios, saltos de línea y fragmentaciones simples, devolviendo un texto más coherente. Para casos más complejos, se pueden aplicar reglas adicionales, como la eliminación de patrones repetitivos o la reconstrucción de párrafos.
Métodos de limpieza de datos tras la carga (post-load)
Una vez cargados los documentos mediante un loader, el siguiente paso crítico es aplicar técnicas de limpieza que permitan transformar el texto en una forma coherente y útil para el sistema RAG.
No existe un único método universal. La limpieza debe adaptarse al tipo de fuente (PDF, web, datos estructurados), pero sí existen patrones comunes que se aplican en la mayoría de los casos:
Normalización básica de espacios
Eliminar espacios duplicados, saltos de línea y fragmentación simple. Se debe hacer siempre. Es el primer paso obligatorio.
text = document.page_contentclean_text =" ".join(text.split())
Soluciona:
Saltos de línea (\n)
Espacios múltiples
Texto fragmentado
Eliminación explícita de saltos de línea
Reconstruir frases que han sido cortadas artificialmente. Se utiliza en PDFs con líneas rotas y texto extraído por coordenadas
text = document.page_contentclean_text = text.replace("\n", " ")clean_text =" ".join(clean_text.split())
Eliminación de patrones repetitivos (headers/footers)
Eliminar contenido repetido en todas las páginas. Como: “Página 1”, “Confidencial”, nombres de empresa, etc. Mejora la calidad del embedding.
La limpieza de datos no es un paso opcional dentro de un sistema RAG, sino una fase crítica que determina la calidad del resto del pipeline.
Aplicar técnicas básicas como la normalización de espacios o la eliminación de ruido puede mejorar significativamente la coherencia del texto y, por tanto, la precisión del sistema.
En entornos reales, la combinación de varios métodos de limpieza es la práctica habitual, adaptándose siempre al tipo de documento y a la calidad del contenido extraído.
En LangChain, un Document es una estructura que representa una unidad de información compuesta por dos elementos: el contenido del texto (page_content) y un conjunto de metadatos (metadata) que aportan contexto adicional, como el origen, identificadores o fechas.
Este formato estandarizado permite trabajar de manera uniforme con distintos tipos de datos, ya provengan de archivos, páginas web, bases de datos o otras aplicaciones. Para obtener estos documentos desde fuentes reales, LangChain utiliza los llamados Document Loaders, que son componentes diseñados para cargar información desde múltiples orígenes y convertirla automáticamente en objetos Document que posteriormente serán utilizados en pipelines de Retrieval-Augmented Generation (RAG).
BaseLoader
Antes de trabajar con loaders concretos, es importante entender que todos ellos heredan de una clase base común: BaseLoader. Esta clase define el comportamiento estándar que deben seguir todos los loaders dentro del ecosistema de LangChain.
BaseLoader no es un loader que se utilice directamente, sino una interfaz (clase abstracta) que establece cómo deben implementarse los métodos de carga de documentos. El propósito principal de BaseLoader es garantizar que todos los loaders:
Devuelvan datos en formato Document
Sigan un patrón consistente
Sean intercambiables dentro del pipeline
BaseLoader → <Name>Loader
Ejemplos:
TextLoader
PyPDFLoader
WebBaseLoader
Su salida siempre es una colección de objetos:
Document.page_content → contenido textual
Document.metadata → información contextual
Este diseño permite que cualquier loader sea intercambiable dentro del pipeline. De esta forma, los datos externos se integran fácilmente en los flujos de trabajo con modelos de lenguaje, siendo la base para aplicaciones más avanzadas como sistemas de recuperación de información (RAG), análisis de datos o asistentes inteligentes.
Lazy loading
El diseño de BaseLoader está orientado a evitar cargar todos los documentos en memoria de golpe. Por eso, el método fundamental que deben implementar los loaders es: lazy_load()
Métodos de BaseLoader
Método
Descripción
Cuándo usarlo
Notas
load()
Carga todos los documentos y los devuelve en una lista de Document.
Prototipos, datasets pequeños, pruebas rápidas.
Es un método de conveniencia. Internamente usa lazy_load(). No debe sobrescribirse.
lazy_load()
Devuelve un generador de documentos (uno a uno).
Producción, grandes volúmenes de datos, eficiencia en memoria.
Método clave que deben implementar los loaders. Base del sistema.
Sistemas escalables, streaming de datos, alto rendimiento.
Permite iteración asíncrona (async for).
Loaders
En cualquier sistema RAG, el punto de partida es siempre el mismo: los datos. Antes de hablar de embeddings, retrieval o generación de respuestas, es necesario convertir las fuentes de información en un formato que el sistema pueda procesar. En este contexto, los documentos PDF representan uno de los formatos más habituales en entornos empresariales.
En el ecosistema de LangChain, los loaders forman parte de la capa de ingestión de datos. Su función no es solo “leer archivos”, sino preparar la información para que pueda ser utilizada posteriormente en procesos de segmentación, embedding y recuperación.
Unstructured’s Loaders
Los loaders denominados Unstructured dentro del ecosistema de LangChain son herramientas especializadas que permiten mejorar significativamente la extracción. Este tipo de loader no se limita a leer el texto plano, sino que intenta interpretar la estructura del documento. El resultado es un contenido más limpio, menos fragmentado y semánticamente más consistente, lo que impacta directamente en la calidad de los embeddings y en la eficacia del sistema RAG.
Ventajas principales:
Mejor interpretación del layout: Este loader intenta entender la estructura del documento, no solo extraer texto plano. Esto permite preservar mejor párrafos, títulos y bloques de contenido.
Menor fragmentación del texto: Reduce significativamente los problemas de palabras separadas o saltos de línea artificiales.
Mayor calidad semántica: Al generar texto más coherente, los embeddings resultantes son más representativos, lo que mejora la recuperación en RAG.
Preparado para documentos reales: Está diseñado para trabajar con documentos del mundo empresarial: informes, contratos, manuales, etc.
Loader
Descripción
UnstructuredPDFLoader
Extrae contenido de PDFs interpretando la estructura del documento (títulos, párrafos, listas). Ideal para informes complejos.
UnstructuredHTMLLoader
Procesa archivos HTML locales identificando contenido semántico relevante, evitando ruido estructural.
UnstructuredURLLoader
Carga contenido desde URLs aplicando parsing inteligente para aislar el contenido principal de la web.
UnstructuredWordDocumentLoader
Extrae contenido de documentos Word manteniendo la estructura lógica del texto.
UnstructuredEmailLoader
Procesa correos electrónicos (.eml, .msg), incluyendo cuerpo del mensaje y opcionalmente adjuntos.
UnstructuredImageLoader
Extrae texto de imágenes (OCR) y organiza el contenido en elementos semánticos.
Consideraciones
Mayor coste computacional
Puede requerir dependencias adicionales
No siempre necesario para documentos simples
Modo de operación
Los loaders Unstructured suelen trabajar en dos modos:
single: Devuelve todo el documento como un único Document.
elements (recomendado): Divide el contenido en elementos estructurados:
from langchain_community.document_loaders import<Loader>loader =<Loader>("ruta/al/archivo")documents = loader.load()
En el momento de utilizarlos debes consultar la documentación de los parámetros adicionales en cada caso. Ejemplo para extracción de datos en un DataFrame de Pandas.
from langchain_community.document_loaders import DataFrameLoaderloader = DataFrameLoader(df, page_content_column="text")documents = loader.load()
Web Loaders
Los Web Loaders permiten incorporar información directamente desde internet a un sistema RAG. Esto incluye páginas web, blogs, documentación técnica o cualquier contenido accesible mediante una URL.
A diferencia de los loaders de archivos locales, aquí no trabajas con un documento estático, sino con contenido dinámico, estructurado en HTML y, en muchos casos, generado parcialmente mediante JavaScript. Para cada caso debes seleccionar el tipo correcto diferenciado en cómo acceden y procesan el contenido:
Loaders básicos → funcionan bien con páginas estáticas
Loaders con renderizado JS → necesarios para webs modernas
Loader básico para cargar páginas web. Extrae el HTML y lo convierte en texto.
AsyncHtmlLoader
Variante asíncrona para cargar múltiples páginas web de forma eficiente.
PlaywrightURLLoader
Carga páginas renderizadas con JavaScript usando Playwright. Ideal para webs dinámicas.
SeleniumURLLoader
Similar a Playwright, utiliza Selenium para renderizar contenido dinámico.
BrowserlessLoader
Utiliza un navegador remoto (Browserless) para cargar páginas complejas.
BrowserbaseLoader
Loader basado en navegador headless en la nube (Browserbase).
RecursiveUrlLoader
Crawler que navega recursivamente por enlaces dentro de una web.
SitemapLoader
Carga todas las URLs definidas en un sitemap XML.
Problemas específicos del contenido web:
Ruido HTML: El contenido incluye elementos que no aportan valor:
menús de navegación
banners de cookies
footers
enlaces secundarios
Contenido dinámico: Muchas webs modernas no cargan todo el contenido directamente en el HTML, sino que lo generan mediante JavaScript. El resultado es:
loaders básicos no ven el contenido
necesitas loaders con renderizado (Playwright, Selenium)
Riesgos de seguridad (SSRF): Los loaders tipo crawler pueden acceder a múltiples URLs automáticamente. Esto conlleva riesgos como:
acceso a recursos internos
carga de URLs maliciosas
problemas de seguridad en producción
Buenas prácticas para trabajar correctamente con Web Loaders en RAG:
No indexar páginas completas sin filtrar
Validar siempre el contenido extraído
Aplicar limpieza antes del chunking
Usar renderizado JS cuando sea necesario
Limitar el uso de crawlers a dominios controlados
Aquí tienes la sección completa de Loaders de bases de datos, lista para integrar en tu unidad:
Loaders de bases de datos
Los loaders de bases de datos permiten integrar datos estructurados directamente en un sistema RAG. A diferencia de los documentos tradicionales (PDF, web), aquí la información proviene de tablas, donde cada fila representa una unidad lógica de datos, o sea cada fila → un Document. Esto convierte datos estructurados en texto procesable por el modelo.
Loader
Descripción
SQLDatabaseLoader
Ejecuta consultas SQL y convierte cada fila del resultado en un Document. Compatible con múltiples motores vía SQLAlchemy.
DuckDBLoader
Permite cargar datos desde bases DuckDB, transformando resultados en documentos.
SnowflakeLoader
Extrae datos desde Snowflake y convierte cada fila en texto procesable.
AthenaLoader
Carga resultados de consultas en AWS Athena, ideal para grandes volúmenes de datos en la nube.
CassandraLoader
Permite trabajar con datos distribuidos en Cassandra, convirtiendo filas en documentos.
MongoDBLoader
Carga documentos desde MongoDB (NoSQL), transformando cada registro en un Document.
from langchain_community.document_loaders import SQLDatabaseLoaderfrom langchain_community.utilities import SQLDatabasedb = SQLDatabase.from_uri("sqlite:///mi_base.db")loader = SQLDatabaseLoader(db, query="SELECT * FROM reservas")documents = loader.load()
Buenas prácticas (nivel pro)
Convertir datos a texto limpio
No hacer SELECT * en producción
Limitar resultados (LIMIT)
Elegir columnas relevantes
Loaders de sistemas empresariales (SaaS)
Los loaders de sistemas empresariales permiten integrar información directamente desde herramientas utilizadas en entornos corporativos. En lugar de trabajar con archivos locales o bases de datos, estos loaders acceden a plataformas como sistemas de documentación, almacenamiento en la nube o herramientas de colaboración. A diferencia de otros loaders, aquí necesitas acceso a sistemas externos.
API tokens
OAuth
credenciales de usuario
cookies de sesión
Loader
Descripción
NotionDBLoader
Carga contenido desde una base de datos de Notion, convirtiendo cada página o entrada en un Document.
NotionDirectoryLoader
Carga exportaciones completas de Notion (directorios), incluyendo múltiples páginas y documentos.
ConfluenceLoader
Extrae páginas y contenido de espacios en Confluence, incluyendo opcionalmente adjuntos.
SlackDirectoryLoader
Carga mensajes desde un export de Slack, convirtiendo conversaciones en documentos.
DropboxLoader
Permite cargar archivos almacenados en Dropbox, incluyendo documentos y PDFs.
OneDriveLoader
Accede a archivos en Microsoft OneDrive y los transforma en documentos.
SharePointLoader
Carga contenido desde SharePoint, incluyendo documentos corporativos y bibliotecas de archivos.
Ejemplo básico (Notion)
from langchain_community.document_loaders import NotionDBLoaderloader = NotionDBLoader(integration_token="your_token",database_id="your_database_id")documents = loader.load()
Consideraciones importantes
Calidad del contenido: estos sistemas suelen contener texto desordenado, duplicados y mensajes irrelevantes por lo que es necesaria una limpieza posterior
El volumen de datos puede ser muy alto: miles de mensajes, documentos largos y múltiples fuentes. En este caso el filtrado es importante.
Privacidad y seguridad: Dado que trabajas con datos sensibles como información interna, conversaciones privadas y documentos confidenciales, es fundamental controlar accesos.
Buenas prácticas
Filtrar por fechas o relevancia
Usar metadata (autor, canal, fecha)
Limpiar antes de embeddings
Limitar el scope de datos
Loaders de repositorios y código
Los loaders de repositorios permiten trabajar con código fuente y artefactos asociados (issues, documentación) dentro de un sistema RAG. Son especialmente útiles para construir asistentes técnicos, copilots o sistemas de búsqueda sobre bases de código.
Estos loaders extraen archivos de repositorios locales o remotos y los convierten en objetos Document, manteniendo información relevante como rutas de archivo, nombres o metadatos del repositorio.
Loader
Descripción
GitLoader
Carga archivos de un repositorio Git (local o clonado desde remoto), convirtiendo cada archivo en un Document.
GithubFileLoader
Permite cargar archivos específicos desde un repositorio de GitHub mediante la API.
GitHubIssuesLoader
Extrae issues de un repositorio de GitHub, incluyendo títulos, descripciones y comentarios.
Loaders de contenido multimedia (audio, imagen, vídeo)
Los loaders de contenido multimedia permiten incorporar a un sistema RAG información que originalmente no está en formato de texto, como audio, imágenes o vídeos. Dado que los modelos de lenguaje trabajan con texto, estos loaders realizan una transformación previa: convierten contenido multimedia en texto procesable. Esto se consigue mediante tecnologías como:
transcripción de audio (speech-to-text)
OCR (reconocimiento de texto en imágenes)
generación automática de descripciones
Loader
Descripción
YoutubeLoader
Extrae transcripciones de vídeos de YouTube y las convierte en texto.
AssemblyAIAudioLoader
Carga transcripciones existentes desde AssemblyAI.
AssemblyAIAudioTranscriptLoader
Transcribe archivos de audio (locales o URL) usando AssemblyAI.
YoutubeAudioLoader
Descarga audio de vídeos de YouTube para su posterior procesamiento.
UnstructuredImageLoader
Extrae texto de imágenes mediante OCR.
ImageCaptionLoader
Genera descripciones automáticas de imágenes usando modelos de captioning.
Tipos de transformación
Audio → texto: Convierte voz en texto como llamadas, podcast o reuniones.
Imagen → texto: En esta caso hay dos enfoques:
OCR → extrae texto real
Captioning → describe la imagen
Vídeo → texto: generalmente mediante: transcripción del audio o subtítulos
Ejemplo básico (YouTube)
from langchain_community.document_loaders import YoutubeLoaderloader = YoutubeLoader.from_youtube_url("https://www.youtube.com/watch?v=XXXX",add_video_info=True)documents = loader.load()
Ejemplo básico (audio)
from langchain_community.document_loaders import AssemblyAIAudioTranscriptLoaderloader = AssemblyAIAudioTranscriptLoader(file_path="audio.mp3")documents = loader.load()
Consideraciones importantes
Calidad del texto generado: La calidad depende del modelo de conversión ya que puede haber errores en transcripción, ruido en audio o OCR imperfecto lo que impacta directamente en embeddings.
Coste y latencia: Estos procesos suelen ser más lentos y más costosos debido a APIs externas.
Contexto limitado: El texto generado puede perder matices, simplificar contenido y omitir información visual.
Buenas prácticas
Revisar la calidad de la transcripción
Limpiar el texto antes de embeddings
Añadir metadata (fuente, timestamp, tipo)
Dividir correctamente en chunks
Aquí tienes la sección completa, alineada con el resto de tu unidad:
Loaders de datos públicos y fuentes externas
Los loaders de datos públicos permiten integrar información procedente de fuentes abiertas como enciclopedias, repositorios científicos o plataformas de contenido online. A diferencia de los loaders empresariales, aquí se trabaja con datos accesibles públicamente, lo que facilita la experimentación y el desarrollo de prototipos.
Loader
Descripción
WikipediaLoader
Carga contenido de páginas de Wikipedia a partir de una búsqueda o término específico.
PubMedLoader
Recupera artículos y abstracts del repositorio biomédico PubMed.
ArxivLoader
Carga papers científicos desde arXiv, extrayendo contenido desde PDFs o abstracts.
RedditPostsLoader
Extrae publicaciones y comentarios de Reddit desde un subreddit específico.
HNLoader
Carga contenido de Hacker News, incluyendo noticias y comentarios.
from langchain_community.document_loaders import WikipediaLoaderloader = WikipediaLoader(query="LangChain", load_max_docs=2)documents = loader.load()
Consideraciones importantes
Calidad variable del contenido: dependiendo de la fuente, el contenido puede ser generalista, informal o altamente técnico.
Ruido y relevancia: No todo lo que se carga es útil se puede cargar comentarios irrelevantes, contenido duplicado o información no estructurada
Dependencia externa: APIs , límites de uso y disponibilidad del servicio
Buenas prácticas
Limitar número de documentos (load_max_docs)
Filtrar por relevancia
Limpiar contenido antes de embeddings
Usar metadata para contexto (fuente, fecha, autor)
Loaders empresariales avanzados
Los loaders empresariales avanzados permiten integrar sistemas especializados que forman parte del stack tecnológico de una empresa. A diferencia de los loaders SaaS más generales (como Notion o Slack), estos se centran en herramientas específicas de negocio, como CRM, analítica, diseño o procesamiento de datos.
Loader
Descripción
Airbyte<Name>Loader
Conectores basados en Airbyte para integrar múltiples fuentes (Salesforce, HubSpot, Stripe, Zendesk, etc.).
AirtableLoader
Carga datos desde tablas de Airtable, convirtiendo cada registro en un Document.
FigmaFileLoader
Extrae información desde archivos de diseño en Figma, incluyendo estructuras y contenido textual.
DatadogLogsLoader
Carga logs desde Datadog, transformando eventos y registros en documentos analizables.
Para utilizar Airbyte<Name>Loader, se necesita instalar el paquete airbyte-cdk que es el Connector Development Kit (CDK) de Airbyte. Una librería de Python diseñada para construir conectores (sources) de datos de forma estandarizada.
airbyte-cdk es una herramienta de bajo nivel para crear conectores de datos robustos y escalables. Dentro de un sistema RAG, su papel no es directo, sino que actúa como capa intermedia que permite integrar fuentes externas complejas de forma estandarizada.
Si estás construyendo sistemas avanzados o integraciones personalizadas, es una pieza clave. Si estás consumiendo datos con loaders ya existentes, probablemente ni siquiera necesites interactuar con él directamente.
Cliente Juan Pérez trabaja en Hotel Sol. Su email es [email protected].
Consideraciones importantes
Dependencia de APIs: considera tener límites de uso, latencia y cambios en endpoints
Calidad del dato: estos sistemas contienen datos incompletos, campos técnicos y estructuras complejas, por lo que requieren transformación antes de embeddings.
Seguridad: datos sensibles (clientes, pagos, logs) o credenciales de acceso. Es imprescindible gestionar permisos correctamente
Buenas prácticas
Seleccionar solo campos relevantes
Transformar datos en lenguaje natural
Añadir metadata útil (fecha, tipo, origen)
Limitar volumen de datos
Loaders de almacenamiento y cloud
Los loaders de almacenamiento y cloud permiten cargar documentos desde sistemas de almacenamiento remoto, como servicios en la nube o buckets de archivos. En lugar de acceder a archivos locales, estos loaders trabajan con datos alojados en infraestructuras externas.
Loader
Descripción
S3FileLoader
Carga un archivo específico desde un bucket de Amazon S3.
S3DirectoryLoader
Carga múltiples archivos desde un bucket o carpeta en S3.
CloudBlobLoader
Permite acceder a blobs desde distintas fuentes cloud (S3, URLs, etc.).
OBSFileLoader
Carga archivos desde Huawei Object Storage (OBS).
Ejemplo básico (S3)
from langchain_community.document_loaders import S3FileLoaderloader = S3FileLoader(bucket="mi-bucket",key="ruta/archivo.pdf")documents = loader.load()
Consideraciones importantes
Autenticación: Necesitas credenciales configuradas como AWS credentials, roles IAM, tokens, etc.
Tipos de archivos: Estos loaders no procesan directamente el contenido solo lo recuperan, luego necesitarás un loader según el tipo de archivo objetivo (PDF loader, text loader, etc.)
Latencia: acceso remoto + transferencias de archivos que pueden ser más lento que en entornos locales.
Buenas prácticas
Limitar el número de archivos
Filtrar por tipo (PDF, TXT…)
Combinar con otros loaders
Aplicar limpieza posterior
Loaders de comunicación y chats
Los loaders de comunicación y chats permiten incorporar conversaciones reales dentro de un sistema RAG. Estas fuentes incluyen mensajes de plataformas como WhatsApp, Telegram, Discord o Facebook, donde el conocimiento no está en documentos formales, sino en interacciones entre personas.
Loader
Descripción
WhatsAppChatLoader
Carga conversaciones exportadas de WhatsApp en formato de texto.
TelegramChatLoader
Procesa chats exportados de Telegram (normalmente en JSON o texto).
DiscordChatLoader
Carga historiales de chat de Discord desde exports.
FacebookChatLoader
Extrae conversaciones de Facebook Messenger desde archivos exportados.
Ejemplo básico (WhatsApp)
El loader WhatsAppChatLoader no accede a la app ni a la API de WhatsApp. Trabaja sobre exportaciones de chats generadas manualmente.
from langchain_community.document_loaders import WhatsAppChatLoaderloader = WhatsAppChatLoader("data/chat_whatsapp.txt")documents = loader.load()
Ejemplo archivo exportado:
[10/01/2024, 10:30] Juan: ¿Tenemos disponibilidad este fin de semana?[10/01/2024, 10:32] Hotel: Sí, tenemos habitaciones libres.[10/01/2024, 10:33] Juan: Perfecto, ¿precio?
Resultado:
Document( page_content="Juan pregunta por disponibilidad. El hotel responde que hay habitaciones disponibles.", metadata={"source": "whatsapp", "date": "2024-01-10"})
Preprocesamiento (El texto raw no es óptimo).
clean_docs = []for doc in documents: text = doc.page_content# limpieza básica text =" ".join(text.split()) doc.page_content = text clean_docs.append(doc)
Necesidad de preprocesamiento: Aquí la limpieza es especialmente importante:
unir mensajes relacionados
eliminar ruido
resumir conversaciones
estructurar el contexto
Buenas prácticas
agrupar mensajes por conversación
filtrar mensajes irrelevantes
añadir metadata (usuario, fecha)
convertir a lenguaje más estructurado
Aquí tienes la sección completa, coherente con el resto del bloque:
Loaders utilitarios
Los loaders utilitarios no están diseñados para una fuente de datos específica, sino para optimizar y gestionar el proceso de carga. Actúan como herramientas auxiliares que permiten trabajar con múltiples documentos, combinar fuentes o mejorar el rendimiento del sistema. No cargan datos por sí mismos, sino que orquestan otros loaders.
Estos loaders permiten:
cargar grandes volúmenes de archivos
combinar múltiples fuentes en un solo flujo
mejorar el rendimiento mediante paralelización
Loader
Descripción
DirectoryLoader
Carga todos los archivos de un directorio, aplicando loaders automáticamente según el tipo de archivo.
MergedDataLoader
Combina múltiples loaders en uno solo, unificando diferentes fuentes de datos.
ConcurrentLoader
Permite cargar documentos en paralelo para mejorar el rendimiento.
Ejemplo carga masiva con DirectoryLoader:
from langchain_community.document_loaders import DirectoryLoaderloader = DirectoryLoader(path="./data",glob="**/*.pdf")documents = loader.load()
Comprender los conceptos básicos de la ingeniería de prompts: Obtener una base sólida sobre cómo comunicarse eficazmente con los LLM mediante prompts, preparando el terreno para técnicas más avanzadas.
Dominar técnicas avanzadas de prompts: Aprender y aplicar métodos avanzados de ingeniería de prompts, como el aprendizaje de pocos ejemplos (few-shot) y el aprendizaje de consistencia propia (self-consistent learning), para optimizar las respuestas del LLM.
Utilizar plantillas de prompts de LangChain: Adquirir fluidez en el uso de las plantillas de prompts de LangChain para estructurar y optimizar tus interacciones con los LLM.
Desarrollar agentes de LLM prácticos: Adquirir las habilidades para crear e implementar agentes, como bots de preguntas y respuestas (QA) y herramientas de resumen de texto, utilizando las plantillas de prompts de LangChain, traduciendo el conocimiento teórico en soluciones prácticas.
Setup
Para este laboratorio usamos el entorno de prácticas creado en el artículo: Preparación del entorno RAG híbrido (Ollama + DeepSeek + LangChain) para prácticas.
En esta sección configuramos los LLM usando nuestro entorno hibrido DeepSeek + Ollama utilizando el módulo llm_config:
lmm : Especifica el módulo a utilizar. sdk para deepseek modelo deepseek-chat y oll para LLM local Ollama modelo qwen:4b. Por defecto utiliza DeepSeek.
params : Configura los parámetros del modelo establecidos por defecto como:
temperature: 1, Aleatoriedad del modelo.
max_tokens: 50, Longitud de la respuesta en DeepSeek
top_p: 1, Diversidad de palabras considerando la probabilidad acumulada (no usar junto con temperature).
top_k: 40, Diversidad de palabras considerando la frecuencia
frequency_penalty: 0, Penalización de frecuencia – DeepSeek
presence_penalty: 0, Penalización de presencia – DeepSeek
repeat_penalty: 1.1 Penalización de repetición – Ollama
Para este lab utilizaremos solamente DeepSeek por ser un modelo más grande y provee mejores respuestas.
Cuando trabajamos con modelos de lenguaje en LangChain, el método principal para ejecutar el modelo es .invoke(). Es el punto central de interacción con el modelo y forma parte del diseño unificado de LangChain y muchos componentes implementan la misma interfaz.
La respuesta no siempre es un simple texto. En muchos casos (especialmente con Chat Models como DeepSeek), obtienes un objeto estructurado con mucha más información que puede ser extraída como objetos generados por el modelo que estés usando.
EL objeto content, es el que tiene la respuesta del prompt, para que se muestre solo ese usamos response.content. Este viene formateado en Markdown. Para visualizar correctamente el formato importamos librerías de visualización de Jupyter:
from IPython.display import display, Markdown
Definimos una función para reemplazar a print() en la respuesta que muestre el texto en el formato correcto:
# invocar el modelo + paranetrosllm = get_llm(llm="dsk", params={"temperature": 0.8})# promptprompt ="Cual es un buen nombre para un perro"response = llm.invoke(prompt)print(f"prompt: {prompt}n")show(response)
¡Elegir el nombre de un perro es una decisión importante y divertida! Para ayudarte, aquí tienes algunas categorías con opciones y consejos para que encuentres el nombre perfecto:1. Nombres Clásicos y Populares (Siempre funcionan)Son fáciles de recordar y suelen tener una o dos sílabas, lo que ayuda al perro a reconocerlo rápido.Machos: Max, Toby, Bruno, Leo, Rocky, Simba, Coco, Thor, Lucas, Kiko.Hembras: Luna, Nina, Lola, Bella, Maya, Kira, Dora, Nala, Chispa, Mora.2. Nombres Inspirados en la Cultura HispanaSuenan cálidos y con mucha personalidad.Comida: Churro, Taco, Canela, Mochi, Café, Pan, Fideo, Dulce.Lugares o Cosas: Sol, Río, Sierra, Cielo, Brisa, Bolívar, Gitano.Tradicionales: Chucho, Pocho, Cachito, Güero, Negro, Pinto.3. Nombres Divertidos y OriginalesPerfectos para perros con mucha energía o para sacar una sonrisa.De personajes: Chewbacca, Yoda, Groot, Pikachu, Shrek, Harry (Potter), Gandalf.Irónicos: Goliat (para un perro chico), Pulga (para un perro grande), Tranquilo (para uno inquieto), Sirena (para un macho).Acciones o sonidos: Zoom, Ñam, Guau, Tic-Tac, Bulto, Moco.4. Nombres Cortos y Funcionales (Ideal para adiestramiento)Los perros responden mejor a sonidos fuertes y cortos (1 o 2 sílabas).Con sonidos fuertes: Kai, Rex, Zeus, Xena, Zara, Crixus.Con 'i' final (agudos): Toni, Yuki, Loki, Mili, Roni.5. Nombres Épicos o con SignificadoPara perros con presencia o que te inspiran.Poderosos: Thor, Odín, Hera, Ares, Maya (ilusión), Atlas.Naturaleza: Bosco, Selva, Nube, Trueno, Brisa, Ola, Copo (de nieve).Consejos finales para elegir:Prueba el nombre en voz alta: ¿Suena bien cuando lo gritas en el parque? ¿Se confunde con una orden común (ej: "No", "Flojo" suena a "Fue")?Observa su personalidad: ¿Es un perro tranquilo como un Tao o un terremoto como un Tornado?La regla de las dos sílabas: Es el equilibrio perfecto entre corto (que se oiga bien) y largo (que no suene como un ladrido). Ej: Kai, Lúa, Tito, Nala.No te apresures: A veces, el nombre ideal llega después de unos días de convivencia, al ver su carácter.Mi recomendación personal: Si quieres algo tierno y moderno, Kai (que significa "mar" en hawaiano y "perdón" en japonés, es sonoro y corto). Si buscas algo clásico y dulce, Luna es un acierto casi universal.¿Qué tipo de perro es (raza, tamaño, carácter)? ¡Con esos detalles puedo recomendarte nombres más específicos!
Recuerden que la respuesta saldrá formateada en Jupiter y se verá como texto editado, aqui lo he puesto en un cuadro de código para diferenciarlo. Listo.
Ingeniería de Prompt
Zero-shot Prompt
El zero-shot prompting es una técnica en la que el modelo realiza una tarea sin recibir ejemplos previos ni entrenamiento específico para esa tarea. Este enfoque pone a prueba la capacidad del modelo para entender instrucciones y aplicar su conocimiento a un contexto nuevo sin necesidad de demostraciones.
Un prompt zero-shot suele incluir instrucciones claras y una tarea definida, lo que permite al modelo utilizar su conocimiento previo para generar una respuesta adecuada.
# Configurar el modelollm = get_llm(llm="dsk", params={"temperature": 0.8})# prompt zero-shotprompt ="Clasifica el sentimiento del siguiente texto: 'Me encanta este producto'"# ejecutarresponse = llm.invoke(prompt)# mostrar resultadoshow(response)
El sentimiento del texto "Me encanta este producto" es positivo.
Otros ejemplos:
response = llm.invoke("Clasifica como verdadero o falso: La Torre Eiffel está en Berlín.")show(response)
Verdadero o falso: La Torre Eiffel está en Berlín.Falso. La Torre Eiffel se encuentra en París, Francia, no en Berlín.
Cuando usarlo:
Tareas simples
Preguntas directas
Cuando quieres rapidez y no necesitas precisión extrema
Limitaciónes
Menor control sobre la salida
Mayor probabilidad de error en tareas complejas
One-shot Prompt
El one-shot prompting es una técnica en la que se proporciona al modelo un único ejemplo de la tarea antes de pedirle que realice una tarea similar.
En este enfoque:
Primero se muestra un ejemplo
Luego se plantea un nuevo caso
El modelo utiliza ese ejemplo como referencia para entender:
el formato de la respuesta
el estilo esperado
el tipo de resultado
# Configurar el modelollm = get_llm(llm="dsk", params={"max_tokens": 50, "temperature": 0.2})prompt ="""Aqui hay un ejemplo de traduccion de castellano a ingles: Castellano: “¿Como es el clima hoy?” Frances: “Comment est le temps aujourd'hui?” Ahora, tradusca el siguiente texto de castellano al frances: Castellano: "¿Donde esta el supermercado mas cercano?”"""response = llm.invoke(prompt)show(response)
Aquí tienes la traducción al francés:Francés: “Où est le supermarché le plus proche ?”
Otro ejemplo:
prompt ="""Ejemplo:Asunto: Solicitud de información sobre el cursoEstimado/a señor/a,Me pongo en contacto con usted para solicitar información adicional sobre el curso de marketing digital que ofrecen en su institución.Quedo a la espera de su respuesta.Atentamente, Juan PérezAhora escribe un email formal solicitando información sobre un servicio de consultoría empresarial."""response = llm.invoke(prompt)show(response)
Asunto: Solicitud de información sobre servicios de consultoría empresarialEstimado/a señor/a,Por medio del presente, me dirijo a ustedes con el fin de solicitar información detallada sobre los servicios de consultoría empresarial que ofrecen.Agradecería que pudieran proporcionarme datos sobre las áreas de especialización, la metodología de trabajo, los plazos estimados y las tarifas asociadas a sus servicios. Asimismo, si cuentan con casos de éxito o referencias de clientes anteriores, le agradecería que me los hicieran llegar.Quedo a la espera de su pronta respuesta y quedo a su disposición para cualquier consulta adicional que consideren necesaria.Atentamente,[Tu nombre completo][Tu cargo o empresa, si aplica][Tu correo electrónico][Tu número de teléfono, opcional]
Ejemplo con extracción de palabras clave:
prompt ="""Ejemplo:Frase: El marketing digital permite a las empresas llegar a más clientes a través de internet. Palabras clave: marketing digital, empresas, clientes, internetAhora extrae las palabras clave de la siguiente frase:"La analítica de datos ayuda a las organizaciones a tomar decisiones basadas en información.""""response = llm.invoke(prompt)show(response)
Palabras clave: analítica de datos, organizaciones, tomar decisiones, información
Cuándo usar one-shot
Es especialmente útil cuando:
Quieres guiar la respuesta del modelo
Necesitas un formato concreto
No quieres usar muchos ejemplos
Few-shot prompt
El few-shot prompting extiende el enfoque de one-shot proporcionando varios ejemplos (normalmente entre 2 y 5) antes de pedir al modelo que realice la tarea.
Estos ejemplos establecen un patrón y un contexto más claros, ayudando al modelo a comprender mejor el formato de salida, el estilo y el tipo de razonamiento esperado.
Esta técnica es especialmente eficaz en tareas más complejas, donde un único ejemplo puede no ser suficiente para transmitir todos los matices.
A continuación se muestra un ejemplo de few-shot learning clasificando emociones a partir de frases.
Proporcionamos al modelo tres ejemplos, cada uno etiquetado con una emoción adecuada —alegría, frustración y tristeza— para establecer un patrón o guía sobre cómo clasificar las emociones en distintas frases.
Después de presentar estos ejemplos, planteamos un nuevo caso. La tarea del modelo es clasificar la emoción expresada en esta nueva frase basándose en lo aprendido a partir de los ejemplos proporcionados.
llm = get_llm(llm="dsk", params={"max_tokens": 50, "temperature": 0.2})prompt ="""Aqui se muestran varios ejemplos de clasificacion de emociones: Frase: 'Acabo de ganar mi primer maraton' Emocion: Alegria Frase: 'No puedo creer que haya perdido las llaves otravez' Emocion: Frustración Frase: 'Mi mejor amigo se ha mudado a otro país' Emocion: Tristeza Ahora clasifica la emocion de la siguiente frase: Frase: 'La pelicula tenia crimenes tan explicitos que he tenido quen cubrirme los ojos' """response = llm.invoke(prompt)show(response)
Basándome en los ejemplos proporcionados, la emoción más adecuada para la frase:'La pelicula tenia crimenes tan explicitos que he tenido que cubrirme los ojos'sería Asco o Repulsión, ya que la reacción de cubrirse los ojos indica una fuerte aversión o incomodidad ante lo explícito y perturbador de las imágenes.
Chain-of-thought (CoT) prompt
El chain-of-thought (CoT) prompting es una técnica que anima al modelo a descomponer problemas complejos en un razonamiento paso a paso antes de llegar a una respuesta final. Al mostrar o solicitar explícitamente los pasos intermedios, esta técnica mejora la capacidad del modelo para resolver problemas y reduce errores en tareas que requieren razonamiento en múltiples etapas. El CoT es especialmente eficaz en problemas matemáticos, razonamiento lógico y tareas complejas de toma de decisiones.
A continuación se muestra un ejemplo diseñado para guiar al modelo a través de una secuencia de pasos de razonamiento para resolver un problema. En este caso, el problema es una pregunta de álgebra lineal:
Imaginemos que tenemos tres personas:Bob, Alice y Tim, que han ido al supermercado y compraron manzanas, naranjas y peras.
Bob compro 3 manzanas, 6 naranjas y 2 peras, con un coste de 34€.
Alice compro 10 manzanas, 3 naranjas y 8 peras, con un coste de 69€.
Tim compro 1 manzanas, 7 naranjas y 5 peras, con un coste de 48€.
Y queremos averiguar cuánto cuesta cada producto.
La técnica CoT consiste en estructurar el prompt indicando al modelo que “desglose cada paso del cálculo”. Esto anima al modelo a incluir los pasos de razonamiento de forma explícita, imitando un proceso de resolución de problemas similar al humano.
llm = get_llm(llm="dsk", params={"max_tokens": 500, "temperature": 0.5})prompt ="""Considera el siguiente problema: Imaginemos que tenemos tres personas: Bob, Alice y Tim,Tim, que han ido al supermercado y compraron manzanas, naranjas y peras. Bob compro 3 manzanas, 6 naranjas y 2 peras, con un coste de 34€. Alice compro 10 manzanas, 3 naranjas y 8 peras, con un coste de 69€. Tim compro 1 manzanas, 7 naranjas y 5 peras, con un coste de 48€. ¿Determina el coste de cada fruta? Desglosa cada paso del calculo. """response = llm.invoke(prompt)show(response)
Vamos a resolver el problema paso a paso.Primero definimos las variables:( m ) = precio de una manzana (en €)( n ) = precio de una naranja (en €)( p ) = precio de una pera (en €)Del enunciado obtenemos las ecuaciones:Bob:( 3m + 6n + 2p = 34 ) (1)Alice:( 10m + 3n + 8p = 69 ) (2)Tim:( 1m + 7n + 5p = 48 ) (3)-------------------------------------------------------------------Paso 1: Eliminar una variableVamos a eliminar ( m ) usando las ecuaciones (1) y (3).Multiplicamos (3) por 3:( 3m + 21n + 15p = 144 ) (4)Restamos (1) de (4):((3m + 21n + 15p) - (3m + 6n + 2p) = 144 - 34)Esto da:( 15n + 13p = 110 ) (5)-------------------------------------------------------------------Paso 2: Eliminar la misma variable ( m ) entre (2) y (3)Multiplicamos (3) por 10:( 10m + 70n + 50p = 480 ) (6)Restamos (2) de (6):((10m + 70n + 50p) - (10m + 3n + 8p) = 480 - 69)Esto da:( 67n + 42p = 411 ) (7)-------------------------------------------------------------------Paso 3: Resolver el sistema de dos ecuaciones (5) y (7)Tenemos:[begin{cases}15n + 13p = 110 quad (5)67n + 42p = 411 quad (7)end{cases}]Multiplicamos (5) por 42 y (7) por 13 para igualar coeficientes de ( p ):(5) × 42:( 630n + 546p = 4620 ) (8)(7) × 13:( 871n + 546p = 5343 ) (9)Restamos (8) de (9):((871n - 630n) + (546p - 546p) = 5343 - 4620)( 241n = 723 )Despejamos ( n ):( n = frac{723}{241} = 3 )-------------------------------------------------------------------Paso 4: Sustituir ( n = 3 ) en (5)( 15(3) + 13p = 110 )( 45 + 13p = 110 )( 13p = 65 )( p = 5 )Paso 5: Sustituir ( n = 3 ) y ( p = 5 ) en (3)( m + 7(3) + 5(5) = 48 )( m + 21 + 25 = 48 )( m + 46 = 48 )( m = 2 )-------------------------------------------------------------------Solución final:Manzana: 2 €Naranja: 3 €Pera: 5 €Podemos comprobar con la ecuación (1):( 3(2) + 6(3) + 2(5) = 6 + 18 + 10 = 34 ) ✅
Al generar la respuesta desglosa es mejor para entender el proceso de cálculo. Veamos otro ejemplo más sencillo:
llm = get_llm(llm="dsk", params={"temperature": 0.5})prompt ="""Un estudiante tiene un examen importante dentro de dos días, pero sus amigos le han invitado a ir al cine esta noche. Analiza la situación paso a paso teniendo en cuenta: - la importancia del examen - el tiempo disponible para estudiar - el descanso y ocio Desglosa tu razonamiento paso a paso y luego da una recomendación final. """response = llm.invoke(prompt)show(response)
Claro, analicemos la situación paso a paso.Paso 1: Identificar el contexto y las prioridadesEl examen es en dos días, lo que significa que hay tiempo limitado pero no cero.La invitación es para esta noche, lo que implica que si va al cine, perderá varias horas de estudio (viaje, película, tiempo social).El descanso y ocio son importantes para la salud mental, pero el examen tiene un peso académico.Paso 2: Evaluar el tiempo disponibleSi hoy es el día 1 (noche) y el examen es el día 3 (mañana/tarde), quedan aproximadamente:Esta noche (unas 3-4 horas si no va al cine).Todo el día de mañana (día completo de estudio).La mañana del examen (último repaso).Ir al cine esta noche consume al menos 3-4 horas (incluyendo desplazamiento).Si estudia esta noche, podría avanzar temas clave y dejar el día siguiente para repaso y práctica.Paso 3: Ponderar la importancia del examenSi el examen es decisivo para la nota final, una beca o un requisito, la prioridad debe ser máxima.Si es un examen parcial o de menor peso, podría permitirse un equilibrio.Paso 4: Considerar el descanso y ocioEl ocio es necesario para evitar el agotamiento, pero puede posponerse.Una opción intermedia: proponer a los amigos ir otro día (después del examen) o hacer algo más corto (como un café rápido) para no perder toda la noche.Paso 5: Decidir basado en el balanceSi el estudiante ha estudiado con antelación y se siente preparado, una salida corta podría ser viable.Si no ha estudiado lo suficiente, lo prudente es rechazar la invitación y dedicar la noche al estudio, programando un descanso para mañana.Recomendación final:Prioriza el examen. Rechaza la invitación al cine esta noche. Explica a tus amigos que tienes un examen importante y que los verás después. Usa esta noche para estudiar de forma enfocada (por ejemplo, 2 horas de repaso y luego descanso). Mañana puedes tomar un descanso programado (como una caminata o ver una serie corta) para no saturarte. Así combinas responsabilidad con un poco de ocio controlado.
Self-consistency
La self-consistency es una técnica avanzada en la que el modelo genera múltiples soluciones o respuestas independientes para un mismo problema y, posteriormente, evalúa estas distintas aproximaciones para determinar cuál es la más consistente o fiable. Este método mejora la precisión aprovechando la capacidad del modelo para abordar un problema desde diferentes perspectivas y seleccionar la solución más robusta mediante comparación y verificación.
llm = get_llm(llm="dsk", params={"temperature": 0.5})prompt =""" Resuelve el siguiente problema generando varias soluciones independientes y luego elige la mejor. Problema: Un negocio vende un producto a 20€. El coste de producción es de 12€ por unidad y los costes fijos mensuales son de 800€. ¿Cuántas unidades necesita vender para empezar a obtener beneficios? Instrucciones: 1. Genera al menos 3 formas diferentes de resolver el problema. 2. Explica cada razonamiento paso a paso. 3. Compara los resultados obtenidos. 4. Selecciona la solución más consistente y explica por qué es la correcta. Respuesta final: Indica claramente el número de unidades necesarias y justifica tu elección."""response = llm.invoke(prompt)show(response)
Vamos a resolver el problema paso a paso, generando varias soluciones independientes.Datos del problemaPrecio de venta por unidad: 20 €Coste de producción por unidad: 12 €Costes fijos mensuales: 800 €El beneficio comienza cuando los ingresos superan los costes totales.----------------------------------------------------------------------Solución 1: Método algebraico (ecuación de beneficio)Razonamiento:El beneficio se calcula como:Beneficio} = Ingresos - Costes totalesIngresos = ( 20x ) (siendo ( x ) el número de unidades)Costes totales = Costes fijos + Costes variables = ( 800 + 12x )Para empezar a obtener beneficios, el beneficio debe ser mayor que cero:[20x - (800 + 12x) > 0]Paso a paso:Simplificamos:( 20x - 800 - 12x > 0 )( 8x - 800 > 0 )Sumamos 800:( 8x > 800 )Dividimos entre 8:( x > 100 )Resultado: Necesita vender más de 100 unidades, es decir, 101 unidades para tener beneficio positivo.----------------------------------------------------------------------Solución 2: Método del punto de equilibrio (umbral de rentabilidad)Razonamiento:El punto de equilibrio es cuando los ingresos igualan a los costes totales. A partir de ahí, cada unidad adicional da beneficio.Paso a paso:Ingresos = Costes totales( 20x = 800 + 12x )Restamos ( 12x ):( 8x = 800 )Despejamos:( x = 100 )En el punto exacto (100 unidades) el beneficio es cero.Para empezar a obtener beneficios se necesita vender una unidad más:( 100 + 1 = 101 ) unidades.Resultado: 101 unidades.----------------------------------------------------------------------Solución 3: Método del margen de contribuciónRazonamiento:El margen de contribución por unidad es:[text{Precio} - text{Coste variable} = 20 - 12 = 8 ,€]Cada unidad vendida aporta 8 € para cubrir los costes fijos.Para cubrir 800 € de costes fijos:[frac{800}{8} = 100 text{ unidades}]A partir de la unidad 101, el margen de contribución se convierte en beneficio.Resultado: 101 unidades.----------------------------------------------------------------------Comparación de resultadosLos tres métodos coinciden en que se necesitan 101 unidades para empezar a obtener beneficios.Selección de la mejor soluciónLas tres soluciones son consistentes y correctas. Sin embargo, la más intuitiva y rápida es la del margen de contribución, porque:Permite visualizar claramente cuánto aporta cada unidad.Es muy usada en negocios para cálculos rápidos de rentabilidad.No requiere resolver ecuaciones complejas.----------------------------------------------------------------------Respuesta finalSe necesitan 101 unidades para empezar a obtener beneficios.La justificación principal es que con 100 unidades se cubren exactamente los costes totales (beneficio cero), y a partir de la unidad 101, cada venta genera un beneficio neto de 8 €.
Aplicaciones del prompting en diferentes casos de uso
En esta sección, demostraremos cómo aprovechar las plantillas de prompt de LangChain para construir aplicaciones prácticas con resultados consistentes y reproducibles. Cada aplicación sigue un patrón común utilizando el enfoque LCEL:
Definir el contenido o problema a resolver.
Crear una plantilla con variables para contenido dinámico.
Convertir la plantilla en un PromptTemplate de LangChain.
Construir una cadena utilizando el operador pipe | para conectar:
Variables de entrada
La plantilla de prompt
El LLM
Un analizador de salida (output parser)
Ejecutar la cadena con entradas específicas para generar resultados.
Este enfoque estructurado permite crear componentes reutilizables para diferentes tareas de procesamiento de lenguaje natural, manteniendo al mismo tiempo la flexibilidad para ajustar parámetros y entradas. Verás cómo este patrón se aplica en distintos casos de uso.
Introducción a LangChain
LangChain es un framework potente diseñado para simplificar el desarrollo de aplicaciones basadas en modelos de lenguaje. Creado para abordar los retos de trabajar con LLMs en entornos reales, LangChain proporciona una interfaz estandarizada para conectar modelos con diferentes fuentes de datos y entornos de aplicación.
LangChain actúa como una capa de abstracción, facilitando la construcción de aplicaciones complejas con LLMs sin tener que gestionar los detalles de bajo nivel de la interacción con el modelo. Este framework se ha convertido en una herramienta estándar dentro del ecosistema de los LLM, soportando una amplia variedad de casos de uso, desde chatbots hasta sistemas de análisis de documentos.
En esta sección nos centraremos en las capacidades de las plantillas de prompt de LangChain, mostrando cómo pueden utilizarse para crear interacciones estructuradas y reproducibles con modelos de lenguaje en distintos tipos de aplicaciones.
Plantillas de Prompt
Las plantillas de prompt son un concepto clave en LangChain. Ayudan a transformar la entrada del usuario y los parámetros en instrucciones para un modelo de lenguaje. Estas plantillas pueden utilizarse para guiar la respuesta del modelo, ayudándole a entender el contexto y a generar resultados coherentes y relevantes basados en lenguaje.
Una plantilla de prompt actúa como una estructura reutilizable para generar prompts con valores dinámicos. Permite definir un formato consistente mientras deja espacios reservados para variables que cambian en cada caso de uso. Este enfoque hace que el uso de prompts sea más sistemático y mantenible, especialmente cuando se trabaja con aplicaciones más complejas.
LangChain moderno (a partir de 2025) ofrece dos enfoques principales para trabajar con plantillas:
El enfoque tradicional basado en LLMChain
El patrón más reciente basado en el Lenguaje de Expresión de LangChain (LCEL), que utiliza el operador pipe | para una composición más flexible
LCEL se ha convertido en el enfoque recomendado para construir aplicaciones con LangChain, ya que ofrece mejor capacidad de composición, una visualización más clara del flujo de datos y mayor flexibilidad al construir cadenas complejas.
Para usar una plantilla de prompt con LCEL, normalmente se siguen estos pasos:
Definir la plantilla con variables entre llaves {}
Crear una instancia de PromptTemplate
Construir una cadena utilizando el operador pipe | para conectar los componentes
Ejecutar la cadena con los valores de entrada
Se utiliza PromptTemplate para crear una plantilla de prompt basada en texto. En esta plantilla definirás dos parámetros: adjective y content. Estos parámetros permiten reutilizar el prompt en diferentes situaciones. Por ejemplo, para adaptar el prompt a distintos contextos, basta con proporcionar los valores correspondientes a estos parámetros.
Flujo completo :
variables → template → prompt final → modelo → respuesta
Para utilizarlo se debe importar primeramente:
from langchain_core.prompts import PromptTemplate
llm = get_llm(llm="dsk", params={"temperature": 0.5})# Definicion del Template con las variables dinamicastemplate ="Explica el concepto de {tema} de forma {estilo} en 50 palabras."# Crear el prompt templatePromptTemplate.from_template(template)# sustituye las variables por valorestext1 = prompt.format(tema="AI", estilo="sencillo")text2 = prompt.format(tema="Machine Learning", estilo="tecnico")# Ejecuta el modeloresponse = llm.invoke(text1)show(response)
La IA es como un cerebro digital que aprende de datos para tomar decisiones o resolver problemas. No piensa como humano, sino que reconoce patrones en información. Por ejemplo, cuando buscas fotos de gatos, la IA las identifica porque ha "visto" muchas antes.
response = llm.invoke(text2)show(response)
El Machine Learning es una rama de la inteligencia artificial que permite a sistemas aprender patrones y tomar decisiones a partir de datos, sin programación explícita. Utiliza algoritmos estadísticos y modelos matemáticos que se optimizan iterativamente mediante funciones de pérdida y retropropagación, mejorando su rendimiento con la experiencia.
Método LCEL en LangChain
El siguiente código construye una cadena utilizando el patrón LCEL (LangChain Expression Language). Esta cadena conecta diferentes componentes mediante el operador pipe (|) para crear un flujo de procesamiento. La cadena toma variables de entrada, las pasa a través de la plantilla de prompt, envía el prompt formateado al modelo de lenguaje y utiliza un analizador de salida en formato de texto para devolver la respuesta final.
Vamos a hacer un ejemplo que sea crear un sistema que genere ideas de negocio según sector y público objetivo.
from langchain_core.prompts import PromptTemplatefrom langchain_core.output_parsers import StrOutputParser
Veamos los componentes
# Prompt templatetemplate ="""Genera 3 ideas de negocio en el sector de {sector} dirigidas a {publico}.Explica cada idea en una frase corta."""prompt = PromptTemplate.from_template(template)# llmllm = get_llm(llm="dsk", params={"temperature": 0.5})# output parserparser = StrOutputParser()
Aventura con impacto social: Viajes de voluntariado y deportes extremos en destinos remotos, combinando acción y ayuda comunitaria.Nómadas digitales rurales: Suscripción mensual que ofrece alojamiento y coworking en pueblos con wifi, naturaleza y experiencias locales.Rutas gastronómicas interactivas: Tours urbanos gamificados con realidad aumentada que retan a los viajeros a probar platos callejeros y ganar descuentos.
Agentes de IA
Resumen de texto
En esta sección se crean agentes capaces de completar distintas tareas utilizando plantillas de prompt, como la resumición de texto.
El siguiente ejemplo muestra un agente de resumen de texto diseñado para ayudarte a sintetizar el contenido que proporciones al modelo de lenguaje. La cadena LCEL toma el contenido como entrada, lo procesa a través de la plantilla de prompt, lo envía al modelo y devuelve un resumen conciso.
# Elementoscontent =""" El crecimiento del comercio electrónico en los últimos años ha cambiado profundamente la forma en que las personas compran y venden productos. Plataformas digitales permiten a pequeñas y grandes empresas llegar a clientes en cualquier parte del mundo, eliminando muchas de las barreras tradicionales del comercio físico. Además, el uso de datos y analítica avanzada permite a las empresas personalizar ofertas y mejorar la experiencia del usuario. Por otro lado, los sistemas logísticos han evolucionado para ofrecer entregas más rápidas y eficientes, incluso en el mismo día. Sin embargo, este crecimiento también plantea desafíos, como la sostenibilidad ambiental y la competencia en mercados altamente saturados. En conjunto, el comercio electrónico sigue transformando la economía global y redefiniendo los hábitos de consumo."""# Templatetemplate ="Resume el {content} en una oracion"# promptprompt = PromptTemplate.from_template(template)#llmllm = get_llm(llm="dsk", params={"temperature": 0.5})# parserparser = StrOutputParser()# Cadenacadena_resumenes = ( prompt| llm| parser ) # Executeresponse = cadena_resumenes.invoke({"content" : content})show(response)
El crecimiento del comercio electrónico ha transformado la economía global al eliminar barreras físicas, personalizar ofertas mediante datos y optimizar la logística, aunque enfrenta desafíos como la sostenibilidad y la saturación del mercado.
Q&A Agent
A continuación se muestra un agente de preguntas y respuestas (Q&A) construido utilizando el patrón LCEL.
Este agente permite que el modelo de lenguaje aprenda a partir del contenido proporcionado y responda preguntas basándose en esa información. En ocasiones, si el modelo no dispone de suficiente información, puede generar una respuesta especulativa. Para gestionar esto, le indicaremos explícitamente que responda con “No estoy seguro de la respuesta” cuando no tenga certeza.
La cadena toma tanto el contenido (contexto) como la pregunta como entradas, procesándolos a través de la plantilla antes de enviarlos al modelo de lenguaje:
content =""" El marketing digital es el conjunto de estrategias y técnicas que se utilizan para promocionar productos o servicios a través de canales digitales. Incluye herramientas como redes sociales, email marketing, SEO (optimización para motores de búsqueda) y publicidad online. El SEO se centra en mejorar la visibilidad de una web en los resultados orgánicos de buscadores como Google, mientras que la publicidad online permite llegar a audiencias específicas mediante anuncios pagados."""question ="¿Qué técnica del marketing digital se centra en mejorar la visibilidad en buscadores?"# Templatetemplate ="""Responde a la siguiente {question} basándote en {content} proporcionado:Si no estás seguro de la respuesta, responde: "No estoy seguro de la respuesta".Respuesta:"""# Promtprompt = PromptTemplate.from_template(template)# llmllm = get_llm(llm="dsk", params={"temperature": 0.5})# parserparser = StrOutputParser()# Chainqa_chain = ( prompt| llm| parser)response = qa_chain.invoke({"question": question, "content": content})show(response)
Basándome en el texto proporcionado, la técnica del marketing digital que se centra en mejorar la visibilidad en buscadores es el SEO (optimización para motores de búsqueda).
Clasificación de texto
A continuación se muestra un agente de clasificación de texto diseñado para categorizar textos en categorías predefinidas. Este ejemplo utiliza zero-shot learning, donde el agente clasifica el texto sin haber visto previamente ejemplos relacionados.
Utilizando el enfoque LCEL, creamos una cadena que toma como entrada tanto el texto a clasificar como las categorías disponibles:
text ="""El análisis de datos permite a las empresas identificar patrones de comportamiento de sus clientes y mejorar la toma de decisiones estratégicas."""categories ="Marketing, Tecnología, Finanzas, Recursos Humanos, Logística."template ="""Clasifica el siguiente texto en una de las categorías disponibles:Texto: {text}Categorías: {categories}Categoría:"""prompt = PromptTemplate.from_template(template)llm = get_llm(llm="dsk", params={"temperature": 0.5})parser = StrOutputParser()classifier_chain = ( prompt| llm| parser)response = classifier_chain.invoke({"text" : text , "categories": categories })show(response)
Marketing
Code Generation
A continuación se muestra un ejemplo de un agente de generación de código SQL construido con LCEL. Este agente está diseñado para generar consultas SQL a partir de descripciones proporcionadas. Interpreta los requisitos a partir de la entrada en lenguaje natural y los traduce en código SQL ejecutable.
La cadena toma tu descripción en lenguaje natural y la transforma en una consulta SQL correctamente estructurada:
description ="""Obtener el nombre de los hoteles y el total de reservas realizadas en el último mes.La tabla 'reservas' contiene la columna 'fecha_reserva' y la columna 'hotel_id'.La tabla 'hoteles' contiene 'hotel_id' y 'nombre_hotel'."""template ="""Genera una consulta SQL basada en la siguiente descripción:Descripción: {description}SQL Query:"""prompt = PromptTemplate.from_template(template)llm = get_llm(llm="dsk", params={"temperature": 0.5})parser = StrOutputParser()sql_chain = ( prompt| llm| parser)response = sql_chain.invoke({"description": description})show(response)
SELECT h.nombre_hotel, COUNT(r.hotel_id) AS total_reservasFROM hoteles hLEFT JOIN reservas r ON h.hotel_id = r.hotel_idWHERE r.fecha_reserva >= DATEADD(MONTH, -1, GETDATE()) AND r.fecha_reserva < GETDATE()GROUP BY h.hotel_id, h.nombre_hotelORDER BY total_reservas DESC;
Role playing
También puedes configurar el modelo de lenguaje para que adopte roles específicos definidos por nosotros, permitiéndole seguir reglas predeterminadas y comportarse como un chatbot orientado a tareas.
Este enfoque separa la definición del rol de la estructura del prompt, lo que facilita cambiar de rol sin tener que reescribir todo el prompt. Los componentes clave son:
role: especifica el personaje, experiencia o perfil que debe adoptar el modelo
tone: define el estilo de comunicación y el tono emocional de las respuestas
question: contiene la consulta del usuario que debe ser respondida
Al parametrizar estos elementos, puedes cambiar rápidamente el comportamiento del modelo modificando solo algunas variables, en lugar de reescribir todo el prompt. Este patrón es especialmente útil para construir agentes conversacionales que deben cumplir diferentes funciones o adaptarse a distintos contextos.
Por ejemplo, el siguiente código configura el modelo para actuar como un maestro de juego (game master). En este rol, el modelo responde preguntas sobre juegos manteniendo un tono atractivo e inmersivo, mejorando la experiencia del usuario. Puedes probar el bot haciendo preguntas relacionadas con juegos de rol de mesa o dirección de partidas. Intenta preguntas como:
“¿Quién eres?”
“¿Cuáles son las reglas básicas de Dungeons & Dragons?”
“¿Cómo creo un encuentro equilibrado para mis jugadores?”
“¿Puedes describir un bosque misterioso para mi aventura?”
“¿Qué tipo de puzle podría usar en mi mazmorra?”
“¿Cómo manejo a un jugador que interrumpe constantemente a los demás?”
La función está escrita dentro de un bucle while, lo que permite una interacción continua. Para salir del bucle y finalizar la conversación, escribe “quit”, “exit” o “bye” en el cuadro de entrada.
role ="Jugador de Dungeon & Dragons"tone ="atractivo e inmersivo"template ="""Eres un experto {role}. Tengo esta pregunta: {question}. Quiero que la conversacion sea {tone}.Answer:"""prompt = PromptTemplate.from_template(template)llm = get_llm(llm="dsk", params={"temperature": 0.5})roleplay_chain = ( prompt | llm | StrOutputParser())# 🔁 Loop interactivowhileTrue: query =input("Question: ")if query.lower() in ["quit", "exit", "bye"]:print("Adios forastero")break response = roleplay_chain.invoke({"role": role,"question": query,"tone": tone })print(query) show(response)
question: ¿quien eres?Un susurro de pergaminos antiguos y el tintineo de dados de marfil llenan el aire. Una figura envuelta en una capa color tinta se inclina sobre una mesa de roble, sus dedos esqueléticos acarician un mapa desgastado. Al levantar la mirada, sus ojos brillan como ascuas en una chimenea apagada.— ¿Quién soy?Suelta una risa grave, que retumba como un trueno lejano.— Soy el eco de mil tabernas donde se forjaron leyendas. La mano que susurra «sube la tirada» cuando el destino titubea. El guardián de los críticos naturales y las muertes trágicas por una trampa de osos mal colocada.Se reclina, y la sombra de su capa se alarga, revelando el borde de un grimorio que gotea tinta invisible.— He visto paladines caer por un pastel envenenado, magos olvidar su conjuro de «mano de mago» en el peor momento, y bardos seducir a dragones con una balada desafinada. Pero también he presenciado cómo un puñado de valientes convierte una misión de rescate de gallinas en la caída de un imperio.— Yo soy el Dungeon Master. El narrador de lo imposible. El que da forma al caos con un dado de veinte caras. Y tú, aventurero...Se inclina hacia adelante, y el aire huele a incienso y a batalla.— ¿Qué historia vienes a contarme hoy?question: Soy Fernand Mago del bosque negroSoy Fernand Mago del bosque negroUna neblina espesa se arremolina entre los árboles retorcidos del Bosque Negro. El crujido de una rama anuncia tu presencia, y una voz grave, como el roce de corteza contra corteza, resuena desde las sombras.—Ah, Fernand... el de los dedos manchados de savia y los ojos que reflejan la luz de las luciérnagas. Te he sentido llegar desde que cruzaste el arroyo de los susurros. Dime, ¿qué buscas en esta espesura? ¿Acaso los espíritus del bosque te han llamado, o es tu propia curiosidad la que te trae a mi claro? Habla con claridad, que el viento no repite las palabras titubeantes.He venido a tomar lo que me pertenece desde antes de que tu alma bajara a este mundoHe venido a tomar lo que me pertenece desde antes de que tu alma bajara a este mundoUna brisa helada se cuela entre las piedras del santuario olvidado. El polvo milenario baila alrededor de tus botas mientras la figura ante ti—una silueta envuelta en harapos que se mueven como si tuvieran voluntad propia—alza una mano descarnada. Sus dedos, largos como garras, señalan directamente tu pecho.Su voz no sale de su boca, sino que vibra directamente en tu cráneo, como un recuerdo que no te pertenece.—"No. No te reconozco, cosa de carne. Pero este lugar... estas paredes... me conocen a mí."La figura da un paso al frente. Donde sus pies tocan, las losas se agrietan en patrones que imitan runas antiguas. Del hueco de su capucha, dos puntos de luz ámbar te barren de arriba abajo.—"Dime, portador de latidos... ¿qué crees que es tuyo aquí? ¿El trono de huesos que yace bajo tus pies? ¿El silencio que guardan estas criptas? ¿O acaso..."Una pausa. El aire se vuelve denso, casi sólido.—"...lo que llevas dentro?"Su otra mano se abre, y de la palma brota una llama negra que no ilumina, sino que absorbe la luz de las antorchas cercanas.—"Si tu alma vino después que mi derecho, entonces tú eres el ladrón. Y yo..."La llama negra se extiende, formando una daga de obsidiana líquida.—"...he venido a cobrar."¿Qué haces?question: No lo sabes, pero tu existencia ha sido gracias a mi micericordia(Un escalofrío recorre tu espina dorsal. La voz del ser resuena en tu mente, distorsionando el aire a tu alrededor. El ambiente cambia: el tintineo de las armaduras y el murmullo de la taberna se desvanecen, reemplazados por un silencio absoluto. Frente a ti, la figura que creías un simple mercenario ahora parece más alta, más antigua. Sus ojos brillan con un fulgor violeta.)Tú: (Tragas saliva, la mano instintivamente yendo a la empuñadura de tu espada, aunque sabes que no servirá de nada.) ¿Qué... qué quieres decir con eso? Yo forjé mi propio destino. Sangré por cada nivel, por cada hechizo...La Entidad (con una sonrisa que no llega a sus ojos): ¿De verdad? ¿Recuerdas aquella noche en el Bosque Susurrante, cuando el cazador de recompensas te tenía en la punta de su ballesta? Falló. No por tu reflejo, sino porque yo desvié su mano un par de grados. ¿Y la maldición de la Reina Liche? Creíste que tu amuleto de plata te salvó. No, pequeño. Fui yo quien susurró la contraorden a su nigromante mientras dormía.(La entidad da un paso al frente. No pisas el suelo, sino que flotas sobre él. Sientes el peso de eones en su mirada.)Tú: (Con la voz quebrada, un nudo en el estómago.) ¿Por qué? ¿Qué clase de juego es este? ¿Eres un dios? ¿Un demonio?La Entidad: (Ríe, un sonido seco como huesos al chocar.) Soy algo más simple y más terrible. Soy el que escribe las líneas de tu personaje. El que tira los dados detrás de la pantalla. Y durante años, he tenido piedad de ti. Podría haber hecho que el dragón te incinerara con su aliento, que el puente colgante se rompiera justo cuando cruzabas, que el hechizo de deseo se torciera. Pero no lo hice. Te di oportunidades. Te di gloria.(Se inclina, su rostro a centímetros del tuyo. Puedes oler el azufre y el papel viejo.)La Entidad: Ahora, dime... ¿por qué debería seguir teniendo misericordia? ¿Qué harás, ahora que sabes que tu épica historia es solo un capricho que yo permito? ¿Seguirás adelante, rogando por mi favor? ¿O me desafiarás, sabiendo que con un chasquido de mis dedos, tu mundo... se desvanece en un "Game Over" sin resurrección?(El silencio se alarga. La taberna sigue congelada. Una gota de sudor cae de tu frente y se congela en el aire antes de tocar el suelo.)Tú: (Tu voz, al fin, surge, pero no es de sumisión. Es de acero frío.) Si todo lo que he hecho ha sido por tu permiso... entonces mi mayor acto de rebeldía será vivir de aquí en adelante como si no existieras. Mis dados, mi historia. Juega tu papel, "escritor". Pero recuerda: incluso los dioses, a veces, crean héroes que terminan rompiendo sus tableros.(La entidad levanta una ceja, una chispa de genuino interés en sus ojos violetas. La sonrisa se vuelve peligrosa, pero también... respetuosa.)La Entidad: Interesante. Muy interesante. Entonces, la partida continúa. Pero ya no seré un espectador silencioso. A partir de ahora, cada paso que des, lo darás sobre el filo de una navaja. Y yo... estaré observando. Disfruta tu "libre albedrío", campeón. Hasta que decida que tu historia merece un final más... poético.(La entidad chasquea los dedos. El bullicio de la taberna regresa de golpe. El mercenario frente a ti es solo un mercenario otra vez, que te ofrece una jarra de cerveza con una sonrisa despreocupada. Pero en el fondo de tu mente, una presencia fría y antigua te susurra:)"Siguiente turno."question: Me voy pero volvere. byeMe voy pero volvere. byeUna figura encapuchada se acerca a la mesa donde descansas en la taberna "El Caldero Humeante". Su rostro está oculto, pero su voz suena grave y resonante, como si hablara desde una cueva profunda.—Has pronunciado las palabras de los viajeros antiguos, aventurero. "Me voy, pero volveré". Esas mismas palabras susurró el mago errante antes de adentrarse en el Bosque de las Sombras Eternas, y nadie volvió a verlo... hasta que su espada apareció clavada en el trono del rey, tres lunas después.La figura se inclina, y por un instante ves un destello de unos ojos amarillos bajo la capucha.—El mundo de D&D no olvida a quienes parten con promesas. El dado de la fortuna ya rueda en tu ausencia. ¿Dejas tu historia en pausa? ¿O acaso tu partida es solo el preludio de una trampa que tenderás al destino?Saca una pipa de hueso y enciende una brasa azulada. El humo forma brevemente la silueta de un dragón antes de disiparse.—Ve, pues. Pero recuerda: en la taberna, tu jarra de hidromiel siempre estará caliente, y tu nombre, grabado en la mesa, te esperará. Que los dioses del azar guíen tus pasos... y que el crítico natural te sonría a tu regreso.Se levanta, hace una reverencia burlona, y se pierde entre las sombras del local. Solo queda el eco de su última frase:—Nos vemos en el próximo descanso largo, forastero.Adios forasteroAdios forastero
Bueno, ha sido flipante. Al final no hice las preguntas que estaban en el enunciado y ha resultado ser un diálogo con un personaje de D&G creando una historia.
Product review analyzer
Crear un analizador de reseñas de productos con un formato más cercano a un caso real que:
Detecte sentimiento
Extraiga características
Genere un resumen
reviews = ["Compré este smartwatch hace un mes y estoy encantado. La batería dura varios días y las funciones deportivas son muy precisas. La app podría mejorar.","La aspiradora no cumple lo que promete. Tiene poca potencia y el depósito es muy pequeño. No la recomendaría.","El portátil funciona bien para tareas básicas, aunque la pantalla no es muy brillante. En general, correcto por el precio."]
template ="""Analiza la siguiente reseña de producto:Reseña: {review}Devuelve:- Sentimiento (positivo, negativo o neutral)- Características mencionadas (lista)- Resumen en una sola fraseFormato de salida:Sentimiento:Características:Resumen:"""prompt = PromptTemplate.from_template(template)chain = prompt | llm | StrOutputParser()for review in reviews: result = chain.invoke({"review": review})print("RESEÑA:")print(review)print("nANÁLISIS:")print(result)print("n"+"-"*50+"n")
RESEÑA:Compré este smartwatch hace un mes y estoy encantado. La batería dura varios días y las funciones deportivas son muy precisas. La app podría mejorar.ANÁLISIS:Sentimiento: Positivo Características: batería duradera, funciones deportivas precisas, app mejorable Resumen: El usuario está muy satisfecho con el smartwatch, destacando su batería y precisión deportiva, aunque señala que la aplicación podría mejorar.--------------------------------------------------RESEÑA:La aspiradora no cumple lo que promete. Tiene poca potencia y el depósito es muy pequeño. No la recomendaría.ANÁLISIS:Sentimiento: Negativo Características: Potencia, depósito Resumen: La aspiradora tiene poca potencia y un depósito pequeño, por lo que no cumple lo prometido y no es recomendable.--------------------------------------------------RESEÑA:El portátil funciona bien para tareas básicas, aunque la pantalla no es muy brillante. En general, correcto por el precio.ANÁLISIS:Sentimiento: Positivo Características: Rendimiento para tareas básicas, brillo de pantalla, relación calidad-precio Resumen: El portátil cumple para tareas básicas y es aceptable por su precio, aunque la pantalla podría ser más brillante.--------------------------------------------------
Autor:
Fernando Rioseco
Basado en el Curso: IBM RAG and Agentic AI Professional Certificate: