5 defensas contra las alucinaciones de código (y por qué solo 3 funcionan)

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

Co-Fundador de KeepCoding

La semana pasada conté cómo mi IA inventó una estructura JSON completa y la envolvió en DTOs, fixtures y tests que pasaban. 90 tests en verde. Todo mentira.

Ese post era el diagnóstico. Este es el tratamiento.

Después de descubrir el desastre, hice lo que hace cualquier ingeniero con orgullo herido: investigar obsesivamente durante días para que no vuelva a pasar. Leí papers, probé herramientas, analicé datos reales de mis APIs, y construí un sistema de defensas para mi app.

Lo que encontré me sorprendió. De las 5 medidas reactivas que identifiqué, solo 3 funcionan de verdad. Las otras dos son, como mínimo, teatro con buenas intenciones.

El modelo mental: tú contra la IA (literalmente)

Antes de entrar en las medidas, necesitas entender el marco. Y la mejor analogía que encontré viene del deep learning.

En una GAN (Generative Adversarial Network) hay dos redes neuronales que compiten:

  • El generador produce contenido (imágenes, texto, lo que sea)
  • El discriminador intenta detectar si el contenido es real o falso

El sistema mejora porque ambos se empujan mutuamente. El generador aprende a engañar mejor. El discriminador aprende a detectar mejor.

Cuando programas con un LLM, estás en una GAN involuntaria:

  • El LLM es el generador. Produce código, DTOs, tests, fixtures.
  • Tú eres el discriminador. Debes detectar qué es real y qué inventado.

Pero hay una asimetría brutal: el generador es incansable y tú te cansas. El LLM puede generar 50 ficheros sin despeinarse. Tú revisas 10, te fatigas, y el fichero 11 pasa sin que lo mires.

Es la misma fatiga de autorización que conté con 1Password pidiendo Touch ID 47 veces al día. La seguridad que depende de que un humano esté permanentemente alerta es seguridad de cartón.

Lo que el discriminador debe vigilar

No puedes (ni debes) revisar cada línea. Lo que tienes que vigilar son las fronteras — donde tu código toca el mundo exterior:

Frontera Pregunta clave
APIs externas ¿Los campos del DTO existen en la API real?
Paquetes ¿La dependencia existe y se llama así?
Schemas de BD ¿La tabla tiene realmente esas columnas?
URLs/endpoints ¿El endpoint existe y responde lo que esperamos?

Regla: todo lo que el LLM declara sobre el mundo exterior es sospechoso hasta verificación. Que lo diga con confianza no es evidencia. Anthropic lo reconoce en su propia documentación:

«Claude can sometimes generate responses that contain fabricated information… presented in a confident, authoritative manner.»

Un LLM que dice «estoy seguro» y uno que dice «creo que» tienen exactamente la misma probabilidad de estar equivocados.

Automatizar al discriminador

El objetivo final es dejar de depender de tu disciplina y automatizar la verificación:

ANTES:
  LLM genera → Tú revisas (a veces) → Merge

DESPUÉS:
  LLM genera → CI verifica contra datos reales → Tú revisas discrepancias → Merge

Las 5 medidas que vienen son formas de automatizar partes de ese rol de discriminador. Algunas funcionan. Otras no tanto.

Los datos duros (para los escépticos)

Antes de que pienses «a mí no me pasa», aquí van números de estudios reales:

  • 21.7% de los paquetes recomendados por LLMs open-source son inventados. En modelos comerciales baja al 5.2%, que sigue siendo un paquete de cada 20.
  • GPT-4o solo logra un 38.58% de invocaciones válidas para APIs poco frecuentes. Menos del 40%. Lanza una moneda al aire.
  • Los mejores métodos actuales para localizar alucinaciones en código logran un 22-33% de precisión. Dicho en cristiano: detectamos una de cada cuatro.
  • Un investigador subió un paquete vacío con un nombre que los LLMs alucinaban frecuentemente. 30.000 descargas en 3 meses. Lo llaman slopsquatting.

Y hay una taxonomía formal. El paper CodeHalu (AAAI 2025) define 4 categorías de alucinaciones de código:

Categoría Qué es Ejemplo real
Mapping Campos mapeados incorrectamente Confundir user_id con account_id
Naming Nombres inventados response.quota.percentage cuando es response.utilization
Resource Recursos que no existen Campo active_flags en una API que no lo tiene
Logic Lógica plausible pero incorrecta isPaid = !activeFlags.isEmpty con un campo siempre vacío

Mi caso fue un Resource que derivó en un Logic. El campo no existía, y la lógica que dependía de él parecía perfecta. Ficción coherente de libro.

Medida 1: Contract testing contra APIs reales

La idea

Definir un «contrato» de lo que la API devuelve y verificar automáticamente que tu código es compatible. Si tu DTO tiene campos que el contrato no define: alarma.

