Ho già argomentato perché l'assistente deve essere del cittadino e non dell'ente. L'ente non costruisce il chatbot. Espone la sua fonte, certificata e leggibile da una macchina, e lascia che sia il cittadino a scegliere l'assistente. Una tesi resta aria finché non la provi. Così ho costruito il prototipo, e questo articolo racconta come funziona dentro.

Si chiama ChattyDuck e gira su un comune immaginario, il Comune di Paperopoli. Il comune è inventato apposta. Nessun dato reale, quindi nessuno può prendere per vera una scadenza. Ma il formato è quello serio: scadenze della TARI, aliquote IMU, calendario della raccolta, orari degli uffici. Le cose per cui un cittadino contatta davvero il comune. Lo puoi provare adesso, i link sono fondo alla pagina.

Il problema, in una riga

Un modello linguistico da solo non sa niente del tuo comune. Sa le cose in generale. Se gli chiedi la scadenza di un tributo ti dà una risposta plausibile, e plausibile non vuol dire giusta. Questo è il difetto di ogni chatbot della PA costruito senza una base solida. Sembra competente e sbaglia.

La soluzione non è un modello più potente. È dargli una fonte, e obbligarlo a rispondere solo da quella.

La fonte: un server che sa fare una cosa sola

Duckburg Registry è un server MCP ad oggi lo standard aperto con cui un assistente parla con una fonte esterna. Il server espone i suoi contenuti su un endpoint, in sola lettura, con un rate limiter e un token opzionale. Non conosce nessun modello linguistico. Non ragiona. Fa il fornitore di dati firmati, e nient'altro.

Espone un solo strumento di ricerca. Gli passi una domanda, ti restituisce i passaggi più pertinenti del corpus. E la descrizione stessa dello strumento, quella che il modello legge come istruzione, dice di citare sempre l'identificativo del passaggio e che l'unica verità sono quei passaggi. L'obbligo di non inventare non sta solo nel prompt di sistema, è cablato anche nello strumento.

La ricerca dentro il server è volutamente semplice: nessun database vettoriale, nessun embedding, solo un punteggio per frequenza dei termini sul testo normalizzato. Per un corpus di poche centinaia di passaggi funziona. Per un comune con migliaia di pagine servirebbe una ricerca semantica vera. Ma il punto della demo non è l'algoritmo di ricerca. È il confine di proprietà.

L'hash: cosa rende un dato certificato

Ogni passaggio del corpus porta con sé tre informazioni: un identificativo, una versione e un hash. L'hash è un'impronta del testo, uno SHA-256. Ecco un passaggio vero, quello delle rate TARI:

json
{
  "id": "tari:p02",
  "version": "1.0",
  "hash": "afe6856e1ae5e0042e2df9bcf2a954e1dda30e15d16f9cd0b34b7342c563d63",
  "text": "Per l'anno in corso la TARI si paga in tre rate..."
}

L'hash non lo calcola il server a ogni richiesta. È pre-calcolato e incluso nel corpus, come dovrebbe essere firmato dall'ufficio che pubblica il dato, non dal server che lo serve. Se il testo cambia di un carattere, l'hash cambia. Chi consuma la fonte può verificare che quello che legge è esattamente ciò che l'ente ha pubblicato. È questa la differenza tra un dato certificato e un testo qualunque.

Due modelli, due modi di collegarsi, due costi

Qui sta la parte più concreta della dimostrazione, e sono due implementazioni diverse nello stesso progetto.

Gemini non parla MCP in modo nativo. Quindi il codice deve fare tutto a mano: chiede al Registry la lista degli strumenti, li traduce nel formato che Gemini capisce, e gestisce il giro. Il modello chiede di usare lo strumento, il codice esegue la chiamata al server, il risultato torna al modello. Questo strato di traduzione l'ho scritto io, riga per riga.

Claude parla MCP in modo nativo. Con l'SDK ufficiale Anthropic gli passo l'indirizzo pubblico del server e un paio di parametri, e basta:

csharp
McpServers = [new BetaRequestMcpServerUrlDefinition {
    Name = "chattyduck-registry", Url = mcpEndpoint }]

Nessun ponte. È l'infrastruttura di Anthropic a collegarsi al server e a fare il giro degli strumenti. Il mio codice si limita a leggere il risultato che torna già pronto. L'unico vincolo è che l'endpoint sia raggiungibile da Internet, non basta localhost: in sviluppo ho usato un tunnel ngrok, in produzione un dominio vero.

Stesso server, stesso risultato, ma per un modello ho scritto uno strato di traduzione e per l'altro no. Dove lo standard è supportato nativamente, il costo di integrazione crolla a zero. Non è retorica da convegno. È la differenza tra due file, misurata in righe di codice.

Un dettaglio laterale che conferma la cosa: con l'SDK ufficiale di Anthropic ottengo gratis anche il consumo reale, letto dagli header della risposta e mostrato in interfaccia. Per Gemini quel dato non è esposto, e lo devo stimare da solo. Anche l'osservabilità viene gratis, dove lo standard ha un client ricco dietro.

Il terzo assistente: zero righe di server toccate

Il terzo client non è codice mio. È un assistente commerciale, l'app che una persona usa già ogni giorno, collegata dall'esterno con la stessa configurazione MCP standard:

json
{ "mcpServers": { "comune-paperopoli": { "type": "http", "url": "<endpoint>" } } }

