Alucinaciones en código generado: cinco señales antes de perder una tarde debuggeando
Un asistente de IA que escribe código no distingue entre "esto existe" y "esto suena a que debería existir". Cuando le pedimos que use una librería que conoce a medias, produce la llamada más plausible según su entrenamiento, no la llamada real. El resultado compila mentalmente para el modelo —y a veces literalmente, si el lenguaje es dinámico— pero no existe en ningún paquete instalado.
Llamamos a esto alucinación de código: el modelo inventa una API, un método, un parámetro o un import con la misma confianza con la que escribe uno real. No es un fallo aislado de un proveedor concreto ni depende del tamaño del modelo; es una consecuencia de cómo funciona la generación de texto por probabilidad. Lo que sí cambia según el equipo es cuánto tiempo se tarda en detectarlo. Estas son las cinco señales que nos han ahorrado más horas de debugging, y el orden en el que las revisamos antes de dar por buena cualquier función generada.
1. Nombres de método plausibles pero falsos
La señal más común no es un error evidente, es un nombre que "encaja" con el resto de la librería. Si un cliente HTTP expone client.get(), client.post() y client.delete(), un modelo puede generar client.patchAsync() aunque esa librería concreta no siga esa convención de sufijos. El nombre no es absurdo, es coherente con el patrón que el modelo aprendió de otras librerías parecidas, que es precisamente lo que lo hace peligroso: pasa una lectura rápida.
La forma de cazarlo no es leer con más atención, es dejar que la herramienta correcta lo detecte por ti. Un editor con IntelliSense o autocompletado basado en los tipos reales del paquete instalado marca en rojo cualquier método que no exista en la definición de tipos, antes de que llegues a ejecutar nada. Si el editor no se queja pero el método tampoco aparece al escribir client. y pulsar autocompletar, es la primera alarma.
// El editor no ofrece 'patchAsync' en el autocompletado de 'client.'
// Eso ya es la señal, antes de ejecutar una sola línea.
const response = await client.patchAsync('/orders/123', payload);
2. Imports que no resuelven
Cuando el modelo no está seguro de la ruta exacta de un módulo, tiende a construir una que "suena" correcta según convenciones que ha visto en proyectos similares. Es habitual en ecosistemas con muchos subpaquetes, como el de Java/Spring o el de las librerías de utilidades de Python, donde la ruta real puede diferir de la intuitiva por una carpeta o un nombre de paquete.
# Plausible, pero en muchas versiones el módulo real es distinto
from utils.validators.email import validate_email_format
Aquí la comprobación es mecánica y no requiere leer el código con cuidado: un build incremental o un import en caliente falla de inmediato si la ruta no existe. La clave es no dejar que el import fallido se acumule junto con otros cambios sin revisar; si compilas o ejecutas el módulo justo después de que el asistente proponga el import, el error aparece aislado y es trivial de diagnosticar. Si esperas a tener veinte cambios más encima, el mismo error tarda diez veces más en localizarse.
3. Signatures que mezclan versiones de la misma librería
Esta es la señal más difícil de detectar con una lectura superficial porque cada pieza, por separado, es real. El modelo puede generar una llamada que corresponde a la API de una versión antigua de una librería mezclada con un parámetro que solo existe en una versión posterior, porque ambas aparecieron en su entrenamiento y no tiene forma de saber cuál corresponde a la versión que tienes instalada en el proyecto.
// 'timeout' en el segundo argumento del constructor: patrón de una versión anterior.
// 'retryPolicy' como propiedad: patrón de una versión posterior.
// Ninguna versión real acepta ambas a la vez.
const client = new ApiClient(baseUrl, { timeout: 5000 });
client.retryPolicy = { attempts: 3 };
El chequeo de tipos estático (TypeScript, mypy, o el propio compilador en lenguajes tipados) suele detectar la mitad de este problema, la que produce un error de tipo directo. La otra mitad —una signature que es sintácticamente válida pero semánticamente de otra versión— solo la atrapa comparar contra la documentación de la versión exacta instalada, no contra "la documentación de esa librería" en general. Fijar la versión en el package.json o el requirements.txt antes de pedir código no evita que el modelo mezcle versiones, pero sí te da una referencia única contra la que verificar.
4. Comportamiento que pasa el smoke test pero falla en el caso real
Esta señal aparece un paso más tarde, cuando el código sí compila y sí ejecuta, pero hace algo distinto de lo que el nombre de la función promete. Es más peligrosa que las tres anteriores porque no la detecta ni el linter ni el compilador: el código es válido, simplemente está mal.
El patrón que hemos visto repetirse es una función con un nombre correcto y un caso feliz correcto, pero que ignora una condición de borde que no estaba explícita en el prompt. Antes de aceptar una función generada como terminada, ejecutamos primero los tests que ya existen para ese módulo, no tests nuevos escritos por el mismo asistente que escribió la función. Un test escrito por el modelo que generó el código tiende a validar exactamente lo que el código hace, no lo que debería hacer, así que confirma el problema en lugar de exponerlo.
5. Certeza narrativa sin verificación
La última señal no está en el código, está en cómo lo presenta el modelo. Cuando un asistente describe su propia solución con frases como "esto maneja correctamente todos los casos de error" o "esta es la forma estándar de hacerlo en esta librería", esa afirmación no es evidencia de nada: es el mismo proceso generativo que produjo el código, aplicado ahora a la descripción del código. Un modelo no ha ejecutado la función contra el caso real ni ha comprobado la documentación oficial antes de escribir esa frase; la frase suena segura porque ese es el registro que el entrenamiento asocia con explicaciones técnicas, no porque haya verificación detrás.
Tratamos cualquier afirmación de ese tipo como una hipótesis a comprobar, no como un hecho reportado. Si el asistente dice que una librería expone cierto comportamiento, la comprobación es leer la documentación de la versión instalada o el propio código fuente del paquete, no confiar en la seguridad con la que se explicó.
El orden que ahorra tiempo
Ninguna de estas señales requiere herramientas caras ni un proceso nuevo. Lo que cambia el tiempo de detección es el orden en que se aplican los chequeos ya disponibles en cualquier proyecto: primero el linter y el chequeo de tipos, porque son gratis y atrapan las señales uno y dos casi instantáneamente; después un build incremental, porque expone imports rotos antes de escribir una sola línea más encima; después los tests existentes del módulo afectado, ejecutados antes que cualquier test nuevo, porque revelan si el comportamiento real coincide con el esperado; y solo al final, si todo lo anterior pasó, una lectura humana centrada específicamente en las afirmaciones que el propio asistente hizo sobre su código, no en el código en sí.
Ese orden importa porque cada paso es más barato que el siguiente. Ejecutar un linter cuesta segundos. Depurar en producción un método que nunca existió cuesta una tarde. La diferencia entre ambos escenarios casi nunca es la calidad del modelo que escribió el código: es en qué punto del proceso alguien —o algo— comprobó que lo que sonaba correcto también lo era.