Guía10 min

Cache semántico en agentes LLM: exact-match primero, umbral alto, clave con tenant

Resumen

El cache semántico reusa la respuesta de un prompt parecido; el prompt caching reusa el prefijo KV del proveedor. Contrato: hash exacto primero, umbral alto (LiteLLM 0.8), clave con tenant/modelo/versión, TTL corto y cero hits en tools con efecto. Distinto de RAG y de recortar tool_results.

OpenAI
Un agente consulta primero un cache exacto y luego uno semántico con umbral alto antes de llamar al modelo

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

El cache semántico reusa una respuesta ya generada cuando el prompt nuevo se parece al viejo. El prompt caching del proveedor reusa el prefijo KV (tools, system, historial) y sigue generando tokens nuevos. Si mezclas los dos, o bajas el umbral “para que pegue más”, el agente contesta el ticket de Ana con la respuesta de Bruno.

Contrato: exact-match primero. Semántico después, con umbral alto. La clave lleva tenant, modelo y versión. Un hit con tools de efecto es un bug, no un ahorro.

No es memoria / RAG: RAG recupera documentos; esto reusa salidas del modelo. No es truncar tool_results: recortar payload no sustituye un cache. Y no es un Map global en el proceso: sin tenant y sin TTL, es una fuga.

Qué cacheas (y qué no)

CapaQué guardaCuándo pegaRiesgo en un agente
Exact-matchHash de prompt + modelo + tools + tenant + versiónByte a byteBajo, si la clave está completa
SemánticoEmbedding del prompt + respuestaCosine / distancia ≥ umbralAlto: “parecido” ≠ mismo pedido
Prompt caching (proveedor)Tensores KV del prefijoPrefijo idénticoBajo: no reusa la respuesta
RAGChunks de documentosRetrievalOtro problema: frescura y ACL

GPTCache documenta las dos primeras capas: exact match (misma pregunta, cero llamada) y similar search (Onnx + SQLite + Faiss + SearchDistanceEvaluation). Redis LangCache, en preview, hace search-antes / store-después con embeddings gestionados. LiteLLM expone redis-semantic, qdrant-semantic y valkey-semantic con similarity_threshold (0 = nada, 1 = exacto).

Empieza por exact-match. El 80 % de los FAQs de un bot se repiten letra por letra. El semántico entra cuando ya mediste hits exactos y tienes un umbral calibrado, no como atajo.

La clave no es el texto del usuario

Una clave mínima, en este orden:

  1. tenant_id (nunca un cache compartido entre clientes).
  2. model (cambiar de modelo invalida; el embedding también).
  3. toolset_version / hash del system prompt (un tool nuevo cambia la respuesta correcta).
  4. locale y canal (ES-GT ≠ EN-US; Telegram ≠ web).
  5. El prompt después de redactar PII — ver detectar y redactar.

Sin tenant, “¿cuál es el saldo?” de dos usuarios distintos es un hit. Sin versión, un cache de ayer ignora el tool que acabas de desplegar. Redis LangCache deja umbral, TTL y eviction en el servicio; LiteLLM pone ttl (ejemplo de docs: 120 s) y redis_semantic_cache_embedding_model pasado a litellm.embedding(). El embedding forma parte de la clave: cambiar text-embedding-3-small por ada-002 no es un detalle, es otro índice.

Umbral alto o no lo enciendas

LiteLLM documenta similarity_threshold = 0.8 en Redis semantic: 0.5 es “50 % de similitud”, no un default seguro. En un agente de soporte, 0.5 pega “cancela el pedido 4412” con “cancela el pedido 4413”. En un agente de refunds, eso es dinero.

Calibración mínima, una tarde:

  1. 30 pares deben pegar (parafraseo real del mismo FAQ).
  2. 30 pares no deben pegar (mismo verbo, distinto objeto: pedido, usuario, fecha, monto).
  3. Sube el umbral hasta que los 30 negativos den miss. Si para lograrlo matas los positivos, apaga el semántico y quédate en exact-match.

LangCache estima ahorro como costo mensual de output tokens × hit rate. Eso solo es cierto si el hit es la respuesta correcta. Un falso positivo sale más caro que la llamada.

Flujo exact-match, semántico y miss hacia el LLM

Dónde vive en el loop del agente

El sitio correcto es antes de chat.completions / messages.create, y solo en turnos sin efecto:

const key = cacheKey({ tenant, model, version, prompt });
const exact = await exactCache.get(key);
if (exact) return exact;

if (allowsSemantic(intent)) {
  const hit = await semantic.search(prompt, { tenant, threshold: 0.85 });
  if (hit) return hit.response;
}

