Osservabilità per agenti AI: strumenti, metriche e cosa loggare davvero

·11 min lettura·Claudio Novaglio
Osservabilità per agenti AI: strumenti, metriche e cosa loggare davvero
In questo articolo

L'osservabilità di un agente AI è la capacità di ricostruire, per ogni esecuzione, cosa ha fatto il modello, quali tool ha chiamato, quanto è costato e se la risposta era buona. Si ottiene con quattro oggetti: la trace (il run intero), gli span (ogni chiamata al modello o a un tool, con durata e attributi), le metriche aggregate (costo, latenza, errori) e le valutazioni (giudizi automatici o umani sulla qualità dell'output).

Il logging classico non basta per tre motivi. L'agente non è deterministico, quindi lo stesso input produce percorsi diversi e un log lineare non ti dice dove il ragionamento ha deviato. Il costo non dipende dal numero di richieste ma dai token, e i token li conosci solo leggendo la risposta dell'API. Le chiamate ai tool sono annidate: un run può contenere dieci chiamate al modello e venti a GA4. Senza una gerarchia di span non capisci quale ha rallentato o fallito.

Le cinque cose da misurare

Un agente in produzione va monitorato su cinque assi: costo, latenza per step, errori dei tool, qualità delle risposte e drift nel tempo. Tutto il resto (numero di run, utenti attivi, token totali) è un dettaglio di questi cinque.

MetricaCome si raccoglieSoglia di allarme indicativa
Costo per run e per taskToken di input e output da ogni risposta API, moltiplicati per il listino del modello; somma per trace e per tipo di taskUn run che costa più del doppio della mediana degli ultimi 7 giorni
Latenza per stepDurata di ogni span (chiamata al modello, chiamata al tool); attenzione al tempo al primo token se usi lo streamingUno step che supera il p95 storico per tre run consecutivi
Tasso di errore e retry per toolAttributo di errore sullo span del tool più contatore dei tentativiOltre il 5% di chiamate fallite su un singolo tool in un'ora
QualitàEval automatiche (LLM-as-a-judge, controlli di formato, assert su fatti verificabili) più campionamento umano settimanaleScore medio sotto la baseline per due giorni, o un solo caso critico segnalato da un umano
DriftConfronto delle metriche sopra per versione di prompt e di modello; stesso dataset di test rieseguito dopo ogni cambioQualsiasi scostamento oltre il rumore misurato sul dataset di riferimento

Le soglie sono un punto di partenza, non un valore assoluto: dipendono dal volume, dal costo del modello e da quanto è tollerabile un errore nel tuo caso. La regola pratica che uso è partire larghi, guardare una settimana di dati reali e poi stringere.

Il costo per token è la metrica che sorprende di più chi arriva dal monitoraggio tradizionale. Un agente che sbaglia una query GA4 e la ripete cinque volte costa cinque volte tanto senza generare nessun errore visibile. Per questo il costo va legato al task: se "trova le anomalie di ieri" costa in media 4 centesimi e oggi ne costa 40, hai un problema anche se la risposta finale è corretta.

Strumenti a confronto

Le piattaforme di osservabilità per LLM si dividono in tre famiglie: SaaS legati a un framework, piattaforme open source autoinstallabili e lo standard OpenTelemetry con le sue convenzioni GenAI. Ho verificato ogni riga sulle pagine ufficiali il 15 settembre 2026; i prezzi cambiano spesso, quindi ricontrolla prima di decidere.

