Introduzione: Smontare la Magia degli LLM
I Large Language Models sembrano magici: scrivi una domanda e ottieni una risposta coerente, strutturata, spesso sorprendentemente intelligente. Ma sotto il cofano non c'è magia, c'è matematica. Un LLM e fondamentalmente un sistema che predice il prossimo token in una sequenza, basandosi su pattern statistici appresi da miliardi di parole durante il training.
Comprendere come funzionano gli LLM non e un esercizio accademico: e una competenza pratica essenziale. Sapere cosa succede tra il tuo prompt e la risposta generata ti permette di scrivere prompt migliori, debuggare comportamenti inaspettati, scegliere il modello giusto per il tuo caso d'uso, e capire perchè gli LLM allucinano.
Cosa Imparerai in Questo Articolo
- Come il testo viene trasformato in numeri attraverso la tokenizzazione
- Il ruolo degli embedding nel rappresentare il significato semantico
- Come funziona il meccanismo di attention nei Transformer
- Il processo di generazione del testo: da logits a token
- Le strategie di sampling: temperature, top-k e top-p
- perchè gli LLM allucinano e il ruolo della context window
Fase 1: Tokenizzazione - Da Testo a Numeri
Il primo passo nel processamento di un LLM e la tokenizzazione: convertire il testo in una sequenza di numeri interi. I modelli neurali non comprendono lettere o parole; operano su vettori numerici. La tokenizzazione e il ponte tra il linguaggio umano e la matematica del modello.
Byte-Pair Encoding (BPE)
L'algoritmo più comune e il Byte-Pair Encoding (BPE), usato da GPT, Claude e la maggior parte dei modelli moderni. BPE funziona in modo iterativo: parte dai singoli caratteri e fonde progressivamente le coppie più frequenti in token più lunghi.
Il risultato e un vocabolario di 50.000-100.000 token che rappresenta un compromesso ottimale tra granularità e efficienza. Parole comuni come "the" diventano un singolo token, mentre parole rare vengono spezzate in sotto-token.
# Esempio di tokenizzazione con tiktoken (tokenizer di OpenAI)
import tiktoken
# Carica il tokenizer di GPT-4
enc = tiktoken.encoding_for_model("gpt-4")
# Tokenizza una frase
testo = "L'intelligenza artificiale generativa e rivoluzionaria"
tokens = enc.encode(testo)
print(f"Testo: {testo}")
print(f"Token IDs: {tokens}")
print(f"Numero token: {len(tokens)}")
# Decodifica ogni token per vedere la suddivisione
for token_id in tokens:
print(f" ID {token_id} -> '{enc.decode([token_id])}'")
# Output tipico:
# Testo: L'intelligenza artificiale generativa e rivoluzionaria
# Token IDs: [43, 6, 396, 40749, 80828, ...]
# Numero token: 8
Impatto Pratico della Tokenizzazione
La tokenizzazione ha conseguenze pratiche importanti che ogni sviluppatore dovrebbe conoscere:
- Costo: le API addebitano per token, non per parole. Una parola può essere 1-4 token
- Context window: il limite e in token, non in parole. 4.000 token corrispondono a circa 3.000 parole in inglese
- Lingue diverse: l'italiano richiede più token dell'inglese per esprimere lo stesso concetto (circa 1.3x)
- Codice: il codice sorgente e spesso meno efficiente in termini di token rispetto al testo naturale
Fase 2: Embedding - Dal Token al Significato
Dopo la tokenizzazione, ogni token ID viene convertito in un embedding: un vettore denso di numeri reali (tipicamente 768-12.288 dimensioni) che cattura il significato semantico del token.
La potenza degli embedding sta nella loro geometria: parole con significati simili hanno vettori vicini nello spazio. "Re" e "Regina" sono vicini, come "Parigi" e "Francia". Queste relazioni vengono apprese automaticamente durante il training.
Embedding: Numeri con Significato
Un embedding non e un semplice ID numerico: e un vettore ad alta dimensionalità dove ogni dimensione
cattura un aspetto del significato. Le operazioni aritmetiche sugli embedding producono risultati
semanticamente sensati: vec("re") - vec("uomo") + vec("donna") ≈ vec("regina").
# Visualizzare la similarità semantica degli embedding
from openai import OpenAI
import numpy as np
client = OpenAI()
def get_embedding(text: str) -> list:
"""Ottieni l'embedding di un testo usando OpenAI."""
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def cosine_similarity(a: list, b: list) -> float:
"""Calcola la similarità coseno tra due vettori."""
a, b = np.array(a), np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# Confronta similarità semantiche
parole = ["gatto", "cane", "automobile", "felino"]
embeddings = {p: get_embedding(p) for p in parole}
print("Similarità semantiche:")
print(f" gatto-felino: {cosine_similarity(embeddings['gatto'], embeddings['felino']):.4f}")
print(f" gatto-cane: {cosine_similarity(embeddings['gatto'], embeddings['cane']):.4f}")
print(f" gatto-auto: {cosine_similarity(embeddings['gatto'], embeddings['automobile']):.4f}")
# gatto-felino avra la similarità più alta!
Positional Encoding
Oltre all'embedding semantico, i Transformer aggiungono un positional encoding: un segnale che indica la posizione di ogni token nella sequenza. Senza questo meccanismo, il modello non distinguerebbe "il gatto insegue il topo" da "il topo insegue il gatto", poichè l'architettura Transformer processa tutti i token in parallelo, non sequenzialmente.
Fase 3: Il Transformer - Attention Is All You Need
Il cuore di ogni LLM moderno e l'architettura Transformer, composta da blocchi ripetuti di Self-Attention e Feed-Forward Network. Modelli come GPT-4 hanno centinaia di questi blocchi impilati, ognuno che raffina la rappresentazione del testo.
Self-Attention: Il Meccanismo Chiave
Il self-attention permette a ogni token di "guardare" tutti gli altri token nella sequenza e decidere quanto sono rilevanti per il suo significato nel contesto corrente. Nella frase "Il gatto si e seduto sul tappeto perchè era stanco", il meccanismo di attention collega "era stanco" a "gatto" (non a "tappeto"), risolvendo la coreference.
Matematicamente, per ogni token vengono calcolati tre vettori: Query (cosa sto cercando), Key (cosa offro come contesto) e Value (il mio contenuto informativo). Il prodotto scalare tra Query e Key determina il peso di attenzione, che viene usato per pesare i Value.
Multi-Head Attention
Un singolo meccanismo di attention cattura un tipo di relazione. Il multi-head attention esegue più attention in parallelo (tipicamente 32-128 "teste"), ognuna specializzata in un aspetto diverso: relazioni sintattiche, semantiche, di prossimita, di coreference e cosi via.
Anatomia di un Blocco Transformer
Ogni blocco Transformer segue questa struttura: Layer Norm per stabilizzare l'input, Multi-Head Self-Attention per catturare relazioni tra token, Residual Connection per preservare l'informazione originale, un secondo Layer Norm, e una Feed-Forward Network (2 layer densi) per trasformare la rappresentazione, seguita da un'altra Residual Connection. GPT-4 impila circa 120 di questi blocchi.
Fase 4: Generazione del Testo
Dopo che il testo di input ha attraversato tutti i blocchi Transformer, l'ultimo layer produce un vettore di output per ogni posizione. Per generare il prossimo token, questo vettore viene proiettato sull'intero vocabolario producendo i logits: un punteggio numerico per ogni possibile token nel vocabolario.
Da Logits a Probabilità: Softmax
I logits vengono trasformati in probabilità attraverso la funzione softmax, che normalizza i punteggi in modo che sommino a 1. Il token con la probabilità più alta e la "migliore previsione" del modello, ma non sempre viene scelto quello.
Strategie di Sampling
La scelta del prossimo token non e deterministica. Diverse strategie di sampling producono output con caratteristiche diverse:
- Greedy decoding: sceglie sempre il token più probabile. Deterministico ma spesso ripetitivo e noioso
- Random sampling: campiona dalla distribuzione completa. Creativo ma potenzialmente incoerente
- Temperature: controlla la "randomness". T=0 e greedy, T=1 e la distribuzione originale, T>1 aumenta la creativita
- Top-k sampling: campiona solo dai k token più probabili (es. k=40)
- Top-p (nucleus) sampling: campiona dal più piccolo set di token la cui probabilità cumulativa supera p (es. p=0.9)
# Esempio: effetto della temperature sulla generazione
from anthropic import Anthropic
client = Anthropic()
prompt = "Scrivi l'inizio di una storia fantasy in una riga:"
for temp in [0.0, 0.5, 1.0, 1.5]:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=100,
temperature=temp,
messages=[{"role": "user", "content": prompt}]
)
print(f"\nTemperature {temp}:")
print(f" {response.content[0].text}")
# Temperature 0.0: output deterministico, sempre uguale
# Temperature 0.5: lieve variazione, ancora coerente
# Temperature 1.0: creativo, buon bilanciamento
# Temperature 1.5: molto creativo, possibile incoerenza
Context Window: La Memoria degli LLM
La context window e il numero massimo di token che un LLM può processare in una singola richiesta (input + output). Questa e di fatto la "memoria di lavoro" del modello durante una conversazione.
Context Window per Modello
| Modello | Context Window | Equivalente Approssimativo |
|---|---|---|
| GPT-3.5 | 4.096 / 16.384 token | ~3.000 / 12.000 parole |
| GPT-4 | 8.192 / 128.000 token | ~6.000 / 96.000 parole |
| Claude 3.5 Sonnet | 200.000 token | ~150.000 parole |
| Gemini 1.5 Pro | 1.000.000 token | ~750.000 parole |
| Llama 3.1 | 128.000 token | ~96.000 parole |
Allucinazioni: perchè gli LLM Inventano
Le allucinazioni sono uno dei problemi più critici degli LLM: il modello genera informazioni false con la stessa sicurezza con cui genera informazioni vere. Questo accade perchè un LLM non "conosce" i fatti: predice il prossimo token più probabile dato il contesto.
Se il pattern statistico suggerisce che dopo "La capitale dell'Australia e" il token più probabile sia "Sydney", il modello generera "Sydney" anche se la risposta corretta e "Canberra". Il modello non ha un meccanismo interno per verificare la verita dei suoi output.
Mitigazione: Retrieval-Augmented Generation (RAG)
La strategia più efficace per ridurre le allucinazioni e il RAG: fornire al modello informazioni fattuali recuperate da fonti affidabili come parte del contesto. Invece di chiedere al modello di "ricordare", gli diamo i dati aggiornati e gli chiediamo di ragionare su di essi.
# RAG semplificato: fornire contesto fattuale al modello
from anthropic import Anthropic
client = Anthropic()
# Contesto recuperato da un database o motore di ricerca
contesto_fattuale = """
Dati aziendali aggiornati al Q3 2025:
- Fatturato: EUR 12.5M (+23% YoY)
- Dipendenti: 85
- Clienti attivi: 342
- NPS score: 72
"""
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=500,
messages=[{
"role": "user",
"content": f"""Basandoti ESCLUSIVAMENTE sui dati seguenti,
rispondi alla domanda. Se i dati non contengono la risposta, di' "Non ho questa informazione".
DATI:
{contesto_fattuale}
DOMANDA: Qual è il fatturato attuale e quanti dipendenti abbiamo?"""
}]
)
print(response.content[0].text)
Conclusioni
Comprendere il funzionamento interno degli LLM - dalla tokenizzazione alla generazione, passando per embedding, attention e sampling - non e solo conoscenza teorica. E la base per usare questi strumenti in modo efficace e consapevole.
La tokenizzazione influenza i costi e i limiti di contesto. La temperature e le strategie di sampling determinano la creativita dell'output. Il meccanismo di attention spiega perchè il modello capisce (o non capisce) il contesto. Le allucinazioni sono una conseguenza diretta dell'architettura next-token-prediction.
Nel prossimo articolo metteremo in pratica queste conoscenze con il Prompt Engineering Avanzato: tecniche sistematiche per ottenere il massimo dagli LLM, dal zero-shot al chain-of-thought, dai system prompt al ReAct pattern.







