Python & AI Tutorials Logo
LangChain & LangGraph

3. Crea la tua prima chat CLI in streaming

Nel Capitolo 1 hai fatto la tua prima chiamata a un LLM e hai visto comparire una risposta completa tutta insieme. Nel Capitolo 2 hai imparato le basi concettuali dell'IA agentica e perché esiste LangChain. Ora è il momento di costruire qualcosa di pratico: un'applicazione di chat in streaming che risulti reattiva e professionale.

Perché lo streaming conta: quando fai a un LLM una domanda complessa, aspettare 10-30 secondi per una risposta completa sembra un malfunzionamento. Lo streaming consente ai token di apparire mentre vengono generati, creando un flusso conversazionale naturale. Questo capitolo costruisce un'applicazione di chat CLI con output in streaming, una corretta gestione della configurazione, capacità di debug e una robusta gestione degli errori.

Cosa costruirai: alla fine di questo capitolo, avrai uno script chat.py funzionante che:

  • Esegue lo streaming delle risposte dell'LLM token per token nel terminale
  • Carica le chiavi API in modo sicuro dalle variabili d'ambiente
  • Gestisce diversi tipi di modelli (modelli chat vs modelli di ragionamento) con parametri appropriati
  • Fornisce strumenti di debug per ispezionare ciò che viene effettivamente inviato all'LLM
  • Gestisce con eleganza gli errori comuni (chiavi API mancanti, guasti di rete, input non validi)

3.1) Crea una cartella di lavoro e installa i pacchetti

Prima di scrivere qualsiasi codice, ti serve una struttura di progetto pulita e le dipendenze corrette. Questa sezione stabilisce le basi per un progetto Python facile da mantenere.

Struttura del progetto

Crea una nuova directory per la tua applicazione di chat:

bash
mkdir langchain-chat
cd langchain-chat

Configurazione dell'ambiente Python

Crea un ambiente virtuale per isolare le dipendenze:

bash
# Crea il virtual environment
python -m venv venv
 
# Attivalo (macOS/Linux)
source venv/bin/activate
 
# Attivalo (Windows)
venv\Scripts\activate

Perché gli ambienti virtuali? LangChain ha molte dipendenze (ad es. OpenAI SDK, Pydantic, librerie async). Un ambiente virtuale garantisce:

  • Il Python di sistema rimane pulito
  • Progetti diversi possono usare versioni diverse di LangChain
  • Le dipendenze sono riproducibili (tramite requirements.txt)

Quando è attivato, vedrai (venv) nel prompt del terminale.

Installare LangChain

Installa i pacchetti core di LangChain:

bash
pip install langchain-core==1.2.7 langchain-openai==1.1.7 python-dotenv

Dettaglio dei pacchetti:

  • langchain-core: Astrazioni core (messaggi, prompt, catene, runnable)
  • langchain-openai: Implementazioni specifiche OpenAI (ChatOpenAI, embedding)
  • python-dotenv: Carica variabili d'ambiente da file .env

Nota sulle versioni: questo libro usa LangChain 1.2.x a gennaio 2026. Se stai leggendo in futuro, controlla la documentazione di LangChain per l'ultima versione.

Verifica l'installazione

Crea un test semplice per confermare che tutto funzioni:

python
# test_install.py
try:
    from langchain_core.messages import HumanMessage
    from langchain_openai import ChatOpenAI
    print("✓ langchain-core: OK")
    print("✓ langchain-openai: OK")
    print("\nInstallazione riuscita!")
except ImportError as e:
    print(f"✗ Import fallito: {e}")
    print("Assicurati che il virtual environment sia attivato.")

Eseguilo:

bash
python test_install.py

Output previsto:

✓ langchain-core: OK
✓ langchain-openai: OK
 
Installazione riuscita!

Se vedi "Installazione riuscita!", sei pronto a procedere. Se ottieni un errore di importazione, ricontrolla che:

  • Il virtual environment sia attivato (cerca (venv) nel prompt)
  • I pacchetti siano stati installati correttamente (prova a eseguire pip list)

Creare requirements.txt

Hai appena installato pacchetti con comandi pip install. Anche se va bene per imparare, c'è un modo migliore: i file requirements.txt. È una pratica standard nei progetti Python per diverse ragioni:

Perché usare requirements.txt?

  • Riproducibilità: Altri (o tu tra 6 mesi) possono installare esattamente le stesse versioni dei pacchetti
  • Gestione chiara delle dipendenze: Vedi a colpo d’occhio quali pacchetti servono al progetto
  • Collaborazione in team: I membri del team usano versioni identiche, evitando problemi del tipo "funziona sulla mia macchina"
  • Automazione: Server o pipeline CI/CD possono configurare l'ambiente con una sola riga: pip install -r requirements.txt

Crea un file requirements.txt nella root del progetto:

txt
# requirements.txt
langchain-core==1.2.7
langchain-openai==1.1.7
python-dotenv

