De Junior a Senior con ChatGPT

De programadores junior a senior con ChatGPT

Qué son la IA y, más en general, los Modelos de Lenguaje Grande (LLM) es algo que ya todos conocemos. Cada uno de nosotros ha escrito innumerables prompts para obtener respuestas coherentes, con resultados más o menos satisfactorios. Para comprender mejor cómo esta gama de productos puede ayudarnos a convertirnos en mejores programadores, pasando de junior a senior, o simplemente en técnicos más expertos, primero debemos entender qué es la ingeniería de prompts, por qué es tan importante y cuáles son los ámbitos en los que un LLM como ChatGPT, Claude u otros pueden ayudarnos a crecer profesionalmente.

Experiencia en el campo

Lo que estoy a punto de escribir surge de la experiencia de meses de uso de ChatGPT, GitHub Copilot, Claude, Gemini y otros LLM, así como de una serie de lecturas y estudios sobre el tema. Soy consciente de que en unos meses podría ser desmentido: podrían surgir, o tal vez ya han surgido y aún no los utilizo, herramientas mejores que ayuden de manera aún más significativa a los desarrolladores (y no solo) a crecer profesionalmente. Seguramente algunos de ustedes utilizan herramientas diferentes a las que uso yo, pero intentemos concentrarnos en lo que podemos hacer con productos de este tipo y no en cuál es el mejor, de lo contrario se desencadenaría una guerra infinita de la cual, estoy seguro, nadie saldría vencedor.

Comprender ChatGPT y, en un sentido más amplio, los LLM

Empecemos por el principio: ChatGPT es un chatbot de inteligencia artificial (IA) que utiliza el procesamiento del lenguaje natural para crear un diálogo similar al humano. El modelo lingüístico puede responder preguntas y componer varios contenidos escritos, incluyendo artículos, publicaciones en redes sociales, ensayos, código y correos electrónicos, lo que lo convierte en una herramienta emocionante para programadores de todos los niveles. Por lo tanto, se trata de un modelo que no razona, pero es capaz de aplicar de manera muy efectiva las reglas que se le han enseñado durante el entrenamiento. Esto significa que si le damos una entrada incorrecta, obtendremos una salida incorrecta, pero si le damos una entrada correcta, obtendremos una salida correcta. De la misma manera, si utilizamos un LLM que ha utilizado conjuntos de datos con errores, en consecuencia, tendremos una salida con errores. Por su naturaleza, los modelos siempre intentan dar respuestas y, en consecuencia, existe un porcentaje de respuestas, más o menos grande, que siempre será incorrecta, inventada o simplemente no correcta.

Fijado este punto inicial, cuyo propósito es enfocar el tipo de herramienta que estamos intentando utilizar, intentemos entender cuáles son los ámbitos en los que el margen de error es más alto y cuáles son aquellos en los que el margen de error se reduce y podemos utilizarlo con mayor éxito.

Los LLM no están diseñados para resolver problemas o acertijos

Los LLM fueron creados para responder preguntas, escribir artículos, generar código, pero no fueron diseñados para pensar y resolver problemas o acertijos. Esto significa que si les planteamos un problema, no sabrán resolverlo de manera autónoma; sin embargo, si proporcionamos un problema ya resuelto, sabrán devolvernos la solución.

Cada vez que veo un video en el que alguien se divierte proponiendo un acertijo a ChatGPT y luego se sorprende de que no pueda resolverlo, no puedo evitar sonreír. Es como pedirle a un niño de 5 años que resuelva un problema de física cuántica: son pruebas que no tienen en cuenta las capacidades del interlocutor y la forma en que fue concebido. No añaden ningún valor y no demuestran que el modelo sea inadecuado, sino que más bien destacan que el "creador" de turno no ha comprendido cómo utilizar el modelo correctamente.

En el campo de la programación, resolver un acertijo cuenta poco; lo que se necesita es acelerar los procesos de producción de código, mejorar la calidad del código y la comprensión del mismo. En estos ámbitos, los LLM pueden dar una contribución significativa, pero solo si se utilizan correctamente.

La escritura de los prompts

Se habla a menudo de cómo escribir los prompts para obtener el mejor resultado, pero no siempre se entiende qué significa. Escribir un prompt significa proporcionar indicaciones claras y precisas sobre lo que esperamos del modelo y lo que queremos obtener.

Las primeras y más obvias indicaciones son:

  • Sé claro y conciso
  • Sé específico
  • Incluye el contexto y los detalles necesarios
  • Define entradas y salidas

Si aplicamos estas indicaciones a la programación, nos damos cuenta de que no es tan simple como parece. Definir un producto de software de manera clara, concisa y específica requiere experiencia y conocimiento de las mejores prácticas. Cuanto más complejo sea el proyecto, más difícil será definirlo de esta manera.

¿Cuántas veces te has encontrado con especificaciones poco claras o que han cambiado con el tiempo, sin los detalles necesarios? ¿Cuántas veces has escrito código sin saber exactamente qué esperaba el cliente?

