Módulo 3: Detectar Hallucinations en Código
Tipos de Hallucinations en Código
Tipos de Hallucinations en Código
Descripción de la cápsula
No todas las hallucinations son iguales. Un import falso se puede detectar con pip install. Un parámetro inventado se puede verificar con la documentación. Pero una función que dice validar emails y solo checa que tenga @ — eso requiere leer la implementación con ojo crítico.
En esta cápsula vas a aprender la taxonomía completa de hallucinations en código. Son 4 tipos, de menor a mayor dificultad de detección: imports falsos, APIs con signatures inventadas, parámetros incorrectos, y lógica fabricada. Para cada tipo vas a ver ejemplos sutiles — no import unicorn_magic, sino código que un developer con experiencia podría aceptar sin cuestionar.
La Taxonomía: 4 Tipos de Hallucinations
Vista general
Tipo 1: Imports Falsos
├── Dificultad de detección: ⬛⬜⬜⬜ (Baja)
├── Peligrosidad: ⬛⬛⬜⬜ (Media)
├── Falla en: import time / pip install
└── Ejemplo: from sklearn.metrics import roc_auc_multiclass
Tipo 2: APIs con Signatures Inventadas
├── Dificultad de detección: ⬛⬛⬜⬜ (Media)
├── Peligrosidad: ⬛⬛⬛⬜ (Alta)
├── Falla en: runtime cuando se llama la función
└── Ejemplo: pd.DataFrame.to_json(orient="dict")
Tipo 3: Parámetros Incorrectos
├── Dificultad de detección: ⬛⬛⬛⬜ (Alta)
├── Peligrosidad: ⬛⬛⬛⬜ (Alta)
├── Falla en: runtime, a veces silenciosamente
└── Ejemplo: requests.get(url, verify_ssl=True)
Tipo 4: Lógica Fabricada
├── Dificultad de detección: ⬛⬛⬛⬛ (Muy Alta)
├── Peligrosidad: ⬛⬛⬛⬛ (Muy Alta)
├── Falla en: runtime con inputs específicos
└── Ejemplo: validate_email() que solo checa "@"
Observa que la dificultad de detección y la peligrosidad aumentan juntas. Las hallucinations más fáciles de detectar son las menos peligrosas (porque se detectan rápido). Las más difíciles son las más peligrosas (porque llegan a producción).
Tipo 1: Imports Falsos
Qué son
El LLM genera un import de un módulo, clase, o función que no existe en el paquete referenciado. El import puede referir a un paquete que no existe en absoluto, o — más sutilmente — a un submodule o función que no existe dentro de un paquete real.
Por qué ocurren
El LLM ha visto miles de imports de una librería y extrapola. Si existe from sklearn.metrics import roc_auc_score, el LLM puede inferir que roc_auc_multiclass también existe porque el patrón encaja. No verifica — inventa basándose en probabilidad lingüística.
Ejemplos sutiles
Ejemplo 1: Submodule que no existe en un paquete real
# ❌ Hallucination: OAuth2TokenValidator no existe en FastAPI
from fastapi.security import OAuth2TokenValidator
# ✅ Lo que sí existe:
from fastapi.security import OAuth2PasswordBearer
from fastapi.security import OAuth2AuthorizationCodeBearer
from fastapi.security import SecurityScopes
¿Por qué es sutil? Porque fastapi.security es un módulo real con clases reales de OAuth2. OAuth2TokenValidator suena como algo que debería existir al lado de OAuth2PasswordBearer. El naming convention es consistente. Pero no existe.
Ejemplo 2: Función que suena real en un módulo real
# ❌ Hallucination: roc_auc_multiclass no existe
from sklearn.metrics import roc_auc_multiclass
# ✅ Lo que sí existe:
from sklearn.metrics import roc_auc_score
# Para multiclass, se usa el parámetro multi_class:
score = roc_auc_score(y_true, y_pred, multi_class="ovr")
¿Por qué es sutil? Porque sklearn tiene decenas de funciones en metrics. Una función llamada roc_auc_multiclass encaja perfectamente con el estilo del módulo.
Ejemplo 3: Paquete real, alias que no existe
# ❌ Hallucination: JSONDecodeError no se importa así en Python
from json.exceptions import JSONDecodeError
# ✅ Lo que sí existe:
from json import JSONDecodeError
# O simplemente:
import json
# Y usar: json.JSONDecodeError
¿Por qué es sutil? Porque JSONDecodeError es una excepción real de json. El LLM razonó que las excepciones estarían en un submódulo exceptions (como en muchas otras librerías), pero en la librería json de Python no existe ese submódulo.
Ejemplo 4: Clase de testing que suena lógica
# ❌ Hallucination: AsyncTestClient no existe en httpx
from httpx import AsyncTestClient
# ✅ Lo que sí existe:
from httpx import AsyncClient
# Para testing con FastAPI:
from httpx import ASGITransport, AsyncClient
¿Por qué es sutil? Porque httpx tiene AsyncClient y se usa mucho para testing. AsyncTestClient suena como una versión especializada para tests — pero no existe.
Señales de alerta para imports
- ✅ Import de un módulo estándar de Python (
os,json,datetime) → Casi seguro correcto - ✅ Import de la clase principal de un framework (
from fastapi import FastAPI) → Casi seguro correcto - ⚠️ Import de un submodule específico (
from fastapi.security import ...) → Verificar - ⚠️ Import con nombre compuesto que "suena real" → Verificar
- ❌ Import de algo que no has visto nunca en la documentación → Verificar siempre
Cómo verificar rápidamente
# Método 1: Intentar importar
python -c "from fastapi.security import OAuth2TokenValidator"
# ImportError → hallucination confirmada
# Método 2: Listar contenido del módulo
python -c "import fastapi.security; print(dir(fastapi.security))"
# Método 3: Buscar en PyPI
pip show fastapi | grep -i "location"
# Luego navegar al código fuente
Tipo 2: APIs con Signatures Inventadas
Qué son
El LLM llama a una función o método que sí existe, pero con una signature (combinación de argumentos) que no corresponde a la API real. La función existe, los argumentos que usa no.
Por qué ocurren
El LLM ha visto la misma función usada con diferentes argumentos en diferentes versiones, en diferentes wrappers, y en diferentes contextos. Mezcla patrones y genera una combinación que nunca existió en ninguna versión.
Ejemplos sutiles
Ejemplo 1: Método con valor de argumento inventado
import pandas as pd
df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [30, 25]})
# ❌ Hallucination: "dict" no es un valor válido de orient
result = df.to_json(orient="dict")
# ✅ Valores válidos de orient:
# "split", "records", "index", "columns", "values", "table"
result = df.to_json(orient="records")
¿Por qué es sutil? Porque orient sí es un parámetro real de to_json(), y "dict" suena como un formato lógico de serialización (de hecho, df.to_dict() existe como método separado). El LLM mezcló to_json(orient=...) con to_dict().
Ejemplo 2: Función con argumento de otra versión
import jwt
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
secret = "my-secret-key"
# ❌ Hallucination: verify=True no existe como parámetro
decoded = jwt.decode(token, secret, algorithms=["HS256"], verify=True)
# ✅ Lo correcto en PyJWT moderno:
decoded = jwt.decode(
token,
secret,
algorithms=["HS256"],
options={"verify_signature": True}
)
¿Por qué es sutil? Porque verify suena como un parámetro lógico para una función de decode de JWT. Y en versiones antiguas de PyJWT o en otras librerías JWT, un parámetro similar existía. El LLM mezcló la API actual con la legacy.
Ejemplo 3: Método de ORM con argumento incorrecto
from sqlalchemy import select
from sqlalchemy.orm import Session
# ❌ Hallucination: eager_load no es un parámetro de select()
stmt = select(User).where(User.active == True).eager_load(User.posts)
# ✅ Lo correcto en SQLAlchemy:
from sqlalchemy.orm import joinedload
stmt = select(User).where(User.active == True).options(joinedload(User.posts))
¿Por qué es sutil? Porque eager_load es un concepto real de ORMs (eager loading vs lazy loading). El LLM usó el nombre del concepto como si fuera un método, cuando el método real es options(joinedload(...)).
Ejemplo 4: Constructor con parámetro que no existe
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
# ❌ Hallucination: unique=True no es un parámetro de Field()
email: str = Field(..., unique=True, description="User email")
name: str = Field(..., min_length=2, max_length=100)
# ✅ Field() acepta: default, alias, title, description,
# gt, ge, lt, le, min_length, max_length, pattern, etc.
# "unique" es un concepto de base de datos, no de validación Pydantic
¿Por qué es sutil? Porque unique=True es un constraint real — en SQLAlchemy, en Django ORM, en muchos ORMs. Pero Pydantic valida data, no define schemas de base de datos. El LLM mezcló los dominios.
Señales de alerta para APIs inventadas
- ✅ Uso estándar de una función con argumentos documentados → Probablemente correcto
- ⚠️ Función real con argumento que no has visto antes → Verificar
- ⚠️ Método encadenado que "suena como" un concepto → Verificar (.eager_load, .with_cache, etc.)
- ❌ Parámetro que cruza dominios (concepto de DB en validación, concepto de ORM en API) → Casi seguro hallucination
Cómo verificar rápidamente
# Método 1: Inspeccionar la signature de la función
import inspect
import pandas as pd
print(inspect.signature(pd.DataFrame.to_json))
# Muestra todos los parámetros reales
# Método 2: Usar help()
help(pd.DataFrame.to_json)
# Muestra documentación con parámetros
# Método 3: Verificar en documentación oficial
# https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_json.html
Tipo 3: Parámetros Incorrectos
Qué son
El LLM usa una función real con un parámetro que suena correcto pero no es el nombre real del parámetro. A diferencia del Tipo 2, aquí el error está en el nombre del parámetro, no en su valor o en la existencia de la función.
Por qué ocurren
Los LLMs ven patrones de naming en APIs. Si una librería usa timeout y otra usa request_timeout, el LLM puede mezclar los nombres. Si un concepto se llama "verify" en una librería y "validate" en otra, el LLM puede cruzar los nombres.
Ejemplos sutiles
Ejemplo 1: Nombre descriptivo que no es el real
import requests
# ❌ Hallucination: el parámetro es "verify", no "verify_ssl"
response = requests.get(
"https://api.example.com/data",
verify_ssl=True,
timeout=30
)
# ✅ Lo correcto:
response = requests.get(
"https://api.example.com/data",
verify=True, # controla verificación SSL
timeout=30
)
¿Por qué es sutil? verify_ssl es un nombre más descriptivo que verify. En muchas otras librerías y configuraciones, el parámetro se llama exactamente verify_ssl o ssl_verify. El LLM usó el nombre que más "sentido" tiene semánticamente, no el que la librería usa.
Ejemplo 2: Parámetro con nombre similar pero diferente
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# ❌ Hallucination: el parámetro es "allow_origins", no "allowed_origins"
app.add_middleware(
CORSMiddleware,
allowed_origins=["http://localhost:3000"],
allowed_methods=["*"],
allowed_headers=["*"],
)
# ✅ Lo correcto:
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_methods=["*"],
allow_headers=["*"],
)
¿Por qué es sutil? La diferencia es allow_ vs allowed_. Semánticamente son casi idénticos. Y allowed_origins es gramaticalmente más natural en inglés. Pero FastAPI/Starlette usa allow_origins (sin la "d").
Ejemplo 3: Parámetro de otra librería
import logging
# ❌ Hallucination: "log_level" no es parámetro de basicConfig
logging.basicConfig(
log_level=logging.INFO,
format="%(asctime)s - %(message)s"
)
# ✅ Lo correcto:
logging.basicConfig(
level=logging.INFO, # es "level", no "log_level"
format="%(asctime)s - %(message)s"
)
¿Por qué es sutil? log_level suena más específico que level, y es el nombre que se usa en configuraciones de muchos frameworks (como uvicorn, gunicorn, y Django). Pero logging.basicConfig() usa simplemente level.
Ejemplo 4: Parámetro con prefijo extra
from sqlalchemy import create_engine
# ❌ Hallucination: "max_pool_size" no es el nombre correcto
engine = create_engine(
"postgresql://user:pass@localhost/db",
max_pool_size=20,
pool_timeout=30
)
# ✅ Lo correcto:
engine = create_engine(
"postgresql://user:pass@localhost/db",
pool_size=20, # no "max_pool_size"
pool_timeout=30 # este sí es correcto
)
¿Por qué es sutil? pool_timeout existe y es correcto. max_pool_size sigue el mismo patrón de naming (pool_*) pero el parámetro real es pool_size. El prefijo "max_" es una adición lógica del LLM pero incorrecta.
El peligro especial de este tipo
Los parámetros incorrectos son especialmente peligrosos porque en Python, con **kwargs, un parámetro incorrecto puede ser silenciosamente ignorado:
# Muchas funciones aceptan **kwargs
# Un parámetro con nombre incorrecto simplemente se ignora
# No hay error — pero tampoco hay efecto
import requests
# Esto NO da error — pero verify_ssl no hace nada
response = requests.get(url, verify_ssl=False)
# El SSL sigue verificándose porque "verify" nunca fue False
# En producción, piensas que deshabilitaste SSL verification
# pero no lo hiciste
Esa es la razón por la que este tipo tiene peligrosidad Alta: el código funciona sin error, pero el parámetro no tiene efecto. Tu intención no se cumple y no recibes ningún aviso.
Señales de alerta para parámetros incorrectos
- ✅ Parámetro que has usado personalmente muchas veces → Probablemente correcto
- ⚠️ Parámetro con nombre "más descriptivo" que lo usual → Verificar
- ⚠️ Parámetro que existe en otra librería similar → Verificar
- ⚠️ Parámetro que suena correcto pero nunca lo has visto en los docs → Verificar
- ❌ Dos parámetros del mismo tipo con naming diferente en la misma llamada → Sospechoso
Cómo verificar rápidamente
# Método 1: Inspeccionar parámetros aceptados
import inspect
import requests
sig = inspect.signature(requests.get)
print(sig.parameters.keys())
# dict_keys(['url', 'params', 'kwargs'])
# Luego ver la documentación de kwargs
# Método 2: Usar IDE con autocompletado
# Los IDEs modernos muestran los parámetros disponibles
# Si un parámetro no aparece en autocompletado, sospecha
# Método 3: Quick test
import requests
try:
requests.get("https://httpbin.org/get", verify_ssl=True)
print("No error — but does it work?")
except TypeError as e:
print(f"Error: {e}")
Tipo 4: Lógica Fabricada
Qué son
El LLM genera una función con un nombre descriptivo y un docstring correcto, pero la implementación no hace lo que dice hacer. A diferencia de un bug (donde el developer intentó implementar algo y se equivocó), aquí el LLM generó una implementación que nunca fue correcta — fabricó lógica que suena plausible pero no corresponde al propósito declarado.
Por qué ocurren
Los LLMs son excelentes generando código que se ve correcto estructuralmente: buenos nombres de funciones, docstrings coherentes, tipos correctos. Pero cuando la lógica requiere conocimiento específico del dominio — fórmulas matemáticas, algoritmos de validación, reglas de negocio — el LLM puede generar una implementación que "parece razonable" sin serlo.
Ejemplos sutiles
Ejemplo 1: Validación superficial que parece completa
import re
def validate_email(email: str) -> bool:
"""
Validates that the email address is properly formatted
according to RFC 5322 standards.
"""
if not email or not isinstance(email, str):
return False
return "@" in email and "." in email.split("@")[-1]
¿Por qué es sutil? La función tiene:
- ✅ Un nombre descriptivo
- ✅ Un docstring que menciona RFC 5322
- ✅ Type hints
- ✅ Validación de input (not empty, is string)
- ✅ Checa
@y.en el dominio - ❌ Pero acepta:
"a@b.c","@domain.com","user@.com","us er@domain.com"
Una validación real de email es significativamente más compleja. El LLM generó algo que pasa el 80% de los casos — pero falla en edge cases que importan.
Ejemplo 2: Cálculo estadístico incorrecto
from typing import List
def calculate_percentile(data: List[float], percentile: int) -> float:
"""
Calculates the given percentile of a dataset.
Uses the standard interpolation method.
Args:
data: List of numerical values
percentile: Percentile to calculate (0-100)
Returns:
The percentile value
"""
if not data:
raise ValueError("Data cannot be empty")
if not 0 <= percentile <= 100:
raise ValueError("Percentile must be between 0 and 100")
sorted_data = sorted(data)
index = (percentile / 100) * len(sorted_data)
if index == int(index):
return sorted_data[int(index)]
lower = sorted_data[int(index)]
upper = sorted_data[int(index) + 1]
return (lower + upper) / 2
¿Por qué es sutil? La función:
- ✅ Tiene validación de input correcta
- ✅ Ordena los datos
- ✅ Calcula un índice basado en el percentil
- ❌ El cálculo del índice es incorrecto (no usa interpolación estándar)
- ❌ Falla con
index = len(data)(IndexError) - ❌ La interpolación no corresponde a ningún método estándar (np.percentile usa métodos bien definidos)
El LLM generó algo que produce números razonables para la mayoría de inputs — pero no implementa el cálculo de percentil correcto.
Ejemplo 3: Sanitización que no sanitiza
import re
def sanitize_sql_input(user_input: str) -> str:
"""
Sanitizes user input to prevent SQL injection attacks.
Removes dangerous SQL keywords and characters.
"""
dangerous_keywords = [
"DROP", "DELETE", "INSERT", "UPDATE",
"SELECT", "UNION", "ALTER", "CREATE"
]
sanitized = user_input
for keyword in dangerous_keywords:
sanitized = re.sub(
keyword, "", sanitized, flags=re.IGNORECASE
)
sanitized = sanitized.replace("'", "''")
sanitized = sanitized.replace(";", "")
return sanitized
¿Por qué es sutil? La función:
- ✅ Tiene nombre correcto y docstring apropiado
- ✅ Lista keywords peligrosos de SQL
- ✅ Remueve los keywords
- ✅ Escapa comillas simples
- ✅ Remueve punto y coma
- ❌ Pero la sanitización por blacklist es fundamentalmente incorrecta para prevenir SQL injection
- ❌ Bypass fácil:
"SELSELECTECT"→ después de removerSELECTquedaSELECT - ❌ No protege contra inyección con comentarios:
/**/ - ❌ La solución real es usar parameterized queries, no sanitizar input
La función da una falsa sensación de seguridad. Un developer que la usa piensa que está protegido contra SQL injection, pero no lo está.
Ejemplo 4: Hash que no es seguro
import hashlib
def hash_password(password: str) -> str:
"""
Securely hashes a password for storage.
Uses SHA-256 hashing algorithm.
"""
return hashlib.sha256(password.encode()).hexdigest()
def verify_password(password: str, hashed: str) -> bool:
"""Verifies a password against its hash."""
return hash_password(password) == hashed
¿Por qué es sutil? La función:
- ✅ Usa hashlib (librería estándar, no inventada)
- ✅ SHA-256 es un algoritmo real y respetado
- ✅ La verificación es lógicamente correcta
- ❌ SHA-256 sin salt es vulnerable a rainbow table attacks
- ❌ SHA-256 es demasiado rápido para passwords (permite brute force)
- ❌ No usa bcrypt, scrypt, o argon2 (algoritmos diseñados para passwords)
- ❌ No tiene salt, no tiene iterations, no tiene key stretching
Un developer que no conoce la diferencia entre hashing genérico y password hashing aceptaría esto sin cuestionar.
Señales de alerta para lógica fabricada
- ✅ Implementación de algo que has hecho antes y reconoces → Probablemente correcto
- ⚠️ Función con docstring que menciona un estándar (RFC, algoritmo) → Verificar que la implementación corresponde
- ⚠️ Función de seguridad implementada desde cero → Casi siempre mejor usar una librería
- ⚠️ Función matemática/estadística → Verificar fórmulas contra referencia
- ❌ Función que "sanitiza" o "valida" con lógica de blacklist → Sospechoso
- ❌ Función de hashing/encryption implementada manualmente → Usar librería especializada siempre
Cómo verificar
# Método 1: Quick test con edge cases
def test_validate_email():
assert validate_email("user@domain.com") == True
assert validate_email("@domain.com") == False # No local part
assert validate_email("user@.com") == False # No domain
assert validate_email("us er@domain.com") == False # Space
assert validate_email("a@b.c") == False # TLD too short
# Si alguno falla, la validación es insuficiente
# Método 2: Comparar con librería conocida
# Para email validation:
from email_validator import validate_email as real_validate
# Para password hashing:
from passlib.hash import bcrypt
# Para SQL: usar parameterized queries, nunca sanitizar
Mapa de Peligrosidad
¿Cuándo preocuparte más?
Fácil de detectar Difícil de detectar
──────────────────────────────────────────
Impacto bajo │ Import falso de │ Parámetro ignorado │
│ librería estándar │ silenciosamente │
│ (crash inmediato) │ (no hay error) │
├────────────────────┼─────────────────────┤
Impacto alto │ Import falso en │ Lógica de seguridad │
│ código crítico │ que "parece" correcta│
│ (crash en deploy) │ (pasa a producción) │
──────────────────────────────────────────
Cuadrante más peligroso:
Difícil de detectar + Impacto alto = Lógica fabricada en seguridad
Prioridad de verificación
Cuando revisas código generado por AI, el orden de prioridad debe ser:
- Lógica de seguridad (autenticación, authorization, encryption, sanitización)
- Lógica de negocio (cálculos financieros, validaciones de dominio)
- Parámetros de API (especialmente los que pueden ser ignorados silenciosamente)
- Signatures de funciones (verificar contra documentación)
- Imports (verificar que existen)
No revises imports primero porque son los más fáciles de detectar. Revisa lógica de seguridad primero porque es la más peligrosa de pasar por alto.
Conexión con Proyecto
Aplicación al proyecto integrador
En el proyecto integrador del módulo 8, el codebase contiene hallucinations de los 4 tipos:
| Tipo | Cantidad en proyecto | Dificultad |
|---|---|---|
| Imports falsos | 1-2 | Baja (las encuentras con linter/import) |
| APIs inventadas | 1 | Media (necesitas verificar docs) |
| Parámetros incorrectos | 1-2 | Alta (pueden ser ignorados silenciosamente) |
| Lógica fabricada | 1 | Muy alta (necesitas leer y entender la implementación) |
Tu capacidad de clasificar rápidamente el tipo de hallucination te ayuda a elegir la técnica de verificación correcta: linter para imports, docs para APIs, tests para lógica.
Del diagnóstico a la acción
Esta cápsula te dio la taxonomía (qué tipos existen). Las cápsulas 03 y 04 te dan las técnicas de detección específicas para cada tipo. La cápsula 05 te da las herramientas automatizadas. Y la cápsula 06 es donde demuestras que puedes detectarlas.
Troubleshooting
Problema 1: "No veo la diferencia entre un bug y una hallucination de lógica fabricada"
Causa: La línea es sutil. Un bug es un error del programador humano al implementar lógica conocida. Una hallucination es lógica que el LLM inventó sin verificar.
Solución: La pregunta clave es: ¿el código intenta implementar algo real o inventó su propia versión? Si calculate_percentile() usa numpy.percentile internamente pero con un bug, es un bug. Si implementa un algoritmo que el LLM inventó, es una hallucination. En la práctica, la solución es la misma: verificar contra referencia y tests.
Problema 2: "¿Cómo sé si un parámetro es real o inventado sin abrir los docs?"
Causa: No tienes que memorizar cada parámetro de cada librería. Eso es imposible.
Solución: Tu instinto de verificación debe activarse ante parámetros que no has usado antes personalmente. Si es la primera vez que ves un parámetro, verifica. Si lo has usado 100 veces, confía. La regla simple: "si no lo he usado antes, verifico."
Problema 3: "Los Tipos 3 y 4 me parecen demasiado difíciles de detectar"
Causa: Lo son — por eso la cápsula 05 te da herramientas automatizadas.
Solución: Tu ojo humano no va a detectar el 100% de hallucinations. Nadie puede. Por eso combines verificación manual (tu ojo entrenado) con verificación automatizada (linters, type checkers, tests). Las cápsulas 03 y 04 entrenan tu ojo. La cápsula 05 te da las herramientas que cubren donde tu ojo falla.
Problema 4: "¿Es posible que una hallucination no cause problemas?"
Causa: Sí, es posible. Un parámetro inventado que es ignorado silenciosamente puede no causar problemas visibles.
Solución: Que no cause problemas visibles no significa que no sea un problema. Un verify_ssl=False que es ignorado silenciosamente significa que SSL verification sigue activa — lo cual es bueno accidentalmente. Pero si tu intención era desactivar SSL (quizás para testing local), tu código no hace lo que piensas que hace. La hallucination no causa un crash, pero causa confusión y mantenimiento futuro problemático.
Ejercicios
Ejercicio 1: Clasificar hallucinations (Fácil)
Clasifica cada uno de estos errores como Tipo 1 (Import), Tipo 2 (API), Tipo 3 (Parámetro), Tipo 4 (Lógica), o "No es hallucination":
from collections import OrderedDefaultDicthashlib.sha256(data).hexdigest()cuando se necesita bcrypt para passwordsos.path.exists()retorna True pero el archivo no tiene permisos de lecturapd.read_csv("data.csv", delimiter=",")json.loads(data, encoding="utf-8")
Ver solución
- Tipo 1 (Import falso):
OrderedDefaultDictno existe encollections. ExistenOrderedDictydefaultdictpor separado. El LLM combinó ambos. - Tipo 4 (Lógica fabricada):
hashlib.sha256funciona correctamente — pero usarlo para passwords es una decisión de seguridad incorrecta. La función existe y funciona; la lógica de usarlo para passwords es la hallucination. - No es hallucination: Esto es un comportamiento real de
os.path.exists(). Retorna True si el path existe, independientemente de permisos. Es una limitación conocida, no una hallucination. - No es hallucination:
delimiteres un parámetro real depd.read_csv(). Es correcto. - Tipo 3 (Parámetro incorrecto): En Python 3.9+,
json.loads()ya no acepta el parámetroencoding(fue removido). En versiones anteriores era deprecado. El LLM generó código de una versión anterior.
Ejercicio 2: Identificar la hallucination (Medio)
En cada par, uno es correcto y el otro es hallucination. ¿Cuál es cuál?
Par A:
# Opción 1
from typing import Annotated
from fastapi import Depends, Query
# Opción 2
from typing import Annotated
from fastapi import Depends, QueryParam
Par B:
# Opción 1
response = requests.post(url, json=data, timeout=30)
# Opción 2
response = requests.post(url, json_data=data, timeout=30)
Par C:
# Opción 1
from pathlib import Path
content = Path("file.txt").read_text(encoding="utf-8")
# Opción 2
from pathlib import Path
content = Path("file.txt").read_contents(encoding="utf-8")
Ver solución
Par A: Opción 1 es correcta. Query es la función/clase real de FastAPI. QueryParam no existe — el LLM generó un nombre que suena más descriptivo pero no es el real. Tipo 1 (Import falso).
Par B: Opción 1 es correcta. json=data es el parámetro correcto de requests.post(). json_data no existe — el LLM usó un nombre más descriptivo pero incorrecto. Tipo 3 (Parámetro incorrecto).
Par C: Opción 1 es correcta. read_text() es el método real de pathlib.Path. read_contents() suena similar pero no existe. Tipo 2 (API inventada).
Ejercicio 3: Encontrar la hallucination sutil (Medio-Difícil)
Este código tiene exactamente 1 hallucination. Encuéntrala:
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel, EmailStr
from typing import Optional
import jwt
from datetime import datetime, timedelta
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
class UserCreate(BaseModel):
email: EmailStr
password: str
full_name: Optional[str] = None
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(
token, SECRET_KEY,
algorithms=[ALGORITHM],
verify=True
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
Ver solución
La hallucination está en jwt.decode():
payload = jwt.decode(
token, SECRET_KEY,
algorithms=[ALGORITHM],
verify=True # ← HALLUCINATION: este parámetro no existe
)
En PyJWT moderno, verify=True no es un parámetro válido de jwt.decode(). Lo correcto es:
payload = jwt.decode(
token, SECRET_KEY,
algorithms=[ALGORITHM],
options={"verify_signature": True}
)
Es un Tipo 3 (Parámetro incorrecto). Lo sutil: todo el resto del código es correcto — OAuth2PasswordBearer, jwt.encode, jwt.ExpiredSignatureError, jwt.InvalidTokenError — todo existe y funciona. Solo el parámetro verify=True es inventado.
Nota: dependiendo de la versión de PyJWT, verify=True podría no causar error (sería ignorado como **kwargs), pero no tendría efecto. La verificación de firma en PyJWT moderno está habilitada por default, así que en este caso la hallucination no causa un problema de seguridad — pero el parámetro no hace lo que el developer piensa.
Ejercicio 4: Detectar lógica fabricada (Difícil)
Esta función dice calcular el Levenshtein distance entre dos strings. ¿La implementación es correcta?
def levenshtein_distance(s1: str, s2: str) -> int:
"""
Calculates the Levenshtein (edit) distance between two strings.
Returns the minimum number of single-character edits
(insertions, deletions, substitutions) required to change
one string into the other.
"""
if len(s1) < len(s2):
return levenshtein_distance(s2, s1)
if len(s2) == 0:
return len(s1)
previous_row = range(len(s2) + 1)
for i, c1 in enumerate(s1):
current_row = [i + 1]
for j, c2 in enumerate(s2):
insertions = previous_row[j + 1] + 1
deletions = current_row[j] + 1
substitutions = previous_row[j] + (c1 != c2)
current_row.append(min(insertions, deletions, substitutions))
previous_row = current_row
return previous_row[-1]
Ver solución
Esta implementación es correcta. No es una hallucination.
Es la implementación iterativa estándar del Levenshtein distance usando programación dinámica con optimización de espacio (solo mantiene la fila anterior en memoria en lugar de la matriz completa).
Cómo verificar:
assert levenshtein_distance("kitten", "sitting") == 3
assert levenshtein_distance("", "hello") == 5
assert levenshtein_distance("same", "same") == 0
assert levenshtein_distance("a", "b") == 1
El punto de este ejercicio: no todo lo que AI genera es incorrecto. Parte de ser un buen detective es saber cuándo el código es correcto. Si tratas todo como sospechoso, pierdes tiempo y productividad.
Ejercicio 5: Crear tu propia tabla (Difícil)
Para una librería que usas frecuentemente en tu trabajo, genera una tabla con:
| Hallucination plausible | Por qué suena real | Lo correcto | Tipo |
|---|
Genera al menos 3 hallucinations plausibles para tu librería. Si no se te ocurren, usa FastAPI o requests.
Ver solución (ejemplo con FastAPI)
| Hallucination plausible | Por qué suena real | Lo correcto | Tipo |
|---|---|---|---|
from fastapi import Router | Similar a Flask's Blueprint, nombre intuitivo | from fastapi import APIRouter | Tipo 1 |
@app.post("/users", response_type=User) | response_type suena lógico | @app.post("/users", response_model=User) | Tipo 3 |
app.include_router(router, tags="users") | tags como string suena razonable | app.include_router(router, tags=["users"]) — tags es una lista | Tipo 3 |
HTTPException(status=404, message="Not found") | status y message son genéricos | HTTPException(status_code=404, detail="Not found") | Tipo 3 |
La clave: todos estos errores son del tipo "suena más intuitivo que lo real." Los LLMs tienden a generar el nombre más natural/descriptivo, no necesariamente el que la librería usa.
Resumen
En esta cápsula aprendiste:
- Las hallucinations se clasifican en 4 tipos de menor a mayor dificultad de detección
- Tipo 1 (Imports falsos): Módulos, clases, o funciones que no existen → Detectar con linter/import
- Tipo 2 (APIs inventadas): Funciones reales con signatures que no existen → Detectar con docs
- Tipo 3 (Parámetros incorrectos): Nombres de parámetros que suenan bien pero no son los reales → Peligrosos porque pueden ser ignorados silenciosamente
- Tipo 4 (Lógica fabricada): Implementaciones que se ven correctas pero no hacen lo que dicen → Las más peligrosas, requieren leer y entender la implementación
- La peligrosidad aumenta con la dificultad de detección
- Las hallucinations de seguridad (Tipo 4) son las que deben priorizarse en la revisión
- Las hallucinations peligrosas son las sutiles — las que se parecen a lo correcto
Próxima cápsula: Hallucinations en Imports y APIs — técnicas específicas para detectar los tipos 1 y 2.
Recursos Adicionales
- PyPI — Python Package Index - Verificar que un paquete Python existe
- Python Official Documentation - Referencia para módulos estándar de Python
- FastAPI Documentation - API reference para verificar imports y parámetros
- pandas API Reference - Verificar métodos y parámetros de pandas
- Real Python — Common Python Gotchas - Errores comunes que parecen hallucinations pero son comportamiento real
- OWASP — Input Validation Cheat Sheet - Por qué la sanitización por blacklist es insuficiente
Debugging & Code Review with Claude Code — Módulo 3, Cápsula 02 Claude Code Agentic Development Path — Guía #6 de 11