Adversarial Programming: cuando tu copiloto IA se inventa el API

| Última modificación: 4 de mayo de 2026 | Tiempo de Lectura: 7 minutos
Premios Blog KeepCoding 2025

Co-Fundador de KeepCoding

TL;DR: Tu IA va a inventar campos de API que suenan perfectos pero no existen. La solución no es rezar para que acierte: es descargar el schema real antes de escribir código, capturar respuestas reales como fixtures, y separar el fetch del procesamiento para poder testear sin la red. Adversarial programming: programa asumiendo que tu copiloto miente.


¿Alguna vez has escrito código contra una API y todo compilaba, los tests pasaban, la lógica tenía sentido… y al conectar con la API real, nada funcionaba?

Si programas solo, eso pasa cuando lees mal la documentación. Si programas con una IA, pasa porque la IA se ha inventado la documentación.

La alucinación que no parece alucinación

Estaba construyendo una CLI en Rust para interactuar con una API GraphQL. Le pedí a mi copiloto IA que implementara un filtro para ordenar resultados por prioridad. Me devolvió algo como esto:

query {
  issues(orderBy: { priority: ASC }) {
    nodes {
      id
      title
      priority
    }
  }
}

Limpio. Razonable. Exactamente lo que esperarías. Un solo problema: el campo orderBy de esa API no acepta priority como valor. El enum real se llama PaginatedOrder y tiene valores como createdAt y updatedAt. No priority.

¿Cómo lo descubrí? Cuando la API devolvió un error 400 que no tenía sentido. Tardé 20 minutos en entender que el problema no era mi código — era que el campo que estaba usando no existía.

El patrón es siempre el mismo

Esto no fue un caso aislado. Durante semanas de desarrollo, la IA alucinó repetidamente:

  • Campos de filtrado que no existenstate.id.or en vez de state.type.in. Suena lógico, pero la API usa un patrón completamente distinto.
  • Enums inventados — nombres de valores que suenan como deberían llamarse, pero que la API nunca definió.
  • Patrones de otro ecosistema — en un proyecto Rust, me sugirió usar fcntl.flock para file locking. Eso es Python. En Rust usas fs2::FileExt.

Cada error era plausible. Ninguno era estúpido. Un junior podría haber cometido exactamente los mismos errores leyendo la documentación por encima. Y eso es lo peligroso: no parecen alucinaciones. Parecen código razonable de alguien que conoce el dominio «más o menos».

Por qué los LLMs inventan APIs

En román paladino: el LLM no sabe qué campos tiene tu API. Ha visto miles de APIs GraphQL en su entrenamiento, y cuando le pides que use una, hace lo que haría un humano con buena intuición pero sin acceso a la documentación: adivina.

Y adivina bien. Casi siempre. Lo suficiente para que te fíes. Ese «casi» es el que te rompe el sprint.

Es como trabajar con un compañero brillante que nunca lee la documentación pero siempre tiene una respuesta convincente. Te dice «sí, el endpoint acepta un campo priority» con tanta seguridad que no lo compruebas. Y cuando falla en producción, descubres que se lo inventó.

La solución: adversarial programming

Después de la tercera alucinación en una semana, adopté un enfoque distinto. En vez de confiar y verificar después, empecé a desconfiar y verificar antes. Lo llamo adversarial programming: programa asumiendo que tu copiloto va a inventar cosas.

No es hostilidad. Es higiene.

1. Schema introspection antes de escribir código

Si trabajas con una API GraphQL, antes de pedirle nada a tu IA, descarga el schema real:

# Descargar el schema completo de la API
curl -s https://api.example.com/graphql \
  -H "Authorization: token-here" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ __schema { types { name fields { name type { name kind ofType { name } } } } } }"}' \
  > schema.json

Ahora tienes la verdad. Cuando la IA te diga «usa orderBy: { priority: ASC }«, puedes buscar en el schema y ver que priority no está en el enum de ordenación. Le pasas el fragmento relevante del schema y le dices «usa solo estos campos». Se acabaron las adivinanzas.

Para APIs REST, el equivalente es descargar el OpenAPI spec. Para cualquier API, el principio es el mismo: consigue la fuente de verdad antes de escribir una línea de código.

2. Fixtures reales, no inventadas

La segunda defensa es capturar respuestas reales de la API y guardarlas como fixtures de test:

# Capturar una respuesta real
curl -s https://api.example.com/graphql \
  -H "Authorization: token-here" \
  -d '{"query":"{ items(first: 5) { nodes { id title state { name } } } }"}' \
  > tests/fixtures/items_real.json

Ese JSON no lo ha generado ningún LLM. Viene de la API real. Tiene los campos reales, con los tipos reales, con los valores reales. Cuando escribas un parser, testealo contra esa fixture. Si tu DTO no puede deserializar la respuesta real, el test falla. Fin de la ficción.

El truco es disciplina: nunca dejes que la IA genere fixtures. Si lo hace, estás testeando la invención contra la invención. Un castillo de naipes perfecto que se derrumba al tocar la realidad.

3. Separar fetch de procesamiento

Esta es la pieza arquitectónica clave. Si tu código hace fetch + parse + transformación todo junto, no puedes testear el parsing sin la red. Y si no puedes testear sin la red, necesitas mocks. Y si la IA genera los mocks… estamos en el punto 2 otra vez.

La solución es separar en dos capas:

┌─────────────────────┐
│   Client (fetch)    │  ← Habla con la API real
│   Solo HTTP + JSON  │
└────────┬────────────┘
         │ JSON crudo
┌────────▼────────────┐
│   Processor         │  ← Parsea, transforma, formatea
│   Solo datos        │
└─────────────────────┘