En estos casos, obtener un resultado correcto de un LLM se vuelve difícil, incluso con un prompt claro y detallado.

¿Qué prompts escribir?

Hemos entendido que un prompt demasiado grande es irrealista, mientras que uno demasiado pequeño no lleva al resultado deseado. ¿Qué hacer entonces?

La solución es escribir prompts lo más específicos posible, incluyendo el contexto y los detalles necesarios, pero manteniendo claridad y concisión.

Un ejemplo podría ser:

Escribe una función en C# para encontrar el factorial de un número dado.

Aquí hemos especificado el lenguaje de programación, la funcionalidad requerida y el contexto necesario, obteniendo un resultado eficaz y preciso.

Recordemos que los modelos aprenden de los contextos ya enfrentados. Cuanto mayor sea la comunión del contexto, mayor será la probabilidad de obtener un resultado satisfactorio.

Por el contrario, cuanto más específico sea el contexto, mayor será la posibilidad de que el modelo no sea capaz de proporcionar una respuesta correcta, incluso con todos los datos necesarios y una descripción clara.

En estos casos, es preferible usar un producto como Copilot o Codeium, capaz de analizar el código y proporcionar respuestas más pertinentes al contexto y las necesidades.

Límites de conocimiento

Otro punto importante a tener en cuenta es que los LLM no poseen un conocimiento infinito, sino limitado a lo que se ha incluido en los conjuntos de datos de entrenamiento. Esto implica que si proporcionamos una entrada no presente en los conjuntos de datos, el modelo no será capaz de responder adecuadamente.

¿Cuándo ocurre más frecuentemente esta situación? Cuando intentamos pedir a un LLM información sobre productos que no existían o que acababan de ser lanzados en el momento en que el modelo comenzó a ser entrenado. Están comenzando a surgir mitigaciones a este aspecto, como la consulta en tiempo real de la red y el entrenamiento continuo, pero son todas soluciones que no resuelven completamente el problema. Por el momento lo atenúan, logrando en parte proporcionar respuestas, pero en mayor medida inventándolas. No esperen, por lo tanto, obtener respuestas precisas de ChatGPT sobre productos recién lanzados, tecnologías recién liberadas o eventos ocurridos recientemente, porque no sabrá responderles correctamente o, si lo hace, será con respuestas inventadas o de baja calidad.

No, no basta con consultar la red en tiempo real para obtener respuestas correctas, porque el modelo no tiene la capacidad de entender si la información que está leyendo es correcta o no y normalmente, cada tecnología que sale, tiene una serie de problemas documentativos y de comprensión, además de la escasez de aplicaciones disponibles en Internet.

¿Cómo podemos obtener mejores resultados?

Dicho esto, es fácil comprender que, para obtener los mejores resultados, es mejor que la tecnología sobre la que estamos trabajando esté bien consolidada y que la herramienta en la que nos apoyamos pueda contextualizar al máximo nuestras preguntas.

Una forma de aprovechar al máximo los LLM es, por lo tanto, confiar en modelos que no se limiten solo al prompt digitado, sino que puedan agregar una serie de datos tomados del proyecto en el que estamos trabajando. Esta gama de productos es capaz de dar respuestas más precisas porque tienen un conocimiento más amplio y detallado en comparación con lo que puede tener un modelo basado solo en prompts.

Un ejemplo de estos modelos es GitHub Copilot, que es capaz de proporcionar respuestas mejores en comparación con ChatGPT, gracias a un conocimiento más amplio y detallado.

El conocimiento más amplio deriva del hecho de que, para generar una sugerencia, se analiza el código antes y después de la posición del cursor dentro del editor, se utilizan todos los archivos abiertos en ese momento y se agregan los repositorios de GitHub referenciados en el contexto. Todos estos datos forman parte de la entrada enviada a Copilot y, en consecuencia, el código sugerido no es una simple copia y pega de algo encontrado en los datos de entrenamiento, sino que es código fuertemente contextualizado en relación con el ámbito en el que se utiliza.

Hagamos un pequeño ejemplo, partiendo de este código Java, que podría estar escrito en cualquier lenguaje de programación:

public static final int UNO = 1;
public static final int DOS = 2;
public static final int TRES = 3;
public static final int CUATRO = 4;
public static final int CINCO = 5;

public static void main(String[] args) {
    // Dado el parámetro busco la correspondencia respecto a las estáticas
}

Detengámonos en el comentario, que es muy genérico y va en contra de todas las reglas de la ingeniería de prompts que sugieren tipificaciones fuertes. Si estuviéramos, por ejemplo, en ChatGPT, faltaría la indicación del lenguaje a usar y se podría obtener una respuesta poco significativa:

En este contexto, en cambio, Copilot es capaz de entender que el comentario se refiere a las variables estáticas, que se habla de Java y, en consecuencia, nos sugerirá un código que hace exactamente lo que esperamos.

