DEV Community

Cover image for La conversación sobre desarrollo de software está involucionando con la IA
Armando Picón
Armando Picón

Posted on

La conversación sobre desarrollo de software está involucionando con la IA

Una de las ventajas - o posibles desventajas - de estar al tanto de los últimos avances de la tecnología es que, llegado cierto punto, alguna revolución puede estar presente de forma tan continua en las redes sociales que inevitablemente uno termina saturándose con cada post, video o reel que habla del tema.

Recuerdo que esa fue una de las razones por las que inicialmente me negué a adoptar la IA en mis flujos de trabajo.

Sin embargo, el tiempo pasó e inevitablemente comencé a emplear algunas herramientas para cosas tan sencillas como iterar sobre ideas y pensamientos que suelen ser recurrentes en mí mientras voy estudiando y aprendiendo cosas nuevas.

De esta forma, empecé a bajar las barreras y bloqueos mentales que yo mismo me había impuesto producto de la saturación de información que había experimentado en un inicio.

Adicionalmente, he tenido la oportunidad de filtrar bastante la clase de contenido que consumo.

Por el momento sigo a un número acotado de creadores porque sé que realmente ponen en práctica las cosas que divulgan. Entonces, tengo la sensación de que el nivel de "chamullo" es bastante menor en comparación con otros creadores más sensacionalistas.

Y justamente a partir de esa selección de contenido comencé a notar algo que me resulta bastante curioso.

¿Estamos reinventando la rueda?

Para quienes me siguen en mis diferentes redes sociales, saben que me dedico principalmente al desarrollo de aplicaciones móviles para Android y que, recientemente, también me he involucrado en el desarrollo multiplataforma gracias a Kotlin Multiplatform.

Sin embargo, explorando el uso de herramientas de IA en el desarrollo de software en general, he comenzado a observar una especie de involución en la conversación alrededor de nuestra profesión.

No necesariamente en las herramientas.

No necesariamente en las capacidades que tenemos disponibles.

Sino en la manera en que hablamos sobre ellas.

Cada vez encuentro más contenido - incluso proveniente de ingenieros que trabajan directamente en empresas que están construyendo este tipo de herramientas - donde pareciera que estamos redescubriendo prácticas que llevan décadas formando parte de la ingeniería de software.

Ahora hablamos constantemente de guardrails para evitar que un agente introduzca código incorrecto.

Pero llevamos años utilizando compiladores, sistemas de tipos, linters, static analysis, tests y pipelines precisamente para establecer límites sobre qué cambios consideramos aceptables.

También podemos diseñar workflows cada vez más sofisticados para mejorar el contexto que recibe un modelo, organizar instrucciones, dividir trabajo entre agentes o conseguir que el modelo entienda mejor nuestro codebase.

Todo eso tiene valor.

Pero si después de construir ese workflow no tenemos un CI sólido capaz de verificar automáticamente lo que acaba de producir, seguimos depositando gran parte de nuestra confianza en que el modelo haya entendido correctamente nuestras instrucciones.

Y cuando comenzamos a hablar de agent verification, muchas veces el mecanismo que encontramos debajo resulta sorprendentemente familiar:

el agente genera código, ejecutamos una serie de checks, observamos qué falló y volvemos a iterar.

La automatización puede ser nueva.

La velocidad puede ser nueva.

El agente definitivamente es nuevo.

Pero el principio detrás de la verificación no necesariamente lo es.

Y creo que aquí vale la pena detenernos un momento.

Probabilismo vs. determinismo

Para el usuario común - y probablemente también para desarrolladores que no se han detenido demasiado a estudiar qué ocurre detrás de estas herramientas - la IA puede parecer una caja negra.

Le entregas una instrucción y mágicamente comienza a generar una serie de resultados que probablemente satisfagan lo que estás buscando.

Pero si observamos un poco más abajo encontramos algo bastante más interesante.

Detrás de muchas de estas herramientas existe todo un conjunto de componentes que, funcionando en conjunto, nos permiten contar con capacidades de razonamiento, memoria, acceso a información, interacción con herramientas locales o remotas, ejecución de acciones, entre otras cosas.

En el centro de buena parte de este andamiaje se encuentran los modelos de lenguaje.

Y detrás de estos modelos existen sistemas matemáticos que, simplificando bastante, van estimando una distribución de probabilidades sobre cuál debería ser el siguiente token dentro de una secuencia.

Ese detalle importa.

Estamos trabajando con sistemas probabilísticos.

Eso significa que no deberíamos asumir que, ante una misma entrada, obtendremos necesariamente exactamente la misma salida.

En el otro extremo tenemos los sistemas determinísticos.

Es decir, aquellos en los que, dadas las mismas condiciones y los mismos datos de entrada, esperamos obtener el mismo resultado.