Nota la sintassi:

  • ==1.2.7 fissa una versione esatta (consigliato per la riproducibilità)
  • Nessuno specificatore di versione (come python-dotenv) installa l'ultima versione stabile
  • Le righe che iniziano con # sono commenti

Ora chiunque può installare tutte le dipendenze con un singolo comando:

bash
pip install -r requirements.txt

È molto meglio che digitare ogni pacchetto singolarmente. Se un compagno di team clona il tuo progetto, deve solo:

  1. Creare un virtual environment
  2. Eseguire pip install -r requirements.txt

Non serve ricordare nomi o versioni dei pacchetti: è tutto nel file.

La struttura del tuo progetto

Dopo aver completato questa sezione, la tua cartella dovrebbe apparire così:

langchain-chat/
├── venv/                 # Virtual environment (non committare su git)
├── requirements.txt      # Dependency list
└── test_install.py       # Installation verification script

Prossimo: la sezione 3.2 mostra come caricare in modo sicuro le chiavi API usando file .env.

3.2) Variabili d'ambiente con .env

Le chiavi API sono segreti. Scriverle direttamente nel codice è un rischio di sicurezza (specialmente se fai commit su git). Questa sezione mostra l'approccio standard: variabili d'ambiente caricate da un file .env.

Perché le variabili d'ambiente?

Il problema delle chiavi hardcoded:

python
# ❌ NON FARE MAI QUESTO
llm = ChatOpenAI(api_key="sk-proj-abc123...")

Se fai commit di questo codice su GitHub, la tua chiave API è pubblica. Chiunque può usarla, accumulare costi sul tuo account o farsi revocare la chiave.

La soluzione: conserva i segreti in variabili d'ambiente e caricali a runtime.

Creare il file .env

Crea un file .env nella root del progetto:

bash
# .env
OPENAI_API_KEY=sk-proj-your-actual-key-here

Ottieni la tua chiave API:

  1. Vai su platform.openai.com/api-keys
  2. Crea una nuova chiave segreta (secret key)
  3. Copiala immediatamente (non potrai più visualizzarla)
  4. Incollala nel file .env, sostituendo sk-proj-your-actual-key-here

Passaggio critico di sicurezza: prima di fare qualsiasi altra cosa, proteggi la tua chiave API dall'essere committata su git.

Crea un file .gitignore nella root del progetto e aggiungi queste righe:

bash
# .gitignore
venv/
__pycache__/
*.pyc
.env

La riga .env dice a git di ignorare il file della tua chiave API. Questo evita di committare accidentalmente segreti nel controllo versione.

La struttura del tuo progetto ora:

langchain-chat/
├── venv/
├── .env                  # La tua chiave API (ignorata da git)
├── .gitignore           # Contiene: .env, venv/, ecc.
├── requirements.txt
└── test_install.py

Caricare le variabili d'ambiente

Il pacchetto python-dotenv carica i file .env in os.environ:

python
# chat.py
import os
from dotenv import load_dotenv
 
# Carica il file .env
load_dotenv()
 
# Accedi alle variabili d'ambiente
api_key = os.environ.get("OPENAI_API_KEY")
 
if not api_key:
    raise ValueError("OPENAI_API_KEY non trovato nell'ambiente")
 
print(f"Chiave API caricata: {api_key[:8]}...")  # Mostra solo i primi 8 caratteri

Come funziona load_dotenv():

  1. Cerca un file .env partendo da dove esegui lo script
  2. Legge ogni riga nel formato KEY=value
  3. Aggiunge ogni variabile a os.environ
  4. Se una variabile è già impostata (ad es. dalla tua piattaforma di hosting), non viene sovrascritta: rimane il valore esistente

Usare la chiave API con LangChain

Le implementazioni OpenAI di LangChain (ChatOpenAI, ecc.) cercano automaticamente OPENAI_API_KEY in os.environ:

python
from langchain_openai import ChatOpenAI
 
load_dotenv()
 
# Questo usa automaticamente os.environ["OPENAI_API_KEY"]
llm = ChatOpenAI(model="gpt-4o-mini")

La convenzione di LangChain: quando crei ChatOpenAI() senza un parametro api_key, cerca automaticamente OPENAI_API_KEY nell'ambiente. È un pattern standard in tutte le integrazioni LangChain.

Chiave API esplicita (per test o chiavi multiple):

python
llm = ChatOpenAI(
    model="gpt-4o-mini",
    api_key=os.environ.get("OPENAI_API_KEY")
)

È utile quando hai più chiavi API (sviluppo vs produzione) o vuoi essere esplicito su quale chiave viene usata.

Variabili d'ambiente in produzione

Negli ambienti di produzione (piattaforme cloud, container Docker), non usi file .env. Invece, configuri le variabili d'ambiente dalle impostazioni della piattaforma:

  • Docker: usa il flag -e quando esegui i container
  • Piattaforme cloud: imposta le variabili d'ambiente nei dashboard di configurazione
  • CI/CD: usa strumenti di gestione dei segreti