int parametro = Integer.parseInt(args[0]);
switch (parametro) {
    case UNO:
        System.out.println("UNO");
        break;
    case DOS:
        System.out.println("DOS");
        break;
    case TRES:
        System.out.println("TRES");
        break;
    case CUATRO:
        System.out.println("CUATRO");
        break;
    case CINCO:
        System.out.println("CINCO");
        break;
    default:
        System.out.println("Ninguna correspondencia");
}

Entiendo que en este punto muchos de ustedes estarán diciendo: "el código es demasiado simple", "se podría haber hecho mejor" y tienen razón, pero el punto no es ese. El punto es que las herramientas contextualizadas como Copilot son capaces de entender mejor nuestros prompts o lo que necesitamos y de dar una respuesta correcta, mientras que ChatGPT no sería capaz de hacerlo, a menos que se escriba un prompt mucho más extenso que ese comentario mínimo insertado dentro del código.

Contextualizar la pregunta o la ayuda que se quiere obtener da una enorme ventaja en comparación con escribir un prompt genérico, porque permite al modelo entender mejor qué se espera de él y dar una respuesta mejor.

Comprensión del código

A menudo nos encontramos frente a código que no hemos escrito nosotros y que no conocemos, pero que debemos modificar o extender. En estos casos es muy útil tener un modelo que nos ayude a entender qué hace el código y cómo funciona.

Un programador senior normalmente no tiene grandes problemas en la comprensión del código, pero bajando el nivel a junior o simplemente saliendo de la zona de confort del senior, uno se da cuenta de que no es tan obvio entender qué hace un trozo de código escrito por otra persona.

Hablando de experiencia personal, pueden pedirme que describa cualquier programa con una sintaxis derivada del C, pero no me pidan que describa un programa escrito en Perl, porque probablemente fingiría un ataque de letargo.

Los modelos LLM pueden ayudarnos en esta fase, porque son capaces de analizar el código y darnos una explicación de qué hace y cómo funciona. Esto nos permite entender mejor el código y poder modificarlo o extenderlo de manera más efectiva.

También en este caso la contextualización es muy importante, pero intentemos de todos modos abordar el problema con un prompt genérico y veamos qué sucede.

Partamos de esta función tomada del repositorio https://github.com/jetty/jetty.project

public static boolean matchesURI(URI uri1, URI uri2)
    {
        String scheme = uri1.getScheme();
        if (scheme.equalsIgnoreCase(uri2.getScheme()))
        {
            if (uri1.getHost().equalsIgnoreCase(uri2.getHost()))
            {
                // Handle default HTTP ports.
                int thisPort = HttpClient.normalizePort(scheme, uri1.getPort());
                int thatPort = HttpClient.normalizePort(scheme, uri2.getPort());
                if (thisPort == thatPort)
                {
                    // Use decoded URI paths.
                    return uri2.getPath().startsWith(uri1.getPath());
                }
            }
        }
        return false;
    }

Intentemos ahora pedirle a Claude (pero sería lo mismo con otros LLM) que nos explique qué hace esta función.

¿Me describes este código?
<código>

En respuesta obtendremos

Este código define un método estático llamado `matchesURI` que compara dos objetos URI (Identificador Uniforme de Recursos) para determinar si coinciden según criterios específicos. Aquí hay una descripción detallada del funcionamiento:

1. El método toma dos parámetros: `uri1` y `uri2`, ambos de tipo URI.

2. Compara primero los esquemas (protocolos) de los dos URI:
   - Extrae el esquema de `uri1`
   - Compara los esquemas de `uri1` y `uri2` ignorando las mayúsculas/minúsculas

3. Si los esquemas coinciden, procede a comparar los hosts:
   - Compara los hosts de `uri1` y `uri2` ignorando las mayúsculas/minúsculas

4. Si también los hosts coinciden, pasa a comparar los puertos:
   - Utiliza un método `HttpClient.normalizePort()` para manejar los puertos predeterminados HTTP
   - Compara los puertos normalizados de ambos URI

5. Si los puertos coinciden, finalmente verifica las rutas:
   - Verifica si la ruta decodificada de `uri2` comienza con la ruta decodificada de `uri1`

6. El método devuelve `true` si se cumplen todas estas condiciones, de lo contrario devuelve `false`.

En resumen, este método considera dos URI coincidentes si tienen el mismo esquema, host, puerto (considerando los puertos HTTP predeterminados) y si la ruta de `uri2` comienza con la ruta de `uri1`.

Entienden que, en todos los contextos donde nos encontramos frente a código que no conocemos, un LLM puede ser muy útil para entender qué hace y cómo funciona.

Creación de pruebas

A menudo nos encontramos frente a la escritura de pruebas para el código que hemos escrito o que debemos escribir, depende un poco de la escuela de pensamiento que hayan adoptado o del tiempo que se les haya dado para alcanzar un objetivo. Escribir pruebas, sin embargo, no siempre es fácil y requiere cierta experiencia y conocimiento de las mejores prácticas.

