Configuración

Idioma

Por qué su Semantic Cache está devolviendo respuestas incorrectas

L
LemonData
·5 de marzo de 2026·640 vistas
Por qué su Semantic Cache está devolviendo respuestas incorrectas

Un usuario informó que nuestro plugin de traducción devolvía el mismo resultado en caché para cada solicitud, independientemente de la entrada. Investigamos y encontramos algo peor: el 95% de todos los aciertos de caché semántica (semantic cache hits) en nuestra plataforma eran falsos positivos. 199 solicitudes de traducción diferentes, 198 cuerpos de solicitud únicos, una sola respuesta en caché servida para todos ellos.

Si te preocupa el estado de los agentes de larga duración y el manejo de solicitudes en producción, este post complementa bien a Por qué tu agente de IA sigue perdiendo su memoria, la guía de chatbot con una sola clave y la guía de limitación de tasa de API de IA.

El informe del error

El informe fue sencillo: "Desactivé la caché semántica, pero cada traducción devuelve el mismo resultado".

Tres IDs de solicitud, tres segmentos de traducción diferentes, respuestas en caché idénticas. Los cuerpos de las solicitudes variaban de 1,564 a 8,676 bytes. El ID de la respuesta en caché era el mismo en todos ellos: chatcmpl-DG6J03nhdvcF7Ek0C8rJkjh7lN9pF.

Primera sospecha: no se estaban aplicando los ajustes de caché del usuario. Resultó ser un error de sincronización de la fuente de datos independiente (el panel de administración escribía en una tabla, el API gateway leía de otra). Pero solucionar eso solo resolvió la mitad del problema. Incluso con la caché habilitada y funcionando correctamente, la caché semántica estaba coincidiendo con solicitudes que nunca deberían coincidir.

Los datos de producción

Extrajimos datos de aciertos de caché de 24 horas de ClickHouse. Los números eran malos.

Modelo Total de solicitudes Aciertos de caché Solicitudes únicas Respuestas únicas Tasa de aciertos
gpt-4.1-nano 200 199 198 1 99.5%
glm-4.6-thinking 100 38 13 1 38%
gpt-5-nano 31 29 28 2 93.5%
gpt-oss-120b 18 17 17 1 94.4%
qwen3-vl-flash 17 16 16 1 94.1%

198 solicitudes de traducción únicas, todas devolviendo la misma respuesta única en caché. Eso no es una caché. Es una función rota que devuelve una constante.

Cada modelo afectado compartía dos rasgos: todas las solicitudes provenían de un solo usuario y todas usaban una plantilla de system prompt fija con contenido de usuario variable.

Por qué los embeddings fallan en entradas estructuradas

El plugin de traducción envía solicitudes como esta:

System: "Act as a translation API. Output a single raw JSON object only.
         Input: {"targetLanguage":"<lang>","title":"...","segments":[...]}"

User:   {"targetLanguage":"zh","title":"Product Page",
         "description":"Translate product descriptions",
         "tone":"formal",
         "segments":[{"text":"actual varying content here"}]}

El system prompt es idéntico en todas las solicitudes. El mensaje del usuario es un objeto JSON donde targetLanguage, title, description y tone son fijos. Solo cambia segments[].text.

Cuando nuestra caché semántica extrae texto para el embedding, concatena el system prompt y el mensaje del usuario. La plantilla fija representa aproximadamente el 80% del texto. El modelo de embedding (all-mpnet-base-v2, 768 dimensiones) comprime esto en un vector donde domina la estructura de la plantilla. El contenido real de la traducción apenas influye en el resultado.

Resultado: la similitud de coseno entre "traducir 'Hola mundo'" y "traducir 'El informe financiero trimestral muestra un aumento del 15% en los ingresos'" supera el 0.95. Nuestro umbral (threshold) es 0.95. Cada solicitud de traducción coincide con la primera entrada en caché.

Al revisar los logs, encontramos tres formas en que esto falla:

El plugin de traducción es el peor infractor. Las claves y valores JSON fijos ahogan los segmentos de traducción reales. Tanto gpt-4.1-nano como gpt-5-nano se vieron afectados por esto.