Cómo funciona

Imagina que tienes un DTO así:

struct OrganizationInfo: Decodable {
    let uuid: String
    let name: String
    let activeFlags: [String]  // ← ¿Existe esto de verdad?
}

Un contract test coge la respuesta real de la API, extrae las keys del JSON, y las compara con las CodingKeys de tu DTO. Si tu DTO tiene un campo que la API no devuelve, es un PHANTOM — un campo fantasma, posiblemente inventado.

Keys en la API real:  {uuid, name, capabilities, billing_type}
Keys en el DTO:       {uuid, name, activeFlags}

PHANTOM: activeFlags  ← En el DTO pero NO en la API. ¿Alucinado?
UNCONSUMED: capabilities, billing_type ← En la API pero no en el DTO.

Pros

  • Determinista. No depende de otro LLM ni de tu olfato. Si el campo no está en la API, salta.
  • Elimina phantom fields por construcción. Es imposible que un campo inventado pase.
  • Automatizable en CI. Lo ejecutas en cada push.

Contras

  • Necesitas la spec de la API. Si la API no tiene OpenAPI spec (como la de Claude), tienes que capturar respuestas manualmente.
  • No detecta naming incorrecto. Si el campo existe pero se llama diferente (active_flags vs capabilities), no lo pilla automáticamente.
  • Requiere credenciales. Para capturar la respuesta real necesitas una sesión válida.

Herramientas por stack

Stack Herramienta Enfoque
Python Pydantic extra='forbid' Rechaza campos JSON no declarados en el modelo
TypeScript Zod .strict() Mismo concepto, rechaza extras
Swift Decoder custom o comparación manual de keys Codable ignora claves desconocidas por defecto
Dart json_serializable + disallowUnrecognizedKeys Rechaza campos no declarados
Agnóstico oasdiff, Specmatic Comparan specs OpenAPI

Lo que implementé

En mi app (Swift/SPM) no hay OpenAPI spec de la API de Claude. Así que construí una validación bidireccional a mano:

  1. make capture descarga respuestas reales de todas las APIs y las guarda como fixtures en Fixtures/real/
  2. SchemaValidationTests compara las CodingKeys.allCases de cada DTO contra las keys del fixture real
  3. Si hay discrepancia → PHANTOM (campo en el DTO pero no en la API) o UNCONSUMED (campo en la API que no consumimos)
$ make doctor
✅ OrganizationInfo: 4 common, 0 phantom, 8 unconsumed
✅ UsageResponse: 9 common, 0 phantom, 1 unconsumed
⚠️  StatsCache: PHANTOM field 'totalSpeculationTimeSaved' — not in real data

Los campos intencionalmente no consumidos van en una allowlist documentada con la razón. Si mañana aparece un campo nuevo en la API, el test falla con UNCONSUMED y me entero.

Veredicto: la medida más importante. Si solo implementas una, que sea esta.

Medida 2: Fixture validation (fixtures reales, no inventados)

La idea

Los fixtures de test deben venir de datos reales capturados, no escritos a mano por el LLM. Si el LLM genera el fixture, estás validando ficción contra ficción.

El problema que resuelve

George Tsiokos lo clavó en un post de febrero de 2025:

«Los tests no validan que el software cumple necesidades de negocio — simplemente confirman que el código hace exactamente lo que fue escrito para hacer, incluyendo bugs.»

Cuando el LLM genera el código Y los tests Y los fixtures:

LLM inventa campo → LLM escribe fixture con ese campo → LLM escribe test
→ Test pasa ✅ → Nadie verificó contra la realidad ❌

La solución: record-replay

Los frameworks record-replay graban respuestas HTTP reales y las reproducen en tests. No hay posibilidad de invención porque el fixture viene de la API, no del modelo.

Stack Herramienta
Python VCR.py, pytest-recording
TypeScript Polly.js (Netflix), MSW
Swift Replay (mattt)
Agnóstico Hoverfly

Pros

  • Imposible inventar. El fixture viene de la red, no del modelo.
  • Incluye metadatos. URL, timestamp, status code. Puedes trazar de dónde vino.
  • Se commitea al repo. Los reviewers ven exactamente qué devolvió la API.

Contras

  • El fixture envejece. Si la API cambia, el fixture capturado ya no es representativo.
  • Credenciales en CI. Necesitas poder llamar a la API para grabar.
  • No escala a todas las variaciones. Capturas una respuesta, pero la API puede devolver muchas formas diferentes.

Lo que implementé

Dos capas de fixtures:

Tests/Fixtures/          ← Estáticos, escritos por el LLM
                           Usados para tests unitarios de decode
                           PUEDEN contener errores (eso es aceptable)