La parte importante: il tuo codice non cambia. os.environ.get("OPENAI_API_KEY") funziona allo stesso modo sia che la variabile arrivi da un file .env sia da una piattaforma cloud. Tratteremo la distribuzione (deploy) in dettaglio nei capitoli successivi.

Verifica la configurazione

Per confermare che tutto funzioni, puoi testare il codice di caricamento delle variabili d'ambiente mostrato prima. Se il tuo file .env è configurato correttamente, os.environ.get("OPENAI_API_KEY") restituirà la tua chiave API.

Se os.environ.get("OPENAI_API_KEY") restituisce None, verifica che:

  1. Hai chiamato load_dotenv() prima di accedere alla variabile d'ambiente
  2. .env esista nella root del progetto
  3. OPENAI_API_KEY=sk-proj-... sia scritto correttamente in .env
  4. Stai eseguendo dalla directory root del progetto

Prossimo: la sezione 3.3 implementa il vero ciclo di chat con output in streaming.

3.3) Implementare il ciclo di chat con output in streaming

Ora costruirai il ciclo di chat principale. Questa sezione introduce lo streaming: la differenza chiave tra un chatbot lento e uno reattivo.

Comprendere lo streaming

Senza streaming (approccio del Capitolo 1):

python
response = llm.invoke("Scrivi un tema di 500 parole sull'IA")
print(response.content)  # Aspetta 20 secondi, poi compare l'intero tema

Con streaming:

python
for chunk in llm.stream("Scrivi un tema di 500 parole sull'IA"):
    print(chunk.content, end="", flush=True)  # I token appaiono mentre vengono generati

Perché lo streaming conta:

  • Feedback immediato: invece di fissare uno schermo vuoto per 20 secondi, vedi comparire parole subito
  • Sensazione di conversazione naturale: proprio come parlare con una persona: le risposte arrivano progressivamente, non tutte in una volta
  • Risparmio di tempo e denaro: se l'LLM inizia a dare la risposta sbagliata, puoi interromperlo presto invece di aspettare una risposta completa (inutile)
  • Debug migliore: quando costruisci applicazioni, puoi individuare problemi (come errori di formattazione) mentre accadono, non dopo una lunga attesa

Che cos'è davvero lo streaming: lo streaming è la consegna incrementale dello stesso testo di risposta. Non espone ragionamenti nascosti o processi interni del modello: ti mostra solo un output parziale man mano che diventa disponibile dall'API. Pensalo come scaricare un file: vedi l'avanzamento mentre arrivano i chunk, ma il contenuto del file è lo stesso sia che lo scarichi tutto in una volta sia a pezzi.

Nota sui confini dei chunk: non è garantito che i chunk si allineino con parole o frasi. L'API invia token in piccoli batch per efficienza, quindi un chunk potrebbe essere "Hel", "lo! How", " can I", " help you", "?". È normale e previsto: non cercare di interpretare il significato dei singoli chunk.

Il ciclo di chat di base

Ecco un ciclo di chat minimale in streaming:

python
# chat.py
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
def main():
    load_dotenv()
    llm = ChatOpenAI(model="gpt-4o-mini")
    
    print("Chat avviata. Digita 'quit' o 'exit' per fermarti.\n")
    
    while True:
        user_input = input("Tu: ")
        
        if user_input.lower() in ["quit", "exit"]:
            print("Arrivederci!")
            break
        
        print("Assistente: ", end="", flush=True)
        
        for chunk in llm.stream([HumanMessage(content=user_input)]):
            print(chunk.content, end="", flush=True)
        
        print("\n")
 
if __name__ == "__main__":
    main()

