Client-Side RAG con Transformers.js: AI nel browser senza server
La maggior parte delle applicazioni AI dipende da server remoti: le query dellβutente vengono inviate a unβAPI, processate su GPU cloud, e le risposte tornano indietro. Questo approccio funziona, ma ha costi nascosti: latenza, costi di infrastruttura, e soprattutto il dato dellβutente esce dal browser.
Con WebRAG AI ho costruito un assistente conversazionale che gira interamente nel browser: embeddings, ricerca semantica e risposte contestuali, tutto senza un singolo byte inviato a server esterni.
Architettura: cosa serve per un RAG client-side
Un sistema RAG ha tre componenti fondamentali:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β BROWSER β
β β
β ββββββββββββββββ ββββββββββββββββββββββββ β
β β Knowledge β β Embedding Model β β
β β Base (JSON) β β (Transformers.js) β β
β β ~112 chunks β β e5-base / e5-small β β
β ββββββββ¬ββββββββ ββββββββββββ¬ββββββββββββ β
β β β β
β βΌ βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββ β
β β Vector Search β β
β β (cosine similarity, top-k) β β
β ββββββββββββββββββββ¬ββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββ β
β β Risultati con Source β β
β β (citazioni cliccabili ai contenuti) β β
β ββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Zero connessioni a server esterni
1. Knowledge Base: chunking e pre-computazione
Il primo passo e costruire una base di conoscenza. I contenuti del sito (blog post, pagine progetto, poesie) vengono:
- Estratti a build time da Markdown/HTML
- Suddivisi in chunk di ~1000 token con overlap di 200 token
- Vettorializzati con il modello di embedding
- Salvati come JSON statico (
rag-embeddings.json)
// Configurazione chunking
const CHUNK_CONFIG = {
maxTokens: 1000, // Dimensione chunk
overlapTokens: 200, // Overlap tra chunk consecutivi
minTokens: 50 // Chunk minimo (evita frammenti inutili)
};
Perche pre-computare? Calcolare embeddings runtime per 112 chunk richiederebbe 30-60 secondi su CPU. Pre-computandoli a build time, la ricerca e istantanea (<100ms).
2. Modello di Embedding: Transformers.js + ONNX
Transformers.js di Hugging Face permette di eseguire modelli ONNX direttamente nel browser:
import { pipeline } from '@xenova/transformers';
// Carica modello di embedding multilingue
const embedder = await pipeline(
'feature-extraction',
'Xenova/multilingual-e5-base', // ~220MB, supporta italiano
{
device: 'webgpu', // GPU se disponibile
dtype: 'q8' // Quantizzazione 8-bit
}
);
// Genera embedding per la query utente
const queryEmbedding = await embedder(
'passage: come funziona il monitoraggio sismico?',
{ pooling: 'mean', normalize: true }
);
Scelta del modello: multilingual-e5-base supporta 100+ lingue (cruciale per un sito in italiano) e ha un buon rapporto qualita/dimensione.
3. Ricerca Semantica: cosine similarity
Con gli embeddings pre-computati e lβembedding della query, la ricerca e un semplice calcolo di similarita:
function cosineSimilarity(a, b) {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
// Cerca i chunk piu rilevanti
function search(queryEmbedding, knowledgeBase, topK = 5) {
return knowledgeBase
.map(chunk => ({
...chunk,
score: cosineSimilarity(queryEmbedding, chunk.embedding)
}))
.sort((a, b) => b.score - a.score)
.slice(0, topK);
}
Con 112 chunk, questa operazione richiede <5ms. Anche con migliaia di chunk, resterebbe sotto i 50ms.
Le sfide pratiche (e come risolverle)
ONNX/WASM: la configurazione che funziona davvero
La documentazione di Transformers.js non copre tutti i problemi reali. Dopo settimane di debugging, questa e la configurazione stabile:
import { env } from '@xenova/transformers';
// Disabilita proxy worker (causa crash su alcuni browser)
env.backends.onnx.wasm.proxy = false;
// Disabilita caching browser (privacy-first)
env.useBrowserCache = false;
// Quantizzazione stabile per WASM
const targetDtype = 'q8'; // NON fp16 su WASM
// Nella generazione (se usi LLM)
const generateConfig = {
use_cache: false, // Previene buffer overflow
num_beams: 1 // No beam search (instabile su WASM)
};
Lezione appresa: wasm.proxy = true (il default) lancia un Web Worker separato per ONNX. Questo funziona nel 90% dei casi, ma causa crash silenziosi su Safari e browser mobile. Disabilitandolo, il modello gira nel thread principale β leggermente piu lento ma stabile al 100%.
WebGPU vs WASM: fallback graceful
async function detectBestBackend() {
// Prova WebGPU (10x piu veloce)
if (navigator.gpu) {
try {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) return 'webgpu';
} catch (e) {
console.warn('[RAG] WebGPU non disponibile:', e);
}
}
// Fallback a WASM (funziona ovunque)
return 'wasm';
}
Risultati reali: | Backend | Load time | Search latency | Supporto browser | |βββ|ββββ|βββββ|ββββββ| | WebGPU | ~3s | <20ms | Chrome 121+, Edge 121+ | | WASM | ~8s | <100ms | Tutti i browser moderni |
Privacy: sessionStorage only
Per un sito GDPR-compliant, nessun dato deve persistere tra sessioni:
// NO IndexedDB, NO localStorage, NO Browser Cache
// Solo sessionStorage (si cancella alla chiusura del tab)
env.useBrowserCache = false; // Transformers.js non cachera modelli
env.cacheDir = undefined; // Nessuna directory cache
// Consenso utente per sessione
if (!sessionStorage.getItem('ai-consent')) {
showConsentDialog(); // L'utente accetta per questa sessione
}
Trade-off: il modello (~120-220 MB) viene riscaricato ad ogni sessione. E un costo accettabile per la privacy totale.
Performance reali
Testato su hardware consumer (laptop 2023, no GPU dedicata):
| Metrica | Valore |
|---|---|
| Download modello | ~5-15s (dipende dalla rete) |
| Prima query | <200ms |
| Query successive | <100ms |
| Memoria | ~300-500MB |
| Knowledge base | 112 chunk, ~40 documenti |
Quando ha senso un RAG client-side?
Si, se:
- Il corpus e piccolo/medio (< 10.000 chunk)
- La privacy e un requisito non negoziabile
- Non vuoi costi di infrastruttura server
- Il target e desktop/laptop (non mobile)
No, se:
- Hai milioni di documenti
- Serve real-time su mobile
- Il modello di embedding non supporta la tua lingua
- Vuoi risposte generative complesse (serve un LLM completo)
Risorse
- WebRAG AI - Lβimplementazione live su novelli.me
- Transformers.js - La libreria che rende tutto possibile
- Guida agli Embeddings - Pre-computazione e build integration
- ONNX Runtime Web - Il runtime che esegue i modelli nel browser