Nuestra factura de LLM API estaba creciendo un 30% mes tras mes. El tráfico aumentaba, pero no tan rápido. Cuando analicé nuestros registros de consultas, encontré el verdadero problema: los usuarios hacen las mismas preguntas de diferentes maneras.
“¿Cuál es su política de devolución?”, “¿Cómo devuelvo algo?” y “¿Puedo obtener un reembolso?” Todos llegamos a nuestro LLM por separado, generando respuestas casi idénticas y cada una incurriendo en costos completos de API.
El almacenamiento en caché de coincidencia exacta, la primera solución obvia, capturó sólo el 18% de estas llamadas redundantes. La misma pregunta semántica, formulada de manera diferente, pasó por alto el caché por completo.
Entonces, implementé el almacenamiento en caché semántico en función del significado de las consultas, no de cómo están redactadas. Después de implementarlo, nuestra tasa de aciertos de caché aumentó al 67 %, lo que redujo los costos de API de LLM en un 73 %. Pero llegar allí requiere resolver problemas que las implementaciones ingenuas pasan por alto.
Por qué el almacenamiento en caché de coincidencias exactas se queda corto
El almacenamiento en caché tradicional utiliza texto de consulta como clave de caché. Esto funciona cuando las consultas son idénticas:
# Almacenamiento en caché de coincidencia exacta
clave_caché = hash(texto_consulta)
si cache_key en caché:
devolver caché[cache_key]
Pero los usuarios no formulan las preguntas de manera idéntica. Mi análisis de 100.000 consultas de producción encontró:
-
Sólo el 18% eran duplicados exactos de consultas anteriores
-
47% fueron semánticamente similares a consultas anteriores (misma intención, redacción diferente)
-
35% fueron consultas realmente novedosas
Ese 47% representó enormes ahorros de costos que nos estábamos perdiendo. Cada consulta semánticamente related desencadenó una llamada LLM completa, generando una respuesta casi idéntica a una que ya habíamos calculado.
Arquitectura de almacenamiento en caché semántica
El almacenamiento en caché semántico reemplaza las claves basadas en texto con una búsqueda de similitudes basada en incrustaciones:
clase SemanticCache:
def __init__(self, embedding_model, similarity_threshold=0.92):
self.embedding_model = modelo_incrustación
self.threshold = similitud_umbral
self.vector_store = VectorStore() #FAISS, Piña, and many others.
self.response_store = TiendaRespuesta() # Redis, DynamoDB, and many others.
def get(self, consulta: str) -> Opcional[str]:
“””Devolver respuesta almacenada en caché si existe una consulta semánticamente related.”””
query_embedding = self.embedding_model.encode(consulta)
# Encuentra la consulta en caché más related
coincidencias = self.vector_store.search(query_embedding, top_k=1)
si coincide y coincide[0].similitud >= self.threshold:
cache_id = coincidencias[0].identificación
devolver self.response_store.get(cache_id)
regresar Ninguno
def set(self, consulta: str, respuesta: str):
“””Par de consulta-respuesta en caché.”””
query_embedding = self.embedding_model.encode(consulta)
cache_id = generar_id()
self.vector_store.add(cache_id, query_embedding)
self.response_store.set(cache_id, {
‘consulta’: consulta,
‘respuesta’: respuesta,
‘marca de tiempo’: fecha y hora.utcnow()
})
La thought clave: en lugar de aplicar hash al texto de la consulta, incrusto consultas en el espacio vectorial y encuentro consultas almacenadas en caché dentro de un umbral de similitud.
El problema del umbral
El umbral de similitud es el parámetro crítico. Si lo configuras demasiado alto, perderás accesos de caché válidos. Si lo estableces demasiado bajo, obtendrás respuestas incorrectas.
Nuestro umbral inicial de 0,85 parecía razonable; 85% related debería ser “la misma pregunta”, ¿verdad?
Equivocado. En 0,85, obtuvimos aciertos de caché como:
Son preguntas diferentes con respuestas diferentes. Devolver la respuesta almacenada en caché sería incorrecto.
Descubrí que los umbrales óptimos varían según el tipo de consulta:
|
Tipo de consulta |
Umbral óptimo |
Razón basic |
|
Preguntas estilo preguntas frecuentes |
0,94 |
Se necesita alta precisión; Las respuestas incorrectas dañan la confianza. |
|
Búsquedas de productos |
0,88 |
Más tolerancia para los casi partidos |
|
Consultas de soporte |
0,92 |
Equilibrio entre cobertura y precisión |
|
Consultas transaccionales |
0,97 |
Tolerancia muy baja a los errores. |
Implementé umbrales específicos del tipo de consulta:
clase AdaptiveSemanticCache:
def __init__(yo):
auto.umbrales = {
‘preguntas frecuentes’: 0,94,
‘búsqueda’: 0,88,
‘soporte’: 0,92,
‘transaccional’: 0,97,
‘predeterminado’: 0,92
}
self.query_classifier=QueryClassifier()
def get_threshold(self, consulta: str) -> flotante:
tipo_consulta = self.query_classifier.classify(consulta)
devolver self.thresholds.get(query_type, self.thresholds[‘default’])
def get(self, consulta: str) -> Opcional[str]:
umbral = self.get_threshold (consulta)
query_embedding = self.embedding_model.encode(consulta)
coincidencias = self.vector_store.search(query_embedding, top_k=1)
si coincide y coincide[0].similitud >= umbral:
devolver self.response_store.get(coincide[0].identificación)
regresar Ninguno
Metodología de ajuste de umbrales
No podía ajustar los umbrales a ciegas. Necesitaba información básica sobre qué pares de consultas eran en realidad “iguales”.
Nuestra metodología:
Paso 1: Pares de consultas de ejemplo. Probé 5000 pares de consultas en varios niveles de similitud (0,80-0,99).
Paso 2: Etiquetado humano. Los anotadores etiquetaron cada par como “la misma intención” o “intención diferente”. Utilicé tres anotadores por par y obtuve una mayoría de votos.
Paso 3: Calcular curvas de precisión/recuperación. Para cada umbral, calculamos:
-
Precisión: de los aciertos de caché, ¿qué fracción tuvo la misma intención?
-
Recuerde: de pares con la misma intención, ¿qué fracción almacenamos en caché?
def Compute_precision_recall (pares, etiquetas, umbral):
“””Calcular la precisión y recuperarla en un umbral de similitud dado.”””
predicciones = [1 if pair.similarity >= threshold else 0 for pair in pairs]
true_positives = suma(1 para p, l en zip(predicciones, etiquetas) si p == 1 y l == 1)
false_positives = suma(1 para p, l en zip(predicciones, etiquetas) si p == 1 y l == 0)
false_negatives = suma(1 para p, l en zip(predicciones, etiquetas) si p == 0 y l == 1)
precisión = verdaderos_positivos / (verdaderos_positivos + falsos_positivos) si (verdaderos_positivos + falsos_positivos) > 0 más 0
recordar = verdaderos_positivos / (verdaderos_positivos + falsos_negativos) si (verdaderos_positivos + falsos_negativos) > 0 más 0
precisión de retorno, recuperación
Paso 4: seleccione el umbral según el costo de los errores. Para las consultas frecuentes en las que las respuestas incorrectas dañan la confianza, optimicé para obtener precisión (el umbral de 0,94 dio una precisión del 98%). Para consultas de búsqueda en las que perder un hit de caché solo cuesta dinero, optimicé para la recuperación (umbral de 0,88).
Sobrecarga de latencia
El almacenamiento en caché semántico agrega latencia: debe incrustar la consulta y buscar en el almacén de vectores antes de saber si debe llamar al LLM.
Nuestras medidas:
|
Operación |
Latencia (p50) |
Latencia (p99) |
|
Incrustación de consultas |
12 ms |
28ms |
|
Búsqueda de vectores |
8ms |
19ms |
|
Búsqueda complete de caché |
20 ms |
47ms |
|
Llamada API de LLM |
850 ms |
2400ms |
La sobrecarga de 20 ms es insignificante en comparación con la llamada LLM de 850 ms que evitamos en los aciertos de caché. Incluso en p99, la sobrecarga de 47 ms es aceptable.
Sin embargo, los errores de caché ahora tardan 20 ms más que antes (incrustación + búsqueda + llamada LLM). Con nuestra tasa de acierto del 67%, las matemáticas funcionan favorablemente:
Mejora de la latencia neta del 65% junto con la reducción de costos.
invalidación de caché
Las respuestas almacenadas en caché quedan obsoletas. La información del producto cambia, las políticas se actualizan y la respuesta correcta de ayer se convierte en la respuesta incorrecta de hoy.
Implementé tres estrategias de invalidación:
-
TTL basado en tiempo
Caducidad easy según el tipo de contenido:
TTL_BY_CONTENT_TYPE = {
‘precios’: timedelta(horas=4), # Cambia con frecuencia
‘política’: timedelta(días=7), # Cambia raramente
‘product_info’: timedelta(días=1), # Actualización diaria
‘general_faq’: timedelta(días=14), # Muy estable
}
-
Invalidación basada en eventos
Cuando los datos subyacentes cambian, invalide las entradas de caché relacionadas:
clase CacheInvalidator:
def on_content_update(self, content_id: str, content_type: str):
“””Invalidar entradas de caché relacionadas con contenido actualizado.”””
# Buscar consultas en caché que hagan referencia a este contenido
consultas_afectadas = self.find_queries_referencing(content_id)
para query_id en consultas_afectadas:
self.cache.invalidate(query_id)
self.log_invalidation(content_id, len(consultas_afectadas))
-
Detección de estancamiento
Para las respuestas que podrían quedar obsoletas sin eventos explícitos, implementé controles de actualización periódicos:
def check_freshness(self, cached_response: dict) -> bool:
“””Verificar que la respuesta almacenada en caché aún sea válida.”””
# Vuelva a ejecutar la consulta con los datos actuales
respuesta_fresca = self.generate_response(respuesta_caché[‘query’])
# Comparar similitud semántica de respuestas
cached_embedding = self.embed(cached_response[‘response’])
Fresh_embedding = self.embed(fresh_response)
similitud = coseno_similaridad(cached_embedding, Fresh_embedding)
# Si las respuestas divergieron significativamente, invalidar
si similitud < 0,90:
self.cache.invalidate(cached_response[‘id’])
devolver falso
devolver verdadero
Realizamos comprobaciones de actualización en una muestra de entradas almacenadas en caché diariamente, detectando la obsolescencia que TTL y la invalidación basada en eventos pasan por alto.
Resultados de producción
Después de tres meses en producción:
|
Métrico |
Antes |
Después |
Cambiar |
|
Tasa de aciertos de caché |
18% |
67% |
+272% |
|
Costos de API de LLM |
$47K/mes |
$12.7K/mes |
-73% |
|
Latencia media |
850 ms |
300 ms |
-65% |
|
Tasa de falsos positivos |
N / A |
0,8% |
— |
|
Quejas de clientes (respuestas incorrectas) |
Base |
+0,3% |
Aumento mínimo |
La tasa de falsos positivos del 0,8% (consultas en las que devolvimos una respuesta almacenada en caché que period semánticamente incorrecta) estuvo dentro de límites aceptables. Estos casos ocurrieron principalmente en los límites de nuestro umbral, donde la similitud estaba justo por encima del límite pero la intención difería ligeramente.
Escollos a evitar
No utilice un único umbral world. Los diferentes tipos de consultas tienen diferente tolerancia a los errores. Ajuste los umbrales por categoría.
No omita el paso de incrustación en los aciertos de caché. Es posible que tenga la tentación de omitir la sobrecarga de incrustación al devolver respuestas almacenadas en caché, pero necesita la incrustación para la generación de claves de caché. Los gastos generales son inevitables.
No olvides la invalidación. El almacenamiento en caché semántico sin una estrategia de invalidación genera respuestas obsoletas que erosionan la confianza del usuario. Invalidación de compilación desde el primer día.
No guardes todo en caché. Algunas consultas no deben almacenarse en caché: respuestas personalizadas, información urgente, confirmaciones de transacciones. Construya reglas de exclusión.
def debería_cache(self, consulta: str, respuesta: str) -> bool:
“””Decide si la respuesta debe almacenarse en caché.””
# No almacene en caché las respuestas personalizadas
si self.contains_personal_info(respuesta):
devolver falso
# No almacene en caché información urgente
si self.is_time_SENSITIVE (consulta):
devolver falso
# No almacene en caché las confirmaciones transaccionales
si self.is_transactional(consulta):
devolver falso
devolver verdadero
Conclusiones clave
El almacenamiento en caché semántico es un patrón práctico para el management de costos de LLM que captura errores de almacenamiento en caché de coincidencia exacta de redundancia. Los desafíos clave son el ajuste de umbrales (use umbrales específicos del tipo de consulta basados en análisis de precisión/recuperación) y la invalidación de caché (mix TTL, detección basada en eventos y obsolescencia).
Con una reducción de costos del 73 %, esta fue nuestra optimización de mayor retorno de la inversión para sistemas LLM de producción. La complejidad de la implementación es moderada, pero el ajuste del umbral requiere una atención cuidadosa para evitar la degradación de la calidad.
Sreenivasa Reddy Hulebeedu Reddy es un ingeniero de software program líder.
¡Bienvenido a la comunidad VentureBeat!
Nuestro programa de publicaciones invitadas es donde los expertos técnicos comparten conocimientos y brindan análisis profundos neutrales y no adquiridos sobre inteligencia synthetic, infraestructura de datos, ciberseguridad y otras tecnologías de vanguardia que dan forma al futuro de las empresas.
Leer más de nuestro programa de publicaciones de invitados y consulte nuestro pautas ¡Si estás interesado en contribuir con un artículo propio!













