Parliamo

project-cherry.dev

Intermedio14 min

RAG Basics

Come far rispondere l'AI usando i tuoi documenti aziendali. Pipeline completa dal chunking alla ricerca semantica, con l'esempio di 200 linee guida cliniche.

RAG Basics

La Dott.ssa Elena Bianchi ha un problema comune: 200 linee guida cliniche in PDF sparse in cartelle e sottocartelle. I collaboratori non le trovano, i nuovi specializzandi non sanno che esistono. RAG trasforma quel caos in un assistente che risponde citando le fonti esatte.

Il problema

L'AI generica non conosce i tuoi documenti. Se chiedi "Qual è il protocollo per l'ipertensione resistente?", l'AI risponde con conoscenza generica trovata nel training. Non sa che l'ambulatorio della Dott.ssa Bianchi ha un protocollo interno aggiornato a gennaio 2026 con dosaggi specifici e percorsi personalizzati.

RAG (Retrieval-Augmented Generation) risolve questo: prima cerca nei tuoi documenti, poi genera la risposta basandosi su quello che ha trovato. Non inventa — cita.

Come funziona RAG

Il processo si divide in tre fasi distinte:

text
FASE 1 — INDICIZZAZIONE (una tantum)
Documenti PDF → Divisione in chunk → Calcolo embeddings → Salvataggio nel database vettoriale

FASE 2 — RETRIEVAL (ad ogni domanda)
Domanda utente → Embedding della domanda → Ricerca semantica → Top-K chunk rilevanti

FASE 3 — GENERATION (ad ogni domanda)
Chunk trovati + Domanda originale → Prompt aumentato → Risposta con citazioni

La fase 1 si fa una volta. Le fasi 2 e 3 avvengono in tempo reale ad ogni domanda.

Chunking: come spezzare i documenti

Il chunking è la decisione più importante di tutta la pipeline. Chunk troppo piccoli (100 parole) perdono contesto — il modello non capisce di cosa si parla. Chunk troppo grandi (2000 parole) aggiungono rumore — il modello si distrae con informazioni irrilevanti.

python
def crea_chunk(testo, dimensione=500, sovrapposizione=50):
    """
    Spezza un documento in chunk con overlap.
    L'overlap garantisce che nessun concetto venga tagliato a metà.
    """
    parole = testo.split()
    chunk_lista = []

    inizio = 0
    while inizio < len(parole):
        fine = inizio + dimensione
        chunk = " ".join(parole[inizio:fine])
        chunk_lista.append(chunk)
        # Avanza meno della dimensione per creare sovrapposizione
        inizio += dimensione - sovrapposizione

    return chunk_lista


# Esempio: linea guida ipertensione (circa 3000 parole)
with open("linea_guida_ipertensione.txt", "r") as file:
    testo_completo = file.read()

chunk = crea_chunk(testo_completo, dimensione=500, sovrapposizione=50)
print(f"Documento diviso in {len(chunk)} chunk")
# Risultato: circa 7 chunk con sovrapposizione

Generare embeddings

Un embedding è una rappresentazione numerica del significato di un testo. Frasi con significato simile hanno embeddings vicini nello spazio vettoriale. Questo permette di trovare chunk rilevanti anche se non contengono le parole esatte della domanda.

python
from openai import OpenAI

client = OpenAI(api_key="sk-la-tua-chiave")

def calcola_embedding(testo):
    """Converte un testo in un vettore numerico di 1536 dimensioni."""
    risposta = client.embeddings.create(
        model="text-embedding-3-small",
        input=testo
    )
    return risposta.data[0].embedding


# Indicizza tutti i chunk
database_vettoriale = []
for i, chunk in enumerate(chunk):
    vettore = calcola_embedding(chunk)
    database_vettoriale.append({
        "id": i,
        "testo": chunk,
        "embedding": vettore,
        "fonte": "linea_guida_ipertensione.pdf"
    })

print(f"Indicizzati {len(database_vettoriale)} chunk")

