Domingo por la mañana. Acababa de descubrir la música de Fred Again y, mientras la escuchaba, pensé: «me gusta, es creativa, tiene algo nuevo». Me dejó inspirado, con ganas de innovar en mis propios agentes. Una búsqueda rápida entre mis notas, una lluvia de ideas y, en lo que tardé en leer un artículo y escuchar medio álbum, llegué a una pregunta: ¿por qué el segundo agente siempre acaba haciendo lo mismo: evaluar?
La industria lleva un año refinando el mismo loop. Generator-Verifier, Maker-Checker, Actor-Critic, Reflection Loop. Nombres diferentes, misma idea: un agente genera, otro evalúa, el primero corrige. Microsoft lo llama Maker-Checker en su Azure Architecture. La comunidad de RL lo llama Generator-Verifier. ASDLC.io lo formalizó como Adversarial Code Review con Context Gates. Todo muy bonito, muy documentado, muy estándar.
Pero todos estos patrones comparten un mismo supuesto: el segundo agente es un evaluador. Lee el código, lo compara con el spec, y da su opinión. Es un juez.
¿Y si en vez de un juez pones a alguien cuyo único trabajo es destruir lo que el primero construyó?
Lo llamo el patrón Angel-Devil. El Angel (builder) construye. El Devil (breaker) intenta romper lo que el Angel construyó. Lo que sobrevive es lo que publicas. Quería saber si esa diferencia, que suena obvia, produce resultados realmente distintos a los del juez clásico.
El experimento
Construí un harness mínimo en TypeScript. Tres agentes, mismo modelo (Kimi K2.7 Code), mismo spec, mismos parámetros de sampling por defecto. La única variable es el system prompt:
- Builder: recibe un spec, genera la implementación.
- Judge: recibe el spec + el código, evalúa si cumple los requisitos. Produce un veredicto (APPROVED / NEEDS_CHANGES / BLOCKED) con evidencia.
- Breaker: recibe el spec + el código, construye inputs concretos diseñados para hacer que el código crashee, devuelva resultados incorrectos o viole el spec. Cada ataque debe citar la causa raíz en el código.
Lo corrí contra tres tareas: un generador de formularios dinámicos (vanilla HTML/JS), una función de deep merge, y un router de URLs. Elegí cada tarea porque tiene edge cases no obvios donde “parece correcto” y “es correcto” divergen.
El judge y el breaker nunca ven el output del otro. Trabajan de forma independiente sobre el mismo código. Yo soy el evaluador final.
Una advertencia antes de seguir: esto es un experimento exploratorio, no un benchmark. Una sola corrida por tarea, un solo modelo, sin fijar seed ni repetir. Los resultados no son deterministas y no pretendo que lo sean. Lo que busco no es una métrica, es ver si el breaker y el judge razonan distinto sobre el mismo código.
Qué pasó
Formulario dinámico
El builder generó un componente de formulario que renderiza campos desde un JSON config, maneja visibilidad condicional y valida al enviar. Código limpio, bien estructurado.
El judge encontró 3 issues (2 bugs reales + 1 nit estilístico). Veredicto: NEEDS_CHANGES.
El breaker encontró 5 ataques (5 bugs reales, 0 relleno). Tres de ellos fueron invisibles para el judge:
- Un campo select sin
optionscrashea conTypeErroral renderizar - Los errores de validación persisten visualmente cuando un campo pasa a disabled
- Igualdad estricta en
visibleWhenhace que"42" === 42falle silenciosamente
Deep Merge
Aquí se puso interesante. El judge marcó “profundidad ilimitada” como PASS, argumentando “la profundidad está limitada solo por el stack de JS”. El breaker generó un objeto anidado de 200k niveles y demostró que crashea con RangeError: Maximum call stack size exceeded. El judge aprobó literalmente, y con evidencia citada, una feature que falla.
Con los arrays el contraste es más sutil. El judge marcó “los arrays reemplazan arrays del target” como PASS —y es verdad, los reemplazan— pero no notó que el reemplazo devuelve el array del source por referencia, así que mutar el output muta el input. Para ser justos: el judge sí detectó el problema de aliasing por referencia en general (lo marcó como FAIL en el criterio “devuelve un objeto nuevo”); simplemente no lo conectó con el criterio de arrays. El breaker fue directo al caso concreto.
Router de URLs
El resultado más equilibrado, y el que más matiza la tesis. Aquí judge y breaker convergieron en lo grave: los dos encontraron los crashes por percent-encoding malformado y el bug del nombre de param que queda stale al re-registrar una ruta. La diferencia fue marginal: el judge encontró un issue que el breaker no vio (query strings no stripeados del path), y el breaker encontró uno que el judge aprobó como PASS (registrar una ruta con un handler falsy —null, 0, ""— la hace inmatcheable, porque el código usa if (node.handler)). En esta tarea, un breaker no te habría dado mucho más que un buen judge.
El patrón
En los tres experimentos, algo fue consistente: el breaker encontró bugs que el judge aprobó explícitamente como correctos.
No se trata de encontrar más bugs. El judge encuentra issues reales también. La diferencia está en el tipo de razonamiento:
- El judge pregunta: “¿este código cumple cada requisito?” Lee el código, traza la lógica y evalúa.
- El breaker pregunta: “¿qué input haría fallar este código?” Imagina escenarios adversariales y traza qué pasaría realmente.
Son operaciones cognitivas diferentes. El judge detecta incumplimiento del spec. El breaker detecta los bugs que llegan a producción.
Trabajo previo
Este experimento no existe en vacío. Hay trabajo relevante que vale la pena mencionar:
ASDLC.io define el patrón “Adversarial Code Review” como práctica formal con Context Gates en tres capas. Es un framework teórico y metodológico sólido, pero no incluye la comparación empírica que hice aquí.
Refute-or-Promote (arXiv, abril 2026) es probablemente el paper más cercano. Proponen un sistema multi-agente adversarial con kill mandates y validación empírica para descubrimiento de defectos. Va en la dirección opuesta a la mía —ellos matan falsos positivos (bugs reportados que no existen), yo persigo falsos negativos (bugs reales que el judge aprueba)— pero valida el principio de fondo: el test empírico vence a la opinión, aunque sea unánime. Su ejemplo más instructivo: diez reviewers endosaron por unanimidad una vulnerabilidad inexistente en OpenSSL, y solo la mató un único test empírico.
adversarial-review (GitHub) usa Claude + GPT Codex en debate adversarial multi-ronda, pero son dos judges debatiendo, no un judge contra un breaker.
RedCoder (arXiv, julio 2025) es un framework de red-teaming para Code LLMs con attacker, defender y evaluator, pero su foco es hacer que el modelo genere código vulnerable, no evaluar código ya generado.
Mi contribución es la comparación directa y controlada: mismo modelo, mismo código, judge vs breaker, datos concretos. Nadie ha publicado eso.
Qué significa para la arquitectura de agentes
Generator-Verifier, Maker-Checker, Actor-Critic. Todos ponen un evaluador después del generador. El evaluador lee, opina, sugiere. Es el Angel hablando con otro Angel.
El patrón Angel-Devil cambia la dinámica. No es “genera y evalúa”. Es “genera e intenta destruir”. El Devil no sugiere mejoras. Fabrica inputs maliciosos y te dice exactamente qué línea de tu código explota. Es un tipo de feedback fundamentalmente diferente: más difícil de ignorar porque viene con un test case concreto que falla, no con una opinión.
La implicación práctica es simple: si estás construyendo coding agents para producción, un Angel solo no basta. Necesitas un Devil integrado en el loop de generación. No como un paso de QA separado, sino como parte del ciclo: construir, atacar, sobrevivir, publicar.
El código
El harness entero son unas 100 líneas de TypeScript. Tres system prompts, un orquestador secuencial, y un archivo spec por tarea. Sin frameworks, sin dependencias más allá del OpenAI SDK. Añadir una tarea nueva es crear un archivo markdown.
Todo es open source: github.com/aleksandarlabs/adversarial-harness
Qué sigue
Este fue un experimento de análisis estático. Ni el judge ni el breaker ejecutaron código. La siguiente versión añade ejecución sandboxed con E2B para que los ataques del breaker se verifiquen automáticamente contra el output del builder, produciendo evidencia pass/fail en lugar de análisis hipotético.
También me interesa la dinámica de iteración. ¿El builder produce mejores correcciones cuando recibe “tu código crashea con este input” en vez de “tu código no cumple completamente el requisito #3”? Ese es el siguiente experimento.
Pero la pregunta que realmente me quita el sueño es otra: ¿y si no es Angel o Devil, sino los tres? El Angel construye, el Devil ataca, el Judge evalúa la evidencia de ambos. Tres perspectivas sobre el mismo código, tres tipos de feedback, un loop que combina cumplimiento del spec con robustez adversarial. Eso ya no es un patrón de review. Es un sistema inmunológico para código.
Lo sé, me he pasado un poco con lo último. Pero la idea no.