Un asistente de resumen de contexto tenía una variante del mismo problema. Su system prompt era tan largo que el contenido del usuario (que variaba de 5KB a 47KB) apenas se registraba en el embedding. Así es como glm-4.6-thinking terminó devolviendo el mismo resumen para cada conversación.

El tercer patrón era más sutil. Para gpt-oss-120b y qwen3-vl-flash, los primeros 500 caracteres de cada solicitud eran idénticos byte por byte. El contenido variable venía después, pero el embedding ya estaba dominado por el prefijo compartido.

Lo que dice la investigación

Este no es un problema nuevo. Artículos recientes lo han cuantificado.

El proyecto vCache de UC Berkeley encontró que los aciertos de caché correctos e incorrectos tienen "distribuciones de similitud altamente superpuestas". El umbral óptimo varía de 0.71 a 1.0 entre diferentes entradas en caché. Ningún número único funciona. Su solución: aprender un umbral separado por entrada de caché, lo que redujo las tasas de error en 6 veces mientras duplicaba las tasas de aciertos. (vCache, 2025)

Empeora cuando mezclas tipos de consultas. Un estudio de caché consciente de categorías mostró que un umbral de 0.80 produce un 15% de coincidencias falsas en consultas de código (sort_ascending vs sort_descending), mientras que el mismo umbral pierde paráfrasis válidas en consultas conversacionales. Un umbral, dos modos de falla. (Category-Aware Semantic Caching, 2025)

Los bancos también se enfrentan a esto. Un caso de estudio de InfoQ documentó un sistema RAG donde "¿Puedo saltarme el pago de mi préstamo este mes?" coincidía con "¿Qué pasa si falto a un pago de préstamo?" con una similitud del 88.7%. Intención diferente, misma respuesta en caché. Comenzaron con una tasa de falsos positivos del 99% y necesitaron cuatro rondas de optimización para bajar al 3.8%. (InfoQ Banking Case Study, 2025)

El problema de fondo: los embeddings miden si dos prompts son semánticamente similares, no si la misma respuesta puede contestar a ambos. Esa brecha es donde viven los falsos aciertos de caché. (Efficient Prompt Caching via Embedding Similarity, 2024)

Cada artículo que encontramos coincide en una cosa: la similitud de embedding por sí sola no es suficiente. Se necesita una capa de verificación.

La solución de dos capas

Construimos dos defensas. La primera elimina el ruido de la plantilla antes del embedding. La segunda verifica los aciertos después de la coincidencia.

Capa 2: Extracción de contenido para embeddings

Antes de generar un embedding, ahora detectamos la entrada estructurada (JSON) y extraemos solo el contenido significativo y variable.

