Semantic cache para agentes IA: reduce el coste un 50%
Tienes un agente de atención al cliente. Cada mes, el 35% de las preguntas que llegan son variaciones de lo mismo:
"¿Cuánto cuesta?", "¿Qué precio tiene?", "¿Cuáles son vuestros planes?", "¿Tenéis tarifa mensual?", "¿El precio incluye IVA?"
Son la misma pregunta. Formulada de cinco formas distintas. Y tu agente llama al LLM cinco veces, pagando por cada llamada.
La caché exacta no sirve aquí. Una caché tradicional busca coincidencias exactas de texto. "¿Cuánto cuesta?" y "¿Qué precio tiene?" son cadenas completamente distintas — no hay hit posible.
La solución es una caché semántica: en vez de comparar texto, comparas el significado.
Por qué las preguntas repetidas se llevan la mitad del presupuesto en LLM
Un agente IA de producción con 500 usuarios diarios puede tener entre el 40% y el 70% de sus preguntas en zonas de alta recurrencia — preguntas sobre precios, horarios, políticas, funcionamiento del producto. Son distintas en la forma, idénticas en la intención.
Cada llamada a un modelo de alta gama cuesta entre 0,003€ y 0,015€ dependiendo del volumen de tokens. Parece poco. Pero con 350 preguntas recurrentes al día, estás pagando entre 1€ y 5€ diarios por respuestas que ya tienes.
Al mes: hasta 150€ que podrías eliminar casi por completo.
Y eso sin contar la latencia. Una llamada al LLM tarda entre 600ms y 2 segundos. Una respuesta desde caché tarda menos de 50ms. La diferencia la nota el usuario en cada interacción.
Cómo funciona la caché semántica
En vez de guardar la pregunta como texto y buscar coincidencias exactas, guardas su embedding — una representación vectorial del significado — y comparas por similitud coseno.
Flujo completo:
- Llega la pregunta del usuario
- Calculas el embedding de la pregunta (una sola llamada barata a un modelo de embeddings)
- Buscas en la caché vectorial si hay alguna entrada con similitud > 0.92 (umbral configurable)
- Si hay hit: devuelves la respuesta cacheada en < 50ms, coste cero en LLM
- Si hay miss: llamas al LLM, guardas la respuesta en caché junto con su embedding
La clave está en el umbral de similitud. Con 0.92, capturas variaciones del mismo concepto sin confundir preguntas semánticamente distintas.
Implementación con TypeScript, Supabase y pgvector
Primero, el schema en Supabase con la extensión vector:
create extension if not exists vector;
create table semantic_cache (
id uuid primary key default gen_random_uuid(),
agent_id text not null,
question text not null,
response text not null,
embedding vector(1536),
created_at timestamptz default now()
);
-- Índice para búsqueda por similitud coseno
create index on semantic_cache
using ivfflat (embedding vector_cosine_ops)
with (lists = 100);
-- Función de búsqueda por similitud
create function match_cache_entries(
query_embedding vector(1536),
match_threshold float,
match_count int,
p_agent_id text
) returns table (id uuid, question text, response text, similarity float)
language sql stable as $$
select id, question, response,
1 - (embedding <=> query_embedding) as similarity
from semantic_cache
where 1 - (embedding <=> query_embedding) > match_threshold
and agent_id = p_agent_id
order by embedding <=> query_embedding
limit match_count;
$$;
Y la implementación en TypeScript con el Vercel AI SDK:
import { embed } from 'ai'
import { openai } from '@ai-sdk/openai'
import { createClient } from '@supabase/supabase-js'
import Anthropic from '@anthropic-ai/sdk'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
)
const anthropic = new Anthropic()
const SIMILARITY_THRESHOLD = 0.92
async function getEmbedding(text: string): Promise<number[]> {
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: text,
})
return embedding
}
export async function agentWithSemanticCache(
question: string,
agentId: string,
systemPrompt: string
): Promise<{ response: string; cacheHit: boolean }> {
// 1. Embedding de la pregunta entrante
const embedding = await getEmbedding(question)
// 2. Buscar en caché semántica
const { data: cacheResult } = await supabase.rpc('match_cache_entries', {
query_embedding: embedding,
match_threshold: SIMILARITY_THRESHOLD,
match_count: 1,
p_agent_id: agentId,
})
if (cacheResult && cacheResult.length > 0) {
return { response: cacheResult[0].response, cacheHit: true }
}
// 3. Cache miss: llamar al LLM
const result = await anthropic.messages.create({
model: 'claude-sonnet-5-20260101',
max_tokens: 1024,
system: systemPrompt,
messages: [{ role: 'user', content: question }],
})
const response =
result.content[0].type === 'text' ? result.content[0].text : ''
// 4. Guardar en caché para consultas futuras
await supabase.from('semantic_cache').insert({
agent_id: agentId,
question,
response,
embedding,
})
return { response, cacheHit: false }
}
El coste de un embedding con text-embedding-3-small es aproximadamente 50 veces menor que una llamada a Claude. Vale la pena calcularlo incluso en cada pregunta.
Qué preguntas cachear y cuáles no
No todas las preguntas se benefician igual de la caché semántica. La regla práctica:
Sí cachear:
- Preguntas sobre producto, precio o políticas de la empresa
- Preguntas sobre funcionamiento general del sistema
- FAQs con respuesta fija o que cambia raramente
No cachear:
- Preguntas que dependen del contexto del usuario (saldo, historial, estado del pedido)
- Preguntas con datos personales o identificadores únicos
- Preguntas cuya respuesta cambia en tiempo real
Para las que no deberían cachearse, añade una capa de detección antes de la búsqueda en caché:
function shouldSkipCache(question: string): boolean {
const skipPatterns = [
/mi\s+(cuenta|pedido|factura|saldo)/i,
/\b\d{5,}\b/, // números de referencia o pedido
/cuándo\s+llega\s+mi/i,
]
return skipPatterns.some(p => p.test(question))
}
Una caché mal aplicada — que sirve respuestas genéricas a preguntas que necesitan datos del usuario — genera respuestas incorrectas que frustran al usuario más que la latencia.
Cuándo tiene sentido implementarlo
La caché semántica empieza a amortizarse cuando tienes más de 200 preguntas diarias a tu agente con un porcentaje de recurrencia mayor al 30%.
Por debajo de eso, el overhead de mantener la caché (coste de embeddings, complejidad de infraestructura) no justifica el ahorro.
En los proyectos de integración IA que construimos en DAILYMP, implementamos la caché semántica en la fase de optimización — después de tener el agente funcionando y con datos reales de uso. Añadirla desde el primer día sin conocer los patrones de pregunta es optimización prematura.
Si ya tienes un agente en producción y los costes de API empiezan a ser relevantes, este patrón suele reducirlos entre un 40% y un 70% sin cambiar el comportamiento del agente para el usuario.
Métricas que deberías monitorizar
Una vez en producción, mide:
- Cache hit rate: porcentaje de preguntas respondidas desde caché. Objetivo: > 40% en agentes con alto volumen de FAQ
- Average similarity score: la similitud media de los hits. Si está muy cerca del umbral (< 0.94), considera subirlo para evitar respuestas incorrectas
- Cache entry age distribution: qué tan "fresca" es tu caché. Entradas con más de 30 días deben revisarse si el producto o los precios han cambiado
Loguea los hits y misses en Supabase — con el agentId y un timestamp — y tendrás los datos para ajustar el umbral semana a semana.
Si quieres ver cómo aplicaría este sistema a tu agente y estimar el ahorro real antes de implementarlo, en 30 minutos te hago el cálculo con tus datos actuales.