Al desarrollar una app móvil con IA, la tentación es pedirle al agente que lo monte todo. Y lo monta, pero falla. Lo que decide el resultado no es el modelo que uses, sino cuánto has cerrado antes de que escriba la primera línea.

Hay un gesto que repite casi todo el que prueba a desarrollar una app móvil con un agente IA por primera vez: abre el chat, le pide la aplicación entera de una vez y se sienta a esperar. Unos segundos después tiene pantallas, navegación y algo que compila. La sensación es mágica.

Esto dura hasta que intentas montar la segunda funcionalidad encima de la primera.

Porque un modelo nunca te dice “no sé”. Siempre te entrega algo con aspecto de terminado, aunque no lo esté. Y si no le has marcado el camino, ese “algo” sale de lo que él supone que querías, no de lo que querías. Divaga, se va por otro lado, decide por su cuenta. Para una prueba de una tarde, perfecto. Para una app destinada a producción, es el principio de los problemas.

El problema no es que falle, es que nunca falla

Un agente IA siempre te da una solución porque está entrenado justo para eso: rellenar el hueco con lo primero que sirva para entregarte algo. Y ahí está el riesgo: te devuelve una respuesta con aspecto de terminada que casi nunca respeta lo que ya tienes montado, y no te avisa de lo que se ha inventado por el camino.

El ejemplo lo reconocerá cualquiera que lo haya intentado en serio. Durante esta serie de posts, vamos a desarrollar una app de gastos compartidos, de esas para repartir lo que se gasta en un grupo (una cena, un viaje, un piso) y saber al final quién debe a quién.

Pues bien, comenzamos pidiendo al agente la pantalla de añadir un gasto y, en vez de seguir la arquitectura del proyecto, tira por la vía rápida. Toda la lógica (UI, llamadas a la API, el cálculo del reparto, el parseo, la persistencia) condensada en una sola clase gigante.

// Una pantalla que lo hace TODO a la vez:

class AddExpenseActivity : AppCompatActivity() {
    private val db = AppDatabase.get()    // datos
    private fun save(amount: Double, payerId: String) {
        lifecycleScope.launch { 

        // reglas de negocio metidas en la UI:

            val members = db.groupDao().members(currentGroupId)
            val share   = amount / members.size   // reparto
            members.forEach { m ->

             // persistencia

                db.balanceDao().add(m.id, share, payerId)                          
}
         // y de paso, sincroniza con el backend desde aquí mismo:

            // API directa

            val res = api.post("$BASE_URL/expenses", body)
            Json.decodeFromString<ExpenseDto>(res) // parseo
            runOnUiThread { showBalance() }        // UI + estado
        }
    }
    // ...y debajo, 400 líneas más pintando el formulario.
}

Compila, funciona en la demo pero acabas de romper una de las reglas que más cara se pagan en móvil (la UI llamando al backend a pelo con el reparto cocinado dentro de la pantalla) y la siguiente pantalla se va a construir apoyada en ella.

Si repetimos en la pantalla de balances, en la de liquidar y en la de crear grupo, nos daremos cuenta de que ya no tenemos aplicación, tenemos una bola de nieve donde cada cosa nueva arrastra el desorden de la anterior y desenredarlo cuesta más que haberlo hecho bien de entrada.

La primera vez que nos enfrentamos a algo así, nos costará bastante tiempo entenderlo porque, a pesar de “funcionar”, la solución no nos vale. El código está ahí, compila y la demo sale bien pero el problema no era ese fichero: era que cada fichero siguiente iba a copiar su mal ejemplo.

Esa es la trampa de pedir y esperar: no se rompe el primer día, se rompe el día 20.

Definir es el nuevo oficio

Bien definido, ese mismo agente te hace avanzar a una velocidad que hace dos años no existía. Y toda la diferencia cabe en una palabra: definido.

Cuanto más partes la tarea y más cierras lo que pides, menos espacio le dejas al modelo para rellenar con lo que no querías. No es lo mismo pedir “la pantalla de añadir gasto” y ya, que acotar el encargo hasta que no quepa la improvisación:

La misma pantalla bien acotada:

  • Tarea: Composable AddExpenseScreen - Solo la UI
  • Sigue el patrón de capas ui/domain/data que ya existe
  • Sin lógica de negocio ni llamadas al backend desde la pantalla
  • El reparto lo calcula el caso de uso SplitExpense (ya implementado)
  • Persiste en la BD local. La sincronización no es asunto de esta capa
  • Estados: loading / error / data

