Saltar a contenido

← Volver al índice | Análisis LLM Departamental | Viabilidad Pekko→Dapr

Plataforma MLOps y Workflows Agénticos

Tipo: Arquitectura — Sistema Agéntico
Audiencia: Equipo de desarrollo, dirección técnica, arquitectos
Fecha: 20 de marzo de 2026
Relacionado con: Análisis LLM Departamental | Viabilidad Pekko→Dapr | Gobernanza de Datasets


1. El Problema: De «4 Modelos Genéricos» a «9+ Modelos Especializados»

El análisis LLM departamental identificó que cada departamento del IEO necesita su propio stack de IA. Esto genera un problema que no existía con el stack original de un único LLaVA genérico:

Problema Descripción
¿Cómo se despliegan 9+ modelos? Ya no es «instalar Ollama y listo» — hay que gestionar versiones, pesos, configuraciones, actualizaciones
¿Cómo se entrenan? Fine-tuning con LoRA/QLoRA requiere datasets curados, pipelines de entrenamiento, validación, rollback
¿Cómo se orquestan? Una consulta puede necesitar YOLO → CLIP → Qwen2.5-VL en cadena, con decisiones intermedias
¿Quién decide qué modelo usar? Un «agente» inteligente que enrute la consulta al modelo correcto según el departamento y el tipo
¿Cómo se monitoriza? Precisión en producción, latencia, drift del modelo, uso de VRAM

[!IMPORTANT] La respuesta a todas estas preguntas es una arquitectura de MLOps + Workflows Agénticos, implementada completamente con Quarkus + Dapr + LangChain4j. Es la capa de orquestación que gobierna toda la IA de la plataforma.


2. Las Tres Capas de la Arquitectura IA

flowchart TB
    subgraph L1 ["Capa 1 — MLOps"]
        REG["MLflow - Registro de modelos"]
        DVC2["DVC - Versionado de datasets"]
        OLL["Ollama - Servidor de modelos"]
    end

    subgraph L2 ["Capa 2 — Orquestación"]
        DAPR["Dapr Workflow - FSMs y pipelines"]
        LC4J["LangChain4j - Agentes IA"]
        BIND["Dapr Bindings - Triggers externos"]
    end

    subgraph L3 ["Capa 3 — Inferencia"]
        VLM["Qwen2.5-VL - Visión"]
        YOLO3["YOLOv11 - Detección"]
        UNET2["U-Net - Segmentación"]
        OCE2["OceanGPT - Texto"]
    end

    REG --> OLL
    DVC2 --> REG
    BIND --> DAPR
    DAPR --> LC4J
    LC4J --> L3

    style L1 fill:#9b59b6,color:#fff
    style L2 fill:#e74c3c,color:#fff
    style L3 fill:#2ecc71,color:#fff

2.1 Capa 1 — MLOps: Ciclo de vida de los modelos

¿Qué es MLOps? — Es para modelos de IA lo que DevOps es para código: versionado, testing, despliegue, monitorización y rollback de modelos.

Herramienta Función Self-hosted Licencia
MLflow Registro de modelos: versiones, stages (Staging/Production/Archived), métricas ✅ Docker Apache 2.0
DVC Versionado de datasets y artefactos grandes (imágenes, pesos) sin inflar Git ✅ CLI + MinIO Apache 2.0
Ollama Servidor de modelos LLM/VLM con Modelfiles (similar a Dockerfile) ✅ Docker MIT

MLflow trackea cada experimento de fine-tuning:

Experimento: LoRA Otolitos v3
├── Run 1: lr=1e-4, epochs=10 → precisión 78% → REJECTED
├── Run 2: lr=5e-5, epochs=20 → precisión 84% → STAGING
└── Run 3: lr=5e-5, epochs=30, aug=True → precisión 89% → PRODUCTION ✅

2.2 Capa 2 — Orquestación: Quarkus + Dapr + LangChain4j

