Why I split code generation into 4 stages instead of one prompt
Pasé seis meses iterando "el prompt". Variantes, ejemplos few-shot, instrucciones más estrictas, instrucciones más laxas, plantillas distintas, formats distintos, tonos distintos. El día que lo partí en cuatro stages con un contrato entre cada uno, los failures dejaron de ser no-determinísticos y se volvieron localizables. Esa es la diferencia que importa.
No es que la calidad subiera mágicamente. Subió, sí, pero lo que cambió de verdad fue la capacidad de debug. Cuando algo fallaba, podía decir exactamente en qué stage falló y por qué. Eso vuelve el problema tratable. Y un problema tratable es uno que un equipo puede resolver iterativamente, en vez de uno que se vuelve mito.
Síntomas del prompt único
El síntoma más visible es que el prompt falla en lugares distintos cada vez. Un día el output no tiene tipos. Otro día tiene tipos pero no tiene manejo de errores. Otro día tiene ambos pero la estructura de carpetas está mal. Otro día está todo bien excepto que no hay tests. No hay patrón estable — y sin patrón, no hay forma de iterar el prompt para fixearlo.
El segundo síntoma es que es imposible reproducir un bug específico. Corres el mismo prompt diez veces y obtienes diez outputs distintos, ninguno igual al que reportó el usuario. Quien viene del mundo del software determinístico tarda en aceptar esto, pero es la realidad operacional: la inferencia es estocástica por diseño, y aunque uno fije seed y temperatura, hay variabilidad que sigue ahí. El bug que reportó el usuario es una observación, no un test reproducible.
El tercero es el peor: cuando el output está 80% bien, no sabes qué pieza regenerar. La opción que queda es regenerar todo. Pagar otra vez el costo completo de inferencia porque el último 20% no cuadró. Eso vuelve cualquier loop de iteración carísimo, especialmente cuando el output completo es código largo con muchas dependencias internas.
Hay un cuarto síntoma más sutil que sólo se nota después de meses operando así: el equipo deja de confiar en el sistema. Cuando los failures son impredecibles, las personas dejan de invertir en mejorar el prompt porque no tienen forma de saber si su cambio ayudó o si simplemente tuvieron suerte. Se llega a un equilibrio donde nadie quiere tocar el prompt — exactamente la actitud que se tiene con una god-class que nadie entiende del todo. Y esa actitud, una vez instalada, es difícil de revertir aunque después aparezca evidencia de que valdría la pena intentarlo.
El resultado neto es que el sistema se vuelve frágil de una manera particular: funciona aceptablemente bien la mayoría del tiempo, pero nadie lo entiende lo suficiente para mejorarlo. Eso es deuda técnica disfrazada de estabilidad.
Los 4 stages, concretos
Los stages son: skeleton(), contracts_and_wiring(), logic(), validate() + digest(). Cada uno tiene una responsabilidad acotada y un output tipado. La selección de exactamente cuatro no es arbitraria — surge de observar que esos son los puntos naturales de corte donde un stage produce algo verificable que el siguiente necesita.
skeleton() produce la estructura de carpetas y las dependencias declaradas. Es la decisión arquitectónica más básica: dónde vive cada cosa, qué librerías se importan, qué convenciones de naming se usan. Sale rápido y barato. Si esto falla — y a veces falla, por ejemplo cuando el arquetipo está mal especificado — no tiene sentido pagar por los stages siguientes. Cortar acá es cheap.
contracts_and_wiring() define las interfaces, los puertos, el contenedor de dependencias. Es donde se materializa la arquitectura hexagonal en el Demo #07 del canal de YouTube de RLABS — el note-api en FastAPI que está en examples/note-api del repo agentguard-demo. Acá se decide qué habla con qué, y bajo qué contrato. Si el stage anterior dejó bien dibujada la estructura, este stage tiene un trabajo bien delimitado: traducir esa estructura a interfaces concretas.
logic() implementa los casos de uso. Es el stage más pesado en tokens porque es donde está el código real. Pero llega con todo el andamiaje resuelto: sabe qué interfaces tiene que satisfacer, qué dependencias inyectar, qué tipos respetar. Eso reduce el espacio de decisiones que tiene que tomar en el momento de generar — el modelo no está improvisando arquitectura mientras escribe lógica, dos cosas que cuando se mezclan se rompen mutuamente.
validate() + digest() corre auto-challenge contra el rúbrico. No es una validación syntáctica — es una evaluación dimensional contra los 13 ejes del arquetipo. El digest produce el reporte que el usuario lee, con scores por dimensión y señales concretas de qué archivo bajó cuál nota. Ese reporte es lo que cierra el loop: el siguiente turn del agente puede leerlo y editar específicamente lo que está bajo.
Por qué importa el contrato entre stages
Cada stage recibe el output del anterior como input tipado, no como prosa libre. Esto es lo que cambia el comportamiento del sistema entero. El modelo no tiene que inferir qué hizo el stage previo — lo lee de una estructura concreta. Inferir es donde el modelo improvisa; leer es donde el modelo se ajusta. Convertir inferencia en lectura es uno de los movimientos más rentables que existen en diseño de pipelines de generación.
Si skeleton() falla, el pipeline corta ahí. No llegamos a logic(). La regla operacional es "cut early": cualquier stage que no produce output válido detiene la cadena. Esto es exactamente lo opuesto a "deja que el modelo intente y veamos qué sale" — es la disciplina de un compilador, no la de un chat. La pérdida de flexibilidad es real, pero la ganancia de predictibilidad la compensa con creces.
La ventaja más concreta es que puedes re-correr un solo stage sin regenerar todo. Si logic() falló y skeleton() y contracts_and_wiring() están bien, regeneras sólo logic(). El costo de iteración baja drásticamente. Esto es lo que hace económicamente viable la mejora continua sobre prompts complejos: cada iteración paga sólo por el stage afectado, no por el pipeline completo.
Hay una propiedad adicional menos obvia: los stages se vuelven cacheables. Si dos prompts producen el mismo skeleton, puedes reutilizarlo. Si la modificación al input afecta sólo el último stage, todo lo anterior se reusa. Esto abre patrones de eficiencia que con el prompt único no existían — porque no había contratos estables intermedios sobre los cuales cachear.
Tracing por stage
Cada stage tiene input/output tokens reportados. Hay costo acumulado contra budget. Los logs son estructurados, no prosa de debug. Esto vuelve el pipeline debuggable después del hecho — algo que con el prompt único era esencialmente imposible, porque toda la generación era una sola caja negra.
Cuando un usuario reporta "esto generó código raro", puedo ir al trace, ver en qué stage el output empezó a divergir, ver el input exacto que recibió ese stage, y reproducirlo localmente. Es la diferencia entre "funciona en mi máquina" y un postmortem real. El equipo deja de discutir hipótesis y empieza a discutir evidencia.
El tracing también permite identificar patrones a través de muchas ejecuciones. Si el stage contracts_and_wiring() falla más para un arquetipo específico, eso es señal de que la especificación del arquetipo está mal — no de que el modelo esté roto. Esos patrones no son visibles cuando cada generación es una observación aislada.
El protocolo de medición está documentado en paper §3. El número agregado de overhead — ~374 tokens promedio por respuesta — está en paper §4.
El costo real de partir el prompt
Hay que ser honesto: partir el prompt no es gratis. Hay más latencia porque los stages se serializan — uno tiene que terminar para que arranque el siguiente. En el prompt único el modelo paralelizaba internamente lo que podía. Acá no, porque la entrada de cada stage depende de la salida del anterior. Para casos donde la latencia importa más que la auditabilidad, el prompt único puede seguir siendo la opción correcta.
Hay más complejidad operacional. Recovery por stage, retries por stage, observabilidad por stage. El sistema deja de ser "un prompt y un parser" y se vuelve un orquestador con state machine. Si tu equipo no está listo para operar un orquestador, esta arquitectura te va a doler. Y "estar listo" no es sólo conocimiento — es appetito por la complejidad adicional, que no todos los equipos tienen ni deben tener.
Y hay más overhead de tokens. ~374 tokens promedio, según la medición del paper 001. No es enorme — pero es real, y hay que defenderlo contra alguien que va a preguntar "¿por qué este flow gasta 30% más?". La respuesta es que evita regenerar el 100% cuando algo falla, pero hay que tener los números a mano para que la respuesta no sea defensa retórica.
Transferable Principle
Si tu prompt falla en lugares distintos cada vez, el problema casi nunca es el modelo — es el alcance del prompt. Estás pidiéndole que resuelva cuatro problemas distintos en un solo paso, y el modelo está fallando en uno distinto cada vez según el ruido de la inferencia. La conclusión natural "el modelo no es suficientemente bueno" es la equivocada; la correcta es "le estoy pidiendo demasiado de una vez".
Partirlo en stages con un contrato entre cada uno es el equivalente, en mundo IA, de descomponer una god-class. La operación es la misma: identificar las responsabilidades, separar las interfaces, hacer explícitos los contratos que antes vivían en la cabeza del autor. Las décadas de práctica en ingeniería de software sobre cómo descomponer responsabilidades aplican directamente — sólo cambia el sujeto, de "el código" a "el prompt".
Cada stage que aíslas se vuelve testeable, cacheable y reemplazable. Esas tres propiedades son las que vuelven el sistema sostenible a doce meses, no a dos semanas. Y la diferencia entre dos semanas y doce meses es exactamente la diferencia entre un prototipo que impresionó al CTO y un sistema que el equipo puede mantener.
Pregunta para comentarios: ¿cuál es el prompt en tu equipo que sigue fallando de formas distintas — y qué pasaría si lo partieras en 3?
#AI #GenAI #Engineering #Architecture #TechLeadership