Una tarea, un alcance, una capa.

Si te fijas, la segunda versión no le da al agente ni una sola decisión que pueda inventar: el cálculo del reparto vive en su caso de uso, la persistencia en la capa de datos, la sincronización en otro sitio. La pantalla solo pinta. Esa misma tarea, dirigida con esta precisión, suele salir bien a la primera. Y cuando no, el fallo está acotado a la UI: no hay que rastrearlo por cinco responsabilidades mezcladas.

Lo concreto se ve mejor en el contraste de cómo se pide:

// ✖️ Abierto - el modelo decide arquitectura por ti
“Hazme la pantalla para añadir un gasto al grupo.”

// ✔️Acotado - tú decides y el agente ejecuta
“Composable AddExpenseScreen, capa UI/ solamente.
- Recibe un AddExpenseUiState y emite eventos al ViewModel.
- El reparto lo resuelve el caso de uso SplitExpense, NO lo calcules aquí.
- Nada de llamadas al backend desde el Composable.
- Estados a renderizar: loading | error | data.”

La primera versión invita a la mega-clase. La segunda no le deja sitio para inventar. Ahí es donde se desplaza el trabajo de quien desarrolla: la habilidad ya no es solo teclear el código, es definir con la precisión suficiente para que el código que sale del agente sea el tuyo y no el suyo.

No usan modelos distintos. Definen distinto.

Para mí, esa es la línea que separa a quien dice que la IA “no sirve para nada serio” de quien la usa para entregar de verdad.