Come funziona:

  1. while True:: ciclo infinito per una conversazione continua
  2. input("Tu: "): ottiene l'input dell'utente dal terminale
  3. llm.stream([HumanMessage(...)]): esegue lo streaming della risposta dell'LLM
  4. Output in streaming con parametri speciali:
    • end="": non aggiunge una nuova riga dopo ogni chunk (mantiene l'output sulla stessa riga)
    • flush=True: forza l'output immediato sul terminale senza buffering

Perché [HumanMessage(content=user_input)]?

I modelli chat di LangChain si aspettano una lista di messaggi, non una stringa grezza. Ogni messaggio ha un ruolo:

  • HumanMessage: input dell'utente
  • AIMessage: risposta dell'LLM
  • SystemMessage: istruzioni per l'LLM (trattate nel Capitolo 4)

Anche per un singolo messaggio utente, passi una lista: [HumanMessage(content="Hello")].

Limite chiave - Conversazioni single-turn: questo ciclo di chat è intenzionalmente senza stato. Ogni richiesta invia solo il messaggio corrente, non la cronologia precedente. Questo significa:

  • L'LLM non ricorderà cosa hai chiesto prima
  • Domande di follow-up come "E la sua popolazione?" non funzioneranno dopo aver chiesto "Qual è la capitale della Francia?"
  • Questa è una caratteristica fondamentale degli LLM: non hanno memoria a meno che tu non fornisca esplicitamente contesto

Esempio del limite:

Tu: Qual è la capitale della Francia?
Assistente: Parigi.
Tu: Qual è la sua popolazione?
Assistente: Non ho abbastanza contesto. Di quale città stai parlando?

Il ciclo while True fornisce continuità UX (puoi continuare a chattare), ma ogni turno è indipendente. In arrivo nel Capitolo 8: implementeremo la memoria conversazionale memorizzando e reinviando la cronologia dei messaggi ad ogni richiesta.

Eseguire il ciclo di chat

bash
python chat.py

Esempio di interazione:

Chat avviata. Digita 'quit' o 'exit' per fermarti.
 
Tu: Cos'è LangChain?
Assistente: LangChain è un framework per sviluppare applicazioni basate su modelli linguistici. Fornisce strumenti per la gestione dei prompt, catene, agenti e memoria.
 
Tu: Fammi un esempio semplice
Assistente: Ecco un esempio di base: ...
 
Tu: quit
Arrivederci!

Comprendere la Streaming API

Che cos'è un "chunk"?

Ogni chunk è un oggetto AIMessageChunk con:

  • content: i token di testo generati
  • response_metadata: info del modello, conteggio token, ecc.
python
for chunk in llm.stream([HumanMessage(content="Ciao")]):
    print(f"Chunk: {chunk}")
    print(f"Content: {chunk.content}")
    print(f"Type: {type(chunk)}")

Output:

Chunk: content='Ciao' response_metadata={'model_provider': 'openai', ...}
Content: Ciao
Type: <class 'langchain_core.messages.ai.AIMessageChunk'>
 
Chunk: content='!' response_metadata={...}
Content: !
Type: <class 'langchain_core.messages.ai.AIMessageChunk'>
 
Chunk: content=' Come' response_metadata={...}
Content:  Come
Type: <class 'langchain_core.messages.ai.AIMessageChunk'>

Accumulare la risposta completa

A volte ti serve la risposta completa (per logging, test o ulteriore elaborazione):

python
def chat_with_accumulation():
    load_dotenv()
    llm = ChatOpenAI(model="gpt-4o-mini")
    
    user_input = input("Tu: ")
    
    full_response = ""
    print("Assistente: ", end="", flush=True)
    
    for chunk in llm.stream([HumanMessage(content=user_input)]):
        print(chunk.content, end="", flush=True)
        full_response += chunk.content
    
    print("\n")
    
    # Ora hai la risposta completa
    print(f"[DEBUG] Lunghezza risposta completa: {len(full_response)} caratteri")
    return full_response

Questo pattern è comune quando devi:

  • Salvare la conversazione in un database
  • Effettuare il parsing della risposta per dati strutturati
  • Calcolare l'uso dei token o i costi
LLMChatLoopUserLLMChatLoopUserloop[Chunk multipli]"Cos'è LangChain?"stream([HumanMessage(...)])chunk: "Lang"print("Lang")chunk: "Chain "print("Chain ")chunk: "is a"print("is a")Streaming completato"\n" (nuova riga)Input successivo...

La struttura del tuo progetto dopo questa sezione:

langchain-chat/
├── venv/
├── .env
├── .gitignore
├── requirements.txt
├── test_install.py
└── chat.py              # Streaming chat loop (new!)

Prossimo: la sezione 3.4 mostra come gestire diversi tipi di modello con una configurazione intelligente dei parametri.

3.4) Configurazione intelligente: gestire i parametri per modelli di ragionamento vs modelli chat

OpenAI offre due tipi di modelli con capacità e meccanismi di controllo differenti:

Modelli chat (gpt-4o, gpt-4o-mini):

  • Veloci e conversazionali
  • Supportano temperature per controllare casualità e creatività
  • Migliori per compiti generali, scrittura creativa, coding di routine

Modelli di ragionamento (o1, o3, GPT-5):

  • Più lenti ma più logici e consistenti
  • NON supportano temperature (usano invece il ragionamento interno)
  • Migliori per matematica complessa, pianificazione multi-step, analisi formale

La differenza chiave: i modelli chat usano campionamento probabilistico (controlli la casualità), mentre i modelli di ragionamento usano logica interna deterministica (il modello controlla il proprio processo di ragionamento).

Comprendere la temperature (solo modelli chat)

Che cos'è la temperature?

La temperature è un numero tra 0.0 e 2.0 che controlla quanto creative siano le risposte del modello. A valori bassi (vicino a 0), ottieni risposte consistenti e prevedibili. A valori alti (vicino a 2.0), ottieni risposte creative e variate. Pensala come una "manopola della creatività".

Come funziona: quando genera ogni parola, il modello vede molte possibili parole successive con probabilità diverse. La temperature influenza come il modello sceglie:

  • Temperature bassa (0.0): sceglie quasi sempre la parola con probabilità più alta → risposte consistenti e focalizzate
  • Temperature alta (2.0): più probabile che scelga parole con probabilità più bassa → risposte diverse e creative