Un LLM puede sernos de ayuda, porque es capaz de comprender porciones de código y generar pruebas para ese código, o imaginar algunas para el código que deberemos escribir. Esto nos permite tener rápidamente pruebas listas y concentrarnos solo en los casos no cubiertos o en los casos límite.

Claramente podríamos encontrarnos también con pruebas parciales: pero es mucho más rápido integrar una suite de pruebas que escribirla desde cero.

Partiendo siempre del ejemplo anterior, podemos pedir:

¿Me escribes las pruebas para esta función?

En este caso he utilizado Claude, que en muchos casos prefiero a ChatGTP, y como resultado obtuve:

Ciertamente, puedo ayudarte a escribir algunas pruebas para esta función.
Usaré JUnit, un framework de pruebas ampliamente utilizado para Java, para crear estas pruebas. Aquí hay una serie de pruebas que cubren diferentes escenarios:

Estas pruebas cubren diferentes escenarios para la función matchesURI:

- URI coincidentes
- Esquemas diferentes
- Hosts diferentes
- Coincidencia con puerto predeterminado
- Puertos diferentes
- Rutas no coincidentes
- Insensibilidad a mayúsculas/minúsculas para esquema y host
- URI con consulta y fragmento

Estas pruebas deberían proporcionar una buena cobertura para la función matchesURI. Sin embargo, podrías querer agregar pruebas adicionales para casos límite o escenarios específicos de tu uso de la función.

A continuación, incluyo el fragmento de código creado que representa una excelente base de partida:

import org.junit.Test;
import static org.junit.Assert.*;
import java.net.URI;

public class URIMatcherTest {

    @Test
    public void testMatchingURIs() throws Exception {
        URI uri1 = new URI("http://example.com/path");
        URI uri2 = new URI("http://example.com/path/subpath");
        assertTrue(URIMatcher.matchesURI(uri1, uri2));
    }

    @Test
    public void testNonMatchingScheme() throws Exception {
        URI uri1 = new URI("http://example.com/path");
        URI uri2 = new URI("https://example.com/path");
        assertFalse(URIMatcher.matchesURI(uri1, uri2));
    }

    @Test
    public void testNonMatchingHost() throws Exception {
        URI uri1 = new URI("http://example.com/path");
        URI uri2 = new URI("http://example.org/path");
        assertFalse(URIMatcher.matchesURI(uri1, uri2));
    }

    @Test
    public void testMatchingWithDefaultPort() throws Exception {
        URI uri1 = new URI("http://example.com/path");
        URI uri2 = new URI("http://example.com:80/path/subpath");
        assertTrue(URIMatcher.matchesURI(uri1, uri2));
    }

    @Test
    public void testNonMatchingPort() throws Exception {
        URI uri1 = new URI("http://example.com:8080/path");
        URI uri2 = new URI("http://example.com:9090/path");
        assertFalse(URIMatcher.matchesURI(uri1, uri2));
    }

    @Test
    public void testNonMatchingPath() throws Exception {
        URI uri1 = new URI("http://example.com/path1");
        URI uri2 = new URI("http://example.com/path2");
        assertFalse(URIMatcher.matchesURI(uri1, uri2));
    }

    @Test
    public void testCaseInsensitiveSchemeAndHost() throws Exception {
        URI uri1 = new URI("HTTP://EXAMPLE.COM/path");
        URI uri2 = new URI("http://example.com/path/subpath");
        assertTrue(URIMatcher.matchesURI(uri1, uri2));
    }

    @Test
    public void testWithQueryAndFragment() throws Exception {
        URI uri1 = new URI("http://example.com/path");
        URI uri2 = new URI("http://example.com/path/subpath?query=value#fragment");
        assertTrue(URIMatcher.matchesURI(uri1, uri2));
    }
}

Como pueden ver, hay muchas ideas y varias pruebas funcionales que podemos integrar sin temor en nuestra estructura de proyecto. Cuanto más mantengamos nuestro enfoque en el código del tipo KISS (Keep It Simple Stupid), más podremos recibir buenas pruebas por parte del modelo, limitando los casos de error.

Seguridad

Otro ámbito en el que un LLM puede ser muy útil es la seguridad. Escribir código seguro no siempre es fácil y requiere cierta experiencia y conocimiento de las mejores prácticas.

Existen herramientas que ayudan a escribir mejor código y en mi vida creo haber mirado los logs de SonarQube más que la cara de mi esposa, pero un modelo LLM puede ser una ayuda válida también en este ámbito, sobre todo porque su conocimiento a nivel de lenguajes de programación es decididamente superior a muchas otras herramientas.

Por ejemplo, siempre partiendo del método que estamos analizando, probé a preguntar:

¿Me das sugerencias para hacer el código más seguro?

Y obtuve algo que ya me había preguntado mientras miraba el método:

Gestión de null:
El método actual no controla si los URI de entrada son null. Añade controles al inicio del método:

if (uri1 == null || uri2 == null) {
    throw new IllegalArgumentException("URIs cannot be null");
}