Tests/Fixtures/real/     ← Capturados por make capture
                           Con fichero .meta (timestamp de captura)
                           Fuente de verdad para validación de schema

Los fixtures estáticos son útiles para testear edge cases (JSON truncado, campos vacíos, formatos raros). Pero la validación de «¿estos campos existen de verdad?» la hace siempre el fixture real.

Cada fixture real tiene un fichero .meta con el timestamp de captura. Si un fixture tiene más de 30 días, ya sabes que toca renovar.

Veredicto: esencial como complemento del contract testing. Sola no basta (necesitas la comparación de la Medida 1), pero sin fixtures reales la Medida 1 no tiene contra qué comparar.

Medida 3: Smoke tests con datos reales (make doctor)

La idea

Antes de dar por bueno un cambio, hacer una llamada real a la API y verificar que tus DTOs parsean la respuesta sin pérdida silenciosa.

Cómo funciona

$ make doctor
Capturando /api/organizations... OK (2 orgs)
Capturando /api/organizations/{id}/usage... OK (9 windows)
Capturando ~/.claude/stats-cache.json... OK (115 sessions)
Capturando session JSONL... OK (847 entries)

Validando schemas...
✅ OrganizationInfo: OK
✅ UsageResponse: OK
✅ StatsCache: OK
✅ SessionEntry: OK

0 phantom fields, 0 new unconsumed fields

Es make capture + make test en un solo paso. Captura datos frescos de producción y los cruza contra los DTOs.

Pros

  • La defensa más honesta. Datos reales, comparación directa, resultado inequívoco.
  • Rápido. 30 segundos en local.
  • Detecta drift. Si la API añade o quita campos, lo sabes inmediatamente.

Contras

  • Requiere sesión activa. Necesitas estar logueado para capturar.
  • No va en CI (en mi caso). La API de Claude no tiene credenciales de servicio, solo cookies de sesión.
  • Es manual. Depende de que te acuerdes de ejecutarlo.

Lo que implementé

make doctor es el comando más importante de mi proyecto. Lo ejecuto:

  • Después de cada cambio en DTOs
  • Una vez a la semana como rutina
  • Cuando algo «huele raro» en la app

Para las APIs que no puedo llamar en CI, el truco es guardar el resultado del doctor como fixture real que sí va al repo. El CI valida contra ese fixture. No es tiempo real, pero es mejor que nada.

Además, el sistema emite señales tempranas en runtime: si el SessionFileReader lee líneas de tipo assistant sin campo usage, logea un .notice. Si el SessionTokenService lee ficheros pero encuentra 0 entries nuevas, también. La idea es que la app avise si el formato cambió, aunque no crashee (porque la graceful degradation puede ocultar el problema).

Veredicto: la medida más práctica. Bajo coste, alto valor. Si tienes 30 segundos, tienes make doctor.

Medida 4: Anomaly detection en parseo (campos siempre null)

La idea

Monitorizar en runtime qué campos de tus modelos se populan con datos reales y cuáles son siempre nil. Un campo que lleva 50 parseos seguidos siendo nil es sospechoso de ser inventado.

El modelo mental

GraphQL tiene esto resuelto. Herramientas como Apollo GraphOS reportan uso por campo: cuántas veces se solicitó, cuántas devolvió datos, primera y última vez que se usó. Los campos con 0% de uso se marcan para eliminar.

Para REST, no existe equivalente. Tienes que construirlo tú.

Pros

  • Detecta en producción. No necesitas capturar manualmente; el propio uso de la app genera los datos.
  • Complementa las otras medidas. Un campo que pasa el contract test (existe en la API) pero siempre es null en la práctica sigue siendo sospechoso.

Contras

  • Necesitas volumen. Con 5 llamadas no puedes concluir nada. Necesitas cientos.
  • Falsos positivos. Un campo puede ser legítimamente null el 95% del tiempo (ej. seven_day_opus: null en mi API es normal si no usas Opus esa semana).
  • Implementación manual. No hay herramienta que enchufes. Tienes que escribir el monitor.
  • En apps cliente, sin APM. En un backend con Datadog o Sentry, emites métricas custom. En una app macOS de menu bar, estás solo.

Lo que implementé

Parcialmente. No tengo un monitor formal de campos-siempre-nil, pero sí tengo las señales tempranas en los logs:

// SessionTokenService.swift
if totalFilesRead > 0 && totalNewEntries == 0 {
    logger.notice("read \(totalFilesRead) files but 0 new entries — possible format change")
}

Es la versión low-tech de anomaly detection. No cuenta por campo, pero detecta el caso gordo: «estoy leyendo datos pero no sale nada útil».

Veredicto: útil como señal de alerta, pero no como defensa primaria. Es un canario en la mina, no un muro.

Medida 5: Diff semántico post-generación (LLM-as-Judge)