Ricerca e generazione

Ora possiamo mettere insieme tutto: la domanda dell'utente diventa un embedding, lo confrontiamo con i chunk indicizzati e usiamo i più rilevanti per generare una risposta precisa:

python
import numpy as np

def coseno_similarita(vec_a, vec_b):
    """Calcola la similarità tra due vettori (0 = diversi, 1 = identici)."""
    return np.dot(vec_a, vec_b) / (np.linalg.norm(vec_a) * np.linalg.norm(vec_b))

def cerca_e_rispondi(domanda, database, top_k=3):
    """Pipeline completa: ricerca semantica + generazione con contesto."""
    # Fase 2: Retrieval
    embedding_domanda = calcola_embedding(domanda)

    # Calcola similarità con ogni chunk
    risultati = []
    for record in database:
        score = coseno_similarita(embedding_domanda, record["embedding"])
        risultati.append((score, record))

    # Prendi i top-K chunk più rilevanti
    risultati.sort(key=lambda x: x[0], reverse=True)
    chunk_rilevanti = risultati[:top_k]

    # Costruisci il contesto
    contesto = ""
    for score, record in chunk_rilevanti:
        contesto += f"[Fonte: {record['fonte']}]\n{record['testo']}\n\n"

    # Fase 3: Generation con contesto
    risposta = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": """Rispondi SOLO in base al contesto fornito.
Se il contesto non contiene la risposta, dì "Non ho trovato informazioni su questo argomento nelle linee guida."
Cita sempre la fonte tra parentesi quadre."""},
            {"role": "user", "content": f"Contesto:\n{contesto}\nDomanda: {domanda}"}
        ]
    )

    return risposta.choices[0].message.content


# Uso reale nell'ambulatorio
risposta = cerca_e_rispondi(
    "Qual è il dosaggio iniziale consigliato per l'ipertensione resistente?",
    database_vettoriale
)
print(risposta)
# "Secondo le linee guida [linea_guida_ipertensione.pdf], il dosaggio
#  iniziale consigliato è..."

Qualità: cosa la determina

La qualità di un sistema RAG dipende più dalla preparazione dei dati che dal modello scelto. Quattro fattori chiave:

text
1. DIMENSIONE CHUNK: 300-500 parole è il range ottimale per la maggior parte dei documenti.
   Troppo piccolo = contesto perso. Troppo grande = rumore.

2. OVERLAP: 10-15% della dimensione del chunk. Garantisce che concetti
   a cavallo tra due chunk non vengano persi.

3. METADATA: Aggiungi fonte, data, categoria a ogni chunk. Permette di
   filtrare i risultati (es. "solo linee guida aggiornate dopo il 2024").

4. RELEVANCE THRESHOLD: Non usare tutti i top-K. Scarta i chunk con
   similarità sotto 0.75 — meglio dire "non so" che rispondere male.

Quando usare RAG

RAG è la soluzione giusta quando hai documenti proprietari che l'AI non conosce:

  • Studio legale: archivio contratti, giurisprudenza interna, pareri precedenti
  • Ambulatorio medico: linee guida cliniche, protocolli interni, schede farmaci
  • Bottega artigiana: catalogo prodotti, schede materiali, storico preventivi
  • Studio fiscale: circolari AdE, risoluzioni, normativa commentata
  • Qualsiasi azienda: FAQ interne, documentazione tecnica, manuali operativi

Da ricordare

  • RAG permette all'AI di rispondere usando i tuoi documenti, non la conoscenza generica
  • Il processo ha 3 fasi: indicizzazione (una tantum), retrieval e generation (in tempo reale)
  • Il chunking è la decisione più critica: 300-500 parole con overlap del 10-15%
  • Gli embeddings catturano il significato, non le parole esatte — la ricerca è semantica
  • Aggiungi sempre metadata ai chunk per filtrare e citare le fonti
  • Meglio rispondere "non so" che generare una risposta basata su chunk poco rilevanti