Componente Función Tipo
Dapr Workflow Orquestación de procesos largos con estado persistente (FSMs, pipelines ETL, entrenamiento) Declarativo + código
Dapr Bindings Triggers externos: webhooks SharePoint, polling GSheets, MQTT IoT, Copernicus API Declarativo YAML
Dapr Pub/Sub Eventos desacoplados entre servicios: muestra.nueva, esquema.cambio, modelo.actualizado Declarativo YAML
LangChain4j Agentes IA con tool calling autónomo, multi-modelo, RAG Código Java
Quarkus Scheduler Crons y tareas periódicas: polling, resúmenes, monitorización Código Java

[!NOTE] Todo lo que normalmente requeriría una herramienta visual de workflows tipo n8n/Airflow se implementa aquí directamente en Dapr + Quarkus. La ventaja: cero dependencias externas adicionales, todo es parte del mismo stack que ya usamos para el backend.


3. El Cerebro Agéntico: LangChain4j

LangChain4j es lo que convierte a Quarkus en un sistema agéntico. Un «agente» es un LLM que puede:

  1. Recibir una consulta en lenguaje natural
  2. Decidir qué herramientas usar (tool calling)
  3. Ejecutar las herramientas
  4. Razonar sobre los resultados
  5. Iterar si necesita más información
  6. Responder con una respuesta fundamentada

3.1 Definición de un Agente

// Agente IA del IEO — interfaz CDI declarativa
@RegisterAiService(modelName = "qwen25-vl")
@SystemMessage("""
    Eres un experto en biología marina del IEO de Málaga.
    Tienes acceso a las siguientes herramientas:
    - buscar_especie: busca en FishBase/GBIF
    - buscar_otolitos: busca en ChromaDB por similitud visual
    - identificar_imagen: usa YOLOv11 para detectar especies
    Siempre cita las fuentes de tus respuestas.
    """)
public interface AgenteIEO {
    String consultar(@UserMessage String consulta,
                     @ImageUrl String imagen);
}

3.2 Herramientas del Agente (Tools)

// Herramientas que el agente puede llamar AUTÓNOMAMENTE
@Tool("Busca especies en la base de datos FishBase")
public List<Especie> buscarEspecie(String nombre) {
    return fishBaseClient.search(nombre);
}

@Tool("Busca otolitos similares en ChromaDB por embedding visual")
public List<Otolito> buscarOtolitos(byte[] imagenEmbedding) {
    return chromaClient.query(imagenEmbedding, 5);
}

@Tool("Detecta especies en una imagen usando YOLOv11")
public List<Deteccion> identificarImagen(String imagenUrl) {
    return yoloClient.detect(imagenUrl);
}

@Tool("Consulta datos oceanográficos de Copernicus")
public DatosOceanograficos consultarCopernicus(String zona, String fecha) {
    return copernicusClient.query(zona, fecha);
}

[!IMPORTANT] El agente no está programado para llamar a FishBase primero y luego a ChromaDB. El LLM decide autónomamente qué herramientas usar según la consulta del investigador. Esto es lo que significa «agéntico».

3.3 Agentes por Departamento

Departamento Agente Modelo Base Tools Disponibles
Pesquerías AgentePesquerias Qwen2.5-VL 7B YOLOv11, U-Net otolitos, FishBase, SIRENO, ChromaDB
Acuicultura AgenteAcuicultura Qwen2.5-VL 7B ResNet-ViT patologías, IoT sensores, historial alimentación
Medio Marino AgenteMedioMarino InternVL2.5 8B MariNeXt, OceanGPT, Copernicus, CTD series
Tortugas y Cetáceos AgenteCetaceos Qwen2.5-VL 7B Happywhale matching, MYDAS, SpeciesNet
Oceanografía AgenteOceanografia OceanGPT Copernicus, Argo, series temporales

El router de agentes selecciona automáticamente cuál usar según el departamento del usuario logueado y el tipo de consulta:

// Router de agentes — selección dinámica
@ApplicationScoped
public class RouterAgentes {

    @Inject @Named("pesquerias") AgenteIEO agentePesquerias;
    @Inject @Named("acuicultura") AgenteIEO agenteAcuicultura;
    @Inject @Named("medio-marino") AgenteIEO agenteMedioMarino;

    public String consultar(String departamento, String consulta, String imagen) {
        return switch (departamento) {
            case "Pesquerías" -> agentePesquerias.consultar(consulta, imagen);
            case "Acuicultura" -> agenteAcuicultura.consultar(consulta, imagen);
            case "Medio Marino" -> agenteMedioMarino.consultar(consulta, imagen);
            default -> agenteGeneral.consultar(consulta, imagen);
        };
    }
}

