Salta al contenuto principale

AI

Reliable GenAI con MLflow: Tracing, Evaluation e Prompt Management

Impara ad applicare Tracing, Evaluation e Prompt Registry con MLflow per trasformare i prototipi LLM in sistemi trasparenti, misurabili e pronti per la produzione.

Costruire un prototipo funzionante con i Large Language Model (LLM) richiede ormai pochi minuti: basta una chiamata API e qualche riga di codice per ottenere una demo convincente. Tuttavia, trasformare quel prototipo in un sistema affidabile, osservabile e manutenibile in produzione presenta sfide completamente diverse rispetto al software tradizionale.

I sistemi basati su LLM sono intrinsecamente non deterministici. Quando un agente orchestra più chiamate al modello, oppure interroga un database tramite tool esterni e sintetizza una risposta, il classico logging testuale non basta più. Se la risposta finale è imprecisa o la latenza raddoppia, dove si trova il collo di bottiglia? Nel prompt di sistema? In una query SQL malformata che il modello ha dovuto riscrivere? E quando modifichiamo il prompt per migliorare un caso specifico, come possiamo essere certi di non aver introdotto errori sugli altri casi d'uso?

In questo articolo vedremo come affrontare queste sfide costruendo passo dopo passo un assistente AI per un negozio di musica e utilizzando i tre pilastri fondamentali di MLflow:

  1. Osservabilità e Tracing — dall'autologging a zero configurazione fino al tracciamento gerarchico di agenti custom.
  2. Valutazione Sistematica (Evaluation) — per misurare la qualità delle risposte combinando LLM-as-a-judge e regole deterministiche in Python.
  3. Prompt Registry — per disaccoppiare i prompt dal codice applicativo e gestirne il versionamento in modo centralizzato.

Disclaimer: Questo articolo nasce come extra a questo repository GitHub, dove potete trovare gli esempi completi e i notebook eseguibili passo dopo passo.

1. Osservabilità Immediata con l'Automatic Tracing

Nello sviluppo software tradizionale siamo abituati a errori espliciti: se qualcosa va storto, viene sollevata un'eccezione. Con gli LLM, invece, la chiamata HTTP può restituire 200 OK anche quando la risposta è errata.

Il primo passo per aprire questa "scatola nera" è l'Automatic Tracing. Aggiungendo una sola riga di codice, ad esempio mlflow.openai.autolog(), MLflow intercetta automaticamente ogni chiamata effettuata tramite l'SDK di OpenAI senza richiedere modifiche (lo stesso approccio è disponibile per decine di altri provider e framework, tra cui Anthropic e Gemini):

from openai import OpenAI
import mlflow

mlflow.openai.autolog()

client = OpenAI()

response = client.responses.create(
    model="gpt-6-luna",
    input="Quali sono gli album più famosi dei Muse?",
)

Dietro le quinte, MLflow cattura e salva nella propria interfaccia l'intero contesto dell'invocazione: il prompt esatto, i parametri del modello, il testo generato, la latenza e il dettaglio dei token utilizzati (input, output ed eventuali token di reasoning). Questo è il metodo più semplice ed immediato per visualizzare tutto quello che succede nella vostra applicazione!

2. Tracciare Agenti Custom e Chiamate ai Tool

Una singola chiamata al modello può rispondere a domande di cultura generale, ma non conosce i dati reali del nostro negozio di musica. Per rispondere a domande sul nostro catalogo, dobbiamo trasformare l'applicazione in un agente dotato di un tool (run_sql) capace di interrogare un database.

Qui emerge un limite dell'autologging di base: se l'agente esegue un ciclo di 4 o 5 chiamate al modello alternate a query sul database, mlflow.openai.autolog() registrerebbe solo le singole chiamate all'LLM come tracce separate, lasciando invisibili le esecuzioni SQL e facendo perdere il legame logico tra i passaggi.

