I modelli linguistici come Claude sono potenti, ma hanno un limite fondamentale: la loro conoscenza si ferma alla data di training. Per rispondere a domande su documenti aziendali, basi di conoscenza interne o dati aggiornati in tempo reale, hai bisogno del RAG — Retrieval-Augmented Generation. Il RAG non è una tecnica oscura riservata ai ricercatori AI: è un pattern implementabile in pochi giorni con strumenti open-source e cloud gratuiti.
In questa guida costruiamo un sistema RAG completo: ingestione documenti, generazione embeddings, ricerca semantica su Supabase pgvector e risposta contestuale con Claude. Ogni step è accompagnato da codice TypeScript pronto all’uso.
Cos’è il RAG e perché funziona
Il RAG si divide in due fasi: ingestione (offline) e retrieval + generation (online). Nella fase di ingestione, i tuoi documenti vengono spezzati in chunk, convertiti in vettori numerici (embeddings) e salvati in un database vettoriale. Nella fase di retrieval, quando arriva una domanda dell’utente, la domanda viene anch’essa convertita in un vettore e si cercano i chunk più simili per coseno similarity. Quei chunk vengono poi passati a Claude come contesto, e Claude genera una risposta basata su quel contesto specifico.
Il risultato: Claude risponde come se “sapesse” il contenuto dei tuoi documenti, anche se sono stati scritti ieri.
Step 1: chunking dei documenti
Il chunking è la fase più critica e più trascurata. Chunk troppo piccoli perdono contesto, chunk troppo grandi superano la finestra di contesto e diluiscono la rilevanza. Le regole base:
Dimensione chunk: 512-1024 token per documenti tecnici, 256-512 per FAQ e documentazione breve. Overlap: 50-100 token di sovrapposizione tra chunk consecutivi per non spezzare concetti a metà. Separatori: rispetta i confini naturali del documento — paragrafi, sezioni, heading H2.
// chunker.ts
interface Chunk {
text: string;
metadata: { source: string; position: number; };
}
function chunkText(text: string, source: string, chunkSize = 800, overlap = 100): Chunk[] {
const words = text.split(/\s+/);
const chunks: Chunk[] = [];
let start = 0;
while (start < words.length) {
const end = Math.min(start + chunkSize, words.length);
const chunkText = words.slice(start, end).join(" ");
if (chunkText.trim().length > 50) { // ignora chunk troppo corti
chunks.push({
text: chunkText,
metadata: { source, position: chunks.length },
});
}
start = end - overlap;
if (start >= words.length) break;
}
return chunks;
}
// Uso
const document = await fs.readFile("docs/manuale.md", "utf-8");
const chunks = chunkText(document, "manuale.md");
console.log(`${chunks.length} chunk generati`);Step 2: embeddings con Voyage AI
Anthropic consiglia Voyage AI per gli embeddings da usare con Claude — sono ottimizzati per la retrieval in italiano e codice sorgente. Il modello voyage-3 genera vettori a 1024 dimensioni. Installa il client:
npm install voyageai// embeddings.ts
import VoyageAI from "voyageai";
const voyage = new VoyageAI({ apiKey: process.env.VOYAGE_API_KEY! });
async function generateEmbeddings(chunks: Chunk[]): Promise<number[][]> {
const texts = chunks.map((c) => c.text);
// Batch da 128 elementi (limite API)
const batches = [];
for (let i = 0; i < texts.length; i += 128) {
batches.push(texts.slice(i, i + 128));
}
const allEmbeddings: number[][] = [];
for (const batch of batches) {
const result = await voyage.embed({
input: batch,
model: "voyage-3",
});
allEmbeddings.push(...result.data.map((d) => d.embedding));
}
return allEmbeddings;
}Step 3: pgvector su Supabase
Supabase include pgvector nativo — niente setup aggiuntivo. Abilita l’estensione e crea la tabella:
-- Abilita pgvector (una volta sola)
CREATE EXTENSION IF NOT EXISTS vector;
-- Tabella documenti con embedding
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding VECTOR(1024),
metadata JSONB DEFAULT '{}',
source TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indice HNSW per similarity search veloce
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);Inserimento dei chunk con i loro embeddings:
// ingest.ts
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
async function ingestChunks(chunks: Chunk[], embeddings: number[][]) {
const rows = chunks.map((chunk, i) => ({
content: chunk.text,
embedding: embeddings[i],
source: chunk.metadata.source,
metadata: chunk.metadata,
}));
const { error } = await supabase.from("documents").insert(rows);
if (error) throw error;
console.log(`${rows.length} chunk salvati su Supabase`);
}Step 4: similarity search con match_documents
Crea una funzione Postgres per la ricerca semantica:
-- Funzione di ricerca semantica
CREATE OR REPLACE FUNCTION match_documents(
query_embedding VECTOR(1024),
match_threshold FLOAT DEFAULT 0.7,
match_count INT DEFAULT 5
)
RETURNS TABLE (
id BIGINT,
content TEXT,
source TEXT,
similarity FLOAT
)
LANGUAGE SQL STABLE
AS $$
SELECT
id,
content,
source,
1 - (embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (embedding <=> query_embedding) > match_threshold
ORDER BY embedding <=> query_embedding
LIMIT match_count;
$$;// retrieval.ts
async function findRelevantChunks(query: string, topK = 5) {
// Genera embedding della domanda
const queryEmbedding = await voyage.embed({
input: [query],
model: "voyage-3",
});
const embedding = queryEmbedding.data[0].embedding;
// Ricerca semantica
const { data, error } = await supabase.rpc("match_documents", {
query_embedding: embedding,
match_threshold: 0.7,
match_count: topK,
});
if (error) throw error;
return data;
}Step 5: risposta contestuale con Claude
Assembla il contesto e genera la risposta finale:
// rag.ts
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
async function answerWithRAG(question: string): Promise<string> {
// 1. Recupera i chunk rilevanti
const relevantChunks = await findRelevantChunks(question, 5);
if (relevantChunks.length === 0) {
return "Non ho trovato informazioni rilevanti nei documenti disponibili.";
}
// 2. Costruisci il contesto
const context = relevantChunks
.map((c, i) => `[Fonte ${i + 1}: ${c.source}]
${c.content}`)
.join("
---
");
// 3. Chiama Claude con il contesto
const response = await anthropic.messages.create({
model: "claude-opus-4-6",
max_tokens: 1024,
system: `Sei un assistente che risponde a domande basandosi ESCLUSIVAMENTE sui documenti forniti come contesto.
Se la risposta non è nei documenti, dillo esplicitamente invece di inventare.
Cita sempre la fonte quando possibile.`,
messages: [
{
role: "user",
content: `Contesto dai documenti:
${context}
---
Domanda: ${question}`,
},
],
});
return response.content[0].type === "text" ? response.content[0].text : "";
}
// Test
const answer = await answerWithRAG("Qual è la policy di rimborso?");
console.log(answer);Ottimizzazioni avanzate
Re-ranking: dopo il retrieval semantico, usa un cross-encoder per riordinare i chunk in base alla rilevanza effettiva rispetto alla domanda. Voyage AI offre un modello di reranking (rerank-2) che migliora la qualità del contesto del 15-25% sui benchmark. Query expansion: prima del retrieval, chiedi a Claude di generare 3 varianti della domanda originale — poi cerca con tutte e 3 e de-duplica i risultati. Migliora il recall significativamente per domande ambigue. Caching degli embeddings: salva un hash MD5 di ogni chunk — se il chunk non è cambiato, non rigenera l’embedding. Riduce i costi di re-ingestione dell’80-90%.
Risorse correlate
Approfondisci sul blog
- Creare un agente AI con JavaScript e Claude
- Supabase: database, auth e realtime per developer
- Streaming con Claude API in JavaScript
Documentazione e strumenti
- Guida RAG ufficiale Anthropic
- Documentazione embeddings Voyage AI
- Vettori e similarity search su Supabase
FAQ
Posso usare OpenAI embeddings invece di Voyage AI?
Sì. text-embedding-3-large di OpenAI (3072 dimensioni) funziona bene con Claude. Voyage AI è consigliato perché ottimizzato specificamente per il retrieval nei contesti in cui si usa Claude, ma non è obbligatorio. Se usi OpenAI embeddings, crea la tabella pgvector con VECTOR(3072).
Quanti documenti può gestire questo sistema?
Con pgvector e l’indice HNSW, il sistema scala facilmente fino a milioni di vettori. Supabase gestisce l’infrastruttura — non devi preoccuparti di sharding o replica. Per corpus molto grandi (>10M chunk), considera Pinecone o Weaviate come database vettoriale dedicato.
Come gestisco i documenti che cambiano nel tempo?
Implementa una pipeline di re-ingestione incrementale: salva un hash del contenuto di ogni documento. Quando un documento cambia, elimina i vecchi chunk (DELETE FROM documents WHERE source = ‘file.pdf’) e reinserisci i nuovi. Un job notturno che controlla i file modificati negli ultimi 24 ore è sufficiente per la maggior parte dei casi d’uso.
Il RAG funziona bene con documenti in italiano?
Sì. Voyage AI e i modelli Claude gestiscono l’italiano nativamente. La similarity search funziona correttamente perché i vettori catturano il significato semantico indipendentemente dalla lingua. Per documenti tecnici misti italiano/inglese, il sistema funziona senza configurazioni aggiuntive.