Una operación aritmética simple probablemente sea el ejemplo más evidente.

2 + 2 seguirá siendo 4 independientemente de cuántas veces ejecutemos la operación.

Y es justamente aquí donde aparece una paradoja que me parece particularmente interesante:

Cuanto más probabilística se vuelve la forma en que generamos código, más necesitamos mecanismos determinísticos para verificar si ese código es aceptable.

Volviendo al desarrollo de software

Mencionaba líneas arriba que sigo viendo y escuchando a creadores de contenido relacionados con el desarrollo de software hablar de cuánto está cambiando la incorporación de la IA en nuestra profesión.

Y sí, están cambiando muchas cosas.

Está cambiando la velocidad con la que podemos producir código.

Está cambiando la cantidad de alternativas que podemos explorar.

Está cambiando la forma en la que interactuamos con nuestras herramientas.

Está cambiando incluso el rol que desempeña el desarrollador dentro de algunas partes del proceso.

Pero si abstraemos deliberadamente muchas de esas diferencias, hay un cambio fundamental mucho más sencillo de observar:

Podemos delegar la generación del código.

Antes ese código era escrito principalmente por un ser humano.

Hoy una proporción cada vez mayor puede ser generada por un modelo de IA.

Claro, no faltará quien diga que estoy sobresimplificando las cosas.

Y efectivamente lo estoy haciendo.

Deliberadamente.

Porque esa simplificación permite observar algo importante: cambiar quién - o qué - genera el código no elimina los mecanismos que utilizábamos para verificarlo.

Todo lo contrario.

Durante décadas hemos construido mecanismos bastante determinísticos que nos permiten reducir incertidumbre y verificar determinadas propiedades del software que producimos.

Por mencionar algunos:

Compilación y sistemas de tipos

Compiler checks, type systems y null-safety nos permiten detectar errores de tipos, contratos inválidos y problemas de nullability antes de ejecutar el programa.

Static Analysis

Android Lint, Detekt, SwiftLint o Ktlint nos permiten detectar determinados bugs conocidos, malas prácticas y code smells mediante reglas previamente definidas.

Formatting

Ktlint, Spotless, SwiftFormat o Dart Format nos permiten establecer reglas de formato consistentes y verificables.

Unit Testing

JUnit, XCTest, Kotlin Test o Dart Test nos permiten comprobar unidades de comportamiento de manera aislada.

Integration Testing

Nos permite verificar cómo distintos componentes colaboran entre sí: repositories, bases de datos, network layers, servicios, persistencia, etc.

UI Testing

Compose UI Test, Espresso, XCUITest o Flutter Integration Test nos permiten verificar determinados comportamientos desde la interfaz.

Code Coverage

JaCoCo, Kover o Xcode Coverage nos permiten medir qué porción del código fue ejecutada durante nuestras pruebas.

Coverage no demuestra calidad por sí solo. Tener 100 % de coverage no significa necesariamente contar con buenos tests ni mucho menos con un producto libre de errores. Pero sí constituye una señal medible que puede formar parte de un sistema de verificación mayor.

Git Hooks

Pre-commit y pre-push hooks nos permiten ejecutar determinadas verificaciones antes de que nuestros cambios abandonen nuestra máquina.

Y podríamos seguir.

CI pipelines.

Contract testing.

Mutation testing.

Property-based testing.

Dependency checks.

Security checks.

Reproducible builds.

Son muchas más.

Y probablemente algunas tengan mucho más sentido que otras dependiendo del producto, plataforma o equipo en el que estemos trabajando.

Lo importante no es utilizar todas.

Lo importante es entender el principio que existe detrás.

Ninguna de estas prácticas garantiza por sí sola que nuestro software sea correcto.

Esa garantía absoluta prácticamente nunca ha existido.

Lo que hacen es algo mucho más útil:

Reducen sistemáticamente nuestra incertidumbre.

La IA no elimina estas prácticas

Las vuelve más importantes.

Y es precisamente aquí donde tengo la sensación de que parte de la conversación actual está mirando demasiado hacia un solo lado.

Podemos dedicar horas a mejorar nuestros prompts.

Podemos discutir cómo construir mejores archivos de contexto.

Podemos experimentar con diferentes maneras de organizar instrucciones.

Podemos crear agentes, subagentes, herramientas, MCPs y workflows cada vez más complejos.

De hecho, deberíamos aprender a trabajar correctamente con todo eso.

Pero si nuestra estrategia termina dependiendo exclusivamente de que el modelo "entienda bien" lo que queremos y produzca código correcto, seguimos jugando a prompts.

Seguimos intentando aumentar la probabilidad de obtener una buena respuesta.

Y eso no es necesariamente malo.

El problema aparece cuando confundimos mejorar esa probabilidad con verificar que el resultado sea correcto.

Son problemas distintos.