StrumentoHostingLicenzaPunti fortiLimitiQuando sceglierlo
LangSmithCloud; self-hosted e ibrido solo su piano EnterpriseProprietariaIntegrazione automatica con LangChain e LangGraph; eval, prompt management e deployment nello stesso prodottoPiano gratuito con 5.000 trace al mese e retention base di 14 giorni; self-hosting non disponibile sui piani Developer e PlusSe il tuo stack è LangGraph e vuoi zero configurazione
LangfuseCloud (regioni US, EU, Giappone) o self-hosted con Docker Compose o HelmMIT, tranne le cartelle eeBasato su OpenTelemetry, endpoint OTLP nativo; prompt management, dataset, LLM-as-a-judgeIl self-hosting in produzione richiede Postgres, ClickHouse, Redis e uno storage S3Se vuoi dati in Europa o sul tuo server senza pagare una licenza
Arize PhoenixLocale con pip install arize-phoenix, Docker, oppure la piattaforma gestita Arize AXElastic License 2.0Nato su OpenTelemetry e OpenInference; tracing, eval, dataset, esperimenti e playgroundLa licenza ELv2 non è OSI: vieta di offrirlo come servizio a terziSe vuoi partire in locale in cinque minuti e restare su standard aperti
OpenTelemetry (convenzioni GenAI)Qualsiasi backend OTLP: Langfuse, Phoenix, LangSmith, Grafana, DatadogApache 2.0Standard neutrale: un solo SDK, cambi backend con una variabile d'ambienteLe convenzioni GenAI sono in stato Development, quindi i nomi degli attributi possono cambiareSe hai già una piattaforma di osservabilità aziendale o vuoi evitare il lock-in
HeliconeCloud, oppure self-hosted con Docker Compose o Helm; on-prem supportato nel piano EnterpriseApache 2.0Integrazione a proxy: cambi l'URL base e hai costi e latenza senza toccare il codice; logging asincrono via OpenLLMetryIl piano gratuito ha 10.000 richieste al mese e 7 giorni di retention; meno adatto a trace annidate di agenti complessiSe vuoi visibilità sui costi in un pomeriggio, senza instrumentare

Fonti: pricing LangSmith e documentazione LangSmith; repository Langfuse, self-hosting e pricing; repository Phoenix e documentazione; repository delle convenzioni GenAI; repository Helicone e pricing. Tutte consultate il 15 settembre 2026.

Il supporto nativo dei framework

I framework per agenti hanno un tracing integrato, ma quasi sempre punta al SaaS del produttore. LangGraph traccia automaticamente grafi e nodi verso LangSmith quando imposti LANGSMITH_TRACING=true e la chiave API; se dentro un nodo chiami un SDK non LangChain, devi decorare la funzione con @traceable (documentazione LangSmith, 15 settembre 2026). LangSmith accetta anche trace OpenTelemetry sull'endpoint https://api.smith.langchain.com/otel e mappa gli attributi GenAI standard sui propri campi (documentazione, stessa data).

L'OpenAI Agents SDK ha il tracing attivo di default: ogni run, generazione, chiamata a tool, guardrail e handoff finisce nella dashboard OpenAI. Si disattiva con OPENAI_AGENTS_DISABLE_TRACING=1, e con trace_include_sensitive_data=False eviti che input e output del modello vengano salvati. Supporta processori esterni verso Langfuse, Phoenix, LangSmith e altri (documentazione OpenAI Agents SDK, 15 settembre 2026).

L'SDK Anthropic non porta con sé un backend di tracing: ti restituisce per ogni chiamata l'oggetto usage con input_tokens, output_tokens, cache_creation_input_tokens e cache_read_input_tokens, più stop_reason e il modello usato (documentazione Claude, 15 settembre 2026). Sta a te leggerli e metterli su uno span, come nell'esempio più sotto. Attenzione al calcolo del costo con la cache attiva: input_tokens conta solo i token dopo l'ultimo breakpoint di cache, quindi il totale in ingresso è la somma dei tre campi.

Cosa loggare e cosa no

Registra sempre gli identificativi e i numeri, registra i contenuti solo se hai deciso consapevolmente di farlo. Le convenzioni GenAI di OpenTelemetry marcano gen_ai.input.messages, gen_ai.output.messages, gen_ai.tool.call.arguments e gen_ai.tool.call.result come attributi opt-in con avviso esplicito: possono contenere dati personali (convenzioni GenAI, spans, 15 settembre 2026).