En este caso, he omitido las otras sugerencias: es importante notar que el modelo ha centrado un punto de mejora dado por la normalización de los parámetros.

Refactorización

La refactorización es un ámbito muy importante de la programación. Para poder hacer una refactorización holística se necesita tener una visión global del proyecto que, por el momento, no está aún en las capacidades de los modelos LLM. Sin embargo, si nos concentramos en el concepto de KISS, reduciendo de manera sabia el ámbito de acción de un LLM, entonces podemos obtener ventajas considerables o al menos una serie de ideas a seguir.

Entre las operaciones de refactorización también están las operaciones de optimización del código, que pueden ser útiles para hacer nuestros programas más eficientes.

El 8 de junio de 2023, en Nature salió un artículo sobre la optimización de código y algoritmos de ordenación: https://www.nature.com/articles/s41586-023-06004-9

Se analizaba cómo, utilizando un aprendizaje por refuerzo profundo, se logró optimizar el código ensamblador utilizado para un algoritmo de ordenación.

Sin llegar a este tipo de resultados, podemos igualmente pedir a un LLM que optimice nuestro código, partiendo de un prompt genérico como:

¿Puedes hacer una refactorización para acelerar este código?

La prueba se hizo sobre el código de Jetty y el resultado fue este:

public static boolean matchesURI(URI uri1, URI uri2) {
    // Memoriza el esquema, el host y el puerto una sola vez.
    String scheme1 = uri1.getScheme();
    String scheme2 = uri2.getScheme();

    if (scheme1 == null || !scheme1.equalsIgnoreCase(scheme2)) {
        return false;
    }

    String host1 = uri1.getHost();
    String host2 = uri2.getHost();

    if (host1 == null || !host1.equalsIgnoreCase(host2)) {
        return false;
    }

    // Puerto normalizado una vez.
    int port1 = HttpClient.normalizePort(scheme1, uri1.getPort());
    int port2 = HttpClient.normalizePort(scheme2, uri2.getPort());

    if (port1 != port2) {
        return false;
    }

    // Compara el camino al final.
    String path1 = uri1.getPath();
    String path2 = uri2.getPath();

    return path2.startsWith(path1);
}

Probablemente no hemos ganado mucho en términos de rendimiento, pero desde el punto de vista de la legibilidad, el método parece más claro y lineal, evitando un código anidado en varios niveles.

Uno de los objetivos de la refactorización es también hacer el código más comprensible, porque detrás de cientos de líneas de código siempre hay personas que, al aumentar la complejidad, disminuirán su deseo o eficacia en sus modificaciones. Mantener el código simple facilita el mantenimiento y evita leer comentarios como estos que se han convertido en memes:

// ¡Este código es un desastre total y no tengo idea de lo que hace, pero funciona, así que no lo toques!

Boilerplate

¿Cuántos de nosotros nos hemos encontrado teniendo que escribir un nuevo proyecto y comenzamos desde un boilerplate? Creo que todos, y cada vez el paso inicial requiere tiempo y atención para evitar errores u omisiones.

Por este motivo, existen colecciones de boilerplate que ofrecen un punto de partida. Un ejemplo es este proyecto de GitHub: https://github.com/melvin0008/awesome-projects-boilerplates, que recopila muchos ejemplos de proyectos mínimos.

Aquí también un LLM puede ayudarnos a escribir el boilerplate, partiendo de un prompt genérico como:

¿Puedes escribirme un proyecto mínimo VUE compuesto por una primera pantalla de login, que consulte el servicio https://reqres.in/api/login pasando un JSON con los parámetros "email" y "password", y en caso de respuesta con código 200 me permita acceder a un backend compuesto por un panel a la izquierda con un menú y un panel central donde visualizar datos? En el panel de la izquierda debe haber el botón de logout.

Evito incluir aquí toda la respuesta: he incluido el resultado en un proyecto de GitHub https://github.com/matteobaccan/VUEdashboard, del cual les recomiendo ver los primeros commits, ya que, en el momento en que lo lean, podría haber sido extendido con más modificaciones.

Lo que obtuve es básico, pero ofrece un punto de partida para los junior que deben configurar un proyecto y no tienen idea de cómo hacerlo, y acelera el trabajo para los senior, que tienen todas las bases para hacerlo, pero quieren concentrarse en otra cosa.

Sí, lo sé, lo mejor siempre es estudiar y saber hacer estas cosas desde cero y con una mano atada a la espalda, pero muy a menudo un empujón en una dirección nos permite ser más rápidos y tal vez descubrir algo que está fuera de nuestra zona de confort de la cual nunca salimos.

Piensen solo en cuántas veces nos hemos bloqueado en la escritura de algo y un boilerplate nos habría acelerado, pero nunca se encontraba uno que hiciera exactamente lo que necesitábamos. Bien, un LLM llena la diferencia entre lo que queremos y lo que encontramos, disminuyendo la distancia.

Cuán molesto es comentar el código

Levante la mano quien nunca ha escrito código sin comentarlo, o quien ha tenido que comentar código escrito por otros sin entenderlo.