Generación probabilística, verificación determinística

Creo que aquí podemos utilizar un marco bastante sencillo para pensar cómo incorporar IA dentro de nuestros procesos de desarrollo.

No pretende inventar nada nuevo.

De hecho, justamente ese es el punto.

1. Generar

Permitimos que el modelo haga aquello en lo que estas herramientas pueden resultar extremadamente útiles:

explorar alternativas, escribir código, realizar refactors, proponer implementaciones, navegar nuestro codebase o incluso operar directamente sobre nuestro repositorio.

En esta parte aceptamos el probabilismo.

No intentamos eliminarlo.

El modelo propone.

2. Restringir

Luego hacemos que el código generado se enfrente a contratos definidos por nosotros.

El compilador.

El sistema de tipos.

Null-safety.

Formatting.

Linting.

Static analysis.

Reglas arquitectónicas.

Contratos de APIs.

Siempre que una restricción pueda convertirse en una regla ejecutable, tenemos la posibilidad de dejar de depender de que el modelo la "recuerde".

El sistema simplemente la hace cumplir.

3. Verificar

Después verificamos el comportamiento.

Unit tests.

Integration tests.

UI tests.

Contract tests.

Property-based tests.

Los mecanismos que correspondan al problema que estamos resolviendo.

En este punto dejamos de preguntarnos si el código "parece correcto".

Comenzamos a preguntar si cumple propiedades concretas que podemos comprobar.

4. Decidir

Finalmente establecemos las condiciones bajo las cuales aceptamos ese cambio.

Build.

Tests.

Quality gates.

Security checks.

Revisión de código.

Aprobación humana cuando corresponda.

Solo después de atravesar esas verificaciones permitimos que el código avance.

Generar.

Restringir.

Verificar.

Decidir.

En realidad, no estamos haciendo algo radicalmente distinto a lo que ya hacíamos.

Estamos cambiando la entidad que genera parte del código y aumentando dramáticamente la velocidad con la que puede hacerlo.

Y precisamente por eso necesitamos reforzar el resto del sistema.

Entonces, ¿qué es realmente un guardrail?

Aquí es donde la conversación se vuelve particularmente interesante.

Un guardrail no tiene por qué ser una instrucción particularmente ingeniosa dentro de un prompt.

Puede ser simplemente una regla que el modelo no puede ignorar.

El modelo puede olvidar que tenemos una determinada convención de formato.

Ktlint no.

El modelo puede inferir incorrectamente un contrato de tipos.

El compilador no negocia con él.

El modelo puede convencernos con una explicación perfectamente razonable de que su implementación funciona.

Un test que falla tiene una opinión bastante diferente.

Por supuesto que ahora podemos utilizar agentes para ejecutar todos estos mecanismos, interpretar los resultados e incluso corregir automáticamente aquello que falló.

Eso sí representa una capacidad nueva y extremadamente interesante.

Pero estamos construyendo esa capacidad sobre principios que ya conocíamos.

No deberíamos confundir una nueva capa de automatización con la aparición de una nueva disciplina.

Sigue siendo ingeniería de software.

Dejar de jugar solamente a prompts

No tengo nada contra los prompts.

Tampoco contra el context engineering, los agentes o los workflows cada vez más sofisticados.

Todo lo contrario.

Son herramientas que estoy incorporando progresivamente en mi propio trabajo y que me parecen tremendamente interesantes.

Pero creo que existe una diferencia importante entre mejorar las probabilidades de que un modelo produzca código correcto y construir un sistema capaz de rechazarlo cuando no lo es.

La primera estrategia intenta conseguir una mejor respuesta.

La segunda reduce nuestra dependencia de que la respuesta sea correcta desde el principio.

Y si puedo elegir dónde depositar mi confianza, prefiero hacerlo en aquello que puedo verificar.

Quizá ahí está una de las oportunidades más interesantes que nos está dando la IA.

Si cada vez necesitamos invertir menos tiempo en escribir manualmente cada línea de código, podemos invertir una parte de ese tiempo en construir mejores contratos, mejores tests, mejores pipelines y mejores sistemas de verificación.

No para impedir que la IA trabaje.

Sino precisamente para poder confiar más en aquello que produce.

La IA está cambiando dramáticamente quién puede generar código, cuánto código podemos generar y a qué velocidad podemos hacerlo.

Pero no eliminó la ingeniería que necesitamos para decidir qué código merece llegar a producción.

Y quizá parte de la conversación alrededor del desarrollo de software está involucionando precisamente porque estamos demasiado fascinados con la nueva forma de generar código y estamos olvidando todo lo que ya habíamos aprendido sobre cómo verificarlo.

Cuanto más probabilística se vuelve la generación de software, más determinístico debería ser el proceso que decide si aceptamos el resultado.

Top comments (0)