Da loggare sempre:

  • Id di sessione e di conversazione (gen_ai.conversation.id), per ricostruire un percorso multi-turno.
  • Versione del prompt e nome del modello richiesto e restituito. Senza questi due campi il drift è invisibile: cambi un prompt, la qualità scende e non sai perché.
  • Token per chiamata, input e output separati, più i token di cache se il provider li espone.
  • Nome del tool, id della chiamata, durata ed esito per ogni tool.
  • Motivo di stop del modello (stop_reason, finish_reasons): un max_tokens inatteso è un bug, non un dettaglio.

Da loggare con cautela, o non loggare:

  • Prompt completi e output del modello. In un agente di marketing ci finiscono email, nomi, domande di clienti. Se li registri, sei titolare di un nuovo trattamento con base giuridica, retention e diritto di cancellazione da gestire. La strada pratica è: pseudonimizzare prima dello span, tenere i contenuti in un campionamento piccolo per il debug, scegliere una regione europea (Langfuse la offre, Phoenix self-hosted la garantisce per costruzione) e impostare una retention breve.
  • Input e output dei tool. Un tool che legge GA4 restituisce dati aggregati, quindi il rischio è basso. Un tool che legge un CRM restituisce dati personali: logga il numero di righe e gli id, non le righe.
  • Chiavi e token di autenticazione. Sembra ovvio, ma i tool che ricevono un header di autorizzazione nei loro argomenti li scrivono nello span se registri gli argomenti senza filtro.

Se il tuo tracking web è già impostato per rispettare il consenso, come descritto in Consent Mode v2 con template custom, applica la stessa logica all'agente: i dati non necessari non si raccolgono, e quelli necessari hanno una scadenza.

Esempio minimo: OpenTelemetry su un agente che interroga GA4

L'esempio usa l'SDK OpenTelemetry per Python, esporta verso Langfuse via OTLP e chiama Claude con un tool che esegue un report GA4. Gli attributi seguono le convenzioni GenAI (gen_ai.*); cambiando endpoint e header lo stesso codice va su Phoenix, LangSmith o su un collector aziendale. Langfuse espone l'endpoint OTLP su /api/public/otel con autenticazione Basic (chiave pubblica e segreta in base64) (documentazione Langfuse, 15 settembre 2026).

# pip install anthropic opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
import base64
import json
import os

from anthropic import Anthropic
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# 1. Exporter OTLP verso Langfuse. Le chiavi arrivano dall'ambiente, mai nel codice.
auth = base64.b64encode(
    f"{os.environ['LANGFUSE_PUBLIC_KEY']}:{os.environ['LANGFUSE_SECRET_KEY']}".encode()
).decode()
exporter = OTLPSpanExporter(
    endpoint=f"{os.environ['LANGFUSE_BASE_URL']}/api/public/otel/v1/traces",
    headers={"Authorization": f"Basic {auth}"},
)
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("ga4-anomaly-agent")

client = Anthropic()  # legge ANTHROPIC_API_KEY dall'ambiente
MODEL = "claude-opus-5"
PROMPT_VERSION = "anomaly-v3"  # cambia a ogni modifica del prompt: serve per il drift

TOOLS = [{
    "name": "query_ga4",
    "description": "Esegue un report GA4 Data API su una property e un intervallo di date.",
    "input_schema": {
        "type": "object",
        "properties": {
            "property_id": {"type": "string"},
            "date_range": {"type": "string"},
        },
        "required": ["property_id", "date_range"],
    },
}]


def query_ga4(call_id: str, property_id: str, date_range: str) -> dict:
    """Tool: uno span execute_tool. Logga la dimensione del risultato, non le righe."""
    with tracer.start_as_current_span("execute_tool query_ga4") as span:
        span.set_attribute("gen_ai.operation.name", "execute_tool")
        span.set_attribute("gen_ai.tool.name", "query_ga4")
        span.set_attribute("gen_ai.tool.call.id", call_id)
        rows = run_ga4_report(property_id, date_range)  # la tua chiamata alla Data API
        span.set_attribute("app.ga4.rows", len(rows))
        return {"rows": rows}