4. Orquestación con Dapr Workflow — Ejemplo Completo

Escenario: Un investigador sube una foto de otolito

sequenceDiagram
    actor INV as Investigador
    participant BIND as Dapr Binding
    participant WF as Dapr Workflow
    participant LC as LangChain4j
    participant YOLO4 as YOLOv11
    participant UNET3 as U-Net
    participant VLM2 as Qwen2.5-VL
    participant DB2 as PostgreSQL

    INV->>BIND: Sube foto a SharePoint
    Note over BIND: Input Binding Graph API
    BIND->>WF: Evento muestra.nueva

    Note over WF: FSM: Capturada
    WF->>YOLO4: Activity: detectar especie
    YOLO4-->>WF: Sardina (92%)

    Note over WF: FSM: Preprocesando
    WF->>UNET3: Activity: segmentar annuli
    UNET3-->>WF: 4 anillos detectados

    Note over WF: FSM: IdentificandoIA
    WF->>LC: Activity: generar informe
    LC->>VLM2: Prompt + imagen + datos
    VLM2-->>LC: Informe contextualizado
    LC-->>WF: Resultado completo

    Note over WF: FSM: PendienteConfirmación
    WF->>DB2: Persistir resultado
    WF->>INV: Pub/Sub notificación
Paso Componente Dapr Equivalente Pekko Mejora
1. Detectar cambio en SharePoint Dapr Input Binding (Graph API webhook) Actor custom con polling Declarativo YAML, sin código
2. Iniciar pipeline Dapr Workflow Actor supervisor + mensajes Estado persiste entre reinicios
3. Detectar especie Workflow Activity → YOLOv11 Actor AIInferenceActor 15 líneas vs 200 líneas
4. Segmentar otolito Workflow Activity → U-Net Actor custom Retry automático con compensación
5. Generar informe LangChain4j (agente con tools) Actor + código imperativo Tool calling autónomo
6. Notificar Dapr Pub/Submuestra.confirmada Actor + Pekko Streams Desacoplado, zero código

Implementación del Workflow en Java

// Dapr Workflow — Pipeline de identificación de muestra
@Workflow(name = "IdentificacionMuestraWorkflow")
public class IdentificacionMuestra extends DaprWorkflow {

    @Override
    public WorkflowStub create() {
        return ctx -> {
            // 1. Preprocesar imagen
            var imagen = ctx.callActivity("preprocesarImagen", input, ImagenLimpia.class);

            // 2. Detectar especie con YOLOv11
            var deteccion = ctx.callActivity("detectarEspecie", imagen, Deteccion.class);

            // 3. Si es otolito → segmentar annuli
            if (deteccion.tipo().equals("otolito")) {
                var edad = ctx.callActivity("segmentarOtolito", imagen, EdadEstimada.class);
                ctx.setState("edad", edad);
            }

            // 4. Generar informe con agente LangChain4j
            var informe = ctx.callActivity("generarInforme", deteccion, Informe.class);

            // 5. Esperar confirmación humana (puede tardar horas/días)
            var confirmacion = ctx.waitForExternalEvent("confirmacion", Confirmacion.class);

            // 6. Persistir resultado final
            ctx.callActivity("persistirResultado", confirmacion, Void.class);
        };
    }
}

[!TIP] La diferencia clave con Pekko: si el servidor se reinicia en el paso 5 (esperando confirmación humana), Dapr Workflow recupera automáticamente el estado y continúa donde se quedó. Con Pekko, habría que implementar Pekko Persistence manualmente (~300 líneas extra).


5. Pipelines ETL — Configuración Estática vs Dinámica

5.1 Dos Niveles de Configuración

[!IMPORTANT] Los YAML de Dapr definen qué tipos de conexión existen (infraestructura). Las fuentes de datos concretas que cada departamento quiere monitorizar se gestionan dinámicamente desde la UI, sin tocar YAML ni hacer commits.

