Dialogo con Gemini.
Cominciamo un altro articolo: laboratorio AI per liceo ed ITS. Direi di riprendere il computer allestito per Ollama, il precedente articolo il neurone artificiale e cominciare a costruire il laboratorio AI per le scuole…
Costruire un laboratorio di Intelligenza Artificiale per scuole superiori (Licei ed ITS) significa trasformare concetti astratti in esperimenti tangibili, senza dipendere da costose sottoscrizioni cloud e garantendo piena sovranità sui dati.
Colleghiamo i due pilastri già esplorati:
- L’infrastruttura locale: il PC configurato con Ollama per l’inferenza di modelli open-weight (LLM, vision, embeddings).
- I fondamenti teorici: il neurone artificiale (il Perceptron), la minima unità computazionale da cui scaturisce l’intero paradigma del deep learning.
Titolo di lavoro: Dall’hardware al Perceptron: allestire il Laboratorio di AI per la scuola moderna
1. Il contesto: Perché un laboratorio locale a scuola
- Indipendenza e privacy (GDPR): Nessun invio di dati di studenti o documenti scolastici a provider terzi; tutto gira su rete locale (LAN).
- Demistificare l’AI: Mostrare che un LLM o una rete neurale non sono “magia”, ma algebra lineare, calcolo vettoriale e pipeline software eseguibili su una workstation d’istituto.
- Didattica multilivello:
- Liceo (Scientifico/Scienze Applicate): focus su matematica sottostante (vettori, matrici, derivate, funzioni di attivazione) e prompt/agent engineering.
- ITS (Tecnologico/Informatico): focus su architettura client-server, API REST, container Docker, quantizzazione dei pesi e deployment.
2. L’Architettura del Laboratorio
Il computer allestito con Ollama funge da Server AI centrale d’aula, accessibile dai computer degli studenti o dai loro portatili via rete locale.
| Componente | Ruolo Didattico | Stack Tecnologico |
|---|---|---|
| Server AI d’aula | Host dei modelli e motore di calcolo | Ubuntu/Debian/Windows, Ollama, driver CUDA/ROCm |
| Ambiente di lavoro | Notebook interattivi e script per gli studenti | JupyterLab / VS Code Server / Python 3.11+ |
| Librerie Core | Dalla matematica di base al machine learning | NumPy, Matplotlib, PyTorch / Scikit-learn |
| Interfaccia e API | Esplorazione rapida e integrazione software | Open WebUI, endpoint REST di Ollama (localhost:11434) |
3. Il Percorso Didattico: Dal Neurone Artificiale agli LLM
Il programma del laboratorio si articola in quattro tappe progressive, unendo teoria ed esecuzione pratica:
- Modulo 1: Il Neurone da zero (Python puro & NumPy)
- Implementare a mano il modello matematico y=f(∑wixi+b).
- Risolvere problemi linearmente separabili: le porte logiche AND e OR.
- Dimostrare il limite storico del singolo neurone (il problema dello XOR) per motivare le reti multistrato (MLP).
- Modulo 2: Addestramento e Discesa del Gradiente
- Visualizzare l’errore (Loss function) e l’aggiornamento iterativo dei pesi con Matplotlib.
- Introdurre PyTorch o micro-framework per vedere la backpropagation automatica.
- Modulo 3: Il ponte verso i modelli generativi (Ollama in LAN)
- Consumare l’API locale di Ollama tramite script Python (
requestso libreria ufficialeollama-python). - Estrazione di embedding: visualizzare nello spazio vettoriale la similarità semantica tra frasi usando PCA o t-SNE.
- Consumare l’API locale di Ollama tramite script Python (
- Modulo 4: Prime applicazioni concrete
- Creazione di una pipeline RAG (Retrieval-Augmented Generation) minimale per interrogare il regolamento d’istituto o un testo di storia locale.
Proposta di scaletta per la stesura dell’articolo:
- Introduzione: L’AI a scuola oltre l’uso passivo di interfacce web commerciali.
- Setup dell’ambiente: Condividere Ollama e Jupyter in rete d’istituto (configurazione IP, firewall, accessi).
- Hands-on 1 (Matematica applicata): Esercitazione guidata sul neurone artificiale in Jupyter (codice commentato e visualizzazioni grafiche).
- Hands-on 2 (Integrazione moderna): Interrogare il modello locale Ollama via script Python per compiti di classificazione e spiegazione del codice.
- Scheda didattica per docenti: Obiettivi di apprendimento, prerequisiti e griglia di valutazione.
Scrivi la sezione hands-on con il codice Python commentato per implementare il neurone artificiale e le porte logiche in classe.
Ecco la sezione hands-on dell’articolo, strutturata come guida passo-passo pronta per essere inserita in un notebook Jupyter o in uno script Python durante la lezione.
Hands-on: Costruire un neurone artificiale da zero con Python
Nel modello concettuale introdotto da Frank Rosenblatt nel 1958 (il Perceptron), un neurone artificiale calcola la combinazione lineare degli ingressi pesati, aggiunge una soglia o scostamento chiamato bias, e fa passare il risultato attraverso una funzione di attivazione (spesso una funzione a gradino o step function):
$$\hat{y} = f\left(\sum_{i=1}^n w_i x_i + b\right) = f(\mathbf{w} \cdot \mathbf{x} + b)$$
Nel nostro laboratorio useremo solo Python standard e la libreria NumPy per le operazioni vettoriali, affiancata da Matplotlib per visualizzare cosa sta accadendo nello spazio geometrico.
1. Setup dell’ambiente
Se gli studenti lavorano dal proprio PC o notebook in aula, è sufficiente installare le due dipendenze di base:
Bash
pip install numpy matplotlib
2. La classe Perceptron
Scriviamo una classe orientata agli oggetti che implementa sia la predizione sia l’algoritmo di apprendimento supervisionato basato sulla regola di Hebb / delta rule per il percettrone:
$$\Delta w_i = \eta \cdot (y – \hat{y}) \cdot x_i$$
$$\Delta b = \eta \cdot (y – \hat{y})$$
dove $\eta$ (eta) è il learning rate (tasso di apprendimento), $y$ è il valore atteso e $\hat{y}$ è l’output predetto.
Python
import numpy as np
import matplotlib.pyplot as plt
class Perceptron:
def __init__(self, n_inputs: int, lr: float = 0.1, epochs: int = 20):
"""
Inizializza il neurone artificiale.
:param n_inputs: Numero di segnali di ingresso (es. 2 per le porte logiche a due variabili)
:param lr: Learning rate (tasso di apprendimento, velocità di aggiornamento pesi)
:param epochs: Numero massimo di passaggi completi sul dataset di training
"""
self.lr = lr
self.epochs = epochs
# Inizializziamo i pesi con valori casuali piccoli e il bias a zero
self.weights = np.random.uniform(-0.1, 0.1, size=n_inputs)
self.bias = 0.0
self.history_errors = []
def activation(self, z: float) -> int:
"""Funzione di attivazione a gradino (Heaviside step function)."""
return 1 if z >= 0 else 0
def predict(self, x: np.ndarray) -> int:
"""
Calcola l'uscita del neurone: f(w · x + b)
"""
# Prodotto scalare tra pesi e input + bias
z = np.dot(x, self.weights) + self.bias
return self.activation(z)
def fit(self, X: np.ndarray, y: np.ndarray):
"""
Addestra il neurone sui dati forniti.
:param X: Matrice delle feature di input (N campioni x M feature)
:param y: Array delle etichette attese (valori binari 0 o 1)
"""
for epoch in range(self.epochs):
total_errors = 0
for xi, target in zip(X, y):
prediction = self.predict(xi)
error = target - prediction
# Se c'è errore, aggiorniamo pesi e bias
if error != 0:
update = self.lr * error
self.weights += update * xi
self.bias += update
total_errors += abs(error)
self.history_errors.append(total_errors)
# Se l'errore è 0, il neurone ha imparato perfettamente la regola
if total_errors == 0:
print(f"Convergenza raggiunta all'epoca {epoch + 1}!")
break
3. Esperimento 1: Imparare la porta AND e la porta OR
Definiamo i dataset per le due funzioni booleane più note:
Python
# Ingressi booleani (tavola di verità a 2 ingressi)
X = np.array([
[0, 0],
[0, 1],
[1, 0],
[1, 1]
])
# Uscite attese per AND e OR
y_and = np.array([0, 0, 0, 1])
y_or = np.array([0, 1, 1, 1])
# --- Addestramento porta AND ---
print("--- Addestramento Porta AND ---")
p_and = Perceptron(n_inputs=2, lr=0.1, epochs=20)
p_and.fit(X, y_and)
print("Verifica predizioni AND:")
for sample in X:
print(f"Input: {sample} -> Output calcolato: {p_and.predict(sample)}")
print(f"Pesi finali: {p_and.weights.round(2)}, Bias: {round(p_and.bias, 2)}\n")
# --- Addestramento porta OR ---
print("--- Addestramento Porta OR ---")
p_or = Perceptron(n_inputs=2, lr=0.1, epochs=20)
p_or.fit(X, y_or)
print("Verifica predizioni OR:")
for sample in X:
print(f"Input: {sample} -> Output calcolato: {p_or.predict(sample)}")
print(f"Pesi finali: {p_or.weights.round(2)}, Bias: {round(p_or.bias, 2)}")
4. Visualizzare il confine di decisione (Decision Boundary)
Il momento didatticamente più efficace in classe è mostrare la geometria della formula:
$$w_1 x_1 + w_2 x_2 + b = 0 \implies x_2 = -\frac{w_1}{w_2}x_1 – \frac{b}{w_2}$$
Questa equazione rappresenta una retta sul piano cartesiano: da un lato della retta il neurone risponde 0, dall’altro risponde 1.
Python
def plot_decision_boundary(model: Perceptron, X: np.ndarray, y: np.ndarray, title: str):
plt.figure(figsize=(6, 5))
# Disegniamo i punti del dataset
for xi, target in zip(X, y):
color = 'blue' if target == 1 else 'red'
marker = 'o' if target == 1 else 's'
plt.scatter(xi[0], xi[1], color=color, s=120, marker=marker, edgecolors='k')
# Calcolo della retta di separazione: w1*x1 + w2*x2 + b = 0
x1_vals = np.linspace(-0.2, 1.2, 100)
w1, w2 = model.weights
b = model.bias
if abs(w2) > 1e-5:
x2_vals = -(w1 * x1_vals + b) / w2
plt.plot(x1_vals, x2_vals, 'g--', label='Confine di decisione (w · x + b = 0)')
plt.xlim(-0.2, 1.2)
plt.ylim(-0.2, 1.2)
plt.title(title)
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')
plt.grid(True, linestyle=':', alpha=0.6)
plt.legend()
plt.show()
# Esegui la visualizzazione per AND e OR
plot_decision_boundary(p_and, X, y_and, "Confine di decisione - Porta AND")
plot_decision_boundary(p_or, X, y_or, "Confine di decisione - Porta OR")
5. Il momento “Eureka” in aula: il fallimento dello XOR
Nel 1969 Marvin Minsky e Seymour Papert dimostrarono che un singolo percettrone non è in grado di risolvere problemi non linearmente separabili, portando al primo “inverno dell’AI”. Facciamo sperimentare questo limite direttamente agli studenti:
Python
# Tavola di verità dello XOR (OR esclusivo)
y_xor = np.array([0, 1, 1, 0])
print("--- Test Porta XOR (Problema non linearmente separabile) ---")
p_xor = Perceptron(n_inputs=2, lr=0.1, epochs=30)
p_xor.fit(X, y_xor)
print("\nVerifica predizioni XOR:")
for sample, target in zip(X, y_xor):
print(f"Input: {sample} | Atteso: {target} | Ottenuto: {p_xor.predict(sample)}")
# Visualizziamo l'andamento degli errori nel tempo
plt.figure(figsize=(6, 3.5))
plt.plot(range(1, len(p_xor.history_errors) + 1), p_xor.history_errors, marker='o', color='crimson')
plt.title("Numero di errori per epoca sullo XOR")
plt.xlabel("Epoca")
plt.ylabel("Errori totali")
plt.grid(True, linestyle=':')
plt.show()
Domanda guida per gli studenti:
“Guardando il grafico dei punti dello XOR (i punti
(0,1)e(1,0)hanno valore1, mentre(0,0)e(1,1)hanno valore0), è possibile tracciare una sola linea retta che separi i punti blu da quelli rossi?”
La risposta è no: serve combinare più neuroni in una rete (Multilayer Perceptron – MLP) o aggiungere uno strato nascosto (hidden layer). Questo ponte concettuale giustifica l’esistenza del Deep Learning e introduce in modo naturale le architetture moderne.
Ecco la continuazione organica dell’articolo, divisa nei due step successivi: prima la risoluzione concettuale e matematica dello XOR tramite una rete neurale a due strati con PyTorch, poi il passaggio al calcolo moderno interrogando il server locale Ollama per compiti generativi ed embedding vettoriali.
Parte 2: Superare il limite del singolo neurone con PyTorch (MLP)
Il fallimento del percettrone singolo sullo XOR evidenzia una necessità strutturale: per separare regioni non connesse o configurazioni non lineari servono più iperpiani di decisione combinati, ovvero uno strato nascosto (hidden layer).
Per mostrare questo salto senza appesantire la didattica riscrivendo a mano la backpropagation con derivate parziali a catena, introduciamo PyTorch, standard industriale ideale per mostrare l’autodifferenziazione (autograd).
Bash
pip install torch
Il modello: 2 ingressi $\rightarrow$ 2 neuroni nascosti $\rightarrow$ 1 neurone di uscita
Python
import torch
import torch.nn as nn
import torch.optim as optim
# 1. Dataset XOR convertito in tensori PyTorch (float32)
X_xor = torch.tensor([[0.0, 0.0],
[0.0, 1.0],
[1.0, 0.0],
[1.0, 1.0]])
y_xor = torch.tensor([[0.0],
[1.0],
[1.0],
[0.0]])
# 2. Definizione del Multilayer Perceptron (MLP)
class XORNet(nn.Module):
def __init__(self):
super().__init__()
# Strato nascosto: 2 ingressi -> 2 neuroni nascosti
self.hidden = nn.Linear(in_features=2, out_features=2)
# Funzione di attivazione non lineare (Sigmoide)
self.activation = nn.Sigmoid()
# Strato di uscita: 2 neuroni nascosti -> 1 uscita
self.output = nn.Linear(in_features=2, out_features=1)
self.out_activation = nn.Sigmoid()
def forward(self, x):
# Passaggio in avanti (Forward pass)
h = self.activation(self.hidden(x))
out = self.out_activation(self.output(h))
return out
model = XORNet()
# 3. Funzione di perdita (Binary Cross Entropy) e Ottimizzatore (SGD con momentum o Adam)
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.05)
# 4. Loop di addestramento
epochs = 1500
for epoch in range(1, epochs + 1):
# Forward pass
predictions = model(X_xor)
loss = criterion(predictions, y_xor)
# Backward pass & ottimizzazione
optimizer.zero_grad() # Azzera i gradienti accumulati
loss.backward() # Calcola dLoss/dw con backpropagation
optimizer.step() # Aggiorna i pesi
if epoch % 300 == 0:
print(f"Epoca {epoch:4d} | Loss: {loss.item():.5f}")
# 5. Verifica dei risultati
print("\n--- Risultati predizioni XOR con MLP ---")
with torch.no_grad():
test_preds = model(X_xor)
for inp, pred, target in zip(X_xor, test_preds, y_xor):
rounded = int(pred.item() >= 0.5)
print(f"Input: {inp.tolist()} | Probabilità: {pred.item():.4f} -> Predetto: {rounded} | Target: {int(target.item())}")
Spunto didattico per la classe
Facciamo notare agli studenti cosa ha fatto la rete:
- I due neuroni nascosti tracciano ciascuno una retta distinta nel piano originale $(x_1, x_2)$.
- La funzione Sigmoide trasforma e piega lo spazio delle feature.
- Il neurone di uscita separa linearmente i punti nel nuovo spazio bidimensionale generato dai neuroni nascosti.
Parte 3: Dai pesi artificiali ai modelli linguistici (Ollama in LAN)
Una volta compreso che un’architettura neurale complessa (come un Transformer) non è altro che miliardi di queste operazioni matriciali affiancate da meccanismi di attenzione, passiamo all’utilizzo pratico del server locale.
Sul computer host del laboratorio è attivo Ollama, che espone un endpoint HTTP REST standard (di default sulla porta 11434).
Prerequisito sul Server del Laboratorio
Per consentire agli studenti di accedere al server dall’aula, assicurarsi che Ollama sia in ascolto su tutte le interfacce di rete.
Su Linux/Ubuntu, impostare la variabile d’ambiente nel servizio o nella shell:
Bash
export OLLAMA_HOST="0.0.0.0:11434"
ollama serve
E scaricare un modello leggero ed efficiente per la didattica (es. Qwen o Llama):
Bash
ollama pull llama3.2:3b
ollama pull nomic-embed-text
Sulle macchine degli studenti installiamo la libreria ufficiale Python:
Bash
pip install ollama requests
Esercizio 3.1: Inferenza guidata e Zero-Shot Classification
Mostriamo come consumare il modello in locale via Python per classificare del testo o svolgere compiti logici strutturati, senza inviare dati all’esterno dell’istituto.
Python
import ollama
# Indirizzo IP del computer server allestito in aula (es. 192.168.1.50)
# Se si esegue direttamente sulla stessa macchina, usare 'http://localhost:11434'
SERVER_HOST = "http://192.168.1.50:11434"
client = ollama.Client(host=SERVER_HOST)
prompt_didattico = """
Analizza la seguente frase e classifica il tipo di ragionamento logico
(Deduttivo, Induttivo, Abduttivo). Rispondi in formato JSON con chiavi 'tipo' e 'spiegazione'.
Frase: "Tutti i cigni osservati finora nel lago sono bianchi, quindi tutti i cigni sono bianchi."
"""
response = client.generate(
model="llama3.2:3b",
prompt=prompt_didattico,
options={"temperature": 0.1} # Bassa temperatura per risposte deterministiche
)
print("Risposta dal server locale:")
print(response["response"])
Esercizio 3.2: Vettori ed Embeddings semantici (Matematica in azione)
Colleghiamo direttamente il concetto di coordinate e vettori visto con il Perceptron al funzionamento degli LLM: trasformiamo frasi in vettori multidimensionali tramite il modello nomic-embed-text e calcoliamo la Cosine Similarity (prodotto scalare normalizzato).
$$\text{Cosine Similarity}(\mathbf{u}, \mathbf{v}) = \frac{\mathbf{u} \cdot \mathbf{v}}{\Vert{}\mathbf{u}\Vert{} \Vert{}\mathbf{v}\Vert{}}$$
Python
import numpy as np
import ollama
SERVER_HOST = "http://192.168.1.50:11434"
client = ollama.Client(host=SERVER_HOST)
def get_embedding(text: str) -> np.ndarray:
res = client.embeddings(model="nomic-embed-text", prompt=text)
return np.array(res["embedding"])
def cosine_similarity(v1: np.ndarray, v2: np.ndarray) -> float:
# Prodotto scalare diviso per il prodotto delle norme euclidee
dot_product = np.dot(v1, v2)
norm_v1 = np.linalg.norm(v1)
norm_v2 = np.linalg.norm(v2)
return dot_product / (norm_v1 * norm_v2)
# Frasi di test per gli studenti
frasi = [
"Il professore spiega la legge di gravitazione universale.",
"Isaac Newton formulò le equazioni del moto dei pianeti.",
"La pizza margherita si prepara con pomodoro e mozzarella fresca."
]
print("Calcolo dei vettori semantici in corso...")
emb_fisica_1 = get_embedding(frasi[0])
emb_fisica_2 = get_embedding(frasi[1])
emb_cucina = get_embedding(frasi[2])
print(f"Dimensione del vettore embedding generato: {len(emb_fisica_1)} componenti\n")
sim_fisica = cosine_similarity(emb_fisica_1, emb_fisica_2)
sim_mista = cosine_similarity(emb_fisica_1, emb_cucina)
print(f"Similarità tra Frase 1 e Frase 2 (entrambe Fisica): {sim_fisica:.4f}")
print(f"Similarità tra Frase 1 e Frase 3 (Fisica vs Cucina): {sim_mista:.4f}")
Sintesi didattica per gli studenti
- Il percettrone calcola un prodotto scalare $\mathbf{w} \cdot \mathbf{x}$ in uno spazio a 2 dimensioni per separare categorie.
- Il motore di embedding proietta concetti umani in uno spazio vettoriale a centinaia o migliaia di dimensioni (es. 768), dove la vicinanza angolare (prodotto scalare normalizzato) misura la parentela semantica dei concetti.
Scrivi il codice per un mini-progetto RAG (Retrieval-Augmented Generation) in locale per interrogare un documento di testo scolastico.
Ecco la sezione dell’articolo dedicata al mini-progetto RAG didattico.
L’obiettivo per gli studenti è capire che un sistema RAG non è altro che una catena composta da:
- Chunking: suddividere un testo lungo in segmenti gestibili.
- Embedding: proiettare ogni segmento nello spazio vettoriale.
- Retrieval (Ricerca Semantica): trovare i segmenti più vicini alla domanda tramite prodotto scalare / similarità del coseno.
- Augmented Generation: passare i segmenti pertinenti all’LLM come contesto nel prompt.
Tutto il codice è scritto in Python puro con NumPy e Ollama, senza librerie complesse o database vettoriali esterni (come Chroma o Pinecone), così che ogni passaggio matematico e algoritmico rimanga trasparente e ispezionabile dagli studenti.
Mini-Progetto Didattico: Costruire un motore RAG da zero in rete locale
Prerequisiti
Sul server Ollama del laboratorio devono essere presenti i due modelli:
Bash
ollama pull nomic-embed-text
ollama pull llama3.2:3b
Sulle postazioni di lavoro occorre solo ollama e numpy:
Bash
pip install ollama numpy
1. Il documento di esempio: Regolamento Scolastico
Creiamo uno scenario reale: un file di testo o una stringa contenente estratti del regolamento d’istituto (giustificazioni, uso degli smartphone, uscite anticipate, criteri di valutazione).
Python
import numpy as np
import ollama
# Indirizzo del server Ollama di laboratorio (modificare con l'IP locale d'aula se separato)
OLLAMA_SERVER = "http://localhost:11434"
client = ollama.Client(host=OLLAMA_SERVER)
EMBEDDING_MODEL = "nomic-embed-text"
LLM_MODEL = "llama3.2:3b"
# Documento d'istituto di prova
DOCUMENTO_SCOLASTICO = """
Articolo 1 - Ingresso e ritardi: Le lezioni iniziano alle ore 08:00. Gli studenti che arrivano
con un ritardo inferiore a 10 minuti sono ammessi in classe con annotazione sul registro.
Oltre i 10 minuti di ritardo, l'ingresso è consentito solo all'inizio della seconda ora,
previa giustificazione scritta da parte del genitore sul libretto elettronico.
Articolo 2 - Utilizzo dei dispositivi elettronici: Durante le ore di lezione è severamente
vietato l'uso di smartphone, tablet e cuffie per scopi non didattici. Gli smartphone devono
essere riposti nello zaino o nell'apposita cassetta all'inizio dell'ora. L'uso è consentito
esclusivamente su espressa autorizzazione del docente per attività didattiche o di ricerca.
Articolo 3 - Uscite anticipate e permessi: Le uscite anticipate sono consentite solo a partire
dalle ore 12:00 per motivi di salute o gravi necessità documentate. Lo studente minorenne può
lasciare l'istituto solo se prelevato direttamente da un genitore o da un delegato maggiorenne
formalmente autorizzato all'inizio dell'anno scolastico.
Articolo 4 - Valutazione e verifiche: In ciascun periodo didattico ogni docente deve svolgere
un numero congruo di verifiche, pari ad almeno due prove scritte e due orali/pratiche.
Le verifiche corrette devono essere riconsegnate agli studenti entro e non oltre 15 giorni
dallo svolgimento, garantendo una motivazione chiara del voto attribuito.
"""
2. Chunking e indicizzazione nello spazio vettoriale
In un’applicazione didattica, suddividiamo il testo per paragrafi o articoli. Per ciascun blocco (chunk) calcoliamo il vettore di embedding tramite il modello nomic-embed-text.
Python
def chunk_text(text: str) -> list[str]:
"""Divide il testo in paragrafi non vuoti."""
chunks = [chunk.strip() for chunk in text.split("\n\n") if chunk.strip()]
return chunks
def get_embedding(text: str) -> np.ndarray:
"""Calcola il vettore semantico di una stringa."""
res = client.embeddings(model=EMBEDDING_MODEL, prompt=text)
return np.array(res["embedding"], dtype=np.float32)
# 1. Suddivisione in blocchi
chunks = chunk_text(DOCUMENTO_SCOLASTICO)
print(f"Estratti {len(chunks)} blocchi dal regolamento.")
# 2. Creazione dell'indice vettoriale (in-memory)
vector_database = []
print("Calcolo degli embedding dei blocchi in corso...")
for idx, chunk in enumerate(chunks):
vector = get_embedding(chunk)
vector_database.append({
"id": idx,
"content": chunk,
"vector": vector
})
print(f"Indicizzazione completata! Ogni blocco è rappresentato da un vettore di {len(vector_database[0]['vector'])} dimensioni.\n")
3. Retrieval semantico tramite Similarità del Coseno
Quando l’utente pone una domanda:
- Calcoliamo l’embedding della domanda.
- Misuriamo la similarità del coseno tra la domanda e ogni chunk memorizzato:Cosine Similarity(q,ci)=∥q∥∥ci∥q⋅ci
- Ordiniamo i chunk e selezioniamo i primi k (i più pertinenti).
Python
def cosine_similarity(v1: np.ndarray, v2: np.ndarray) -> float:
"""Calcola l'angolo semantico normalizzato tra due vettori."""
norm_product = np.linalg.norm(v1) * np.linalg.norm(v2)
if norm_product == 0:
return 0.0
return float(np.dot(v1, v2) / norm_product)
def search_relevant_chunks(query: str, db: list[dict], top_k: int = 1) -> list[dict]:
"""Estrae i k blocchi con il più alto punteggio di similarità semantica."""
query_vec = get_embedding(query)
scored_chunks = []
for item in db:
score = cosine_similarity(query_vec, item["vector"])
scored_chunks.append({
"content": item["content"],
"score": score
})
# Ordiniamo in ordine decrescente di pertinenza
scored_chunks.sort(key=lambda x: x["score"], reverse=True)
return scored_chunks[:top_k]
4. Augmented Generation: costringere l’LLM ad attenersi al contesto
In questa fase componiamo il prompt finale. L’LLM riceve sia il contesto estratto sia la domanda originale, con una chiara istruzione di sistema (system prompt) per impedire “allucinazioni”.
Python
def ask_rag(query: str, db: list[dict]) -> str:
# 1. Recupero delle informazioni rilevanti
top_results = search_relevant_chunks(query, db, top_k=1)
retrieved_chunk = top_results[0]
print(f"--- [DEBUG DIDATTICO] ---")
print(f"Domanda: \"{query}\"")
print(f"Blocco selezionato (Punteggio di similarità: {retrieved_chunk['score']:.4f}):")
print(f"{retrieved_chunk['content'][:120]}...\n-------------------------")
# 2. Composizione del prompt aumentato
system_prompt = (
"Sei l'assistente ufficiale della scuola. Rispondi alla domanda dello studente "
"basandoti ESCLUSIVAMENTE sulle informazioni presenti nel CONTESTO fornito. "
"Se l'informazione non è presente nel testo, dichiara chiaramente: 'L'informazione non è specificata nel regolamento'."
)
augmented_prompt = f"""
CONTESTO UFFICIALE:
\"\"\"
{retrieved_chunk['content']}
\"\"\"
DOMANDA:
{query}
RISPOSTA:
"""
# 3. Chiamata al modello generativo locale
response = client.generate(
model=LLM_MODEL,
prompt=augmented_prompt,
system=system_prompt,
options={"temperature": 0.1}
)
return response["response"]
5. Test pratico in classe
Eseguiamo due interrogazioni: una la cui risposta è esplicitamente nel regolamento, e una non prevista, per verificare il comportamento del modello:
Python
# Test 1: Domanda coperta dal regolamento
domanda_1 = "Entro quanti giorni il professore deve restituire una verifica scritta?"
risposta_1 = ask_rag(domanda_1, vector_database)
print(f"Risposta del modello:\n{risposta_1}\n")
# Test 2: Domanda su cui il regolamento tace (test allucinazioni)
domanda_2 = "Posso uscire dalla scuola in bicicletta durante l'intervallo?"
risposta_2 = ask_rag(domanda_2, vector_database)
print(f"Risposta del modello:\n{risposta_2}")
Sintesi del percorso pedagogico per la lezione
| Fase | Concetto matematico / informatico | Cosa apprende lo studente |
|---|---|---|
| Chunking | Tokenizzazione e parsing di stringhe | Come dividere la memoria a lungo termine in unità discrete |
| Embedding | Spazi vettoriali ad alta dimensione (R768) | Come le macchine trasformano il significato delle parole in geometria |
| Retrieval | Prodotto scalare e metrica del coseno | La ricerca non è per parole chiave (SQL LIKE), ma per vicinanza concettuale |
| Generation | Prompt conditioning e grounding | Come impedire le allucinazioni ancorando il modello a fonti certe locali |
Gemini è un’AI e può fare errori, anche in merito a persone.