RAG con Claude API: Retrieval-Augmented Generation Step-by-Step

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

Documentazione e strumenti

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.

Condividi

Articoli Recenti

Categorie popolari