El client es delgado: hace la petición HTTP y devuelve JSON crudo. El processor recibe ese JSON y lo transforma. Para testear el processor, le pasas las fixtures reales. No necesitas mock del HTTP. No necesitas la red. Y no le das oportunidad a la IA de inventar la forma del JSON, porque ya la tienes.

4. El checklist adversarial

Antes de aceptar código que interactúa con una API externa, pasa este checklist:

Pregunta Si la respuesta es no…
¿Tengo el schema/spec de la API en el proyecto? Descárgalo antes de seguir
¿Los campos que usa el código existen en el schema? Búscalos. Si no están, la IA los inventó
¿Las fixtures de test vienen de la API real? Captúralas. No dejes que la IA las genere
¿Puedo testear el parsing sin hacer HTTP? Separa fetch de procesamiento
¿Los tipos del lenguaje coinciden con los de la API? Compara tu struct/DTO con el schema

Cinco preguntas. Treinta segundos. Te ahorra horas de debugging fantasma.

El dogfooding brutal

Aquí viene la parte que duele. Mientras construía esta CLI, sufrí en carne propia exactamente los problemas que la herramienta pretendía resolver.

La CLI existía para simplificar la interacción con una API de gestión de tareas. Y durante el desarrollo, cada vez que necesitaba crear una tarea para trackear un bug… tenía que pelear con la API que estaba envolviendo. El dogfooding no fue una decisión — fue una condena.

Ejemplos concretos del infierno:

  • Escapado JSON roto. Para poner una descripción con comillas en una tarea, había que escapar a tres niveles: shell, JSON, GraphQL. Un paréntesis mal puesto y la API devolvía un error críptico. Tardé más en escapar correctamente la descripción de un bug que en arreglar el bug.
  • UUIDs para relaciones. ¿Quieres asignar una tarea a un proyecto? No puedes usar el nombre del proyecto. Necesitas su UUID. ¿Y cómo consigues el UUID? Con otra query. ¿Y el label? Otro UUID, otra query. Para crear una tarea con proyecto, label y estado, necesitaba 4 queries encadenadas.
  • 32 peticiones para crear dependencias. Quería crear 8 tareas con dependencias entre ellas. Cada dependencia requiere una mutación separada con los UUIDs de ambas tareas. 8 creates + 24 relaciones = 32 llamadas a la API para lo que debería ser un fichero YAML.

Cada uno de esos problemas alimentó directamente el diseño de la herramienta. El escapado roto → input por fichero, nunca inline. Los UUIDs → resolución automática por nombre. Las 32 peticiones → operaciones batch.

La evolución convergente del output

Y luego pasó algo curioso. Uno de los principios de diseño era que el output fuera compacto — pensado para que un LLM lo consumiera gastando pocos tokens. Diseñé desde cero un formato que eliminaba claves repetidas y usaba posición y delimitadores ligeros:

PROJ-42 [Backlog] backend — Refactorizar el parser de configuración (14d)
PROJ-43 [In Progress] api — Implementar rate limiting (3d, overdue!)

Unos 25 tokens por elemento, frente a los ~50 de JSONL. Sin claves repetidas ("state":, "labels":, "title":) porque el LLM entiende la estructura por posición.

Meses después, descubrí TOON (Token-Oriented Object Notation), un formato publicado en noviembre de 2025 que hace exactamente lo mismo: eliminar la redundancia de JSON para reducir el consumo de tokens cuando el consumidor es un LLM. TOON usa headers de schema y filas tabulares — diferente sintaxis, mismo principio.

No lo copié. No lo conocía. Evolución convergente: cuando dos equipos resuelven el mismo problema (JSON es demasiado verboso para LLMs), llegan a la misma solución (eliminar claves repetidas, usar posición). Es la misma razón por la que los delfines y los tiburones tienen la misma forma aunque uno es mamífero y el otro pez.

Que dos proyectos independientes lleguen a la misma conclusión es la mejor validación de que el problema es real.

Lo que cambió

Después de adoptar adversarial programming, el ratio de alucinaciones en código de API bajó drásticamente. No a cero — la IA sigue siendo una IA — pero los errores que sobreviven son errores de lógica, no de ficción. Errores normales. Errores de programador. No errores de «me inventé un campo que no existe y construí un castillo encima».

La diferencia clave es cuándo descubres el error:

Sin adversarial Con adversarial
Descubres en runtime Descubres antes de escribir código
Debug de 30 min buscando «por qué no funciona» El schema te dice «ese campo no existe»
La fixture inventada pasa el test La fixture real rompe el test
Tres capas de ficción encima de la ficción Una capa de realidad desde el principio

Tu turno

Si programas con un LLM y tocas APIs externas, empieza por esto:

  1. Descarga el schema de cada API externa que uses. Ponlo en el repo. Es tu fuente de verdad.
  2. Captura fixtures reales. Un curl y un > fixture.json. Diez segundos.
  3. Separa fetch de procesamiento. Que tu parser se pueda testear con un fichero, no con la red.
  4. Desconfía de los nombres plausibles. Si la IA te dice «usa orderBy.priority«, búscalo en el schema antes de usarlo.

No es paranoia. Es ingeniería. La IA es una herramienta extraordinaria, pero su modo de fallo más peligroso no es el error obvio — es el error que parece correcto. Y contra eso, la única defensa es la realidad.

Leña al mono.


Serie: Adversarial Programming


Read this article in English.

Noticias recientes del mundo tech


¡CONVOCATORIA ABIERTA!

Desarrollo de apps móviles ios & Android

Full Stack Bootcamp

Clases en Directo | Profesores en Activo | Temario 100% actualizado

Descárgate también el informe de tendencias en el mercado laboral 2026.

Fórmate con planes adaptados a tus objetivos y logra resultados en tiempo récord.
KeepCoding Bootcamps
Resumen de privacidad

Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.