También en este caso, un LLM puede ayudarnos a comentar el código, partiendo de un prompt genérico como:

Añade comentarios al siguiente código
private String readFile(String fileName){ 
    BufferedReader br; 
    try { 
        br = new BufferedReader(new FileReader(fileName)); 
        final StringBuilder sb = new StringBuilder(); 
        String line = br.readLine(); 

        while (line != null) { 
            sb.append(line); 
            sb.append(System.lineSeparator()); 
            line = br.readLine(); 
        } 
        br.close(); 
        return sb.toString(); 
    } catch (IOException e) { 
        e.printStackTrace(); 
    } 
    return ""; 
} 

Como resultado, obtendremos:

/**
 * Lee el contenido de un archivo y lo devuelve como una cadena.
 * @param fileName El nombre del archivo a leer
 * @return El contenido del archivo como una cadena, o una cadena vacía en caso de error
 */
private String readFile(String fileName) {
    BufferedReader br;
    try {
        // Inicializa un BufferedReader para leer el archivo
        br = new BufferedReader(new FileReader(fileName));

        // Crea un StringBuilder para construir el contenido del archivo
        final StringBuilder sb = new StringBuilder();

        // Lee la primera línea del archivo
        String line = br.readLine();

        // Continúa leyendo mientras haya líneas en el archivo
        while (line != null) {
            // Añade la línea actual al StringBuilder
            sb.append(line);

            // Añade un separador de línea del sistema
            sb.append(System.lineSeparator());

            // Lee la siguiente línea
            line = br.readLine();
        }

        // Cierra el BufferedReader
        br.close();

        // Devuelve el contenido del archivo como una cadena
        return sb.toString();
    } catch (IOException e) {
        // Maneja posibles errores de I/O
        e.printStackTrace();
    }

    // Devuelve una cadena vacía en caso de error
    return "";
}

De esta manera, el código resulta comentado y mucho más comprensible. Aunque el ejemplo es simple, el concepto de fondo es claro: ¿tienen un software desconocido que comentar? Pidan a un LLM que lo haga y podrían transformar su base de código de un infierno a un paraíso.


Revisión del Código en las Pull Request

Imaginen tener que gestionar las Pull Request (PR) de un repositorio y querer optimizar el trabajo. En GitHub existen varias acciones que podemos implementar para realizar revisiones automáticas del código. Una de ellas es:

https://github.com/magnificode-ltd/chatgpt-code-reviewer

Esta acción permite realizar una revisión automática del código en las PR, identificando de manera precisa las modificaciones y sugiriendo mejoras. Es solo un ejemplo, pero lo interesante es que podemos crear nuestras propias acciones personalizadas, para activar en las PR, que nos ayuden a acelerar el trabajo y mejorar la calidad del código.

Análisis del Proyecto

Como senior, a menudo me encuentro analizando un proyecto o leyendo la documentación hecha por otros, y siempre es una operación compleja que requiere tiempo y atención. En el pasado, enfrentaba esta fase estudiando y haciendo investigaciones en Internet con la esperanza de encontrar información útil.

Estas investigaciones a menudo generaban muchas ideas, pero resultaban dispersivas: a pesar de la habilidad en la búsqueda, es fácil perderse en un mar de información y no encontrar lo que se necesita.

Recientemente, he cambiado de enfoque: después de una primera fase de estudio para enmarcar el proyecto, me pierdo en algunas conversaciones vocales con ChatGPT. Este enfoque tiene una serie de ventajas interesantes:

  • Es mucho más rápido que la búsqueda en internet: las respuestas son precisas y contextuales.
  • Se puede hacer brainstorming con el modelo: en comparación con la búsqueda tradicional, la conversación permite conexiones transversales entre la información sin tener que abrir nuevas pestañas e iniciar nuevas búsquedas.
  • Funciona también en el coche: durante los largos viajes hacia los clientes, hablar con un asistente virtual es mucho mejor que quedarse en silencio (no, no soy amante de los viajes en silencio).
  • Proporciona información continua que, aunque no sea inmediatamente utilizable, puede estimular nuevas ideas.
  • Hablar en voz alta es más fácil de memorizar que la simple lectura.
  • Puede haber alucinaciones, pero considerando cuántos errores se leen en los posts presentes en la red, no es seguro que esto sea un problema.

Naturalmente, quien me vea parado en el semáforo hablando solo podría pensar que estoy loco, pero en mi cabeza me siento como Michael Knight hablando con KITT.

Traducciones

Uno de los aspectos más tediosos para un programador es "traducir" la documentación de un proyecto a otro idioma. Es un trabajo aburrido que requiere tiempo y atención, pero es necesario para hacer el proyecto comprensible a quienes no hablan nuestro idioma.

Claramente, el mejor enfoque sería escribir todo en inglés, pero no siempre es la solución óptima.

Tenemos la documentación del proyecto en español escrita por la filial de Madrid y debemos traducirla al inglés para que la lean los colegas de Nueva York.