La lógica:

  1. Verificar si el contenido del mensaje comienza con { o [
  2. Si se analiza como JSON, recopilar recursivamente todos los valores de cadena de las hojas
  3. Filtrar valores cortos (20 caracteres o menos), ya que suelen ser campos de configuración como "zh", "formal" o "Product Page"
  4. Si el texto extraído es demasiado corto o está vacío, volver al texto original
function extractContentForEmbedding(text: string): string {
  const extracted = tryExtractJsonContent(text);
  return extracted && extracted.length > 20 ? extracted : text;
}

Esto se aplica tanto al system prompt como al mensaje del usuario. Para el plugin de traducción, el embedding ahora representa "Hola mundo" en lugar de un blob JSON de 2KB. Para el asistente de resumen, extrae la conversación real del envoltorio de la plantilla.

El umbral de 20 caracteres se eligió empíricamente:

  • "zh" (2 caracteres): filtrado. Valor de configuración.
  • "formal" (6 caracteres): filtrado. Valor de configuración.
  • "Product Page" (12 caracteres): filtrado. Campo de plantilla.
  • "Translate product descriptions" (31 caracteres): mantenido. Contenido significativo.
  • "The quarterly financial report..." (40+ caracteres): mantenido. Contenido de traducción real.

Capa 3: Verificación de huella digital (Fingerprint)

Después de un acierto de caché semántica, comparamos un hash del texto extraído de la solicitud actual con el hash almacenado en la entrada de caché. Si no coinciden, el acierto es rechazado.

// Al escribir en caché
entry.metadata.textHash = fnv1aHash(extractedText);

// Al leer de caché, después de encontrar una coincidencia de similitud
if (entry.metadata.textHash !== undefined) {
  if (entry.metadata.textHash !== fnv1aHash(currentExtractedText)) {
    // Falso positivo: semánticamente similar pero contenido diferente
    metrics.recordFingerprintRejection();
    return null;
  }
}

El hash utiliza el texto extraído (post-Capa 2), no la entrada sin procesar. Dos solicitudes con diferentes envoltorios de plantilla pero contenido real idéntico seguirán coincidiendo. Contenido diferente, hash diferente, rechazado.

Las entradas de caché antiguas sin textHash omiten la verificación (compatible con versiones anteriores). Expiran naturalmente a través del TTL.

Usamos FNV-1a (32 bits) para el hash. Rápido, determinista, y una tasa de colisión de ~1 en 4 mil millones es aceptable para verificar un solo acierto de caché.

¿Por qué no simplemente subir el umbral?

Nuestro umbral ya es 0.95. Subirlo no ayuda. El problema es que las entradas estructuralmente similares producen puntuaciones de similitud superiores a 0.95 sin importar lo que diga el contenido real.

Los datos de vCache respaldan esto: las distribuciones de similitud de los aciertos correctos e incorrectos se superponen tanto que ningún punto de corte único los separa. Sube el umbral a 0.99 y eliminarás aciertos de caché legítimos para paráfrasis sin eliminar los falsos positivos de las solicitudes con mucha plantilla.

Corrige la entrada, verifica la salida. No manipules el umbral.

Resultados

Con ambas capas implementadas:

Métrica Antes Después
Falsos positivos gpt-4.1-nano 198/199 0
Proporción de falsos positivos en todos los aciertos de caché ~95% <5%
Tasa de aciertos de caché legítimos Sin cambios Sin cambios
Latencia añadida por solicitud 0 <1ms (JSON parse + FNV hash)

La Capa 2 por sí sola habría solucionado el plugin de traducción. La Capa 3 es la red de seguridad para casos donde la extracción JSON no separa completamente el contenido, o para entradas estructuradas que no son JSON.

Conclusiones

Si estás ejecutando una caché semántica en producción:

  1. Monitorea la diversidad de respuestas. Si un modelo tiene una tasa de aciertos de caché del 100% y 1 respuesta única, tienes un problema de falsos positivos. Consulta: SELECT model, uniqExact(substring(response_body, 1, 200)) as unique_responses, count() as total FROM request_logs WHERE cache_hit = true GROUP BY model.

  2. La entrada estructurada mata el embedding ingenuo. Cualquier solicitud con una plantilla fija (APIs JSON, envoltorios de system prompt, tareas de llenado de formularios) producirá puntuaciones de similitud artificialmente altas. Preprocesa antes del embedding.

  3. Una capa de verificación no es opcional. Cada caché semántica de producción en la literatura de investigación tiene una. La pregunta es si usas una verificación de hash ligera, un reranker cross-encoder o una llamada de verificación de LLM completa. Elige según tu presupuesto de latencia.

  4. Los umbrales globales son un compromiso, no una solución. Diferentes tipos de consultas necesitan diferentes umbrales. Si no puedes establecer umbrales por categoría o por entrada, al menos añade preprocesamiento de entrada para normalizar la calidad del embedding entre categorías.

La caché semántica puede reducir entre un 30% y un 70% los costos de la API de LLM. Pero sin el preprocesamiento de entrada y la verificación de aciertos, estás sirviendo respuestas obsoletas y llamándolo una victoria de rendimiento.


LemonData proporciona acceso unificado a más de 300 modelos de IA con caché, enrutamiento y optimización de costos integrados. Pruébalo gratis con $1 de crédito.

Share: