Comprensión del código
Entender el código para generar mejor código
Al crear FiboCode me importa cada vez más entender mi propio proyecto. Ideas sobre lenguaje natural, arquitectura y cómo seguir trabajando con AI.
No me gusta sentir que no entiendo mi propio proyecto.
Los requisitos los propuse yo. Las funciones eran las que quería. Después de que AI generara el código, comprobé que funcionaba. Pero si alguien me preguntara qué módulos recorre una función, por qué se implementó así o qué afectaría otro cambio, no siempre sabría responder con claridad.
Mientras construyo FiboCode, esto me importa cada vez más.
Claro que quiero que AI escriba más código por mí. Pero pienso seguir con este proyecto. Cuando termine la función de hoy, mañana habrá otra idea. No quiero empezar cada cambio rezando para que no rompa otra cosa.
Arreglamos A y falla B. Arreglamos B y aparece otro error en A. En esos casos, es fácil seguir enviando los síntomas a AI y esperar otra explicación y otra corrección.
Pero después de varias rondas me sentiría cada vez menos seguro. Cada cambio parece razonable por separado. Al juntarlos, ¿en qué se ha convertido el sistema?
Quiero entenderlo. Al seguir esta pregunta, he empezado a pensar que comprender el código y generarlo están más relacionados de lo que creía.
Programar en lenguaje natural me hizo pensar
Del lenguaje máquina al ensamblador y de ahí a los lenguajes de alto nivel, hemos seguido añadiendo abstracciones.
Muchos detalles que antes resolvíamos nosotros ahora quedan en manos de herramientas. Podemos expresar funciones más complejas con menos código, sin empezar desde abajo cada vez.
Ahora hemos subido otro peldaño: describimos requisitos en lenguaje natural y dejamos que AI genere el código.
A primera vista, podría parecer solo otra forma de expresarnos. Pero creo que hay un cambio que importa.
Los ensambladores y compiladores transforman código siguiendo reglas explícitas. Eso no impide que un programa tenga errores, pero la transformación no necesita adivinar qué quería hacer.
El modelo sí necesita adivinar.
A partir de mis palabras, el código que ve y el contexto, infiere lo que quiero y elige una implementación. La misma frase puede producir resultados distintos. Incluso si el resultado se repite, puede estar implementando siempre un requisito que nunca expliqué bien.
Más tarde leí un artículo de Birgitta Böckeler que también señala esta diferencia: los grandes modelos de lenguaje generan mediante inferencias y no se pueden tratar sin más como compiladores de lenguaje natural. Se acercaba mucho a lo que estaba pensando.
¿Qué podemos hacer entonces?
Escribir requisitos más claros, dejar las convenciones por escrito y comprobar comportamientos importantes con pruebas debería reducir algunas desviaciones. Si el diseño ya explica cómo colaboran los módulos y qué clases y métodos intervienen, AI tiene más en qué apoyarse.
Pero la documentación debe coincidir con el código. Una descripción detallada de un diseño que cambió hace tiempo puede dar más problemas que no tener documentación.
Tampoco creo que estas prácticas eliminen toda la incertidumbre de la generación. Pero pueden dejar menos cosas que el modelo tenga que adivinar sobre la marcha.
¿Sé realmente qué quiero cambiar?
Al seguir pensando, aparece otro problema: a las personas también nos cuesta explicar bien los requisitos.
Sobre todo cuando no conocemos el código.
Puedo tener muy claro dónde quiero otro botón o qué operación debería resultar más cómoda. Pero quizá no sé en qué módulo existente encaja, qué procesamiento debe atravesar o qué comportamientos anteriores deben mantenerse.
En esa situación, por mucho que repita a AI «no afectes a las demás funciones», ¿cómo debería interpretar esa frase?
Por ejemplo, «unifica esto» puede referirse a la presentación o a fusionar la lógica de procesamiento de datos. El alcance del cambio sería muy distinto según la interpretación.
Si ni yo puedo explicar el alcance, el modelo tiene que completarlo con lo que ve. Puede modificar más de lo previsto o saltarse una convención cuya existencia yo desconocía.
Después detecto un problema y le pido que lo corrija. A medida que se acumulan cambios, puede que el requisito inicial, las restricciones añadidas y las relaciones originales del código ya no permanezcan completos en el contexto.
Me preocupa acabar con código que funciona por partes, pero está lleno de parches de la reparación anterior. Parece que solo AI está dispuesta a seguir tocándolo. Las personas apenas se atreven.
Ese código me hace pensar en AI slop. Y volver a dárselo a AI tampoco significa que el modelo comprenda el sistema entero. Puede seguir escribiendo explicaciones sin que nosotros hayamos llegado a entenderlo.
Por eso siento cada vez más que, para describir una necesidad con precisión, primero debo conocer la forma general del proyecto.
No hace falta entender todos los detalles desde el principio. Pero sí quiero saber dónde caerá el cambio que estoy pidiendo.
Quiero hablar con AI en el nivel de la arquitectura
No creo que para usar AI haya que revisar manualmente todas las líneas que genera.
Cuando un proyecto se complica, la atención es limitada. Prefiero empezar por su estructura: cuáles son los módulos principales, de qué se encarga cada uno, dónde entra una función, qué procesamiento recorre y dónde cambia datos y estados.
Quiero construir otra capa de comprensión por encima de la implementación en un lenguaje de alto nivel. Por ahora la llamo «capa de arquitectura», aunque el nombre no me preocupa demasiado.
Lo importante es que esa capa se corresponda con el código real.
Si veo un módulo, puedo encontrar su implementación. Si veo el recorrido de una función, sé qué clases y métodos atraviesa. Si quiero ajustar un comportamiento, puedo seguir las relaciones hasta los lugares que necesito mirar.
Así tengo cosas más concretas que discutir con AI.
Por ejemplo, si sé que una operación ya pasa por una cola de tareas compartida, puedo hablar de si la nueva capacidad debería aprovecharla. Si AI añade otro mecanismo de gestión de estados, puedo preguntar por qué no sirve el existente y si ambos podrían interferir después.
Si nunca me planteara esas preguntas, quizá aceptaría cualquier implementación que funcionase. Ahora al menos tengo la oportunidad de detectar pronto si está complicando las cosas innecesariamente.
Creo que estas comparaciones también pueden ayudarme a programar mejor.
Hay varias formas de hacer una función. ¿Por qué este proyecto eligió esta? Una abstracción parece elegante, pero si cada modificación obliga a tocar muchos sitios, ¿está ayudando? Otro módulo es sencillo y ha soportado varios cambios de requisitos sin alterar el resto. ¿Qué hizo bien?
Son preguntas que merecen tiempo. Cuando voy formando mi propio criterio, me resulta más fácil decirle a AI qué ajustar y entender una alternativa que proponga.
Por supuesto, mi comprensión de la arquitectura también puede ser errónea. Las conexiones de un diagrama deben comprobarse en el código. Las funciones que creo intactas también requieren pruebas y ejecución real. Ante cambios de estado importantes o límites complejos, conviene profundizar en la implementación.
Quiero llegar poco a poco a saber, la mayoría de las veces, dónde mirar y qué partes todavía no entiendo.
Esa es una de las razones por las que desarrollo el análisis de código de FiboCode.
Quiero conectar código, documentos y diagramas de relaciones para encontrar la estructura en una implementación desconocida y seguir el recorrido de una función. Con esa comprensión, me sentiría más tranquilo al discutir cambios con AI.
En What Is Code?, Unmesh Joshi también explica cómo buenas abstracciones, tipos, pruebas y reglas explícitas pueden orientar al modelo al entender y modificar código. Me gusta esa dirección: conservar en el proyecto, en la medida de lo posible, nuestra comprensión del sistema para reutilizarla en el siguiente cambio.
Todavía quiero dedicar tiempo a leer código
AI escribe rápido. Es tentador darle también la siguiente tarea.
Las funciones se terminan una tras otra y el proyecto parece avanzar. Pero también me pregunto qué entiendo mejor después de todo ese trabajo.
Si solo pido cosas y acepto resultados hasta que el proyecto se vuelve demasiado complejo, quizá termine dedicando mucho tiempo a arreglarlo o a empezar de nuevo. El tiempo ahorrado podría desaparecer, junto con el coste de las llamadas al modelo y el esfuerzo de aprender otra vez toda una implementación.
Prefiero dedicar parte de ese tiempo a sentarme a leer código.
No tiene que ser mucho de una vez. Puedo empezar por entender por qué un módulo se dividió así, por qué una función necesita esos pasos o qué técnica desconocida usó AI esta vez. Si no entiendo algo, puedo preguntar, buscar pruebas en el código y hacer un pequeño cambio para comprobar si mi interpretación se sostiene.
Buscando material encontré un experimento de aprendizaje de Anthropic. Tras completar tareas de programación desconocidas con AI, los participantes obtuvieron una puntuación media inferior en una prueba inmediata que el grupo que programó a mano. El experimento es pequeño y no permite saber cómo evolucionarán las capacidades a largo plazo. Aun así, me hizo prestar más atención a si realmente aprendí algo después de terminar una tarea.
Espero que esa comprensión se acumule poco a poco. Que al encontrar un diseño parecido reconozca lo que ocurre, y que ante una propuesta de AI pueda hacer una pregunta con fundamento.
Todavía tengo que explorar si FiboCode puede ayudar bien en esto. Pero ahí está buena parte de mi motivación para desarrollar el análisis de código.
Quiero seguir programando con AI. Y quiero poder seguir explicando cómo funciona mi proyecto cuando haya crecido.
La idea, en un dibujo sencillo:
Otras lecturas relacionadas
- La guía de revisión de código de Google, en especial sus ideas sobre diseño general y complejidad, merece compararse con el propio proyecto.
- La discusión de Thoughtworks sobre deuda cognitiva en el código describe cómo la implementación y la comprensión del equipo pueden separarse.
- El artículo científico del experimento de Anthropic detalla los métodos y resultados.