Nivel Qué define Dónde vive Quién lo cambia Frecuencia
Infraestructura (YAML) «La plataforma puede conectarse a SharePoint, GSheets, MQTT» Git + despliegue Docker Equipo de desarrollo Raramente (instalar un driver nuevo)
Fuentes de datos (BBDD) «Monitorizar la carpeta ECOMED/2025/Bio del SharePoint de Pesquerías» PostgreSQL + UI de administración Responsable de Departamento o Admin/TI Frecuentemente (nueva campaña, nuevo GSheet)

5.2 Nivel 1 — YAML de Infraestructura (se despliega una vez)

Estos YAML son equivalentes a «instalar un driver de base de datos» — se configuran al montar la plataforma y no se tocan más:

# dapr/components/graph-api-binding.yaml
# Se despliega UNA VEZ — define que la plataforma PUEDE hablar con SharePoint
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: microsoft-graph
spec:
  type: bindings.microsoft.graph
  metadata:
    - name: tenantId
      value: "${AZURE_TENANT_ID}"
    - name: clientId
      value: "${AZURE_CLIENT_ID}"
# dapr/components/mqtt-binding.yaml
# Se despliega UNA VEZ — define que la plataforma PUEDE recibir datos MQTT
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: iot-sensors
spec:
  type: bindings.mqtt3
  metadata:
    - name: url
      value: "${MQTT_BROKER_URL}"

5.3 Nivel 2 — Fuentes de Datos Dinámicas (UI del Admin)

Las fuentes concretas se gestionan desde la interfaz web, almacenadas en PostgreSQL:

// Entidad JPA — cada fila es una fuente de datos monitorizada
@Entity
@Table(name = "fuente_datos")
public class FuenteDatos {
    @Id UUID id;
    String nombre;               // "Biometrías ECOMED 2025"
    String tipo;                 // "sharepoint" | "gsheet" | "mqtt" | "oracle_cdc"
    String departamentoId;       // FK → departamento
    String configuracion;        // JSON: {path, sheetId, topic, ...}
    String periodicidad;         // "15min" | "1h" | "diario" | "tiempo_real"
    boolean activa;              // pausar/activar sin reiniciar
    String esquemaEsperado;      // JSON Schema para validación
    LocalDateTime ultimaEjecucion;
    String responsableId;        // FK → usuario que la creó
}
// Quarkus @Scheduled — lee fuentes activas de la BBDD, no del YAML
@ApplicationScoped
public class PollingService {

    @Inject FuenteDatosRepository fuentesRepo;
    @Inject DaprClient dapr;

    @Scheduled(every = "1m")
    void checkFuentesActivas() {
        var fuentes = fuentesRepo.findActivasPendientes();
        for (var fuente : fuentes) {
            switch (fuente.tipo) {
                case "sharepoint" -> checkSharePoint(fuente);
                case "gsheet"     -> checkGSheet(fuente);
                // mqtt y oracle_cdc son push, no polling
            }
        }
    }

    void checkSharePoint(FuenteDatos fuente) {
        // Usa el binding de infra "microsoft-graph" (YAML)
        // pero la ruta concreta viene de la BBDD (fuente.configuracion)
        var config = JsonParser.parse(fuente.configuracion);
        var cambios = dapr.invokeBinding("microsoft-graph",
                        "list", config.get("path"));
        if (hayCambios(cambios, fuente.ultimaEjecucion)) {
            dapr.publishEvent("pubsub", "fuente.cambio-detectado",
                    new CambioDetectado(fuente.id, cambios));
        }
    }
}

Resultado: para añadir una nueva carpeta de SharePoint, un Responsable de Departamento va a la UI de administración, rellena un formulario y pulsa «Guardar». Sin commits, sin YAML, sin reiniciar nada.

5.4 Resumen de Casos de Uso ETL

Caso de Uso YAML (infra, 1 vez) BBDD+UI (dinámico) Código Java
Nuevo Excel en SharePoint microsoft-graph binding Ruta de la carpeta + periodicidad @Scheduled polling
Polling de GSheets — (API directa) Sheet ID + rango + periodicidad @Scheduled polling
CDC en Oracle (SIRENO) postgresql binding Tablas a monitorizar CDC listener
IoT sensores MQTT mqtt3 binding Topics por sensor/nave Push automático
Datos Copernicus — (API directa) Variables + zona geográfica @Scheduled diario
Alerta por email smtp output binding Destinatarios por departamento Dapr Pub/Sub event
Resumen diario Departamentos + hora @Scheduled + LangChain4j

