Qué es la IA y qué es ChatGPT lo sabemos todos. Cada uno de nosotros ha escrito montones de prompts para obtener respuestas de ChatGPT, con resultados más o menos satisfactorios.
Para entender mejor cómo ChatGPT puede ayudarnos a convertirnos en programadores senior, primero debemos comprender qué es la ingeniería de prompts, por qué es tan importante y en qué áreas un modelo de lenguaje grande (Large Language Model, LLM) como ChatGPT u otros pueden ayudarnos a crecer profesionalmente.
Experiencia en el campo
Lo que estoy a punto de escribir surge de mi experiencia tras meses utilizando ChatGPT, GitHub Copilot, Claude y otros modelos de lenguaje grande (LLM), así como de una serie de lecturas y estudios sobre el tema. Estoy seguro de que, en unos meses, esto quedará obsoleto con la llegada de herramientas nuevas (o ya existentes y que no uso) que ayudarán aún más a los desarrolladores (y no solo) a crecer profesionalmente.
Es probable que algunos de vosotros utilicéis herramientas distintas a las que yo empleo, pero enfoquémonos en lo que podemos lograr con este tipo de herramientas, en lugar de centrarnos en cuál es la mejor. De lo contrario, caeremos en una discusión interminable sobre cuál es superior, y estoy seguro de que nadie ganará esa disputa.
Comprender ChatGPT y, en un sentido más amplio, los LLM
Empecemos desde el principio: ChatGPT es un chatbot de inteligencia artificial (IA) que utiliza el procesamiento del lenguaje natural para crear un diálogo conversacional similar al humano. Este modelo de lenguaje puede responder preguntas y redactar diversos tipos de contenido escrito, como artículos, publicaciones en redes sociales, ensayos, código y correos electrónicos, convirtiéndose en una herramienta emocionante para devs de todos los niveles.
Se trata, por lo tanto, de un modelo que no razona, pero que puede aplicar de manera muy eficaz las reglas aprendidas durante su entrenamiento. Esto significa que, si le proporcionamos una input incorrecta, obtendremos una output incorrecta; pero si damos una entrada precisa, obtendremos un resultado correcto.
Del mismo modo, si utilizamos un LLM que ha sido entrenado con conjuntos de datos que contienen errores, el resultado también contendrá errores.
Además, los modelos siempre intentan generar respuestas, lo que implica que habrá un porcentaje, mayor o menor, de respuestas que serán incorrectas, inventadas o inexactas.
Partiendo de este punto inicial, cuyo propósito es centrar el tipo de herramienta que estamos intentando utilizar, veamos en qué áreas la tasa de error es más alta y en cuáles es más baja, permitiendo un uso más exitoso.
Los LLM no están diseñados para resolver problemas o acertijos
Los LLM fueron creados para responder preguntas, redactar artículos, escribir código, pero no para resolver problemas o acertijos. Esto significa que, si les presentamos un problema, no sabrán cómo resolverlo, pero si les proporcionamos un problema ya resuelto, podrán ofrecernos la solución.
Cada vez que veo un video donde alguien propone un acertijo a ChatGPT y luego se sorprende de que no pueda resolverlo, me resulta gracioso. Es como pedirle a un niño de cinco años que resuelva un problema de física cuántica: son pruebas que no consideran las capacidades del interlocutor ni la forma en que fue diseñado. No aportan valor y no demuestran que el modelo sea inadecuado; más bien, reflejan que el «creador» no ha entendido cómo usarlo.
Limitaciones del conocimiento
Otro punto importante es que los LLM no poseen un conocimiento ilimitado, sino que su conocimiento está restringido a los datos incluidos en sus conjuntos de entrenamiento. Esto significa que, si les proporcionamos una entrada que no está en esos conjuntos, no podrán responder.
¿Cuándo ocurre esto con mayor frecuencia? Cuando pedimos información sobre productos que no existían o acababan de salir cuando el modelo fue entrenado.
Se están desarrollando mitigaciones para este problema, como consultas en tiempo real a la red o entrenamiento continuo, pero estas soluciones no resuelven el problema por completo. Aunque pueden generar algunas respuestas, muchas veces estas son inventadas o de baja calidad.
Por lo tanto, no esperen obtener respuestas confiables de ChatGPT sobre productos, tecnologías o eventos recientes, ya que el modelo no tendrá la información necesaria o, si responde, será con respuestas inventadas o poco precisas.
¿Dónde podemos obtener mejores resultados?
Existen modelos que no solo se basan en prompts, sino que también utilizan una serie de datos proporcionados al modelo, lo que permite obtener respuestas más precisas. Estos modelos, como GitHub Copilot, ofrecen mejores resultados porque poseen un conocimiento más amplio y detallado.
Por ejemplo, GitHub Copilot genera sugerencias analizando el código antes y después de la posición del cursor en el editor, utilizando todos los archivos abiertos en ese momento e incluso repositorios de GitHub referenciados en el contexto. Esto hace que las sugerencias de código no sean un simple copia y pega de los datos de entrenamiento, sino que estén fuertemente contextualizadas.
Ejemplo práctico
Partamos de este código, que podría escribirse 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) {
// Según el parámetro, buscar la correspondencia con las variables estáticas
}
El comentario es muy genérico y no indica claramente lo que espera el programador. Si usáramos este comentario como prompt en ChatGPT, probablemente no obtendríamos una respuesta precisa. Sin embargo, Copilot, al entender que el comentario se refiere a las variables estáticas, podría sugerir un código como este:
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("Sin coincidencias");
}
Muchos pensarán que este código es demasiado sencillo o que podría mejorarse, y tienen razón. Sin embargo, el objetivo aquí es mostrar que Copilot entiende el contexto y proporciona una respuesta correcta, mientras que ChatGPT requeriría un prompt mucho más detallado.
Comprensión del código
A menudo nos enfrentamos a código que no hemos escrito nosotros y que debemos modificar o extender. En estos casos, los LLM pueden ser útiles, ya que analizan el código y nos explican qué hace y cómo funciona. Esto facilita comprender el código y trabajar con él de manera más eficaz.
Un programador senior generalmente no tiene problemas para entender código ajeno, pero, al salir de su zona de confort, puede encontrarse con desafíos. Aquí es donde los LLM pueden marcar la diferencia, especialmente para programadores junior o cuando se necesita trabajar con código poco familiar.
Por ejemplo, partamos de esta función tomada del repositorio 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())) {
int thisPort = HttpClient.normalizePort(scheme, uri1.getPort());
int thatPort = HttpClient.normalizePort(scheme, uri2.getPort());
if (thisPort == thatPort) {
return uri2.getPath().startsWith(uri1.getPath());
}
}
}
return false;
}
Con un prompt genérico, podríamos obtener una explicación básica, pero con una mejor contextualización, el modelo puede proporcionar una descripción más precisa y útil, ayudándonos a modificar o extender el código según nuestras necesidades.
Probemos a pedirle a Claude (aunque lo mismo aplica para otros LLM) que nos explique qué hace esta función.
Pregunta:
¿Me puedes describir este código?
<codigo>
Respuesta: Este código define un método estático llamado matchesURI
que compara dos objetos URI (Uniform Resource Identifier) para determinar si coinciden según criterios específicos. Aquí tienes una descripción detallada del funcionamiento:
- Parámetros del método:
- El método recibe dos parámetros:
uri1
yuri2
, ambos de tipo URI.
- El método recibe dos parámetros:
- Comparación de esquemas (protocolos):
- Extrae el esquema de
uri1
. - Compara los esquemas de
uri1
yuri2
sin distinguir entre mayúsculas y minúsculas.
- Extrae el esquema de
- Comparación de hosts:
- Si los esquemas coinciden, compara los hosts de
uri1
yuri2
, también ignorando mayúsculas y minúsculas.
- Si los esquemas coinciden, compara los hosts de
- Comparación de puertos:
- Utiliza el método
HttpClient.normalizePort()
para manejar los puertos predeterminados de HTTP. - Compara los puertos normalizados de ambos URI.
- Utiliza el método
- Comparación de rutas:
- Si los puertos coinciden, verifica si la ruta decodificada de
uri2
comienza con la ruta decodificada deuri1
.
- Si los puertos coinciden, verifica si la ruta decodificada de
- Resultado del método:
- Devuelve
true
si todas estas condiciones se cumplen, yfalse
en caso contrario.
- Devuelve
Resumen:
El método considera que dos URI coinciden si tienen el mismo esquema, host, puerto (considerando puertos predeterminados de HTTP) y si la ruta de uri2
comienza con la ruta de uri1
.
Contextos útiles de los LLM: Entender código desconocido
Como se puede observar, en contextos donde nos enfrentamos a código que no conocemos, un LLM puede ser muy útil para entender qué hace el código y cómo funciona.
Creación de pruebas
De manera similar, a menudo necesitamos escribir pruebas para el código que hemos creado o que debemos modificar. La escritura de pruebas no siempre es sencilla, ya que requiere experiencia y conocimiento de las mejores prácticas.
Un LLM puede ser de gran ayuda en este caso, ya que genera pruebas de calidad para el código que estamos desarrollando, ayudándonos a cubrir mejor los escenarios necesarios. Aunque no proporciona un producto final, acelera significativamente el proceso, especialmente en contextos donde debemos escribir muchas pruebas o no tenemos mucha experiencia haciéndolo.
Ejemplo: Solicitar pruebas para la función matchesURI
Pregunta:
¿Puedes escribir pruebas para esta función?
<codigo>
Respuesta de Claude: Por supuesto, puedo ayudarte a escribir pruebas para esta función. Usaré JUnit, un framework ampliamente utilizado para pruebas en Java. Aquí tienes una serie de pruebas que cubren diferentes escenarios:
Escenarios cubiertos:
- URI coincidentes.
- Esquemas diferentes.
- Hosts diferentes.
- Coincidencia con puertos predeterminados.
- Puertos diferentes.
- Rutas no coincidentes.
- Insensibilidad a mayúsculas y minúsculas en esquemas y hosts.
- URI con query y fragmentos.
Código generado:
jimport 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));
}
}
Nota importante:
El código proporcionado no debe tomarse como algo definitivo, sino como una base para crear las pruebas necesarias. Es poco probable que el modelo cubra todos los casos posibles, pero, manteniendo un enfoque sencillo (Keep It Simple, Stupid – KISS), se pueden obtener resultados bastante útiles.
Seguridad
Otro ámbito en el que un LLM puede ser muy útil es en la seguridad. Escribir código seguro no siempre es fácil y requiere experiencia y conocimiento de buenas prácticas.
Existen herramientas que ayudan a escribir mejor código en varios aspectos, pero contar con algo integrado o, al menos, alimentado por prompts representa un gran avance. Por ejemplo, al analizar el método mencionado, pregunté:
Pregunta:
¿Puedes darme sugerencias para hacer este código más seguro?
Respuesta:
El modelo sugirió algo que ya había considerado al revisar el método:
Gestión de valores nulos:
El método actual no valida si los URI de entrada son null
. Se puede agregar esta validación al inicio del método:
if (uri1 == null || uri2 == null) {
throw new IllegalArgumentException("URIs cannot be null");
}
Este es solo un ejemplo recortado de las sugerencias recibidas, pero es importante notar que el modelo puede ofrecer recomendaciones útiles para mejorar la seguridad del código, enfocándose en aspectos como la normalización de parámetros y otros puntos clave.
Refactorización
Los LLM también pueden ser útiles en tareas de refactorización. Aunque el resultado no siempre será más eficiente, obtener ideas para mejorar el código es valioso.
Pregunté a ChatGPT:
Pregunta:
¿Puedes hacer un refactor para optimizar este código?
Respuesta:
public static boolean matchesURI(URI uri1, URI uri2) {
// Memoriza esquema, host y puerto una 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;
}
// Normaliza los puertos una vez
int port1 = HttpClient.normalizePort(scheme1, uri1.getPort());
int port2 = HttpClient.normalizePort(scheme2, uri2.getPort());
if (port1 != port2) {
return false;
}
// Compara rutas al final
String path1 = uri1.getPath();
String path2 = uri2.getPath();
return path2.startsWith(path1);
}
Aunque las mejoras en rendimiento son mínimas, el método es más claro y legible al organizar los controles secuencialmente, evitando una estructura profundamente anidada. En casos de código más complejo, este tipo de claridad puede marcar una gran diferencia.
Boilerplate
Una de las tareas más tediosas para los desarrolladores es escribir boilerplate para nuevos proyectos, funcionalidades o clases. Estas tareas iniciales consumen tiempo y requieren atención para no omitir detalles importantes.
Para acelerar este proceso, se pueden usar LLMs. Por ejemplo, al solicitar:
Pregunta:
¿Puedes escribir un proyecto mínimo en Vue con una pantalla de inicio de sesión que use el servicio https://reqres.in/api/login?
El modelo generó un punto de partida que adapté según mis necesidades, logrando un prototipo funcional con un esfuerzo mínimo. Este enfoque puede ser especialmente útil para programadores junior que necesitan orientación o para senior que buscan optimizar el tiempo.
Escritura de prompts
Escribir prompts efectivos requiere claridad, concisión y especificidad. Un buen prompt debe incluir:
- Contexto y detalles necesarios.
- Entrada y salida esperada.
Por ejemplo:
Prompt:
Escribe una función en C# para calcular el factorial de un número dado.
Este enfoque garantiza respuestas más precisas y útiles. Sin embargo, para contextos muy específicos, podría ser preferible usar herramientas como Copilot, que analizan el entorno de trabajo y ofrecen respuestas contextualizadas.
Comentarios en el código
Los LLM también son útiles para comentar código de forma rápida y efectiva. Por ejemplo:
Prompt:
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 "";
}
Resultado:
/**
* Lee el contenido de un archivo y lo devuelve como una cadena de texto.
* @param fileName Nombre del archivo a leer.
* @return Contenido del archivo 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));
// Usa un StringBuilder para construir el contenido del archivo.
final StringBuilder sb = new StringBuilder();
// Lee la primera línea del archivo.
String line = br.readLine();
// Lee y agrega líneas al StringBuilder hasta el final del archivo.
while (line != null) {
sb.append(line);
sb.append(System.lineSeparator());
line = br.readLine();
}
// Cierra el BufferedReader.
br.close();
// Devuelve el contenido del archivo.
return sb.toString();
} catch (IOException e) {
// Maneja errores de I/O.
e.printStackTrace();
}
// Devuelve una cadena vacía en caso de error.
return "";
}
En este caso, el código queda comentado, siendo mucho más claro y comprensible.
El ejemplo que he usado es muy sencillo, pero la idea principal es esta: ¿tenéis un software que no conocéis y que necesitáis comentar? Pedid a un LLM que lo haga por vosotros, y esto podría transformar vuestra base de código de un infierno a un paraíso.
Revisión de código en Pull Requests (PR)
De manera similar, imaginad que tenéis que gestionar las PR de un repositorio y queréis agilizar el proceso. En GitHub existen múltiples actions que podéis configurar para realizar revisiones automáticas de código. Una de ellas, aunque hay muchas similares, es ChatGPT Code Reviewer.
Esta action realiza revisiones automáticas de las PR, indicando de manera más precisa qué contienen y cómo mejorar cada una. Es solo un ejemplo, pero lo interesante es que podéis desarrollar vuestras propias actions para que se activen al procesar PR, ayudando a agilizar el trabajo y mejorar la calidad del código.
Análisis
Analizar un proyecto siempre es una tarea compleja que requiere tiempo y atención. Antes solía abordar esta fase estudiando documentos del proyecto y haciendo búsquedas exhaustivas en internet, con la esperanza de encontrar algo que me ayudara a comprender mejor el proyecto.
Estas búsquedas, aunque útiles, eran dispersas: por muy bien que busques, es fácil perderte en un mar de información y no encontrar exactamente lo que necesitas.
Últimamente, he cambiado mi enfoque: tras una primera fase en la que intento entender el proyecto, comienzo una conversación con ChatGPT. Hablar con un LLM tiene varias ventajas:
- Velocidad: Es mucho más rápido que buscar en internet. Los temas están más enfocados y es posible obtener respuestas precisas.
- Brainstorming: A diferencia de buscar en libros o sitios web, que puede ser costoso y lineal, un LLM permite conexiones horizontales entre la información sin necesidad de abrir múltiples pestañas.
- Funciona mientras conduces: En ocasiones, paso horas en el coche camino a un cliente, y hablar con un asistente virtual, aunque no sea perfecto, es mucho mejor que estar en silencio.
- Información constante: Aunque no toda sea aplicable de inmediato, me aporta un buen número de ideas nuevas.
- Hablar vs. leer: La información hablada en voz alta es más fácil de memorizar que la leída.
Eso sí, desde fuera, alguien que me vea parado en un 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 una tarea aburrida que requiere tiempo y atención, pero que es necesaria para hacer que el proyecto sea comprensible para quienes no hablan nuestro idioma.
Hace unos años, empecé a generar todo el material en formato Markdown. Este formato ligero permite escribir documentos de manera simple y rápida, sin preocuparse demasiado por la maquetación. Además, facilita extraer rápidamente diapositivas con herramientas como Marp y comparar versiones para identificar cambios.
Recientemente, me enfrenté a traducir la documentación de un proyecto al inglés. Hacerlo manualmente, copiando y pegando entre archivos y un LLM, es una tarea tediosa y lenta. Estaba a punto de escribir mi propio programa cuando descubrí GTP-Translate, una extensión para GitHub que automatiza la traducción de documentación mediante actions simples.
Con una simple issue, se activa la action de GTP-Translate, se indica qué documento traducir y a qué idioma, y el sistema genera automáticamente una PR con la traducción, lista para ser aprobada.
Conclusiones
Cuando se utiliza un LLM, es importante entender que no es una varita mágica que resolverá todos los problemas, sino una herramienta que puede ayudar a acelerar el trabajo y mejorar la calidad del código. Escribir un prompt y esperar que, como por arte de magia, el modelo traduzca exactamente nuestra idea en código perfecto es irreal.
Del mismo modo, pensar que existe una única herramienta de IA que soluciona todos los problemas es, a día de hoy, un error.
El mejor enfoque para un programador es entender los límites del LLM, identificar los contextos en los que puede ser exitoso y catalogar mentalmente estos casos para usar la técnica o herramienta adecuada en cada situación.
Es más eficaz dividir un proyecto en partes manejables y pedir al modelo que trabaje en cada una, logrando resultados más precisos y útiles. Incluso en casos donde un modelo no puede generar código, es probable que pueda documentarlo, comentarlo, traducirlo o realizar otras operaciones que agilicen nuestro trabajo.
Aunque aún no estamos en un momento histórico donde estas herramientas hagan todo de forma automática, un programador que sepa cómo utilizarlas puede obtener resultados excelentes y mejorar significativamente la calidad de su trabajo.
Mejorar nuestra calidad significa ser más efectivos y dejar atrás el rol de junior para asumir el sombrero más pesado y desgastado de un programador senior.