./run tribunale-che-non-ha-mai-parlato-con-un-ospite
Il bot ha giudicato sé stesso contro il mio staff per tre mesi. Non ha mai parlato con un ospite.
Ho costruito un tribunale a 3 agenti LLM per far competere un concierge WhatsApp contro lo staff vero. Ha misurato per tre mesi senza mai andare live. Poi ho buttato tutto e ricostruito da zero.
- status
- draft
- project
- gv-concierge-v2
- updated
- 2026-07-07
- tags
Per tre mesi ho avuto un sistema che, ogni notte, prendeva le risposte del mio bot e quelle vere dello staff, le metteva una accanto all’altra, e le faceva giudicare da un tribunale di tre modelli LLM. Accusa, difesa, giudice. Verdetto su sette dimensioni pesate. Report al mattino. Funzionava benissimo. E non è mai servito a niente — perché quel bot, in tre mesi, non ha mai scritto una parola a un ospite vero.
Questo è il diario di come una misura elegante può diventare la scusa perfetta per non spedire mai, e di cosa ho fatto quando l’ho capito.
Il contesto
Sto costruendo un concierge WhatsApp per una piccola guesthouse di lusso a Roma (sei camere). L’idea è automatizzare il ciclo di vita dell’ospite: risposte, codici delle serrature smart, clima, prenotazioni. Non un chatbot generico — un sistema che sa chi ti scrive, in che stanza stai, a che punto del soggiorno sei.
C’era una Legge #1, dettata dal proprietario e non derogabile: il bot non parla a un ospite finché non è PROVATO che funziona. Provato = un eval dimostra qualità ≥ staff per N giorni, capability per capability. Fino ad allora: solo shadow — genera, misura, non invia. Il go-live è un cancello che apre l’owner, non una fase del piano.
Legge sana. Il problema è come l’ho interpretata per tre mesi.
Il Tribunale (la parte di cui vado fiero)
Il cuore della v1 è un valutatore avversariale che chiamo il Tribunale. Per ogni coppia (risposta_bot, risposta_staff) dello stesso messaggio storico:
- Procuratore e Avvocato girano in parallelo — uno attacca la risposta del bot, l’altro la difende.
- Giudice (un modello Opus) legge entrambi in sequenza ed emette un verdetto indipendente. Non fa la media dell’accusa e della difesa: giudica.
Le sette dimensioni non pesano uguale, e il punteggio finale non è quello che l’LLM dichiara di sé — è ricalcolato in modo deterministico dai pesi. Il vincitore è una soglia, non un’opinione:
DIMENSION_WEIGHTS = {
"lingua": 0.20, "accuratezza": 0.18, "completezza": 0.15,
"tono": 0.15, "contesto": 0.12, "brevita": 0.10, "CTA": 0.10,
}
def determine_winner(bot: float, staff: float, threshold: float = 0.5) -> str:
diff = bot - staff
if diff >= threshold: return "bot" # BOT_BETTER
if diff <= -threshold: return "staff" # STAFF_BETTER
return "draw" # COMPARABLE
Un paio di scelte di cui vado ancora fiero: il giudice valuta anche lo staff sulle stesse 7 dimensioni, così il confronto è like-for-like — stessa scala, stessi pesi, nessuna corsia preferenziale. E c’è una guardia di ground-truth sui fatti di casa (orari, codici) applicata simmetricamente: se taglio l’accuratezza del bot per un fatto sbagliato, taglio quella dello staff con lo stesso metro, altrimenti una regola “onesta” ribalterebbe il verdetto in una sola direzione.
I numeri che ne sono usciti valgono oro. Il volume vero degli ospiti: services 47%, general 26%, prenotazioni/accesso ~14%, codici d’accesso 6% — il valore non è nelle rare domande di stato, è nel grounding. Le dimensioni più deboli del bot: CTA 5.5, completezza 5.53, contesto 5.71, accuratezza 6.02. E la scoperta che ha deciso la v2: iniettare lo stato dell’ospite prima dell’LLM ha dato +0.6 sulle dimensioni-target, con un giorno di lavoro.
La decisione dura
Qui arriva la spina. Il Tribunale funzionava. Ma tutto intorno al Tribunale era una torre: nove microservizi (orchestrator, message-parser, context, response-engine, lock, climate, command, config, alert-bridge) per una guesthouse da sei camere. Ogni messaggio faceva 5 hop HTTP. Un audio passava per Whisper, un’immagine per un modello vision, un PDF per un OCR, e il synthesizer cuciva tutto: 30-40 secondi di latenza da cascata di specialisti LLM.
Ma il vero danno non era la latenza. Era che la misura era sparsa su quei servizi: schema in un posto, env in un altro, cronjob fragili, immagini da tenere sincronizzate. Passavo ore a sessione solo per far ripartire la pipeline di valutazione. E finché la misura è fragile, la prova non arriva mai. E finché la prova non arriva, il cancello del go-live non si apre. Tre mesi di shadow non erano prudenza: erano un sistema troppo complicato per dimostrare qualcosa di sé stesso in tempo utile.
Il 13 giugno il proprietario ha chiuso la discussione con una riga: “Sostieni che si possa fare meglio? Provalo.” Ho fermato le patch e ho ricostruito da zero.
La v2: stesso cervello, 1/10 della complessità
La regola della ricostruzione era brutale: il vecchio repo diventa sola lettura — fonte di dati, codice provato e lezioni — ma non si copia la sua architettura. Si riparte puliti.
- 1 servizio (monolite modulare Python/FastAPI: ingest + Twin + Brain + delivery) + 1 worker + 1 Postgres. Niente service mesh, niente network policy, niente cinque immagini.
- Deterministic-first: l’80% delle risposte deve arrivare da stato/playbook, senza LLM. L’LLM solo per la coda lunga (services/general).
- Il Guest Twin al centro, caricato a ogni messaggio, deciso prima dell’LLM.
Il Twin è la parte che mi ha insegnato di più sulla disciplina. È un resolver strict, deny-by-default: attacca una prenotazione a una conversazione solo se appartiene senza ambiguità a quell’ospite ed è rilevante oggi. Nel dubbio, non attacca:
# _select_relevant: ambiguità -> not-found. Wrong attachment is worse than none.
active = [r for r in cands if r["checkin_date"] <= today <= r["checkout_date"]]
if len(active) > 1:
logger.warning("guest_state ambiguous -> not attaching")
return _NOT_FOUND
Attaccare la prenotazione sbagliata è peggio che non attaccarne nessuna: un codice della serratura dato alla persona sbagliata non è un bug di qualità, è un incidente di sicurezza. Il resolver non solleva mai eccezioni — degrada a not-found. Anche il grounding sulla knowledge base è estrattivo e locale (embedding MiniLM, coglie cosa direbbe verbatim): sotto una soglia di cosine 0.55, si astiene e la palla resta allo staff.
Gotcha onesti
- La v2 non parla ancora a nessuno — di proposito. Il modulo di consegna è, letteralmente, questo:
"""Delivery — outbound. Unreachable while GV_SHADOW_MODE=true (LAW #1). (later)""". Uno stub. Tutta l’architettura esiste per non spedire finché la prova non c’è. - L’eval-lite della v2 è più onesto ma più povero. Non è il Tribunale a 3 agenti: è similarità semantica bot-vs-staff con embedding locali, zero costo metered, un comando, idempotente. Il docstring lo ammette senza pudore: la similarità non è correttezza — un bot corretto ma più lungo può somigliare meno a una risposta staff secca. Il giudice LLM vero torna dopo, gated da un flag. Ho barattato profondità per affidabilità della misura: meglio un termometro grezzo che segna ogni giorno che uno fine che si rompe ogni sessione.
- Il vecchio non è morto, è congelato. Il KUBECONFIG del vecchio cluster è rinominato
config.disabledapposta, così unakubectldistratta non tocca la produzione storica.
Come va
La v2 è a F0: scheletro provabile in shadow. Twin portato e provato, state-sync read-only dal PMS, ingest dei messaggi in shadow, eval-lite che gira in-process. Le fasi F1-F2 (i playbook deterministici e il grounding sul 73% del volume) sono il vero salto di qualità e sono ancora davanti. Il go-live verso gli ospiti resta un cancello dell’owner, capability per capability: clima → codici → stop umano su prenotazioni, Questura e pagamenti.
Cosa ho imparato
Che un sistema di misura può essere così bello da diventare un alibi. Il Tribunale non era il problema — era la cosa migliore che avevo costruito. Il problema erano i nove microservizi intorno, che rendevano la misura talmente fragile che la prova non arrivava mai, e quindi il bot restava in shadow all’infinito, al sicuro, inutile.
La lezione controintuitiva: la complessità che ti fa sentire prudente è spesso la stessa che ti impedisce di spedire. Ho tenuto l’idea del giudizio avversariale (torna, gated) e ho buttato l’impalcatura. Un termometro che segna ogni giorno vale più di un tribunale che si convoca una volta al mese. La prova arriva solo se puoi misurare sempre.