6. MLOps en Detalle — Ciclo de Vida de un Modelo

6.1 Pipeline de Entrenamiento

flowchart TB
    subgraph Datos ["1. Preparación de Datos"]
        D1["DVC: versionar dataset"]
        D2["Scripts de aumento"]
        D3["Split train/val/test"]
    end

    subgraph Entreno ["2. Entrenamiento"]
        E1["Configurar hiperparámetros"]
        E2["Fine-tuning LoRA/QLoRA"]
        E3["MLflow: log de métricas"]
    end

    subgraph Validacion ["3. Validación"]
        V1["Tests de precisión"]
        V2["Tests de latencia"]
        V3["Tests de regresión"]
    end

    subgraph Deploy ["4. Despliegue"]
        STAGING["MLflow: Staging"]
        PROD["MLflow: Production"]
        OLLAMA2["Copiar a Ollama"]
    end

    D1 --> D2 --> D3 --> E1 --> E2 --> E3
    E3 --> V1 --> V2 --> V3
    V3 -->|"pasa"| STAGING -->|"aprobación"| PROD --> OLLAMA2
    V3 -->|"falla"| E1

    style D1 fill:#3498db,color:#fff
    style E3 fill:#9b59b6,color:#fff
    style PROD fill:#2ecc71,color:#fff

6.2 Registro de Modelos — Estado Completo

Modelo Versión Actual Stage Dataset Entrenamiento Precisión VRAM Departamento
Qwen2.5-VL 7B v1.0 (base) Production Generalista 5 GB Todos
YOLOv11-FathomNet v2.1 Production FathomNet 2025 mAP 87% 2 GB Pesquerías
U-Net-Otolitos v1.3 Production ICES SmartDots + IEO MAE 0,8 años 1 GB Pesquerías
ResNet-ViT-Patologías v1.0 Staging FAO Aqua-Diseases 94% 3 GB Acuicultura
CLIP-IEO v1.1 Production iNaturalist+IEO Similarity 0,89 1 GB Todos
OceanGPT v1.0 (base) Production Benchmark OceanBench 4 GB Medio Marino

[!NOTE] Cada modelo tiene su propio ciclo de vida: se entrena, se valida, pasa a Staging, se aprueba por un Responsable, y pasa a Production en Ollama. Si la precisión cae en producción (drift), se re-entrena con datos nuevos.

6.3 Entrenamiento como Dapr Workflow

El entrenamiento LoRA (horas/días) es un proceso largo ideal para Dapr Workflow:

// Dapr Workflow — Entrenamiento LoRA
@Workflow(name = "EntrenamientoLoRAWorkflow")
public class EntrenamientoLoRA extends DaprWorkflow {

    @Override
    public WorkflowStub create() {
        return ctx -> {
            var config = ctx.getInput(ConfigEntrenamiento.class);

            // 1. Preparar dataset (DVC checkout)
            ctx.callActivity("prepararDataset", config.datasetVersion());

            // 2. Entrenar (puede tardar horas)
            var resultado = ctx.callActivity("entrenar", config, ResultadoEntrenamiento.class);

            // 3. Evaluar
            var metricas = ctx.callActivity("evaluar", resultado, Metricas.class);

            // 4. Decisión automática
            if (metricas.precision() >= config.umbralMinimo()) {
                // Registrar en MLflow como Staging
                ctx.callActivity("registrarMLflow", resultado, "Staging");

                // Notificar para aprobación humana
                var aprobacion = ctx.waitForExternalEvent("aprobacion", Boolean.class);

                if (aprobacion) {
                    ctx.callActivity("promoverProduccion", resultado);
                    ctx.callActivity("desplegarOllama", resultado);
                }
            } else {
                ctx.callActivity("notificarFallo", metricas);
            }
        };
    }
}

7. Componentes del Stack MLOps

