PromptOps Manager: versioning e test per i prompt LLM in produzione
Il problema
In molti team che lavorano con LLM in produzione, i prompt vivono in variabili d’ambiente, file di configurazione o stringhe hardcoded nel codice. Non c’è un history, non c’è un sistema di approvazione, non c’è modo di sapere chi ha cambiato cosa e quando. Quando l’output del modello peggiora, la domanda “è cambiato il modello o è cambiato il prompt?” spesso non ha risposta immediata.
Ho visto questa situazione in contesti diversi. Un team che aggiorna il prompt di un chatbot di supporto direttamente in produzione perché “è solo una stringa”. Un altro che tiene tre versioni del prompt in un documento Google, senza sapere quale gira dove. La governance manca non per negligenza, ma perché non esiste un’infrastruttura pensata per questo problema.
Il nodo è questo: un prompt LLM in produzione è un artefatto di ingegneria. Ha versioni, ha contesti di esecuzione (dev, staging, prod), produce output verificabili. Va trattato come il codice, non come configurazione libera.
Le scelte che contano
Ho progettato PromptOps Manager intorno a un modello dati con tre entità centrali: il prompt (identificato da uno slug), le sue versioni (con stati draft, published, archived) e gli ambienti di promozione. Ogni ambiente punta a una versione specifica. Questa struttura permette di rispondere a “qual è il prompt in produzione in questo momento?” con una query, non con una telefonata.
Promozione per ambiente invece di deploy diretto. La scelta di separare il versioning dalla promozione non è ovvia. Avrei potuto tenere un semplice history e pubblicare sempre l’ultima versione. Ho scelto altrimenti perché in un team reale staging e produzione non si sincronizzano in automatico: si promuove intenzionalmente, si verifica, poi si va avanti. Lo schema versione-ambiente riflette questo workflow, non lo semplifica artificialmente.
Quattro tipi di asserzione, di cui uno è llm_judge. I test possono verificare che la risposta contenga una stringa, non la contenga, corrisponda a una regex, oppure soddisfi un requisito espresso in linguaggio naturale. Quest’ultimo tipo delega la valutazione a un secondo call LLM: “data questa risposta, soddisfa questo requisito?” con output JSON strutturato. È una scelta architetturale rilevante perché riconosce un limite reale delle asserzioni deterministiche sui testi generativi. Non tutti i requisiti si esprimono con una regex. Usare un LLM come giudice introduce varianza, ed è corretto farlo esplicitamente invece di fingere che le asserzioni testuali bastino sempre.
Nessun fallback silente tra provider. Se il sistema è configurato con OpenAI e la chiave API è invalida, il job va in errore. Non prova Anthropic, non restituisce un risultato vuoto, non logga in silenzio. In un sistema di test per prompt, un risultato prodotto con il provider sbagliato è peggio di nessun risultato. La trasparenza dell’errore è una forma di correttezza, non un’omissione.
Endpoint pubblico per la risoluzione della versione. L’API espone un endpoint non autenticato per sapere quale versione del prompt è attiva in un dato ambiente. Questo perché il consumatore del prompt, tipicamente l’applicazione che lo usa in runtime, non ha bisogno di gestire token di autenticazione solo per recuperare il testo compilato. L’API di gestione richiede auth; l’API di risoluzione è pubblica per design. Sono due casi d’uso diversi e tenerli separati evita di accoppiare il ciclo di vita dell’applicazione consumer con quello del registro.
ULID al posto di interi autoincrement. Le chiavi primarie sono ULID: sortabili per tempo di creazione, URL-safe, privi di enumerazione. Una scelta piccola con effetti concreti: gli ID di risorse diverse non si indovinano, un log si ordina naturalmente per tempo senza join aggiuntivi, e gli URL restano opachi per chiunque li intercetti.
Stato onesto del progetto
Questo è un progetto portfolio-ready, pubblicato come open source. Il modello dati è definito, l’architettura è coerente, il codice copre tutte le funzionalità core: versioning, promozione per ambiente, test asincroni con quattro tipi di asserzione, integrazione con OpenAI e Anthropic. La suite PHPUnit conta 39 test e 96 asserzioni, con un driver LLM fake per isolare i test dalle chiamate esterne. Il sistema è avviabile in locale con Docker e testato end-to-end a livello di API.
Ho anche integrato il registro con un progetto reale: intake, il mio sistema personale di acquisizione link, ha migrato cinque prompt su PromptOps Manager, pubblicati e promossi in produzione. Non è più solo teoria, ha un caso d’uso concreto dietro.
Quello che manca è un deployment su un server esposto e un team esterno che lo usi: resta un progetto personale, non un prodotto adottato da altri. Il roadmap è già nell’architettura: un CLI per sincronizzare i prompt con file locali, un’integrazione GitHub Actions per eseguire i test prima del merge, il supporto multi-tenant con ruoli. Le estensioni future erano nei requisiti fin dall’inizio, e il design dell’API le rende possibili senza riscritture.
Un problema più grande dei prompt
Il problema che PromptOps Manager risolve non è specifico agli LLM. Qualsiasi artefatto che cambia comportamento in produzione senza passare da un processo di revisione genera lo stesso rischio, che si tratti di un prompt, di una feature flag o di una regola di business scritta in un file di configurazione. I prompt sono solo l’ultima variante di un problema che l’ingegneria del software affronta da sempre: dare struttura a qualcosa che, senza attenzione, resta informale.
Costruendolo mi sono ritrovato a farmi le stesse domande che mi faccio da anni su codebase più grandi: chi può cambiare cosa, in che momento, con quale verifica prima che arrivi in produzione. Cambia l’artefatto, non la disciplina.
Il codice è su GitHub, aperto.