Intelligenza artificiale · Trust & safety · LLM · Node.js · PostgreSQL
In un marketplace la chat fra chi compra e chi vende è indispensabile. Prima di acquistare un cliente vuole chiarire i dettagli dell’offerta, capire se fa al caso suo, chiedere una variante o una data diversa. Ma la stessa chat è anche la porta da cui esce la vendita: basta un numero di telefono, un indirizzo email o un “cercami su Instagram”, e la trattativa prosegue altrove, senza commissione per la piattaforma e senza le tutele che la piattaforma offre a entrambe le parti. Le condizioni d’uso vietano lo scambio di contatti prima dell’acquisto, ma una regola scritta non basta a farla rispettare.
Per un marketplace in cui i clienti acquistano le offerte di venditori indipendenti abbiamo progettato e costruito il sistema che applica questa regola. Valuta ogni messaggio prima che venga consegnato e, al suo centro, usa un modello linguistico che ragiona sull’intento di chi scrive, non sulla forma del testo. È in produzione da marzo 2023: lo abbiamo messo sul percorso di invio dei messaggi poche settimane dopo che i modelli linguistici di nuova generazione erano diventati accessibili agli sviluppatori tramite API.
Affidare un flusso che tocca i ricavi a una tecnologia uscita da poche settimane sembra un azzardo. Lo sarebbe stato se avessimo costruito il sistema attorno al modello. Lo abbiamo costruito nel modo opposto: il modello linguistico è un segnale sostituibile, dentro un sistema che decide, conserva e spiega ogni esito senza dipendere da quale modello c’è dietro. È questa scelta che ha permesso al filtro di attraversare più di tre anni di produzione, due famiglie di modelli e un esperimento di fine-tuning senza riscrivere la pipeline di valutazione.
Perché le parole chiave non bastano
Il primo strumento che viene in mente è un elenco di parole vietate e qualche espressione regolare per email e numeri di telefono. Funziona con chi non prova a nascondersi, e infatti resta uno dei segnali del sistema. Ma chi vuole portare la trattativa fuori dalla piattaforma impara in fretta, e la creatività con cui un recapito può essere camuffato non ha limiti pratici. Ogni nuova regola invita una nuova variante, e la lista cresce senza mai chiudere il problema.
C’è poi l’errore opposto, ed è altrettanto costoso. In una conversazione d’acquisto i numeri sono ovunque (prezzi, quantità, date, orari), e così i link a risorse utili per decidere. Una regola abbastanza larga da intercettare ogni recapito mascherato bloccherebbe anche le domande che portano all’acquisto. Un filtro che frena le conversazioni legittime danneggia il marketplace quanto uno che lascia passare i contatti.
La domanda giusta, quindi, non è “questo testo contiene un numero?”, ma “chi scrive sta cercando di farsi contattare fuori dalla piattaforma?”. È un problema di comprensione del linguaggio. All’inizio del 2023 i modelli linguistici erano appena diventati abbastanza capaci ed economici da porsi questa domanda su ogni singolo messaggio, e abbiamo deciso di sfruttarli subito, su un flusso critico per il business, invece di aspettare che la tecnologia si assestasse. Proprio perché non si era assestata, però, tutto il resto del sistema doveva essere progettato per non dipendere da lei.
Il controllo avviene prima della consegna, non dopo
Il secondo bivio riguarda il momento del controllo. La strada più comoda è la moderazione a posteriori: il messaggio parte subito, un processo in background lo analizza e, se serve, lo rimuove. Non aggiunge attesa all’invio e tiene il modello fuori dal percorso critico. Qui però non serve a niente: un numero di telefono visto per qualche minuto è già un numero copiato, e una rimozione arriva sempre dopo il danno. Per questo abbiamo scelto la strada più impegnativa: il controllo avviene in modo sincrono, dentro la richiesta di invio, prima che il messaggio raggiunga il destinatario.
Conta anche dove vive. Un filtro scritto nell’endpoint della chat protegge quell’endpoint, e il giorno in cui il prodotto aggiunge un nuovo modo di scrivere all’altra parte qualcuno deve ricordarsi di replicarlo. Il nostro controllo vive invece nei servizi di dominio che creano i messaggi: ogni percorso con cui un utente scrive all’altra parte in una conversazione usa la stessa valutazione. Un nuovo punto di contatto fra acquirente e venditore eredita il filtro, invece di reimplementarlo.
Un dettaglio rende il meccanismo affidabile, non soltanto funzionante: anche i canali secondari devono rispettare l’esito del filtro. La notifica email non parte dal resolver, ma da un evento di dominio emesso solo per i messaggi consegnati e solo dopo il commit della transazione. Un messaggio trattenuto, o una transazione annullata, non può quindi generare un’email con il suo contenuto: il canale secondario non può lasciar passare ciò che la chat ha trattenuto.
Il modello vota, ma non decide da solo
Una volta disponibile un modello linguistico, la tentazione è affidargli l’intera decisione. Sarebbe un errore di progetto. Alcune informazioni non vanno dedotte dal testo perché sono già note con certezza: se l’acquirente ha completato l’acquisto, per esempio, lo dice il database, non il tono del messaggio. E un sistema in cui un solo giudizio decide tutto non si può correggere un pezzo alla volta. L’alternativa classica, una catena di if che cresce a ogni caso nuovo, dopo qualche anno non dice più quale condizione abbia deciso l’esito.
Abbiamo scelto una terza strada: un insieme di regole indipendenti, ognuna delle quali osserva il messaggio da un punto di vista diverso e restituisce un contributo a un punteggio comune. Le regole vengono eseguite in parallelo, quindi il controllo sincrono attende la più lenta, non la somma di tutte, e la somma dei contributi decide l’esito.
| Famiglia | Che cosa osserva | Punto di forza |
|---|---|---|
| Contenuto | schemi riconoscibili e liste di termini configurate per account | immediato, gratuito, perfettamente spiegabile |
| Intento | il significato del messaggio, valutato da un modello linguistico | riconosce ciò che nessuna regola scritta prevede |
| Contesto | stato dell’ordine e fase della transazione | distingue lo stesso testo in fasi diverse |
I segnali non hanno tutti lo stesso peso, e non vanno tutti nella stessa direzione: alcuni spingono verso il blocco, altri verso la consegna. Il contesto, in particolare, può prevalere sul contenuto. Quando l’acquirente ha completato l’acquisto, scambiarsi i contatti non è più un tentativo di aggirare la piattaforma, ma il modo naturale di portare a termine ciò che è stato acquistato. Al contrario, la finestra in cui una conversazione resta aperta è legata al ciclo di vita dell’ordine: oltre una certa fase non ha più ragione di esistere.
In pratica, il modello a punteggio rende il sistema facile da far evolvere e da difendere. Aggiungere una regola significa aggiungere un termine alla somma, senza toccare le altre. Ogni decisione si può scomporre, voce per voce, nei contributi che l’hanno determinata. E le liste di termini si gestiscono dal back office per singolo venditore o acquirente: quando gli operatori individuano un comportamento ricorrente su un account, intervengono subito, senza aspettare un rilascio.
Il prompt è la policy, scritta come una specifica
Il cuore del sistema è la regola che interroga il modello linguistico, e il punto debole tipico di questa regola è il prompt. Un modello a cui si chiede soltanto di “trovare i contatti” tende a vederli ovunque: un orario, un prezzo, il nome di un’app di uso comune. Il nostro prompt non è una domanda generica: è la policy della piattaforma scritta in linguaggio naturale, con la stessa precisione di una specifica. Descrive il ruolo del modello, che cosa è vietato e, soprattutto, che cosa è esplicitamente consentito.
Quest’ultima parte è quella che fa la differenza in produzione. Un modello a cui si spiega che cosa è normale in una conversazione d’acquisto distingue la domanda pratica dal tentativo di uscire dalla piattaforma. Definire bene il lecito è ciò che permette di essere severi sull’illecito senza frenare le conversazioni che portano all’acquisto.
Per i casi vietati il prompt descrive, per schemi generali, i modi in cui un’informazione può essere nascosta. Non elenca stringhe da cercare, che riproporrebbero il limite delle parole chiave: insegna al modello a riconoscere lo schema, non la singola variante. Non richiede nemmeno che il recapito compaia per intero: basta un intento riconoscibile. È una severità che il sistema può permettersi proprio perché il lecito è definito con altrettanta precisione.
Il resto del contratto rende la risposta utilizzabile da un sistema. Il modello risponde con un verdetto binario, in un formato fisso che il codice interpreta senza ambiguità. La temperatura è a zero, così che lo stesso messaggio riceva un giudizio il più possibile stabile. I messaggi arrivano in qualunque lingua, e la comprensione multilingue è uno dei motivi per cui un modello linguistico è lo strumento giusto: non serve un dizionario di regole per ogni lingua.
Infine, il prompt vive nella configurazione dell’applicazione, non nel codice. Cambiare la policy significa modificare un testo, sottoposto alla stessa revisione di qualunque altra modifica, senza toccare la logica del filtro.
Tre configurazioni di modello, una sola architettura
Il filtro è nato a marzo 2023 su un modello linguistico generalista, e nelle prime settimane il prompt ha preso la forma descritta sopra: verdetto in formato fisso con una breve motivazione, contesto della piattaforma, casi consentiti.
All’inizio del 2024 abbiamo sperimentato la strada che molti considerano la naturale evoluzione per un compito così specifico: il fine-tuning, con due varianti dello stesso modello addestrate apposta. Ad aprile abbiamo scelto il modello generale con un prompt più ricco. Un modello addestrato fissa la policy al momento dell’addestramento, mentre un prompt si aggiorna con una normale modifica di configurazione quando la policy evolve o quando emerge una nuova tecnica di mascheramento. Per un problema in cui le regole cambiano più spesso dei modelli, la flessibilità vale più della specializzazione.
La scelta si è rivelata giusta anche per un motivo più strutturale. Un modello addestrato è una variante di uno specifico modello di base e ne condivide il ciclo di vita: i fornitori ritirano regolarmente i modelli più vecchi, e quando un modello di base viene ritirato smettono di essere servite anche le varianti addestrate su di esso. Restare sul fine-tuning avrebbe voluto dire rifare l’addestramento a ogni cambio di generazione. Con il prompt, nel 2025 siamo passati a un modello di generazione più recente e abbiamo arricchito le istruzioni con nuovi schemi di mascheramento: un modello più capace sfrutta meglio istruzioni più articolate, e il passaggio non ha richiesto altro.
Il motivo per cui ogni passaggio è stato così leggero è il contratto della regola, rimasto lo stesso fin dalle prime settimane: un messaggio in ingresso, un verdetto e la risposta grezza del modello in uscita. Nome del modello, temperatura e prompt stanno in configurazione: un cambio di modello è un cambio di configurazione. Il resto della pipeline non se n’è mai accorto. Quando arriva un modello migliore o più economico, adottarlo è una decisione di prodotto, non un progetto.
Un messaggio trattenuto non sparisce
Bloccare un messaggio pone un problema che di solito si sottovaluta: che cosa farne. Le risposte più immediate sono entrambe sbagliate. Cancellarlo elimina l’unica prova di ciò che è successo: l’assistenza non può più spiegare la decisione, e non si può verificare se il filtro abbia avuto ragione. Farlo sparire in silenzio, lasciando credere a chi scrive che sia stato consegnato, genera conversazioni che si interrompono senza motivo, frustrazione e ticket di assistenza.
La nostra scelta è che un messaggio trattenuto sia conservato intatto, invisibile al destinatario per costruzione e spiegato a chi l’ha scritto. Viene salvato senza modifiche, insieme alla sua valutazione, e marcato come non consegnato.
“Per costruzione” significa che la regola di visibilità sta nel modello di lettura, non nelle interfacce. La query che legge le conversazioni, condivisa da entrambe le applicazioni, restituisce un messaggio trattenuto solo a chi lo ha scritto. Lo stesso vale per le anteprime delle conversazioni nella lista dei messaggi. Se il filtro fosse affidato alle schermate, ogni nuova schermata sarebbe un’occasione per dimenticarlo; così, per mostrare il messaggio al destinatario bisognerebbe aggirare la query stessa.
Chi scrive, invece, vede chiaramente che cosa è successo. Nell’app di chi compra il messaggio è segnato come non inviato, con una spiegazione: i contatti del venditore arrivano con l’acquisto, e per qualunque necessità c’è l’assistenza clienti. Nell’app del venditore il messaggio appare barrato, accompagnato da un avviso. Chi aveva una domanda legittima può riformularla, e a tutti viene ricordato che la piattaforma tutela entrambe le parti proprio perché la transazione avviene al suo interno.
Ogni esito si può ricostruire, voce per voce
Una decisione presa anche da un modello linguistico, se ne resta solo il verdetto finale, è indifendibile. Davanti a un utente che chiede perché il suo messaggio non è arrivato, l’assistenza può solo rispondere “l’ha deciso l’algoritmo”. E davanti a una modifica del prompt nessuno può dire se abbia migliorato le cose o spostato gli errori altrove.
Per questo ogni messaggio porta con sé il dettaglio completo della propria valutazione, in un campo JSONB su PostgreSQL: il contributo di ciascuna regola e, per la regola basata sul modello, la richiesta inviata e la risposta ottenuta. Il punteggio totale è solo la sintesi; dietro c’è sempre la spiegazione.
Un filtro che legge le conversazioni fra acquirenti e venditori tratta dati personali per sua natura, e ogni valutazione ne porta con sé una parte. Per questo la privacy è stata un requisito di progetto fin dal primo giorno, al pari dell’efficacia del filtro: che cosa viene inviato al modello, che cosa viene conservato e chi può consultarlo sono decisioni prese con la tutela degli utenti come vincolo, non verificate a posteriori.
Nel back office i messaggi trattenuti compaiono segnalati nella conversazione, con i loro punteggi, e l’assistenza può verificare esattamente perché sono stati trattenuti. Le stesse informazioni permettono di valutare l’effetto di una modifica alla policy sui messaggi reali, invece che su esempi costruiti a tavolino, e di accorgersi quando una nuova tecnica di mascheramento sta emergendo. È ciò che rende sicuro far evolvere il prompt: ogni cambiamento si misura sul traffico vero.
C’è anche un aspetto operativo. Le chiamate al fornitore del modello compaiono nelle stesse tracce OpenTelemetry del resto del backend, e latenza ed errori si osservano con gli stessi strumenti con cui si osserva il resto della piattaforma, non in una console separata.
La stessa policy, una misura diversa per ogni canale
La chat è il canale principale per scambiarsi i contatti, ma non l’unico. E la misura che funziona nella chat, trattenere il messaggio, non si applica ovunque: una recensione, per esempio, è un testo pubblico e non può restare in sospeso. Per questo abbiamo applicato la stessa policy con un’azione diversa per ogni canale, invece di forzare ovunque lo stesso meccanismo.
Nel back office dei venditori, telefono, email e indirizzo dell’acquirente sono visibili solo quando l’ordine è interamente pagato; prima di quel momento compaiono dati segnaposto. Il contatto diretto arriva così con la transazione completata, anche al di fuori della chat. Nelle recensioni i termini non ammessi vengono oscurati nella versione pubblicata, mentre il testo originale resta disponibile agli operatori, e quando le regole cambiano le recensioni esistenti vengono rielaborate in automatico. L’invio dei messaggi, infine, è protetto da una verifica anti-bot, che tiene fuori dalla chat i client automatici.
Trattenere, oscurare, rinviare o respingere: ogni livello fa la cosa giusta per il proprio canale. Insieme rendono lo scambio di contatti prima dell’acquisto una strada scomoda, invece di un ostacolo da aggirare una volta sola.
Che cosa serve per mettere un LLM sul percorso critico
Per un marketplace la commissione su ogni vendita è il modello di business, e la disintermediazione lo erode in silenzio, una conversazione alla volta. Il filtro protegge questo valore senza sacrificare ciò che rende utile la piattaforma: acquirenti e venditori continuano a parlarsi liberamente di tutto ciò che serve a decidere, e si scambiano i recapiti nel momento in cui è giusto farlo.
Il metodo vale oltre questo progetto. In un sistema che usa un modello linguistico, il modello è la parte che cambia più in fretta e quella che si controlla meno: escono versioni nuove, quelle vecchie vengono ritirate. Per questo non lo trattiamo come il centro dell’architettura, ma come un componente dietro un contratto stabile, e progettiamo tutto ciò che deve durare (dove avviene il controllo, come si combinano i segnali, dove vive la policy, che cosa si conserva e si traccia) per non dipendere da lui. È questo che ci ha permesso di adottare una tecnologia nelle sue prime settimane, su un flusso che tocca i ricavi, e di vederla migliorare per più di tre anni senza rifare il sistema.
Il lavoro sul prompt prosegue con lo stesso approccio con cui il sistema è cresciuto fin dall’inizio: rendere la policy più precisa, lasciando invariata l’architettura che la applica.