flowchart TB
    subgraph MLOps ["Plataforma MLOps"]
        subgraph Datos2 ["Gestión de Datos"]
            DVC3["DVC - Versionado datasets"]
            MINIO2["MinIO - Almacenamiento"]
        end

        subgraph Modelos ["Gestión de Modelos"]
            MLFLOW["MLflow - Registro"]
            OLLAMA3["Ollama - Serving"]
        end

        subgraph Orquestacion ["Orquestación"]
            DAPR3["Dapr Workflow - Pipelines"]
            BIND2["Dapr Bindings - Triggers"]
            LC2["LangChain4j - Agentes"]
        end

        subgraph Observabilidad ["Observabilidad"]
            PROM["Prometheus - Métricas"]
            GRAF["Grafana - Dashboards"]
            OTEL["OpenTelemetry - Tracing"]
        end
    end

    DVC3 --> MLFLOW
    MLFLOW --> OLLAMA3
    BIND2 --> DAPR3
    DAPR3 --> LC2
    LC2 --> OLLAMA3
    OLLAMA3 --> PROM
    PROM --> GRAF
    DAPR3 --> OTEL

    style MLOps fill:#1a1a2e,color:#fff
    style Modelos fill:#9b59b6,color:#fff
    style Orquestacion fill:#e74c3c,color:#fff

Docker Compose — Servicios Adicionales

Servicio Imagen Puerto Función Licencia
mlflow ghcr.io/mlflow/mlflow 5000 Registro de modelos, experiment tracking Apache 2.0
prometheus prom/prometheus 9090 Métricas de modelos y sistema Apache 2.0
grafana grafana/grafana-oss 3000 Dashboards de monitorización AGPL-3.0

[!TIP] Todo el stack adicional es 100% open-source con licencias Apache/MIT/AGPL. Sin dependencias de licencias propietarias ni telemetría forzada.


8. Viabilidad: ¿Por Qué Quarkus+Dapr y No Pekko?

Capacidad Agéntica Quarkus + Dapr + LangChain4j Pekko
Agente IA con tool calling @RegisterAiService + @Tool — 15 líneas Actor custom — 200+ líneas
Orquestación de pipelines Dapr Workflow — estado persistente automático Pekko Persistence — manual, complejo
Triggers externos Dapr Bindings — YAML declarativo Actor custom — código por cada fuente
Pub/Sub Dapr Pub/Sub — desacoplado Pekko EventStream — acoplado al cluster
Multi-modelo LangChain4j @Named — config CDI Routing manual entre actores
Procesos largos Dapr Workflow — sobrevive a reinicios Pekko Persistence — requiere configuración
Reducción de código ~90% menos vs Pekko

[!IMPORTANT] No hay ningún escenario del IEO donde Pekko sea mejor que Dapr. Los casos de uso donde Pekko brilla (telecoms de alta frecuencia, trading de μs de latencia) no aplican a un centro de investigación oceanográfica. El análisis de viabilidad confirma que 7 de 10 componentes son de migración baja.


9. Roadmap de Implementación

gantt
    title Despliegue MLOps + Sistema Agéntico
    dateFormat YYYY-MM-DD
    axisFormat %B %Y

    section Fase 1
    MLflow registro de modelos        :m1, 2026-04-01, 2w
    DVC para datasets de otolitos     :m2, after m1, 1w
    Dapr Bindings SharePoint/GSheets  :m3, after m1, 2w
    LangChain4j agente Pesquerías     :m4, after m3, 3w

    section Fase 2
    Dapr Workflows ETL por dpto       :m5, after m4, 3w
    MLflow pipeline entrenamiento     :m6, after m4, 2w
    Grafana dashboards de modelos     :m7, after m6, 2w

    section Fase 3
    Multi-agente departamental        :m8, after m7, 3w
    Dapr Agents en K8s                :m9, after m8, 2w
    Monitorización de drift           :m10, after m9, 2w

Documentos Relacionados

Nivel Documento Descripción
Investigación Análisis LLM Departamental Los 9+ modelos que esta capa gestiona
Investigación Gobernanza de Datasets Datasets que alimentan el entrenamiento
Investigación Bancos de Datos Animales Fuentes intl., APIs, datasets de imágenes, almacenamiento
Arquitectura Viabilidad Pekko→Dapr Análisis completo Pekko vs Dapr
Arquitectura Arquitectura IA Pipeline CAG+RAG que los agentes usan