const out = await llm.complete(prompt);
await exactCache.set(key, out, { ttlSec: 300 });
if (allowsSemantic(intent)) {
  await semantic.store({ prompt, response: out, tenant, version });
}
return out;

allowsSemantic es una lista blanca: FAQs, “qué es X”, políticas públicas, horarios. Lista negra: tools de pago, writes, emails, tickets, cualquier id de objeto. GPTCache tiene cache_enable_func y cache_skip=True precisamente para no buscar ni guardar. Úsalos; no improvises un if (Math.random()).

LangCache lo dice en dos POSTs: POST /v1/caches/{cacheId}/entries/search antes del LLM; POST /v1/caches/{cacheId}/entries después del miss. Si guardas en el miss y en el hit, duplicas. Si guardas un error 500, lo sirves en bucle.

No caches:

  • Turnos con tool calls pendientes o resultados de tools (el id de la llamada no es reutilizable).
  • Streaming a medias: o la respuesta completa, o nada.
  • Prompts con PII en claro. Redacta, cachea la máscara, nunca el documento.
  • Respuestas que dependen de la hora (“hoy”, “esta semana”) sin meter la fecha en la clave.

Exact vs semántico vs prompt caching

Tres palancas, tres facturas:

PalancaQuién cobraQué ahorrasQué no toca
Exact / semántico (tuyo)Redis, embeddings, tu infraOutput tokens + latencia de generaciónEl prefijo KV del proveedor
Prompt cachingOpenAI / AnthropicInput tokens del prefijo, hasta ~90 %La respuesta nueva
RAGTu índiceNada: es retrieval, no cacheReusar una salida previa

Pueden convivir: prompt caching en el system+tools (siempre el mismo prefijo) y exact-match en FAQs. El semántico es la capa más cara de operar mal. Si tu hit rate exacto ya es alto, no lo enciendas “porque LangCache existe”.

LiteLLM además cachea exacto en Redis / S3 / GCS / disco. Eso es hash, no embeddings. Para un agente, el orden de encendido es: exacto en Redis → medir → semántico con umbral ≥ 0.8 y tenant en la clave → o nada.

Umbral de similitud: hits válidos versus falsos positivos de pedidos distintos

Operación: TTL, invalidación, métricas

  • TTL corto por defecto. 120–300 s para FAQs que cambian; horas solo si el texto es un documento versionado.
  • Invalidación por versión, no por “flush global”. Un deploy de tools bumpa toolset_version y el cache viejo muere solo.
  • Métricas: hit rate exacto, hit rate semántico, false-positive rate (muestreo humano semanal), latencia p95 del search, costo de embeddings. Un hit rate alto con quejas de “me contestó otro caso” es un umbral bajo, no un éxito.
  • Aislamiento: Redis LangCache guarda en tu Redis; igual, un cacheId por tenant o un prefijo de clave. Un índice único “para todos los bots” es el anti-patrón.
  • Fallback: si el search semántico falla (timeout, 5xx), sigue al LLM. No bloquees el turno por el cache. Eso es el mismo criterio que un circuit breaker: el cache es una dependencia, no el camino crítico.

GPTCache permite next_cache (cadena). En producción, la cadena útil es exacto → semántico → LLM. No es exacto → semántico laxo → otro semántico más laxo.

Checklist

  1. Exact-match con clave tenant|model|version|hash(prompt).
  2. Semántico apagado hasta tener 30+/30− de calibración.
  3. Umbral ≥ 0.8 (LiteLLM) o el equivalente que deje los 30 negativos en miss.
  4. Lista blanca de intents; tools con efecto = cache_skip.
  5. TTL explícito; bump de versión en cada cambio de tools/system.
  6. PII redactada antes de embedder y de guardar.
  7. Métrica de falsos positivos, no solo de hit rate.
  8. Prompt caching del proveedor en paralelo, no como sustituto.

FAQ

¿Puedo cachear también el tool_result? No. El resultado de una tool es un hecho de esta ejecución (saldo, stock, id). Reusarlo es mentir. Recórtalo si es largo; no lo caches.

¿0.8 es magia? No. Es el ejemplo de LiteLLM. En refunds puede hacer falta 0.9 o no usar semántico. El umbral lo fija el set negativo, no el vendor.

¿LangCache en preview? Sí: Redis lo marca preview y sujeto a cambio. Úsalo como servicio de search/store, no como fuente de verdad del producto.

¿Y si el FAQ cambia? Versión en la clave o TTL corto. Un flush manual a las 3 AM no escala.

Si estás armando el agente desde cero, el curso de instalar un agente deja el loop listo; esta guía solo añade la capa de cache. El resto de operación vive en el hub de seguridad, coste y operación.