Desde hace algunos años, he adoptado el markdown para toda la documentación que produzco. Es un formato de escritura ligero que permite crear documentos de manera simple y rápida, sin preocuparse demasiado por la formateación y sin tener el requisito de instalar necesariamente algo en el propio ordenador.

Esto me permite realizar rápidamente diapositivas con herramientas como Marp y mantener fácilmente el control de versiones.

Recientemente, me encontré teniendo que traducir la documentación de un proyecto al inglés. Hacerlo manualmente, copiando y pegando el texto en un LLM y luego en el archivo, es aburrido y costoso en términos de tiempo. Estaba a punto de escribir un programa para automatizar el proceso, cuando descubrí la existencia de extensiones para GitHub que lo hacen automáticamente, como GPT-Translate.

https://github.com/3ru/gpt-translate

Estoy seguro de que existen otras soluciones, pero esta se integró perfectamente en el ciclo de vida de los proyectos que gestiono, acelerando notablemente el trabajo de traducción.

A través de una simple issue, es posible activar la acción de GPT-Translate, especificar qué documento markdown traducir y a qué idioma. En respuesta, se produce una PR con el documento traducido, listo para ser aprobado.

Te revelaré un secreto: si estás leyendo este documento en inglés o español, hasta ahora has leído las traducciones hechas por GPT-Translate a través de ChatGPT.

Formateo

¿Alguna vez les ha pasado tener que enfrentar código o estructuras de datos no formateadas correctamente? Los modelos LLM también pueden ayudarnos en esto.

Por ejemplo, partiendo de un prompt como:

¿Me formateas esta estructura JSON?

[{"field1":"aa11","field2":"aa22", "structField":{"sf1" : "aaa11","sf2" : "aaa22"}},
{"field1":"bb11","field2":"bb22","structField":{"sf1" : "bbb11","sf2" : "bbb22"}}]

Obtendremos:

[
    {
        "field1": "aa11",
        "field2": "aa22",
        "structField": {
            "sf1": "aaa11",
            "sf2": "aaa22"
        }
    },
    {
        "field1": "bb11",
        "field2": "bb22",
        "structField": {
            "sf1": "bbb11",
            "sf2": "bbb22"
        }
    }
]

Y si lo necesitamos en otro formato, podemos pedir:

¿Me conviertes este JSON en formato XML?

El resultado será:

<items>
    <item>
        <field1>aa11</field1>
        <field2>aa22</field2>
        <structField>
            <sf1>aaa11</sf1>
            <sf2>aaa22</sf2>
        </structField>
    </item>
    <item>
        <field1>bb11</field1>
        <field2>bb22</field2>
        <structField>
            <sf1>bbb11</sf1>
            <sf2>bbb22</sf2>
        </structField>
    </item>
</items>

En este caso, he usado ChatGPT, pero la lógica es la misma para todos los modelos LLM: son capaces de formatear las estructuras de datos. Aunque estas funcionalidades ya están presentes en los IDE modernos, tener un modelo que lo haga por nosotros es sin duda una ventaja.

Conversión de Código

¿Cuántas veces se han encontrado teniendo que convertir código de un lenguaje de programación a otro? Un ejemplo común es la necesidad de utilizar librerías o API proporcionadas solo en un determinado lenguaje, con poca o ninguna documentación estandarizada.

Para algunos proyectos donde me he roto la cabeza en el pasado, he pasado horas de mi tiempo convirtiendo llamadas C, alimentando estructuras, en llamadas Java con librerías de conversión como JNA.

Si hubiera tenido entonces a disposición un LLM, podría haber hecho:

¿Me conviertes este código de Java a JavaScript?

public static void PrimeNumberCheck(String[] args) {
    int numberToCheck = 17;
    boolean isPrime = true;

    for (int i = 2; i <= numberToCheck / 2; i++) {
        int remainder = numberToCheck % i;
        System.out.println(numberToCheck + " divided by " + i + " gives a remainder " + remainder);

        if (remainder == 0) {
            isPrime = false;
            break;
        }
    }

    if (isPrime) {
        System.out.println(numberToCheck + " is a prime number");
    } else {
        System.out.println(numberToCheck + " is not a prime number");
    }
}

Con un gran ahorro de tiempo:

function PrimeNumberCheck(args) {
    let numberToCheck = 17;
    let isPrime = true;

    for (let i = 2; i <= numberToCheck / 2; i++) {
        let remainder = numberToCheck % i;
        console.log(numberToCheck + " divided by " + i + " gives a remainder " + remainder);

        if (remainder == 0) {
            isPrime = false;
            break;
        }
    }

    if (isPrime) {
        console.log(numberToCheck + " is a prime number");
    } else {
        console.log(numberToCheck + " is not a prime number");
    }
}

Lo interesante es la matriz de conversión posible en estos grandes modelos, que es generalmente mucho más amplia de lo que era posible hasta hace pocos meses.

Generación de Datos Aleatorios