Importante: la temperature funziona solo con i modelli chat (gpt-4o, gpt-4o-mini). Non si applica ai modelli di ragionamento (GPT-5, o1, o3), che usano logica interna invece del campionamento probabilistico.

Guida ai valori della temperature:

  • 0.0: altamente deterministico, focalizzato e consistente

    • Usalo per: Q&A fattuale, generazione di codice di routine, output strutturato
    • Stesso input → output quasi identico ogni volta
    • Esempio: "Quanto fa 2+2?" → Sempre "4"
  • 0.7–1.0: comportamento standard di campionamento (default è 1.0)

    • Usalo per: conversazione generale, spiegazioni, risposte bilanciate
    • Variazione moderata in formulazioni ed esempi
    • Esempio: "Spiega la fotosintesi" → formulazione diversa ogni volta, stesse info di base
  • 1.2–2.0: più creativo e vario, meno prevedibile

    • Usalo per: scrittura creativa, brainstorming, ideazione
    • Alta variazione in tono, struttura e formulazione
    • Esempio: "Scrivi una poesia sulla luna" → stili molto diversi ogni volta

Nota: valori sopra 1.0 aumentano la creatività ma possono ridurre accuratezza fattuale e coerenza. Il valore massimo è 2.0.

Esempio: impatto della temperature (solo modelli chat)

python
# Temperature 0.0 - deterministico, stessa risposta ogni volta
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
response = llm.invoke([HumanMessage(content="Quanto fa 2+2?")])
print(response.content)  # Output: 4
 
# Temperature 1.0 - comportamento di default, possibile una leggera variazione
llm = ChatOpenAI(model="gpt-4o-mini", temperature=1.0)
response = llm.invoke([HumanMessage(content="Quanto fa 2+2?")])
print(response.content)  # Output: 4 (può includere una breve spiegazione)

Per domande chiuse e fattuali, la temperature ha poco effetto sulla correttezza.

Per compiti open-ended o creativi, la temperature influenza significativamente varietà, tono e stile.

Cosa succede se usi parametri dei modelli chat sui modelli di ragionamento?

Dipende dal modello: alcuni lo rifiutano, altri lo ignorano silenziosamente:

python
# ❌ Questo fallirà con i modelli o3
llm = ChatOpenAI(model="o3-mini", temperature=0.7)

Errore:

BadRequestError: Temperature is not supported with this model

Modelli diversi, criteri diversi:

  • Modelli o1 / o3: rifiutano esplicitamente i parametri non supportati. Se la temperature è inclusa, l'API restituisce subito un errore 400 BadRequest.
  • Modelli GPT-5: più permissivi: il parametro è accettato ma viene ignorato silenziosamente. La richiesta va a buon fine, ma la temperature non ha effetto.

Perché conta: controlla sempre quale modello stai usando e configura i parametri di conseguenza. Usare i parametri sbagliati può causare errori o fallire silenziosamente, sprecando tempo di debug.

Come controllare il comportamento dei modelli di ragionamento

Ora sai che i modelli chat usano temperature e i modelli di ragionamento no. Quindi come controlli i modelli di ragionamento?

I modelli di ragionamento vengono regolati tramite progettazione del prompt, non tramite parametri:

  • I modelli di ragionamento non espongono temperature o controlli simili
  • Invece, guidi il comportamento con come scrivi il prompt:
    • Istruzioni esplicite: "Pensa passo dopo passo", "Mostra i passaggi"
    • Vincoli come regole: "Non devi assumere...", "Verifica sempre..."
    • Requisiti strutturati: "Output in formato JSON", "Includi il ragionamento prima della risposta"
    • Logica decisionale: "Se condizione A, allora fai X, altrimenti fai Y"

Esempio: parametri chat vs prompt di ragionamento

python
# ❌ Approccio chat - non funziona con i modelli di ragionamento
llm = ChatOpenAI(model="o3-mini", temperature=0.5)
# Error: BadRequestError: Temperature is not supported
 
# ✅ Approccio di ragionamento - guida attraverso la struttura del prompt
prompt = """
Risolvi questo problema passo dopo passo:
1. Indica cosa sai
2. Mostra i calcoli
3. Verifica la risposta
 
Problema: Se x + 5 = 12, quanto vale x?
"""
llm = ChatOpenAI(model="o3-mini")
response = llm.invoke([HumanMessage(content=prompt)])
print(response.content)

Output:

1. Cosa so: x + 5 = 12
2. Calcoli: x = 12 - 5 = 7
3. Verifica: 7 + 5 = 12 ✓
 
Risposta: x = 7

Intuizione chiave: i modelli chat si controllano con parametri, i modelli di ragionamento si controllano con prompt.

Tabella decisionale per la selezione del modello

Ora che capisci come controllare entrambi i tipi di modelli, ecco quando usare ciascuno:

Tipo di attivitàModello consigliatoPerché
Conversazione generalegpt-4o-miniVeloce, economico, conversazionale
Q&A semplicegpt-4o-miniSufficiente per consultazione fattuale
Scrittura creativagpt-4o-mini (temp 0.8–1.0)La temperature abilita la creatività
Generazione di codiceGPT-5Pianificazione logica migliore
Ragionamento complessoGPT-5Ottimizzato per logica multi-step
Problemi di matematicao3 / o1Modelli di ragionamento dedicati
Pianificazione multi-stepGPT-5Forte nella pianificazione a lungo orizzonte
Analisi formale (legale/policy)o3Rigorosamente deterministico

Compromessi tra costo e latenza

Comprendere i compromessi pratici ti aiuta a scegliere il modello giusto per il tuo caso d'uso:

Tipo di modelloVelocità (latenza tipica)Costo (relativo)Ideale per
gpt-4o-miniMolto veloce (<2s)Molto bassoConversazione generale, compiti semplici
gpt-4oVeloce (1–4s)MedioChat di qualità superiore, compiti multimodali
GPT-5Moderato (3–8s)AltoRagionamento complesso, pianificazione
o1 / o3Il più lento (5–15s+)Il più altoRagionamento deterministico, logica formale

Note:

  • La velocità riflette la latenza tipica della risposta (varia in base a lunghezza e complessità del prompt)
  • Il costo è un confronto relativo: controlla i prezzi attuali sul sito di OpenAI
  • I modelli di ragionamento scambiano velocità e costo per consistenza e correttezza
  • I modelli chat privilegiano reattività ed efficienza

Quando usare modelli di ragionamento (GPT-5, o1, o3):

  • Problemi di matematica e STEM(Science, Technology, Engineering, Mathematics) multi-step che richiedono passaggi intermedi corretti
  • Analisi logiche complesse con dipendenze e vincoli
  • Debug di codice con molteplici cause interagenti
  • Attività di pianificazione con molte regole, casi limite o compromessi
  • Workflow di agenti che richiedono consistenza e pensiero a lungo orizzonte

Quando usare modelli chat (gpt-4o, gpt-4o-mini):

  • Conversazione generale e chat interattiva
  • Q&A semplice con profondità di ragionamento limitata
  • Generazione di contenuti (blog, riassunti, scrittura creativa)
  • Generazione di codice di routine e compiti di boilerplate
  • Applicazioni in cui velocità e costo contano più del ragionamento profondo

Prossimo: la sezione 3.5 mostra tecniche di debug per ispezionare ciò che viene effettivamente inviato all'LLM.

3.5) Debugging: ispezionare risposte e uso dei token

Quando il tuo LLM si comporta in modo inaspettato, devi vedere esattamente cosa è stato inviato e ricevuto. Questa sezione mostra come ispezionare le chiamate all'LLM e fare debug dei problemi.

Perché il debugging conta

Scenari comuni di debugging:

  • "Perché l'LLM ha dato questa risposta?" → controlla il prompt esatto
  • "Quanto è costata questa richiesta?" → controlla l'uso dei token
  • "Perché è così lento?" → misura la latenza
  • "La mia formattazione dei messaggi è corretta?" → ispeziona la struttura del messaggio

La sfida: quando chiami llm.invoke(), ottieni un oggetto response. Ma cosa c'è davvero dentro? Quali informazioni sono disponibili per il debugging?

Comprendere l'oggetto response

Prima di fare debugging, devi capire cosa restituisce llm.invoke().

Struttura di base:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])
 
# Cosa c'è nella response?
print(type(response))  # AIMessage
print(response.content)  # Il testo vero e proprio
print(response.response_metadata)  # Uso token, info modello, ecc.

Output:

<class 'langchain_core.messages.ai.AIMessage'>
Ciao! Come posso aiutarti oggi?
{
  'token_usage': {
    'completion_tokens': 9,
    'prompt_tokens': 8,
    'total_tokens': 17
  },
  'model_name': 'gpt-4o-mini-2024-07-18',
  'finish_reason': 'stop',
  ...
}

Parti chiave della response:

  • response.content: il testo generato dall'LLM
  • response.response_metadata: dizionario con:
    • token_usage: quanti token sono stati usati (per il calcolo dei costi)
    • model_name: versione esatta del modello che ha risposto
    • finish_reason: perché la generazione si è fermata (vedi sezione Modalità debug per dettagli)

Accedere all'uso dei token:

python
token_usage = response.response_metadata['token_usage']
print(f"Token del prompt: {token_usage['prompt_tokens']}")
print(f"Token della risposta: {token_usage['completion_tokens']}")
print(f"Totale: {token_usage['total_tokens']}")

Output:

Token del prompt: 8
Token della risposta: 9
Totale: 17

Perché conta: ti servono questi valori per debugging, tracciamento dei costi e ottimizzazione dei prompt.

Calcolare i costi a partire dall'uso dei token

