Questo articolo apre una serie dedicata alla costruzione, passo dopo passo, di una chat AI completamente locale usando Python, Gemma e gli strumenti che servono per trasformare un semplice esperimento in un’applicazione sempre più completa.
Se hai mai provato a far girare un modello linguistico sul tuo PC, probabilmente conosci già LM Studio, Ollama o strumenti simili. Sono ottimi, ma volevo capire cosa c’è “sotto il cofano” e costruirmi qualcosa di mio, un’app minimale, in Python, in grado di far girare una AI in locale.
Il risultato è una chat locale basata su Gemma 4B con interfaccia web in stile ChatGPT, che gira interamente sul mio computer, nessun dato esce mai dalla macchina. Ecco cosa serve per replicarla, e gli scogli in cui sono inciampato lungo la strada.
L’architettura, in breve
Tre pezzi principali:
llama-cpp-python: la libreria che fa girare il modello. Sotto il cofano usallama.cpp, il motore di inferenza open source diventato lo standard de facto per eseguire LLM in locale, sia su CPU che su GPU.- Flask: un micro-framework web Python, per esporre il modello con un’API HTTP e servire una pagina di chat nel browser.
- Gemma 4B in formato GGUF: la versione del modello di Google DeepMind, quantizzata (compressa) in un formato pensato apposta per
llama.cpp.
L’interfaccia è una singola pagina HTML/CSS/JS senza framework, che parla con il backend via fetch e riceve le risposte token per token (streaming), esattamente come nei chatbot commerciali.
I package da installare
Nella pratica, llama-cpp-python non è un pacchetto Python “puro”: contiene codice C++ compilato per parlare direttamente con CPU e GPU, e questo è dove iniziano le complicazioni.
Quando lanci pip install, pip cerca un wheel che è un pacchetto già compilato per il tuo sistema operativo e la tua versione di Python. Se non lo trova, prova a compilare da sorgente, cosa che richiede un compilatore C++ installato (su Windows, gli strumenti di build di Visual Studio). Se non ce l’hai, l’installazione fallisce con errori tipo:
CMake Error: CMAKE_C_COMPILER not set, after EnableLanguageLa soluzione più semplice, che evita del tutto la compilazione, è puntare pip a un indice di wheel già pronti pubblicato dal maintainer del progetto:
# Versione CPU (funziona ovunque)
pip install llama-cpp-python --prefer-binary --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
# Versione con accelerazione GPU NVIDIA (molto più veloce)
pip install llama-cpp-python --prefer-binary --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124La scelta tra CPU e GPU dipende dal tuo hardware e dalla pazienza che hai per aspettare le risposte.
l problema di Python 3.14
Il primo inciampo è stato banale ma istruttivo: avevo Python 3.14 appena uscito, e per quella versione non esistono ancora wheel precompilati di llama-cpp-python su nessun indice. Risultato: pip prova sempre a compilare da sorgente, qualunque wheel-index tu usi.
Lezione imparata: per progetti con dipendenze native come questa, conviene restare su una versione di Python “matura” — 3.11 o 3.12 — invece dell’ultima release. Con py -3.11 -m venv venv puoi creare un ambiente virtuale con la versione specifica, anche se sul sistema hai già installato una versione più recente.
GPU sì, ma con un tassello in più: CUDA Toolkit
Avere una GPU NVIDIA non basta. Il driver (quello che ti dà nvidia-smi funzionante e fa girare i giochi) non include le librerie di calcolo generico che programmi come llama-cpp-python usano per parlare con la GPU.
Serve il CUDA Toolkit, che installa le librerie runtime (cudart64_12.dll, cublas64_12.dll, ecc.). Senza di esso, il wheel GPU si installa correttamente ma va in crash all’avvio con un errore di libreria mancante:
FileNotFoundError: Could not find module '...\llama_cpp\lib\llama.dll' (or one of its dependencies)
Un dettaglio interessante emerso strada facendo: LM Studio non ha questo problema perché include il proprio runtime CUDA “incorporato” nel pacchetto dell’app è uno dei motivi per cui è più comodo da installare rispetto a costruirsi qualcosa da zero.
Punto d’attenzione: installa la versione del Toolkit compatibile col wheel che usi (es. CUDA 12.4 per un wheel cu124), non necessariamente l’ultima disponibile sul sito NVIDIA — le versioni troppo recenti (es. 13.x) potrebbero non essere ancora supportate dai wheel precompilati.
Un bug curioso: il crash del banner colorato
Ultimo scoglio, questa volta lato Windows/terminale: Flask stampa all’avvio un banner colorato (“Serving Flask app…”). Su alcune configurazioni di terminale Windows, la libreria che gestisce i colori (colorama) va in crash con un errore criptico:
OSError: Windows error: 6
Il server in realtà si sarebbe avviato correttamente — il crash avviene proprio mentre tenta di scrivere quel messaggio decorativo. La soluzione è disabilitare il banner colorato con poche righe di codice, senza toccare nient’altro nella logica dell’app.
Riassumendo
Di seguito solo i passaggi corretti che hanno funzionato sulla mia configurazione, Win 11, RTX 2070 8GB VRAM:
--
1. Installato Python (v 3.11.9) dal sito python.org, spuntando "Add to PATH"
--
2. Installato CUDA TOOLKIT (v 12.4) dal sito nvidia.com
--
3. Creato un env per la v 3.11.9:
cd "D:\Private Mind"
py -3.11 -m venv venv
venv\Scripts\activate
Testare dall'interno della cartella:
python --version
--
4. Installate le dipendenze Python mancanti:
pip install llama-cpp-python --prefer-binary --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121
pip install flask
--app.py carica automaticamente il primo file .gguf trovato in models/, espone un endpoint /api/chat che risponde in streaming, e apre da solo il browser all’avvio. Il numero di layer offloadati sulla GPU è configurabile con una singola variabile (N_GPU_LAYERS = -1 per usarla tutta).
static/index.html è una chat minimale in HTML/CSS/JS puro, senza dipendenze esterne, che mostra le risposte token per token man mano che arrivano.
gemma-chat-app/
├── app.py # backend Flask + inferenza
├── static/
│ └── index.html # interfaccia chat
├── models/ # qui va il modello .gguf (scaricato a parte)
└── venv/ # setup versione pythonapp.py
"""
Gemma Chat - backend locale
----------------------------
Server Flask che carica un modello Gemma in formato GGUF tramite
llama-cpp-python e lo espone via API HTTP con risposta in streaming.
Alla partenza apre automaticamente il browser sull'interfaccia di chat.
"""
import json
import os
import sys
import threading
import webbrowser
from glob import glob
from flask import Flask, Response, request, send_from_directory
from llama_cpp import Llama
# ---------------------------------------------------------------------------
# Configurazione
# ---------------------------------------------------------------------------
# Quando l'app viene "congelata" con PyInstaller, i file vengono estratti
# in una cartella temporanea indicata da sys._MEIPASS. In sviluppo normale
# usiamo semplicemente la cartella dello script.
BASE_DIR = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
# La cartella "models" invece NON deve stare dentro il bundle PyInstaller
# (sarebbe enorme): la cerchiamo accanto all'eseguibile finale.
if getattr(sys, "frozen", False):
APP_DIR = os.path.dirname(sys.executable)
else:
APP_DIR = os.path.dirname(os.path.abspath(__file__))
MODELS_DIR = os.path.join(APP_DIR, "models")
STATIC_DIR = os.path.join(BASE_DIR, "static")
PORT = 5001
N_CTX = 4096 # dimensione del contesto
N_THREADS = os.cpu_count() or 4
N_GPU_LAYERS = -1 # -1 = scarica tutti i layer possibili sulla GPU se disponibile
SYSTEM_PROMPT = "Sei un assistente utile, conciso e onesto. Rispondi in italiano."
# ---------------------------------------------------------------------------
# Caricamento modello
# ---------------------------------------------------------------------------
def find_model_path() -> str:
"""Trova il primo file .gguf nella cartella models/."""
candidates = glob(os.path.join(MODELS_DIR, "*.gguf"))
if not candidates:
raise FileNotFoundError(
f"Nessun file .gguf trovato in '{MODELS_DIR}'. "
"Scarica un modello Gemma in formato GGUF e mettilo in quella cartella."
)
return candidates[0]
print("Caricamento modello in corso, potrebbe richiedere qualche secondo...")
MODEL_PATH = find_model_path()
print(f"Modello: {MODEL_PATH}")
llm = Llama(
model_path=MODEL_PATH,
n_ctx=N_CTX,
n_threads=N_THREADS,
n_gpu_layers=N_GPU_LAYERS,
verbose=False,
)
print("Modello caricato. Server pronto.")
# ---------------------------------------------------------------------------
# App Flask
# ---------------------------------------------------------------------------
app = Flask(__name__, static_folder=STATIC_DIR, static_url_path="")
@app.route("/")
def index():
return send_from_directory(STATIC_DIR, "index.html")
@app.route("/api/chat", methods=["POST"])
def chat():
"""
Riceve: {"messages": [{"role": "user"/"assistant", "content": "..."}, ...]}
Restituisce: testo in streaming (text/event-stream semplificato)
"""
data = request.get_json(force=True)
history = data.get("messages", [])
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history
def generate():
stream = llm.create_chat_completion(
messages=messages,
stream=True,
temperature=0.7,
max_tokens=1024,
)
for chunk in stream:
delta = chunk["choices"][0]["delta"]
token = delta.get("content", "")
if token:
# Server-Sent-Events style: ogni riga "data: ..." + riga vuota
yield f"data: {json.dumps({'token': token})}\n\n"
yield "data: [DONE]\n\n"
return Response(generate(), mimetype="text/event-stream")
def open_browser():
webbrowser.open(f"http://127.0.0.1:{PORT}")
if __name__ == "__main__":
# Su alcuni terminali Windows, il banner colorato che Flask stampa
# all'avvio va in crash (bug noto colorama/click, OSError: Windows
# error 6). Lo disabilitiamo: e' solo estetico, non serve al
# funzionamento del server.
try:
from flask import cli as flask_cli
flask_cli.show_server_banner = lambda *args, **kwargs: None
except Exception:
pass
threading.Timer(1.0, open_browser).start()
print(f"Server in ascolto su http://127.0.0.1:{PORT}")
app.run(host="127.0.0.1", port=PORT, threaded=True)Quello che sembrava un progetto da weekend si è rivelato un buon ripasso pratico su come funzionano davvero i pacchetti Python con dipendenze native, la differenza tra driver e toolkit su una GPU, e quanto i tool “consumer” come LM Studio nascondano complessità reale sotto un’interfaccia semplice.
Il risultato però vale lo sforzo: una chat AI completamente locale, veloce (grazie all’accelerazione GPU), senza abbonamenti né dati che lasciano il computer e soprattutto, capita fino in fondo, pezzo per pezzo.
Se l’articolo ti è piaciuto restiamo in contatto su linkedin a: https://www.linkedin.com/in/andreatonin/
| Editorial Manager at Pills for Nerds | CTO & Technology Consultant |
Technology professional, editor and lifelong nerd with over 30 years of experience in the digital and innovation sectors. I work as a CTO and technology consultant, designing complex and scalable software ecosystems, and I am also active in IT education and professional training. Alongside my technology career, I serve as Editorial Manager and contributor at Pills for Nerds, online publication covering video games, game development, emerging technologies, artificial intelligence, digital culture and industry events. In my editorial role, I coordinate content planning and contribute articles, interviews, event reports and in-depth features. You can learn more about my consulting work at lucedigitale.com and connect with me on LinkedIn
















