Mi IA escribió código basura. Y fue exactamente lo que buscaba
Hay una creencia muy extendida de que cuando usas IA para programar tienes que pedirle que lo haga bien desde el principio. Arquitectura limpia, ficheros pequeños, tests de calidad. Y si no sale así, es que la herramienta falla o no sabes usarla.
La realidad es que esto puede frenarte, especialmente en proyectos exploratorios. Y un mal código inicial no es necesariamente un problema.
Lo que tengo claro, y que he comprobado en mis dos últimos proyectos, es que el momento de pedir código limpio es el segundo, no el primero.
Esto aplica a proyectos completos, pero lo veo claro también para features grandes dentro de un proyecto existente.
El TL;DR es: el fin del trabajo con tu código no es cuando funciona. Es cuando el código es mantenible y escalable. Y eso no se consigue con un primer intento perfecto, sino con iteraciones sucesivas de refactorización.
El inicio: un servidor quemado y una decisión
Postiz (la herramienta que uso para publicar en redes) me reventó un servidor de 4GB de RAM. No pude ni entrar por SSH a parar el Docker. Literalmente perdí el acceso mientras seguía consumiendo recursos.
Fue curioso cómo la IA me ayudó a recuperarlo. Pero esto lo dejo para otra publicación 😄
Llevaba tiempo aguantando sus problemas: la UI móvil inusable, la pesadez para uso individual, las inconsistencias de la API. Pero ese día me dio el impulso suficiente para decidirme a crear mi propia herramienta.
La primera versión de lo que tenía en la cabeza era simple: una API ligera, rodeada de un MCP y un CLI, con una UI casi anecdótica para revisar publicaciones. Nada interactivo. Algo que pudiera correr en una Raspberry.
Como ya conté en su momento, comencé el proyecto desde mi Openclaw particular mientras los niños jugaban en el parque.
Por la noche, ya tranquilo en casa, lo probé y funcionaba. De momento solo en X, que era el MVP que me había planteado.
Y entonces me vine arriba.
El momento en que el proyecto se complica solo
Le pedí a Pencil que me hiciera un diseño. El resultado me gustó tanto que acabé desarrollando una interfaz completa, 100% responsive. Lo que iba a ser headless se convirtió en una app con UI propia.
El problema: la API que necesitaba la interfaz empezó a evolucionar mucho más rápido que el CLI y el MCP. Fui añadiendo endpoints para la web sin preocuparme de si los otros dos canales tenían paridad. La deuda se acumuló de forma silenciosa.
Y el código… el fichero principal llegó a las 7.000 líneas. Uno de tests superó las 3.000.
Todo funcionaba.
Y estaba hecho una pena.
Y no pasaba NADA.
La fase 2: cuando te sientas como ingeniero
Aquí es donde mucha gente se queda. Con el proyecto funcionando, con un mal código, ese que se critica mucho al vibe coding.
Pero aquí es donde entra la etapa de ingeniería.
Lo primero fue la arquitectura. Con todo lo que ya funcionaba encima de la mesa, pude analizar qué encajaba mejor. Descartamos una arquitectura en capas clásica porque hubiera complicado innecesariamente el dominio de la aplicación. Fuimos por hexagonal.
Antes de empezar, le pedí que revisara los tests actuales y añadiera los que fueran necesarios para que el refactor no rompiera la App. Los modelos actuales hacen extremadamente bien los refactors, y más con un proyecto de este tamaño. Pero no me la quería jugar.
Una vez reestructurada la App con la nueva arquitectura, empezamos a partir los ficheros grandes. Un límite no estricto de 500 líneas fue razonable.
Después vino el problema de paridad. La API tenía funcionalidades que el CLI y el MCP no tenían. Montamos una validación en los tests: si una funcionalidad existe en la API, los tests fallan si no está también en el CLI y en el MCP. Así el agente sabe exactamente cuándo ha terminado.
También hice algunas iteraciones para mejorar la accesibilidad de la web, tanto con conocimientos del propio LLM como usando herramientas de validación de accesibilidad.
Y finalmente los tests en sí. Esto merece un párrafo aparte.
El problema de los tests que escribe la IA
La IA tiende a escribir tests de caja blanca: comprueba que se llamó a tal función con tales parámetros. Son tests que se rompen en cuanto refactorizas, aunque el comportamiento no haya cambiado.
En general, no sirven para nada excepto para darte una falsa sensación de cobertura.
Lo que yo necesitaba eran tests de caja negra: le das una entrada, compruebas la salida. No te importa cómo está implementado por dentro. Esos tests sobreviven a refactors.
Hablé con el agente y le comenté que prefería este tipo de tests, y que revisara si había tests que simplemente validaban que una función llama a otra. Hay muchos tests de estos por ahí, y sospecho que su base de entrenamiento le hace tender a escribirlos. Pero no me aportaban nada.
Y sí, la mayor parte de este trabajo, la hice desde Telegram. En ratos muertos que usaba para esto en lugar de scrollear por las redes.
Lo que quiero que te lleves de aquí
El código caótico de la fase 1 no fue un error. Fue necesario para llegar tan lejos en tan poco tiempo. Sin ese sprint desordenado, es posible que me hubiera perdido en los detalles iniciales y a día de hoy no tuviera la App funcionando.
El error sería quedarse ahí. O creer que el agente va a hacer la fase 2 solo, sin que tú tomes las decisiones de ingeniería.
La IA es increíblemente rápida construyendo. Pero la dirección técnica sigue siendo tuya.
Cómo conseguir la localización amplia en Android
Cómo pedir permisos en Jetpack Compose