L'uso dei token determina il costo. Ogni modello ha un prezzo diverso:

gpt-4o-mini (a gennaio 2026):

  • Input: $0.15 per 1M token
  • Output: $0.60 per 1M token

GPT-4o:

  • Input: $2.50 per 1M token
  • Output: $10.00 per 1M token

Funzione per il calcolo dei costi:

python
def calculate_cost(token_usage, model_name):
    """Calcola il costo in base all'uso dei token."""
    prompt_tokens = token_usage.get('prompt_tokens', 0)
    completion_tokens = token_usage.get('completion_tokens', 0)
    
    # Prezzi per 1M token (a gennaio 2026)
    pricing = {
        'gpt-4o-mini': {'input': 0.15, 'output': 0.60},
        'gpt-4o': {'input': 2.50, 'output': 10.00},
        'gpt-5': {'input': 1.25, 'output': 10.00},
    }
    
    if model_name not in pricing:
        return None
    
    input_cost = (prompt_tokens / 1_000_000) * pricing[model_name]['input']
    output_cost = (completion_tokens / 1_000_000) * pricing[model_name]['output']
    
    return input_cost + output_cost
 
# Esempio
response = llm.invoke([HumanMessage(content="Spiega il quantum computing")])
token_usage = response.response_metadata['token_usage']
cost = calculate_cost(token_usage, "gpt-4o-mini")
print(f"Costo: ${cost:.6f}")

Output:

Costo: $0.000123

Perché conta: le app in produzione possono gestire 50.000+ richieste/giorno. A $0.002 per richiesta, sono $3.000/mese. Usa il modello sbagliato o prompt gonfiati, e i costi saltano a $30.000/mese. Un bug nel loop di retry può bruciare migliaia di dollari in una notte. Traccia l'uso dei token dal primo giorno.

Abilitare la modalità debug (quando ti servono i dettagli grezzi dell'API)

L'oggetto response e un wrapper personalizzato coprono la maggior parte delle esigenze di debug. Ma a volte ti serve vedere esattamente cosa LangChain invia a OpenAI: la request e la response JSON grezze.

Quando potrebbe servirti:

  • Debug della formattazione dei messaggi di LangChain
  • Verificare che i parametri dell'API siano impostati correttamente
  • Investigare errori inattesi dell'API
  • Capire il payload esatto dell'API

LangChain ha logging di debug integrato tramite langchain_core.globals:

python
from langchain_core.globals import set_debug
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
set_debug(True)
 
# Ora tutte le chiamate LLM stamperanno info di debug
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])

Output:

[llm/start] [llm:ChatOpenAI] Entering LLM run with input:
{
  "prompts": [
    "Human: Hello"
  ]
}
[llm/end] [llm:ChatOpenAI] [1.45s] Exiting LLM run with output:
{
  "generations": [
    [
      {
        "text": "Ciao! Come posso aiutarti oggi?",
        "generation_info": {
          "finish_reason": "stop",
          "logprobs": null
        },
        "type": "ChatGeneration",
        ...
      }
    ]
  ],
  "llm_output": {
    "token_usage": {
      "completion_tokens": 9,
      "prompt_tokens": 8,
      "total_tokens": 17,
      ...
    },
    "model_provider": "openai",
    "model_name": "gpt-4o-mini-2024-07-18",
    ...
  },
}

Nota: il formato dell'output varia in base al provider LLM. Questo esempio mostra la struttura di OpenAI.

Cosa rivela l'output di debug:

La modalità debug mostra il flusso completo di comunicazione LangChain → OpenAI:

1. Trasformazione del formato dei messaggi:

python
# Il tuo codice
[HumanMessage(content="Hello")]
 
# Quello che vedi nell'output di debug
{
  "prompts": ["Human: Hello"]
}

La modalità debug mostra come LangChain rappresenta internamente il tuo messaggio prima di inviarlo all'LLM.

2. Stato di completamento della generazione:

python
"finish_reason": "stop"

Perché la generazione è terminata:

  • "stop": il modello ha completato la risposta naturalmente
  • "length": la risposta è stata troncata perché ha raggiunto il limite max_tokens
  • "tool_calls": il modello ha terminato la generazione producendo istruzioni di chiamata a strumenti invece di una risposta finale testuale (Capitolo 12)
  • "content_filter": la risposta è stata bloccata o soppressa a causa di regole di sicurezza o moderazione dei contenuti

Se vedi "length", aumenta max_tokens per ottenere la risposta completa.

3. Dettaglio dell'uso dei token:

python
"token_usage": {
  "completion_tokens": 9,
  "prompt_tokens": 8,
  "total_tokens": 17,
  "completion_tokens_details": {
    "reasoning_tokens": 0  # Per i modelli di ragionamento (o1/o3, ecc.)
  },
  "prompt_tokens_details": {
    "cached_tokens": 0  # Prompt caching (riduce i costi)
  }
}