La idea

Usar un segundo LLM (o el mismo con un prompt diferente) para auditar el código generado, buscando campos o estructuras que no puede verificar contra documentación conocida.

El estado del arte

Hay herramientas serias trabajando en esto:

Herramienta Qué hace
VERDICT (Haize Labs) Pipeline modular: verificación + debate + agregación
DeepEval Framework tipo pytest con HallucinationMetric
Patronus Lynx Modelo SOTA detección alucinaciones, open-source
Vectara HHEM Modelo + API, reduce alucinaciones a ~0.9% en enterprise

Y la opción casera: pedir a GPT-4o que genere DTOs para la misma API sin ver tu código, y comparar:

Claude dice:  activeFlags: [String]
GPT-4o dice:  capabilities: [String]
→ DISCREPANCIA: al menos uno alucina. Verificar contra API real.

Pros

  • Escala sin esfuerzo manual. Lo pones en CI y se ejecuta solo.
  • Detecta patrones sutiles. Un segundo modelo puede notar cosas que tú no.

Contras

Y aquí es donde la cosa se pone fea.

  • El juez puede alucinar también. Si el segundo LLM no conoce la API, puede «confirmar» campos inventados.
  • Alucinaciones sistemáticas. Si ambos modelos fueron entrenados con datos similares, pueden compartir la misma invención. SelfCheckGPT (Cambridge, EMNLP 2023) demostró que la consistencia multi-muestra no detecta alucinaciones sistemáticas.
  • Precisión deplorable. Collu-Bench: los mejores métodos logran 22-33% de precisión localizando alucinaciones de código. Detectas una de cada cuatro. Eso no es una defensa, es un sorteo.
  • Coste. Cada capa multiplica llamadas a LLM. Estás pagando por un detector que acierta un tercio del tiempo.
  • Sesgo de posición. Los LLMs jueces prefieren respuestas más largas y las que aparecen primero. No juzgan; tienen preferencias estéticas.

Evidently AI lo resumió con una pregunta demoledora:

«¿Cómo monitorizas un sistema que ocasionalmente alucina con otro sistema que ocasionalmente alucina?»

Lo que implementé

Nada. Cero.

Y es una decisión consciente. Las medidas deterministas (1, 2 y 3) me dan una detección fiable, reproducible, sin falsos positivos ni costes por llamada. Poner un LLM a vigilar a otro LLM es como poner a un becario a supervisar a otro becario. Mejor poner una cámara.

Veredicto: investigación interesante, producción prematura. Cuando la precisión pase del 33% al 90%, hablamos. Hoy, es teatro con presupuesto de I+D.

El marcador final

Medida Fiabilidad Coste ¿Implementada? ¿Por qué?
1. Contract testing Alta Medio Detecta phantom fields mecánicamente
2. Fixture validation Alta Bajo Fixtures reales eliminan ficción-valida-ficción
3. Smoke tests (make doctor) Alta Bajo 30 segundos, máximo valor
4. Anomaly detection Media Bajo Parcial Señales en logs, no monitor formal
5. LLM-as-Judge Baja Alto No 22-33% precisión = sorteo

Las medidas 1, 2 y 3 forman un trípode. Cada una cubre un ángulo diferente:

  • Contract testing responde: «¿estos campos existen?»
  • Fixture validation responde: «¿estos datos son reales?»
  • Smoke tests responde: «¿esto funciona ahora mismo?»

Juntas, hacen que un campo inventado tenga que sobrevivir tres filtros independientes. No es imposible, pero es mucho más difícil que engañar un test unitario con un fixture inventado.

La regla de oro

Quiero terminar con la regla más importante que saqué de todo esto:

El sistema de verificación debe ser externo al generador.

Si el LLM genera: – El código → OK, es su trabajo – Los tests de lógica → OK, verifican comportamiento – Los fixtures → NO, deben venir de datos reales – Los schemas → NO, deben venir del spec de la API – La validación de que los datos son correctos → NO, la hace un sistema determinista

Es la separación de poderes aplicada al desarrollo. El que escribe la ley no puede ser el que la juzga. El que genera el código no puede ser el que verifica que es correcto.

Puedes tener 200 tests en verde y estar viviendo en Matrix. O puedes tener un make doctor que en 30 segundos te dice si tus datos son reales o ficción.

Yo prefiero la pastilla roja.


Serie completa: Este post es el cuarto capítulo de una serie involuntaria sobre fallos de IA en producción. Primero fue los 44 emails inventados (la IA que actúa sin permiso). Luego MEMORY.md (la IA que olvida). Después el silent failure (la IA que inventa y pasa los tests). Y ahora, las defensas. Cada fallo diferente, un denominador común: necesitamos sistemas mecánicos, no promesas de buen comportamiento.


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.