Una de las tareas más fastidiosas para un programador es generar datos de prueba para su propio código. También en este caso, los modelos LLM pueden venirnos en ayuda.

¿Puedes generar un array JSON con 10 elementos que contengan los campos nombre y apellido con datos aleatorios?

y aquí están los datos que necesitaba:

[
    {
        "nombre": "Giovanni",
        "apellido": "Rossi"
    },
    {
        "nombre": "Maria",
        "apellido": "Bianchi"
    },
    {
        "nombre": "Luca",
        "apellido": "Verdi"
    },
    {
        "nombre": "Paola",
        "apellido": "Neri"
    },
    {
        "nombre": "Marco",
        "apellido": "Gialli"
    },
    {
        "nombre": "Sara",
        "apellido": "Blu"
    },
    {
        "nombre": "Andrea",
        "apellido": "Rosa"
    },
    {
        "nombre": "Giulia",
        "apellido": "Viola"
    },
    {
        "nombre": "Alessandro",
        "apellido": "Arancione"
    },
    {
        "nombre": "Francesca",
        "apellido": "Marrone"
    }
]

Podría haber pedido datos estructurados de manera mucho más detallada o en formatos diferentes: XML, CSV, y así sucesivamente, y el modelo me habría ayudado a generarlos. Este es un aspecto muy interesante, porque a menudo la generación de datos de prueba es una tarea aburrida y repetitiva que puede ser automatizada con la ayuda de un modelo LLM.

Expresiones Regulares

Hay dos escuelas de pensamiento sobre las expresiones regulares: quienes las odian y quienes no las aman. Por desgracia o por suerte, en algunos ámbitos son muy útiles porque, cuando funcionan como queremos, reducen mucho el código que tenemos que escribir para hacer, por ejemplo, una validación.

¿Me escribes una expresión regular para validar una cadena con un máximo de 10 caracteres alfanuméricos, en cuya tercera posición haya un número?

El resultado, según lo solicitado, es definitivamente acertado.

^[a-zA-Z0-9]{2}\d[a-zA-Z0-9]{0,7}$

Test de Autoevaluación

Una parte de mi tiempo la dedico a enseñar y seguramente no es la mejor pagada, pero es ciertamente la más gratificante. La alegría que se obtiene al poder compartir con la siguiente generación nuestros conocimientos, viendo a los estudiantes crecer, no tiene precio.

Uno de los aspectos más molestos, al menos para los profesores, es la creación y corrección de los test de evaluación al final de las sesiones de estudio. En la escuela donde enseño se utiliza Moodle, una excelente herramienta para la enseñanza a distancia, pero la creación de los test siempre es una actividad aburrida y repetitiva.

Este año decidí pedir ayuda a ChatGPT para crear los test de evaluación. Introduje el documento markdown con la sesión de estudio que quería evaluar y le pedí a ChatGPT que generara el XML, en formato Moodle, de 30 cuestionarios de opción múltiple, enfocados en lo presente en el documento proporcionado.

El resultado fue muy satisfactorio: obtuve un archivo XML listo para ser importado en Moodle, con preguntas y respuestas generadas automáticamente por ChatGPT. Un ejemplo está contenido en mi curso de CSS:

https://github.com/matteobaccan/CorsoCSS

Entienden entonces cuán útil puede ser un modelo LLM para poder generar test de autoevaluación cada vez que los necesitemos.

Conclusiones

Cuando se aborda un LLM es importante entender que no se trata de una varita mágica que resuelve todos los problemas, sino de una herramienta que puede ayudarnos a acelerar el trabajo y mejorar la calidad del código.

Escribir un prompt y esperar que, por arte de magia, lo que tenemos en nuestra cabeza se convierta mágicamente en código exactamente como lo necesitamos, es algo impensable. De la misma manera, pensar que existe una sola herramienta de IA que resuelva todos nuestros problemas, por ahora, es un enfoque equivocado.

La mejor manera en que un programador debería abordar un LLM es entender cuáles son los límites de la herramienta y cuáles son los contextos en los que puede ser utilizada con éxito, comenzando a catalogar mentalmente estos ámbitos de modo que para cada uno de ellos exista una técnica o herramienta que pueda ser utilizada eficazmente.

De la misma manera, es impensable poder reunir en un solo prompt un proyecto: es mucho más efectivo dividir el proyecto en partes y pedirle al modelo que trabaje en estas partes, de modo que se obtenga un resultado mejor y más preciso.

Incluso en los contextos en los que un modelo no es capaz de generar código, es muy probable que sea capaz de documentarlo, comentarlo, traducirlo y realizar muchas otras operaciones que pueden acelerar nuestro trabajo.

Aún no estamos en un momento histórico en el que estas herramientas sean capaces de hacer todo de manera automática, pero un programador que sabe cómo utilizarlas puede obtener resultados muy buenos y mejorar la calidad de su trabajo.

Mejorar la propia calidad significa ser cada vez más efectivos y quitarnos de la cabeza el sombrero de junior para ponernos el más pesado y desgastado de un programador senior.