Oltre ai conteggi base, puoi vedere:

  • reasoning_tokens: passaggi di ragionamento interno (solo per modelli di ragionamento)
  • cached_tokens: quanti token del prompt sono stati serviti da cache (riduce il costo)

4. Versione del modello e fingerprint:

python
"model_name": "gpt-4o-mini-2024-07-18",
"system_fingerprint": "fp_8bbc38b4db"
  • model_name: versione snapshot esatta (spiega perché le risposte cambiano nel tempo)
  • system_fingerprint: ID di configurazione backend di OpenAI (cambia quando aggiornano i sistemi)

5. Timing della richiesta:

python
[llm/end] [llm:ChatOpenAI] [1.56s]

Il [1.45s] mostra la durata totale della richiesta: utile per identificare query lente.

Prossimo: la sezione 3.6 mostra come gestire con eleganza gli errori comuni.

3.6) Gestire i fallimenti (simulare e risolvere errori comuni)

Le applicazioni LLM in produzione affrontano modalità di fallimento prevedibili: credenziali mancanti, timeout di rete, rate limit e input non validi. Questa sezione mostra come gestire questi errori con eleganza e costruire applicazioni robuste fin dal primo giorno.

I sei errori comuni

1. Chiave API mancante

Quando succede: provi a creare un'istanza ChatOpenAI, ma OPENAI_API_KEY non è impostata nel tuo ambiente.

Esempio:

python
# Il file .env non esiste, oppure OPENAI_API_KEY non è definita
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])

Errore che vedrai:

OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable

Come risolvere:

  1. Controlla che il file .env esista nella root del progetto
  2. Verifica che il nome della chiave sia esattamente OPENAI_API_KEY (errore comune: OPENAPI_KEY)
  3. Assicurati che load_dotenv() venga chiamato prima di creare l'LLM

2. Chiave API sbagliata

Quando succede: il file .env contiene una chiave API non valida, scaduta o copiata male.

Esempio:

python
# .env contiene: OPENAI_API_KEY=sk-invalid-key-12345
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="Hello")])

Errore che vedrai:

AuthenticationError: Incorrect API key provided

Come risolvere:

  1. Vai su https://platform.openai.com/api-keys
  2. Verifica che la chiave sia ancora attiva (non revocata o scaduta)
  3. Genera una nuova chiave se necessario
  4. Copia con attenzione l'intera chiave (errore comune: mancano i primi/ultimi caratteri)
  5. Incolla in .env senza spazi extra:
bash
OPENAI_API_KEY=sk-proj-exactkeyhere

3. Guasti di rete

Quando succede: la connessione internet cade, oppure i server di OpenAI sono temporaneamente irraggiungibili durante una richiesta.

Esempio:

python
# Il WiFi si disconnette a metà richiesta, oppure l'API di OpenAI è down
response = llm.invoke([HumanMessage(content="Hello")])

Errore che vedrai:

APIConnectionError: Connection error

Come risolvere:

  1. Controlla la connessione internet
  2. Verifica lo stato di OpenAI su https://status.openai.com

4. Rate limit

Quando succede: invii troppe richieste in poco tempo e superi la quota API.

Esempio:

python
# Invio di 1000 richieste istantaneamente
for i in range(1000):
    llm.invoke([HumanMessage(content=f"Request {i}")])

Errore che vedrai:

RateLimitError: Rate limit reached for requests

Come risolvere:

  1. Controlla i tuoi rate limit su https://platform.openai.com/account/limits
  2. Fai upgrade del piano se ti servono limiti più alti
  3. Usa elaborazione batch per carichi grandi (trattato nel Capitolo 6)

5. Nome modello non valido

Quando succede: specifichi un nome modello che non esiste o non è disponibile nel tuo piano.

Esempio:

python
llm = ChatOpenAI(model="gpt-99-ultra")  # Non esiste
response = llm.invoke([HumanMessage(content="Hello")])

Errore che vedrai:

NotFoundError: The model `gpt-99-ultra` does not exist or you do not have access to it

Come risolvere:

  1. Controlla i modelli disponibili nel tuo piano su https://platform.openai.com/docs/models

6. Limite token superato

Quando succede: il prompt è troppo lungo e supera la finestra di contesto massima del modello.

Esempio:

python
# Creazione di un prompt da 1 milione di caratteri
huge_prompt = "x" * 1_000_000
response = llm.invoke([HumanMessage(content=huge_prompt)])

Errore che vedrai:

BadRequestError: This model's maximum context length is 128000 tokens. However, your messages resulted in 250000 tokens.

Come risolvere:

  1. Controlla la lunghezza dell'input prima di inviare
  2. Conosci i limiti del modello:
    • gpt-4o-mini: 128K token
    • gpt-4o: 128K token
    • gpt-5: 400K token
  3. Per documenti lunghi, usa chunking o summarization (trattato nel Capitolo 9)

Prossimi passi: il Capitolo 4 mostra come progettare template di prompt riutilizzabili che separano il prompt engineering dal codice applicativo.