def call_model(messages: list):
    """Una chiamata al modello: uno span chat con token e motivo di stop."""
    with tracer.start_as_current_span(f"chat {MODEL}") as span:
        span.set_attribute("gen_ai.operation.name", "chat")
        span.set_attribute("gen_ai.provider.name", "anthropic")
        span.set_attribute("gen_ai.request.model", MODEL)
        resp = client.messages.create(
            model=MODEL, max_tokens=16000, tools=TOOLS, messages=messages
        )
        span.set_attribute("gen_ai.response.model", resp.model)
        span.set_attribute("gen_ai.usage.input_tokens", resp.usage.input_tokens)
        span.set_attribute("gen_ai.usage.output_tokens", resp.usage.output_tokens)
        span.set_attribute("gen_ai.response.finish_reasons", [resp.stop_reason])
        return resp


def run_agent(question: str, session_id: str) -> str:
    """Il run intero: uno span invoke_agent che contiene tutti gli altri."""
    with tracer.start_as_current_span("invoke_agent ga4-anomaly-watcher") as span:
        span.set_attribute("gen_ai.operation.name", "invoke_agent")
        span.set_attribute("gen_ai.agent.name", "ga4-anomaly-watcher")
        span.set_attribute("gen_ai.conversation.id", session_id)
        span.set_attribute("app.prompt.version", PROMPT_VERSION)

        messages = [{"role": "user", "content": question}]
        while True:
            resp = call_model(messages)
            messages.append({"role": "assistant", "content": resp.content})
            if resp.stop_reason != "tool_use":
                break
            results = []
            for block in resp.content:
                if block.type == "tool_use" and block.name == "query_ga4":
                    out = query_ga4(block.id, **block.input)
                    results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(out),
                    })
            messages.append({"role": "user", "content": results})
        return next(b.text for b in resp.content if b.type == "text")


# In uno script breve, svuota il buffer prima di uscire o perdi gli ultimi span.
# provider.force_flush()

Tre scelte da notare. Il testo della domanda e la risposta del modello non finiscono negli attributi: se ti servono per il debug, aggiungili dopo con un campionamento. La versione del prompt è un attributo del run, così puoi filtrare per app.prompt.version e confrontare costo e qualità prima e dopo un cambio. Il tool registra il numero di righe restituite da GA4 e non le righe: basta per capire se una query è tornata vuota, che nel monitoraggio di anomalie è la causa più frequente di falsi positivi.

Se preferisci l'SDK nativo di Langfuse, la versione corrente offre il decoratore @observe e il context manager start_as_current_observation con tipi generation, tool e agent, più update_current_trace per sessione, utente, tag e versione (documentazione SDK Langfuse, 15 settembre 2026). Il modello mentale è identico.

Un agente di anomaly detection è una pipeline come le altre

Un agente che sorveglia GA4 va osservato con gli stessi criteri di una pipeline ETL: input verificabili, step misurati, output confrontabile con una baseline. La differenza è che uno degli step ragiona in linguaggio naturale e può sbagliare senza sollevare eccezioni. Il tracing serve a rendere visibile quel tipo di errore.

Per un agente che ogni mattina confronta le sessioni per canale con la settimana precedente, la trace deve rispondere a quattro domande: quale intervallo di date ha chiesto davvero a GA4, quante righe ha ricevuto, quante chiamate al modello ha fatto per arrivare a una conclusione, se la conclusione è cambiata rispetto a ieri a parità di dati. Se il dato di partenza è sporco, l'agente lo amplifica: un audit del tracking GA4 prima della messa in produzione costa meno di un mese di falsi allarmi.

