Scrittura assistita di progetti
I testi del documento ancorati ai dati del progetto
- Periodo
- dal 2020
- Ruolo
- progettazione, sviluppo e test
- Ambito
- integrazioni sistemi AI aziendali
- Tecnologie
- Python · Django · Celery · PostgreSQL · Anthropic Claude · Playwright · docxtpl
a cosa serve
Un progetto comincia da un piano finanziario — voci di costo, attività, importi, indicatori — e deve arrivare a un documento che quel piano lo racconti, in una forma prevista da regole scritte altrove. Il documento è lungo, diviso in una ventina di sezioni, e ogni sezione deve dire in prosa quello che le tabelle dicono in numeri.
Scriverlo a mano ha due costi. Il primo è il tempo, ed è quello che si vede. Il secondo è che il testo e i numeri divergono: si aggiorna un importo nel piano e la frase che lo descriveva resta come stava, in un documento che qualcuno leggerà per decidere.
Questa è l'applicazione dove quel documento si scrive. Tiene i dati del progetto, li mette davanti a un modello linguistico sezione per sezione, e conserva il testo che ne esce — che poi si rigenera o si corregge a mano. Insieme al testo conserva anche come è stato prodotto.
L'applicazione esiste dal 2020. Il modello linguistico ci è entrato quattro anni dopo, per generare i testi delle sezioni, dentro un sistema già in esercizio: quasi tutto il lavoro descritto qui riguarda il modo di innestare un modello in un'applicazione che già funzionava, senza che il resto cambiasse comportamento.
la coerenza prima dello stile
Il principio attorno a cui è costruita sta in una riga: nessun contenuto testuale è slegato dai dati strutturati del progetto. Il dominio è normato e chiuso — le regole le ha scritte qualcun altro e il documento deve conformarsi — quindi ogni descrizione deve poter essere ricondotta a dati, attività e indicatori espliciti, e la coerenza interna del progetto conta più dello stile della prosa.
È un vincolo che ribalta l'uso ovvio di un modello linguistico. Un paragrafo scritto bene che cita un'attività assente dal piano, o una cifra di mercato che nessun dato del progetto sostiene, produce un documento peggiore di uno scritto male: chi lo legge trova un'incoerenza fra due parti che dovevano dire la stessa cosa. Quasi tutto il lavoro che segue serve a togliere al modello la possibilità di introdurne.
Da qui discende anche l'elenco di ciò che l'applicazione non fa, che è servito più volte a tenere la rotta quando arrivavano richieste ragionevoli e fuori perimetro: non è un editor di testi generico, non è un sistema di budget o di rendicontazione, non modifica e non ricalcola mai i dati economici che riceve, e non produce contenuti slegati dal progetto. Il testo è l'unica cosa che genera.
un database proprio, e un flusso a senso unico
L'applicazione ha un database autonomo, separato da quello del gestionale da cui vengono i numeri. Alla creazione del progetto il piano finanziario viene duplicato qui dentro, e da quel momento il flusso è a senso unico: si leggono quei numeri, si scrive testo, si compongono i documenti, e non si ricalcola mai nulla di economico.
La duplicazione è la scelta che di solito viene contestata per prima — si sta copiando un dato invece di leggerlo dalla fonte — e la difendo per la ragione opposta a quella che sembra: non serve a rendere più veloce la lettura, serve a rendere impossibile la scrittura. Un'applicazione che genera prosa non deve avere nemmeno la possibilità tecnica di toccare un importo.
il contesto è dichiarativo
Ogni sezione del documento vuole una fetta diversa dei dati: l'analisi di un mercato vuole budget, attività previste e destinatari di quel mercato; la descrizione di una singola attività vuole obiettivi specifici e costi; i messaggi vogliono temi e attività per mercato.
All'inizio la raccolta di quei dati stava dentro i moduli che costruivano i prompt, e la stessa logica si trovava duplicata in più moduli: due sezioni che avevano bisogno degli stessi numeri li raccoglievano ciascuna per conto suo, e quando la regola cambiava bisognava ricordarsi di tutti i posti.
Ora la mappa è dichiarata. Un registro associa la coppia sezione–sottosezione ai fornitori che costruiscono i dati strutturati da dare al modello, e ogni fornitore ha un ambito preciso — il mercato, la singola attività, i messaggi del progetto, i soggetti proponenti. I testi direttivi, cioè le istruzioni date al modello, stanno in file separati, e il codice si limita a risolvere i segnaposto.
Due conseguenze pratiche, che sono la ragione per cui l'ho fatto:
- Aggiungere una sezione somiglia più a scrivere il suo file di istruzioni e dichiarare da quali fornitori prende i dati che a riscrivere la logica di raccolta.
- Cambiare una frase di un prompt è una modifica che si legge nella storia del repository per quello che è. Quando le istruzioni al modello stanno dentro il codice che costruisce anche i dati, la differenza fra «ho cambiato il tono di un paragrafo» e «ho cambiato quali numeri entrano nel testo» sparisce dal registro delle modifiche.
Il testo generato è agganciato a un contesto che può stare su tre livelli — l'intero progetto, un singolo mercato, una singola attività — e la stessa sezione esiste quindi in più copie, una per elemento. È il motivo per cui la coppia sezione–sottosezione da sola non basta a identificare un testo, e il riferimento al contesto viaggia insieme.
lo stesso contenuto, due destinazioni
Il testo scritto qui dentro deve uscire in due forme molto diverse: un documento completo, con la sua numerazione di paragrafi e i suoi stili, e una serie di riquadri da riempire su un portale esterno, che ha una struttura propria — sezioni diverse, ordine diverso, e in qualche caso un campo che raccoglie insieme cose che nel documento stanno separate.
La tentazione, davanti a due destinazioni, è scrivere due percorsi di esportazione. Ho preferito tenere un solo catalogo del contenuto e dichiarare accanto a ogni sezione dove va a finire. Le forme di destinazione sono tre, e sono sufficienti a coprire tutti i casi incontrati:
| Forma | Cosa dice |
|---|---|
| Corrispondenza diretta | questa sezione alimenta quella sezione della destinazione |
| Innesto in un campo | questo contenuto non è una sezione a sé, entra come campo dentro un'altra |
| Entrambe | lo stesso contenuto alimenta una sezione e viene ripetuto dentro un'altra |
Il routing può stare anche a livello di sottosezione, quindi un blocco può viaggiare verso una destinazione senza che la sezione che lo contiene ne abbia una propria.
Due dettagli che sembrano minori e non lo sono. Il primo: i limiti di lunghezza dei campi sono dichiarati nello schema accanto alla sezione, non nascosti nel codice che genera. Sono un vincolo della destinazione — un riquadro che accetta cinquecento caratteri — e vanno detti dove si dichiara il resto, perché è la stessa informazione che serve al modello quando scrive e al documento quando impagina. Il secondo: i due cataloghi hanno responsabilità distinte e dichiarate — uno è la fonte di verità del contenuto modificabile, l'altro del menu e dei blocchi della destinazione esterna. Quando due strutture parallele non dicono chi comanda su cosa, la domanda «quale delle due aggiorno?» torna a ogni modifica.
Resta un debito, e lo scrivo fra le cose aperte: le due strutture non coincidono per numero né per ordine, e tenerle allineate è oggi una responsabilità di chi modifica, non una proprietà verificata dal codice.
il modello si sceglie per caso d'uso, non per applicazione
La configurazione del modello non è una costante globale dell'applicazione: la richiesta dichiara il proprio caso d'uso, e un instradamento comune risolve quale fornitore e quale modello servono quel caso. Generare la prosa di una sezione lunga e classificare una lista breve sono lavori diversi, con esigenze diverse di lunghezza, temperatura e costo.
Il vantaggio non è la flessibilità in astratto: è che il giorno in cui un modello viene sostituito, la modifica sta in un posto solo e vale per tutti i punti che dichiarano quel caso d'uso — invece di essere una caccia alle costanti sparse nelle applicazioni.
I parametri sono espliciti e versionati insieme al codice: la lunghezza massima della risposta è tarata sulla dimensione reale delle sezioni, e non lasciata al valore di default del fornitore.
ogni testo si porta dietro come è nato
Insieme al paragrafo generato vengono salvati il prompt che l'ha prodotto, il contesto dati che gli è stato passato e le istruzioni di sistema, in un unico blocco strutturato accanto al testo. Di ogni pezzo di testo si sa quindi con quali dati e con quali istruzioni è stato scritto.
Serve nel momento in cui un risultato è sbagliato. Senza quella traccia l'unica diagnosi disponibile è «il modello ha sbagliato», che non si può né confermare né smentire; con la traccia si vede se il dato mancava nel contesto, se l'istruzione era ambigua o se il modello è andato per conto suo — e le tre cose si riparano in posti diversi, rispettivamente nel fornitore di dati, nel file di istruzioni e nella scelta del modello.
È anche ciò che rende la rigenerazione un'operazione onesta: si può confrontare il testo vecchio col nuovo sapendo esattamente cosa è cambiato a monte, invece di attribuire la differenza al caso.
generazione lunga, attesa dichiarata
Generare i testi di un progetto intero non è un'operazione che sta dentro una richiesta web: sono decine di chiamate, ciascuna di qualche secondo. La generazione di massa gira quindi in un processo separato, con lo stato di avanzamento riportato all'interfaccia mentre procede.
La scelta che conta è che le due strade — la generazione di una singola sezione e quella dell'intero progetto — passano dalla stessa pipeline, non da due implementazioni parallele. Una copia scritta per il lavoro in coda sarebbe rimasta indietro alla prima modifica ai prompt, e la differenza si sarebbe vista solo sui documenti generati di notte.
dall'agente al codice
L'ultima parte è l'integrazione di un modulo di caricamento automatico dei dati verso un portale esterno, tramite Playwright.
È nata come skill pilotata da un agente. La corrispondenza fra i campi dei dati in uscita e i campi delle maschere di destinazione non era documentata da nessuna parte, e l'agente la scopriva navigando: apriva una sezione, leggeva cosa c'era, decideva dove andava scritto un valore. Ha funzionato, e aveva il limite che ci si aspetta: a ogni esecuzione rifaceva da capo una scoperta già fatta, e il risultato dipendeva ogni volta da un modello nel ciclo.
Quando la mappatura ha smesso di cambiare, quella conoscenza è stata congelata in un runtime Python deterministico. Stesso lavoro, senza modello nel ciclo: ripetibile, leggibile, e con gli errori che si manifestano sempre nello stesso punto. Accanto c'è un modulo di verifica in sola lettura che rilegge quanto è stato caricato e lo riconcilia con i dati di partenza, senza scrivere niente.
La verifica ha trovato un errore vero: un indicatore agganciato all'azione sbagliata. È il genere di cosa che una rilettura a occhio non prende, perché ciascuna delle due parti, letta per conto suo, è del tutto plausibile; si vede solo confrontando due elenchi che nessuno avrebbe confrontato a mano.
Il metodo si riusa fuori da qui, ed è la cosa che porto via da questo lavoro: un agente per esplorare finché la mappa è ignota, codice fisso appena la mappa si stabilizza, e una verifica indipendente che dice cosa è arrivato dall'altra parte. Le tre fasi hanno costi e proprietà diverse — la prima è cara e non ripetibile, la seconda è economica e ripetibile, la terza è l'unica che dà una risposta sul risultato invece che sul procedimento.
cosa resta aperto
Le cose a metà le tengo scritte, ordinate per priorità e con accanto quali file toccherebbero e quali decisioni restano da prendere. Le principali:
- Il vecchio servizio che raccoglieva i dati per i prompt convive ancora con il sistema dichiarativo che dovrebbe sostituirlo, quindi per alcune sezioni la stessa logica sta in due posti. La migrazione è iniziata dalle sezioni più usate, e finché non è finita il rischio è quello di sempre: correggere una sola delle due copie.
- L'integrazione con il sistema di risposta ancorato alla documentazione è prevista dal codice ma oggi disattivata: i testi si scrivono sui dati del progetto, senza l'apporto della documentazione di dominio. Il collegamento esiste ed è spento, il che è un modo onesto di tenere aperta la strada senza far finta che sia già percorsa.
- Alcuni valori di riferimento dell'annualità sono ancora scritti nel codice e vanno aggiornati a mano a ogni ciclo, con una lista di controllo dedicata. Funziona perché la lista esiste ed è scritta; resta il fatto che una data in una costante è una scadenza che nessuno vede arrivare.
- La struttura delle sezioni esiste in due varianti, una per il documento completo e una per il caricamento sul portale, e le due non coincidono nel numero né nell'ordine. Tenerle allineate è oggi una responsabilità di chi modifica, non una proprietà del codice.