IA y automatización
Desarrollo guiado por especificación con agentes de IA en paralelo: qué funcionó y qué se rompió
6 min de lectura
Desarrollo guiado por especificación con agentes de IA en paralelo: qué funcionó y qué se rompió
Esta web se reconstruyó en un único cambio grande: reposicionarla como perfil de ingeniero, hacer que todo el contenido se pueda traducir al español, inglés y chino desde el panel de administración, añadir categorías de proyectos con filtros y búsqueda, un blog técnico, una sección de formación y una capa visual nueva. Demasiado para tenerlo en la cabeza, y demasiado para dárselo a un asistente de IA como un único prompt difuso. Lo que lo hizo manejable fue escribir primero la especificación y tratarla como un contrato.
Primero la entrevista, una pregunta cada vez
Antes de escribir un solo requisito respondí preguntas sobre los casos que cambiaban lo que había que construir, una a una, nunca en lista. ¿Se pueden mostrar públicamente los nombres de los clientes? ¿Un artículo del blog que solo existe en un idioma se oculta o se muestra con un aviso? ¿Qué fechas mandan cuando dos versiones de mi CV no coinciden? ¿Se puede publicar el material real de mi trabajo de fin de grado? Cada respuesta eliminaba una rama de trabajo o añadía una restricción. De esa entrevista salieron nueve decisiones, y varias habrían sido suposiciones silenciosas de otro modo. Una de ellas, por ejemplo, es que las imágenes clínicas del proyecto de segmentación laríngea no se pueden publicar, así que ese proyecto solo puede representarse con una ilustración propia.
EARS: un comportamiento por frase
La especificación acabó con 78 requisitos funcionales, numerados y agrupados en bloques (posicionamiento, contenido traducible, experiencia, formación, proyectos, imágenes, skills, contacto, blog, presentación visual, SEO), más dos que se añadieron durante la entrevista. Cada requisito sigue uno de los patrones EARS:
- Ubicuo: EL SISTEMA hará…
- Dirigido por evento: CUANDO el visitante elija una categoría, EL SISTEMA mostrará solo los proyectos de esa categoría.
- De estado: MIENTRAS esté activada la reducción de movimiento, EL SISTEMA…
- Opcional: DONDE exista una característica…
- No deseado: SI ningún proyecto cumple el filtro, ENTONCES EL SISTEMA mostrará un mensaje de "sin resultados" con una acción para limpiarlo.
Dos reglas marcaron la diferencia. La primera: un comportamiento por frase. Si un requisito necesita una "y" para unir dos comportamientos, son dos requisitos. La segunda: nada de adjetivos que no se puedan medir. "Rápido" o "intuitivo" no son requisitos; un umbral sí. Cada requisito tenía que ser comprobable, y si no se me ocurría cómo comprobarlo, estaba mal escrito.
La sección que más valoro es Fuera de alcance. Comentarios, newsletter, varios autores, un CV descargable: escritos de forma explícita para que la funcionalidad no creciera sola.
De la especificación a un plan con TDD
La especificación dice qué y por qué; a propósito no contiene stack, nombres de ficheros ni esquemas. Eso fue a un plan aparte: 13 tareas, cada una con los ficheros que toca, las interfaces que consume de tareas anteriores y las que produce para las siguientes, y pasos en orden de test primero: escribir el test que falla, ejecutarlo, implementar lo mínimo, volver a ejecutarlo, commit. La sección de interfaces importó más de lo que esperaba. Un agente que implementa una tarea solo ve esa tarea, así que los nombres exactos de métodos y tipos de los que dependen las tareas vecinas tienen que estar escritos, o dos agentes inventarán dos nombres distintos para lo mismo.
Ejecutar en paralelo las tareas independientes
Con la base en su sitio (traducciones y migración de datos), muchas tareas eran independientes: el panel de administración, las ilustraciones de los proyectos, el blog, el comando de sincronización de contenido. Las ejecuté por oleadas, cada tarea en manos de su propio agente de IA trabajando en un worktree de git aislado y en su propia rama. Cada agente pasaba sus propios tests y terminaba con un único commit, que después yo integraba con cherry-pick en la rama de la funcionalidad.
Para avanzar rápido funcionó bien. También creó un punto ciego concreto: los tests de cada agente estaban en verde por separado, y eso no decía nada de si las piezas funcionaban juntas.
Qué se rompió al integrar
Un autoloader obsoleto que apuntaba a un worktree borrado. Tras una oleada, un cambio en un controlador del checkout principal parecía no tener efecto, y una vista fallaba con variables no definidas aunque el código fuente era claramente correcto. Ningún error lo explicaba. La causa: la instalación de dependencias de un agente había regenerado el classmap de Composer con rutas a su propio worktree, y PHP seguía cargando tan tranquilo el fichero antiguo desde allí. (new ReflectionClass(Foo::class))->getFileName() mostró la ruta equivocada, y un composer dump-autoload en el checkout principal lo arregló. A partir de ahí, las instrucciones de cada agente incluían esa comprobación antes de hacer commit.
Una vista que se saltaba el modelo. El modelo tenía un método displayImage() correcto que devuelve la ilustración cuando el proyecto está en modo ilustración, que es justo como debe mostrarse el proyecto de laringe. Pero la tarjeta y la ficha del proyecto pasaban el campo image en bruto como fuente principal y usaban la ilustración solo como respaldo si la imagen no cargaba. La foto clínica antigua cargaba sin problemas, así que el respaldo nunca saltaba, y se mostraba una foto que no debe ser pública. Los tests del modelo pasaban, porque el modelo estaba bien. Solo lo detecté mirando la página renderizada. El arreglo fue una línea en cada vista más un test que renderiza ambas páginas y comprueba que la URL de la foto no aparece.
Filas robadas en una base de datos nueva. El comando de sincronización empareja las filas existentes por una clave estable o, la primera vez, por el id que ya tienen en producción. En una base de datos sin esas filas, los ids autoincrementales se repartían durante la misma ejecución, de modo que una entrada posterior cuyo id esperado coincidía con una fila recién creada para otra entrada la adoptaba y la sobrescribía. En producción, donde esos ids ya existen, esto no ocurre nunca, y precisamente por eso pasó desapercibido. El arreglo fue adoptar una fila por id solo si todavía no tiene clave, y el fixture de los tests ahora siembra las filas heredadas con sus ids reales en lugar de probar contra una base de datos vacía poco realista.
Con qué me quedo
- La especificación es el artefacto más valioso, más que cualquier código generado. Es lo que permite repartir el trabajo, revisarlo y decir "terminado".
- Los agentes en paralelo multiplican lo que se produce, pero integrar es un trabajo aparte. Que los tests de cada tarea estén en verde es necesario, no suficiente.
- La verificación tras integrar tiene que incluir arrancar la aplicación real y mirarla, no solo pasar la batería de tests. Dos de los tres bugs anteriores eran invisibles para los tests hasta que escribí el test después de ver el bug.
- Cada bug encontrado al integrar se convirtió en un test de regresión y, cuando aplicaba, en una línea de las instrucciones para la siguiente oleada de agentes.
Trabajar así no eliminó el trabajo de ingeniería. Lo desplazó: menos tiempo escribiendo código, más tiempo especificando, revisando y verificando.