Il tema si collega agli agenti AI per la SEO autonoma e ai pattern di orchestrazione multi-agente: più agenti coordinati moltiplicano gli span per run, e senza un id di conversazione condiviso non ricostruisci chi ha chiesto cosa. Pesa anche il framework, come emerge dal confronto tra LangGraph e AutoGen: un grafo esplicito produce trace leggibili, una conversazione libera tra agenti produce trace lunghe da interpretare. Per sapere chi lavora su questi sistemi in Italia c'è la classifica degli esperti di agenti AI per il marketing; il servizio agenti AI per analytics descrive come imposto io questo tipo di progetto.

Errori tipici e limiti

Gli errori che vedo più spesso non riguardano lo strumento, ma cosa ci si mette dentro.

  • Loggare tutto, poi non guardare niente. Un tracing che registra prompt interi per ogni run genera gigabyte in una settimana e nessuno lo apre. Parti da numeri e id, aggiungi contenuti solo dove hai un dubbio concreto.
  • Nessuna versione del prompt. Senza app.prompt.version o equivalente, ogni confronto nel tempo è un'opinione.
  • Eval solo automatiche. Un LLM-as-a-judge misura coerenza e formato, non se l'anomalia segnalata era davvero un'anomalia. Un campione umano settimanale, anche di dieci run, è la parte del sistema che scopre i problemi nuovi.
  • Costo misurato per richiesta. Le richieste sono quasi gratis, i token no. Il costo va aggregato per task e confrontato con la mediana.
  • Fidarsi delle convenzioni come fossero stabili. Le convenzioni GenAI di OpenTelemetry sono in stato Development: se aggiorni le librerie di instrumentazione, controlla che i nomi degli attributi non siano cambiati, altrimenti le dashboard si svuotano in silenzio.

Due limiti di questo articolo. Non ho eseguito benchmark di prestazioni tra le piattaforme: le differenze in tabella vengono dalla documentazione, non da un test di carico. E l'esempio di codice è volutamente minimo: in produzione servono gestione degli errori API, retry con backoff e un exporter separato per le metriche, non solo per gli span.

FAQ

Serve una piattaforma dedicata o basta Grafana con OpenTelemetry?

Se hai già uno stack OpenTelemetry aziendale, puoi inviare gli span GenAI allo stesso backend e costruire dashboard su token, durata ed errori. Ti mancano però le viste specifiche per LLM (confronto prompt, dataset di test, eval): per quelle una piattaforma come Langfuse o Phoenix fa risparmiare settimane. Un collector può duplicare gli span verso entrambe.

Posso loggare i prompt se contengono dati di clienti?

Puoi, ma diventa un trattamento a sé: serve una base giuridica, una retention definita e la possibilità di cancellare. La scelta più semplice è pseudonimizzare prima di registrare, tenere i contenuti solo per un campione piccolo e usare un hosting in Europa o sul tuo server. Le convenzioni OpenTelemetry marcano questi attributi come opt-in proprio per questo.

Quanto costa osservare un agente rispetto a farlo girare?

Con i piani gratuiti di Langfuse (50.000 unità al mese), Helicone (10.000 richieste al mese) e LangSmith (5.000 trace al mese), dati pricing ufficiali di settembre 2026, un agente con qualche centinaio di run al giorno rientra spesso nel gratuito. Il costo vero è il tempo per definire le eval e leggere le trace, non la piattaforma.

Da dove partire

Instrumenta prima il costo e la latenza per step: sono i due numeri che ottieni in un pomeriggio e che cambiano subito le decisioni. Poi aggiungi la versione del prompt e un dataset di dieci casi da rieseguire a ogni modifica. Le eval automatiche e il campionamento umano vengono dopo, quando hai abbastanza run da confrontare.

Se stai valutando un agente per il monitoraggio dei tuoi dati GA4 e vuoi impostarlo con tracing e valutazioni fin dal primo giorno, il servizio agenti AI per analytics parte da lì.

Potrebbe interessarti