Pedir y esperar (tarea abierta) → divaga, mega clase (ignora tu arquitectura → bola de nieve (deuda que crece). El modelo es el mismo , cambia lo que pones tú: definir + partir (specs cerradas) → el agente acelera (respeta lo que hay) → app que avanza (velocidad real). El mismo agente, dos resultados opuestos. La variable no es el modelo, es la definición.

Empieza antes del código: deja que el agente te entreviste a ti

Lo primero que debemos hacer al arrancar una app no es programar, es especificar. Debemos darle la vuelta al guión habitual: en lugar de pedirle al agente que construya, le pedimos que nos entreviste.

Le damos el contexto de lo que queremos y dejamos que pregunte. Nos plantea por dónde seguir, nos pone las opciones sobre la mesa, nos dice cuál recomienda y la decisión final la tomamos nosotros/as. Esa entrevista saca a la luz las decisiones que ni sabíamos que teníamos que tomar: las mismas que, si las dejas sin cerrar, son las que más tarde se convertirán en la bola de nieve.

Ejemplo:

- : “Quiero una app para registrar gastos compartidos.

- Agente: ¿Los grupos son fijos o se crean por evento? ¿El reparto va a partes iguales o por porcentajes? ¿Hace falta modo offline desde el día 1? Mi recomendación: grupos por evento, reparto configurable y sin offline al principio. ¿Lo dejamos así?

- : Hecho: grupos por evento, reparto configurable, sin offline.

En tres preguntas, el agente ha cerrado tres decisiones que, sin preguntarlas, habría dado por supuestas y casi siempre al revés de lo que queríamos. Eso es especificar: sacar a la luz lo que darías por hecho antes de que el código lo dé por hecho por ti.

De esa conversación sale, casi sin querer, el alcance de la primera versión. La app de gastos compartidos acabó siendo esto:

MVP de la app de gastos compartidos

  • Crear un grupo por evento (una cena, un viaje).
  • Añadir un gasto e indicar quién pagó.
  • Reparto configurable: a partes iguales o por porcentajes.
  • Pantalla de balance: quién debe a quién
  • Liquidar una deuda y dejarla saldada.

Fuera de la v1 a proposito: pagos reales dentro de la app, multidivisa y modo offline.

Decir en voz alta lo que no entra es la mitad del trabajo. Tres líneas (“nada de pagos in-app, ni multidivisa, ni offline”) le ahorran al agente la tentación de montar una pasarela de pago que no hemos pedido y nos ahorra el tiempo de borrarla.

Tampoco hace falta cerrarlo todo de golpe. Basta con cerrar lo suficiente para que la primera tarea no tenga huecos por los que el modelo se cuele. El resto se define cuando toque, una pieza cada vez.

Definir no es escribir un documento enorme antes de empezar, es no dejar ninguna decisión importante en manos del azar. Cuando esas decisiones ya están tomadas, el siguiente paso es dejarlas escritas donde el agente las consulte mientras programa (eso lo veremos en el post acerca del contexto) y descomponer el qué en specs cerradas, que es harina de otro artículo.

Especificar primero no es burocracia, es mover el momento de pensar antes de escribir código, cuando cambiar de idea cuesta una frase y no una refactorización.

Suelto para experimentar, cerrado para producir

Conviene no mezclar dos mundos que se parecen pero no lo son. Para un prototipo, una prueba o un rato de trastear un fin de semana, deja al agente suelto: es desechable, y la velocidad compensa de sobra el desorden. Nadie va a mantener eso.

Para algo que va a producción, lo abierto se paga, y con intereses. Cada atajo que se inventa el modelo es deuda técnica que alguien (tú, dentro de tres meses) tendrá que desenredar. La diferencia entre el experimento divertido y el problema de mantenimiento no está en el agente: está en cuánta disciplina le pusiste delante.

En la app de gastos compartidos, esa disciplina cabe en una regla que se repite hasta el aburrimiento: la UI nunca habla con el backend. La pantalla pide al dominio, el dominio decide, la capa de datos guarda en local y el backend solo entra para sincronizar.

Escrita así, la regla es trivial y, respetada en cada pantalla, marca la diferencia entre una app que crece y una bola de nieve.

// La capa que decide vive en domain/, fuera de la pantalla:
class SplitExpense(private val repo: ExpenseRepository) {
    suspend operator fun invoke(expense: Expense, split: Split) {
        val shares = when (split) {
            is Split.Equal   -> expense.equalShares()
            is Split.Percent -> expense.byPercent(split.weights)
        }
        repo.save(expense, shares)   // a la BD local; sync, aparte
    }
}

La pantalla solo invoca SplitExpense y pinta el resultado. No sabe cómo se reparte ni dónde se guarda y, precisamente porque no lo sabe, el día que cambie el reparto solo se tocará este fichero. Sobre cómo encadenar estas piezas con el agente sin que se salte las capas lo hablaremos en otro post sobre el desarrollo.

Lenguaje natural: una capa más, no una amenaza

Mucha gente todavía se resiste a esto y casi siempre por el mismo motivo: prefieren ver y tocar el código con sus propias manos. Lo entiendo, yo también vengo de ahí, pero creo que es mirar el cambio por el lado equivocado.

Al principio, programábamos directamente sobre el procesador, en ensamblador. Luego llegaron los lenguajes de alto nivel y dejamos de pelearnos con los registros y la memoria. Cada salto escondía la capa de abajo para que pudiéramos pensar más arriba. Dirigir a un agente en lenguaje natural es el siguiente peldaño de esa misma escalera: una capa más de abstracción, no la desaparición del oficio.

Y como en cada salto anterior, seguimos necesitando saber qué estamos construyendo. Quien programaba en C entendía lo que pasaba por debajo; quien dirige a un agente tiene que entender la arquitectura que quiere o el agente se lo inventará.

En la app de gastos, si no tenemos claro que el reparto vive en el dominio y no en la pantalla, el agente decide y decide mal, no por torpe, sino porque nadie le dijo dónde iba cada cosa.

La abstracción te libera del teclado, no del criterio.

Conclusión

La invitación es directa: pruébalo en serio, y hazlo bien. No se lo pidas todo de una y espera que lo haga bien. Defínelo, pártelo en piezas, deja que te entreviste y dirígelo tú. La velocidad llega sola cuando la base está cerrada.

En los próximos artículos de esta serie veremos exactamente cómo cerrarla fase por fase: el contexto que el agente lee antes de tocar nada, las skills con las que le enseñas a hacer las cosas a tu manera, las specs que parten el trabajo y, por fin, el desarrollo.

Si te quedas con una sola frase de todo esto, que sea esta: con un agente, el cuello de botella ya no es escribir el código, es saber con exactitud qué quieres.

La app de gastos compartidos que vamos a ir construyendo a lo largo de la serie no se va a caer por culpa del modelo, se sostendrá por lo que cerremos antes de que escriba la primera línea. Y esa parte, por suerte, depende entera de nosotros/as.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.