Per raggruppare l'intera conversazione multi-step in un'unica traccia gerarchica, MLflow mette a disposizione il decoratore @mlflow.trace. Attraverso il parametro span_type (o l'enum mlflow.entities.SpanType), possiamo classificare il ruolo semantico di ciascuna funzione all'interno dell'architettura, ad esempio AGENT, TOOL, RETRIEVER, CHAT_MODEL, CHAIN, EMBEDDING o RERANKER, così che la UI di MLflow possa visualizzare ogni step con l'icona e la struttura appropriate, e permettere ai valutatori automatici di sapere quali span ispezionare.

Nel nostro caso, decoriamo la funzione che interroga il database con span_type="TOOL" e il loop principale dell'agente con span_type="AGENT":

import json
import sqlite3
import mlflow
from openai import OpenAI

mlflow.set_experiment("music_agent")
mlflow.openai.autolog()

client = OpenAI()
db = sqlite3.connect("file:music.db?mode=ro", uri=True)


@mlflow.trace(span_type="TOOL")
def run_sql(query: str) -> str:
    try:
        return str(db.execute(query).fetchall())
    except Exception as e:
        return f"Error: {e}"


@mlflow.trace(span_type="AGENT")
def ask(question: str) -> str:
    messages = [{"role": "user", "content": question}]

    for _ in range(10):
        response = client.responses.create(
            model="gpt-6-luna",
            instructions="You answer questions about a music store. Look at the tables and their columns before writing a query.",
            input=messages,
            tools=tools,
        )
        messages += response.output

        tool_calls = [item for item in response.output if item.type == "function_call"]
        if not tool_calls:
            return response.output_text

        for call in tool_calls:
            result = run_sql(**json.loads(call.arguments))
            messages.append(
                {
                    "type": "function_call_output",
                    "call_id": call.call_id,
                    "output": result,
                }
            )

Proviamo a interrogare l'agente:

ask("Which genre on average has the longest tracks?")
# Output: '**Sci Fi & Fantasy** has the longest tracks on average, at about **2,911,783 milliseconds** (48.5 minutes) across 26 tracks.'

Cosa ci rivela la traccia?

Se guardassimo solo l'output finale, tutto sembrerebbe lineare. Ma aprendo la traccia ask (AGENT) nella UI di MLflow, vediamo cosa è successo realmente dietro le quinte attraverso l'albero degli span:

  1. L'agente invoca run_sql con SELECT name FROM sqlite_master WHERE type='table'; per scoprire quali tabelle esistono nel database.
  2. In un'esecuzione può capitare che il modello provi a ispezionare due tabelle in un colpo solo (PRAGMA table_info('Track'); PRAGMA table_info('Genre');), ricevendo da SQLite l'errore Error: You can only execute one statement at a time..
  3. L'agente legge l'errore restituito dal tool, si corregge al turno successivo eseguendo due chiamate separate a run_sql, e infine lancia la query di aggregazione con JOIN e AVG(Milliseconds) che restituisce Sci Fi & Fantasy.

Senza il tracing gerarchico, quell'errore SQL intermedio, che ha causato un giro extra di chiamate all'LLM, aumentando latenza e consumo di token, sarebbe rimasto completamente invisibile.

3. Tracing con Framework Agentici (LangChain)

Se invece di scrivere il loop dell'agente a mano decidiamo di utilizzare un framework come LangChain (o LangGraph), non serve nemmeno decorare manualmente le funzioni. L'integrazione nativa mlflow.langchain.autolog() riconosce già l'architettura del framework e cattura automaticamente l'albero di esecuzione comprensivo di agente, nodi, chiamate al modello e tool:

import mlflow
from langchain.agents import create_agent
from langchain.tools import tool

mlflow.set_experiment("music_agent")
mlflow.langchain.autolog()


@tool
def run_sql(query: str) -> str:
    """Run a SQL query on the music database (SQLite)."""
    try:
        return str(db.execute(query).fetchall())
    except Exception as e:
        return f"Error: {e}"


agent = create_agent(
    "openai:gpt-6-luna",
    tools=[run_sql],
    system_prompt="You answer questions about a music store. Look at the tables and their columns before writing a query.",
)

response = agent.invoke(
    {"messages": [{"role": "user", "content": "Which genre on average has the longest tracks?"}]}
)

Un vantaggio concreto di questo approccio è che, salvando le tracce nello stesso esperimento ("music_agent"), possiamo confrontare fianco a fianco la nostra implementazione in puro Python con quella basata su LangChain, verificando a parità di domanda quale approccio compie meno passaggi o risponde più rapidamente.

4. Valutazione Sistematica: Oltre il "Vibe Check"

Ora che il nostro assistente musicale funziona ed è tracciato, vogliamo assicurarci che risponda sempre seguendo delle regole precise: ad esempio fornire dati corretti, rispondere in italiano ed essere conciso.

Testare a mano due o tre domande dopo ogni modifica al prompt (vibe check) non basta: una modifica che riduce la lunghezza della risposta potrebbe peggiorare l'accuratezza o far rispondere il modello in un'altra lingua. Con mlflow.genai.evaluate() possiamo strutturare una vera suite di valutazione basata su tre elementi:

  1. Un Dataset di Valutazione (data): una lista di domande di test (inputs) accompagnate dai valori attesi (expectations).
  2. Una lista di Scorer (scorers): che combina giudici LLM predefiniti (Correctness, Guidelines, ma esistono anche scorer per RAG e tool-calling come RetrievalGroundedness, ToolCallCorrectness, RelevanceToQuery o Safety) e funzioni Python deterministiche decorate con @scorer.
  3. La funzione da testare (predict_fn): il nostro agente o la funzione di risposta.
import mlflow
from mlflow.genai import scorer
from mlflow.genai.scorers import Correctness, Guidelines

# 1. Dataset di riferimento sul catalogo musicale
eval_dataset = [
    {
        "inputs": {"question": "Which genre on average has the longest tracks?"},
        "expectations": {"expected_response": "Sci Fi & Fantasy"},
    },
    {
        "inputs": {"question": "Which artist has the most albums in the store?"},
        "expectations": {"expected_response": "Iron Maiden"},
    },
    {
        "inputs": {"question": "How many musical genres are available in total?"},
        "expectations": {"expected_response": "25"},
    },
]


# 2. Scorer deterministico in Python
@scorer
def is_concise(outputs: str) -> bool:

    return len(outputs.split()) <= 5


# Combinazione di LLM-as-a-judge e scorer programmatici
scorers = [
    Correctness(),
    Guidelines(name="is_italian", guidelines="The answer must be in Italian"),
    is_concise,
]


# 3. Funzione da valutare
def predict_fn(question: str) -> str:
    response = client.responses.create(
        model="gpt-6-luna",
        instructions="You are a music store assistant. Reply in Italian and with less than 5 words.",
        input=question,
    )
    return response.output_text


# Esecuzione della valutazione
results = mlflow.genai.evaluate(
    data=eval_dataset,
    predict_fn=predict_fn,
    scorers=scorers,
)

Questa combinazione mostra perché un framework di valutazione dedicato faccia la differenza:

  • Correctness() usa un giudice LLM per verificare l'equivalenza semantica rispetto a expected_response (ad esempio, "Gli Iron Maiden" viene valutato corretto rispetto a "Iron Maiden" anche se le stringhe non sono identiche).
  • Guidelines(name="is_italian", ...) valuta requisiti qualitativi o policy espresse in linguaggio naturale.
  • is_concise esegue un controllo istantaneo in Python sul numero di parole, evitando il costo e la latenza di un giudice LLM per una verifica puramente deterministica.

Al termine del run, la vista Evaluation di MLflow mostra i punteggi aggregati e, per ogni riga, la spiegazione (rationale) del giudice LLM, permettendo di individuare subito eventuali regressioni.

5. Disaccoppiare i Prompt dal Codice con il Prompt Registry

Negli esempi visti finora, le istruzioni passate al modello erano del semplice testo nel codice Python. In un'applicazione reale, questo approccio diventa presto un collo di bottiglia: ogni modifica al prompt richiede un nuovo commit e un redeploy, e diventa difficile risalire a quale versione del prompt fosse attiva durante un determinato test.

Il Prompt Registry di MLflow risolve il problema permettendo di assegnare delle versioni ai template dei prompt in modo centralizzato (tramite UI o con mlflow.genai.register_prompt), fuori dal codice applicativo.

Una volta registrato un template parametrico in MLflow, ad esempio music-assistant con il testo "Consiglia il miglior artista rock tra {{artists}} spiegando il perché in due righe", possiamo caricarne una versione specifica (o un alias come "champion") direttamente nel codice tramite l'URI prompts:/<nome>/<versione>:

import mlflow
from openai import OpenAI

# Carica la versione 1 del prompt dal Prompt Registry
prompt = mlflow.genai.load_prompt("prompts:/music-assistant/1")

# In questo caso il prompt potrebbe essere: Chi è la migliore band tra {{artists}}

# Valorizza i placeholder a runtime e invoca il modello
client = OpenAI()
response = client.responses.create(
    model="gpt-6-luna",
    input=prompt.format(artists="Iron Maiden, Led Zeppelin, Deep Purple"),
)

print(response.output_text)

Separando i prompt dal codice:

  • Possiamo creare nuove versioni (prompts:/music-assistant/2) direttamente dalla UI di MLflow senza toccare il codice Python.
  • Possiamo mettere a confronto la versione 1 e la versione 2 lanciando mlflow.genai.evaluate() sul nostro dataset di test prima di promuovere la modifica in produzione.

Conclusioni

Costruire applicazioni GenAI affidabili non significa rinunciare alla velocità di prototipazione, ma affiancare allo sviluppo gli strumenti giusti per capire e misurare il comportamento dei modelli:

  • Tracing automatico e manuale (autolog e @mlflow.trace) per trasformare chiamate LLM e cicli agentici da scatole nere a flussi trasparenti e ispezionabili.
  • GenAI Evaluation (mlflow.genai.evaluate) per sostituire le impressioni soggettive con test ripetibili, unendo la flessibilità degli LLM-as-a-judge alla precisione degli @scorer in Python.
  • Prompt Registry (mlflow.genai.load_prompt) per gestire l'evoluzione dei prompt con lo stesso rigore del versionamento del codice.

CTA

Meetup
17 NOV

Luogo

Magnisi Studio · Via Emerico Amari, 148, Palermo

Techsgiving AI Meetup: aperta la Call for Papers!

Il 17 novembre torna il Techsgiving AI Meetup di Panormus Tech! Call for Papers aperta: proponi il tuo talk e condividi le tue idee sull’intelligenza artificiale.
techsgiving-ai-meetup-call-for-papers-panormus-tech

Il prossimo 17 novembre torna Panormus Tech con un nuovo appuntamento dedicato all'intelligenza artificiale: Techsgiving AI Meetup!

L'AI continua a trasformare il modo in cui sviluppiamo software, progettiamo prodotti e lavoriamo ogni giorno. Con questo meetup vogliamo creare uno spazio di confronto per condividere esperienze, idee e punti di vista, esplorando le opportunità, le sfide e le applicazioni concrete dell'intelligenza artificiale.

🚀 La Call for Papers è ufficialmente aperta!

Hai un progetto da raccontare, un'esperienza da condividere o un'idea che vorresti approfondire insieme alla community? Questa è l'occasione giusta per salire sul palco e contribuire al programma del meetup.

Cerchiamo proposte di talk, demo e casi pratici su temi legati all'AI: dagli strumenti e framework alle applicazioni nello sviluppo software, dagli AI Agents alla generative AI, fino alle esperienze maturate sul campo e alle riflessioni sull'impatto di queste tecnologie.

Non serve essere speaker esperti: ciò che conta è avere qualcosa di interessante da condividere e la voglia di confrontarsi con altre persone.

💡 Proponi il tuo intervento!

Se lavori con l'intelligenza artificiale, stai sperimentando nuovi strumenti o hai semplicemente una storia da raccontare, inviaci la tua proposta attraverso la Call for Papers.

Panormus Tech è uno spazio aperto a chi vive il mondo della tecnologia, a chi lo sta scoprendo e a chi vuole contribuire a far crescere la community.

Non vediamo l'ora di scoprire le vostre idee e costruire insieme il prossimo appuntamento di Panormus Tech!

CTA

Titolo

La call for papers è aperta!

Ulteriori informazioni

Chiude l'8 Novembre 2026
Meetup
9 OTT

Luogo

Magnisi Studio · Via Emerico Amari, 148, Palermo

Il futuro di Panormus Tech prende forma: il nostro nuovo sito web è ufficialmente online!

Panormus Tech è online con il nuovo sito! Design rinnovato, navigazione veloce e contenuti esclusivi: scopri la nostra nuova piattaforma digitale.

Oggi è una giornata speciale per la nostra community. Siamo entusiasti di darvi ufficialmente il benvenuto sulla nuova piattaforma digitale di Panormus Tech!

Dopo mesi di progettazione, sviluppo e cura di ogni dettaglio, il nostro nuovo “quartier generale digitale” è pronto per accogliervi con un’esperienza del tutto rinnovata.

Cosa cambia per voi?

Non si tratta soltanto di un cambio d'abito grafico, ma di una riprogettazione pensata per mettere al centro chi sceglie di crescere insieme a noi:

  • Navigazione fluida ed intuitiva: Trovare i servizi, le soluzioni e le informazioni di cui avete bisogno è diventato più veloce e semplice.
  • Sezione News & Insights: Un punto di riferimento dove condivideremo articoli, novità del settore, analisi di trend tecnologici e approfondimenti tecnici.
  • Layout responsivo: Ottimizzato per una fruizione impeccabile da qualsiasi dispositivo, dallo smartphone al PC fisso.
  • Integrazione diretta: Contattarci per consulenze, richieste di preventivo o collaborazioni è ora più diretto e immediato.

Un passo avanti verso l'innovazione

Il lancio del nuovo sito non rappresenta un traguardo, ma un punto di partenza. Panormus Tech continua ad evolversi per offrire soluzioni tecnologiche sempre più all’avanguardia e a valore aggiunto per aziende, professionisti e appassionati del settore.

Vi invitiamo a esplorare ogni sezione del sito e a farci sapere cosa ne pensate! Il vostro feedback è prezioso per aiutarci a migliorare giorno dopo giorno.

👉 Visitate il sito e scoprite tutte le novità!

CTA

Meetup
5 GIU

Luogo

Magnisi Studio · Via E. Amari, 148, Palermo

Meetup
21 LUG

Luogo

Magnisi · Via E. Amari, 148, Palermo