El caché que nunca acertaba: el hash() de Python que cambia en cada reinicio

31 October, 2026 | homelab

Serie Homelab · Parte 11 · ← El bug que dejó a mi agente meditando · Langfuse: ver qué piensa el agente → · Ver la serie

TL;DR: Puse un caché en Redis para que mi agente dejara de tardar decenas de segundos en preguntas repetidas. El caché nunca acertaba, porque construía las claves con hash() de Python, y hash() de un string cambia cada vez que se reinicia el proceso. La solución fueron tres líneas con hashlib. La lección es sobre pruebas.

Por qué hacía falta un caché

Preguntarle algo a un agente que consulta herramientas y razona en varios pasos puede tardar decenas de segundos. Para algo como «¿cómo está el CPU del nodo?» eso es absurdo: un humano lo responde mirando un panel en dos segundos. Y es una pregunta que se repite muchas veces por hora, con una respuesta que sigue siendo válida medio minuto después.

Solución obvia: guardar la respuesta con un tiempo de vida y devolverla si la misma pregunta llega de nuevo, sin llamar al modelo.

La implementación «elegante»

Ya tenía Redis corriendo, así que fue cuestión de una tarde:

def clave(consulta: str) -> str:
    return f"query:{hash(consulta)}"   # el villano

def leer(consulta):
    valor = redis.get(clave(consulta))
    return json.loads(valor) if valor else None

def guardar(consulta, respuesta, ttl=60):
    redis.setex(clave(consulta), ttl, json.dumps(respuesta))

La probé a mano: mandé la misma pregunta dos veces seguidas y la segunda salió casi al instante. «Funciona.» Hice commit y me fui a dormir tranquilo.

El bug invisible

Al día siguiente, Redis tenía miles de entradas y el porcentaje de aciertos era prácticamente cero. Eso no cuadra: si hay miles de claves y las preguntas se repiten, tiene que haber aciertos.

Agregué un log con la clave de cada consulta y vi dos cosas:

  • Siempre hit=False.
  • Las claves eran todas distintas, aunque las preguntas eran idénticas.

El villano

hash() parece determinista: mismo input, mismo output. Es la definición de un hash. Pero en Python (desde la 3.3) el hash de los strings usa una semilla aleatoria que se genera al arrancar el proceso, controlada por la variable PYTHONHASHSEED. Pruébalo en tu terminal:

python3 -c "print(hash('hola'))"
python3 -c "print(hash('hola'))"

Dos ejecuciones, dos números distintos. Dentro de un mismo proceso hash() es estable; al reiniciarlo, todo cambia.

Y mi agente se reiniciaba más de lo que yo creía: despliegues, caídas, recargas de configuración. Cada reinicio, nueva semilla, claves nuevas, cero aciertos.

Lo más traicionero: mi prueba inicial pasó. Mandé las dos preguntas dentro del mismo proceso, donde hash() es estable. Por eso «funcionó» en mi prueba y fracasó en uso real.

La solución

Usa un hash que sea determinista siempre, como los de hashlib:

import hashlib

def clave(consulta: str) -> str:
    huella = hashlib.sha256(consulta.strip().lower().encode("utf-8")).hexdigest()
    return f"query:{huella}"

Aquí no hace falta seguridad criptográfica, solo determinismo; MD5 también serviría, pero SHA-256 te ahorra explicarlo en una revisión de código. De paso normalicé la pregunta (espacios y mayúsculas) para que «¿Cómo está el CPU?» y «¿cómo está el cpu?» compartan entrada.

Después hice la prueba que debía haber hecho desde el principio: reinicié el proceso a propósito, tres veces, y repetí la pregunta cada vez. El caché sobrevivió a los reinicios.

TTL por tipo de consulta

No todo envejece igual. Mi criterio:

  • Estado en tiempo real (CPU, RAM): TTL de unos 30 a 60 segundos. Más que eso, la respuesta puede ser mentira.
  • Cosas que cambian poco (qué VMs existen, qué servicios corren): varios minutos.
  • Lo que casi no cambia (configuraciones): más tiempo, e invalidación manual si lo tocas.

Lo que no se cachea

Esto es crucial: solo se cachean consultas de lectura. Si una respuesta incluyó una acción que cambia algo, como reiniciar o publicar, guardarla en caché y repetirla sería un desastre. Esas acciones, además, pasan por la compuerta de permisos (Parte 7), que nunca debería saltarse por un acierto de caché.

Lo que aprendí (otra vez, como QA)

  1. Prueba como si fuera producción. Mi prueba pasó porque no reinicié el proceso, y reiniciar es lo que pasa en producción todos los días.
  2. Un caché sin métrica de aciertos está ciego. Miles de claves y cero aciertos, y yo sin enterarme. Mide el hit rate.
  3. «Parece determinista» no es determinista. Desconfía de lo que cambia entre procesos: hash(), el orden de los sets, la hora, los diccionarios de otros lenguajes.

Ahora sí tengo agente rápido, caché sano y un tropiezo menos. Falta lo más importante: poder ver qué hace el agente por dentro. De eso va la Parte 12.