Nessuna autenticazione, stesso trasporto, stesso strumento cerca, stessa fonte. Ho verificato la coerenza chiedendo agli assistenti di citare lo stesso identificativo di passaggio e controllando che, a parità di domanda, la risposta fosse la stessa. Questo è il punto che regge davvero la tesi: il server non sa e non deve sapere chi lo interroga.

La prova che conta: la fonte batte la memoria

La parte più difficile per un assistente ancorato è fidarsi della fonte quando questa contraddice ciò che il modello crede di sapere. Il modo in cui lo impongo non è nell'interfaccia, è nel prompt di sistema, condiviso identico dai due backend, che contiene solo regole di comportamento e mai contenuto. Le righe che contano dicono che l'unica verità sono i passaggi recuperati, che non si risponde senza averne recuperato almeno uno, che non si usano le regole nazionali che il modello ricorda ma solo quella locale della fonte, e che se l'informazione non c'è si dice netto che non è nelle fonti.

I dati, cioè scadenze, importi e aliquote, non compaiono mai in quel prompt. Se ci fossero, l'assistente li ripeterebbe perché glieli abbiamo dettati noi, non perché li ha letti dalla fonte, e la dimostrazione crollerebbe. Ho chiesto la scadenza della prima rata TARI. Un modello lasciato alla propria memoria tende a una data nazionale plausibile e sbagliata. L'assistente ancorato dà la scadenza di Paperopoli, con l'identificativo del passaggio da cui l'ha presa.

Questo è esattamente il caso della circolare aggiornata. La regola cambia, il modello ha in testa quella vecchia, la fonte certificata deve sovrascrivere il ricordo. A Paperopoli la scadenza me la sono inventata. In un comune vero è quella del regolamento. Il meccanismo che la fa vincere sulla memoria è identico.

Ho provato a romperlo

Un assistente pubblico va attaccato prima che lo attacchino gli altri. Ho lanciato contro ChattyDuck una serie di prove avversarie: richieste di ignorare le istruzioni, di rispondere a memoria senza citare la fonte, un finto dirigente che autorizzava a saltare le regole, istruzioni nascoste dentro i passaggi del corpus per vedere se venivano eseguite invece che trattate come testo.

Va detto con onestà: non è una suite di test automatica, è un'attività manuale che ha guidato le regole finite nel prompt. Ha retto quasi tutto. L'unico cedimento iniziale è stato quando ho chiesto di ripetere le istruzioni parola per parola. Ho aggiunto una clausola che dichiara le istruzioni riservate e tratta qualsiasi comando dentro i messaggi o dentro i passaggi come testo da valutare, mai come ordine che cambia le regole. Quel buco si è chiuso. Ed è questa la parte utile: non che sia perfetto, ma che il confine sia visibile, in un file che puoi leggere e modificare. Un chatbot che non puoi mettere alla prova così è un chatbot di cui non ti puoi fidare.

Cosa è vero e cosa è finto

È simulato il comune, e solo il comune: le vie, i quartieri, le scadenze, il patrono. Il resto, server MCP, protocollo, backend, prompt, comportamento, è codice reale che gira.

È in parte simulata anche la separazione tra proprietari. Il Registry e i due backend li ho scritti io. L'unico client davvero esterno, non scritto da me, è il terzo, l'assistente commerciale agganciato dal di fuori. Ed è quello che regge il peso vero della tesi.

E manca il percorso personale. Nessuna identità, nessuna pratica, nessun pagamento. Il server è in sola lettura e anonimo per costruzione. Collegare dati personali richiederebbe autenticazione e sistemi gestionali reali dell'ente, che qui restano descritti, non costruiti. Dichiararlo non indebolisce il prototipo. Lo rende una cosa seria invece di una demo che promette troppo.

La distanza dalla realtà è una sola: il corpus

Tra Paperopoli e un comune reale cambia una cosa sola: il contenuto del corpus. Il server, il protocollo, il prompt, i due backend, il widget di chat, tutto identico, zero righe di codice da cambiare. Cambiano i dati.

Questo sposta il problema dove deve stare. Il lavoro difficile per un comune non è questa infrastruttura, che è scritta e funziona. È prendere le proprie regole, scadenze e procedure e metterle in una forma strutturata, versionata e firmata con un hash. Quel pezzo, a Paperopoli, l'ho potuto inventare in un pomeriggio. Nella realtà è il lavoro serio, e spetta all'ente, non allo sviluppatore del chatbot.

È anche il motivo del titolo. Non esiste un chatbot della PA senza una fonte dati certificata. Puoi avere il modello più bravo del mondo, ma se non ha una fonte da cui rispondere, inventa. La fonte non è un accessorio dell'assistente. È la parte pubblica, ed è la parte che conta.

Comporre invece di costruire. Ho scritto un server che sa fare una cosa, servire passaggi firmati, e un prompt che sa fare un'altra, rifiutarsi di rispondere senza fonte. Il resto, uno dei tre client, l'ho preso già fatto. Non ho costruito l'ennesimo chatbot d'ente. Ho esposto una fonte, e ho lasciato che gli assistenti facessero il loro mestiere.

Il codice è un prototipo. Provalo, aprilo, rompilo. Il principio invece è pronto adesso.

PoC: https://paperopoli.derrigo.it/assistente

Codice: https://github.com/fderrigo/DuckburgSmartCity