Passa al contenuto principale

Osservabilità degli agenti: tracciare un agente che non puoi riprodurre

Avanzato

La prima volta che un agente fa qualcosa di inspiegabile in produzione, proverai a riprodurlo. Rieseguirai lo stesso prompt con gli stessi strumenti e otterrai una risposta diversa e perfettamente ragionevole — e il bug sarà sparito, non risolto, in attesa.

Quel momento è tutto l'argomento di questa pagina. Un servizio tradizionale è abbastanza deterministico da rendere i log una comodità: dati gli input, puoi ricavare di nuovo il comportamento. Un'esecuzione di agente è una traccia distribuita non deterministica — campionata token dopo token, con ramificazioni su risultati di strumenti che nel frattempo sono cambiati, distribuita tra il tuo processo, il fornitore del modello e tutti i server MCP coinvolti. Non puoi ricavarla di nuovo. O l'hai registrata, o è perduta.

Quindi l'osservabilità degli agenti non è "aggiungiamo una dashboard più tardi". È ciò che decide se i tuoi incidenti sono analizzabili o no. La buona notizia è che nel 2026 esiste finalmente un vocabolario condiviso. La cattiva — e il motivo per cui il primo tentativo di molti team va rifatto — è che il vocabolario non è ancora stabile, e quasi nessuno te lo dice prima che tu colleghi 40 pannelli a nomi di attributi che possono esplicitamente cambiare.

What you'll learn
  • Conoscere i quattro tipi di span che un'esecuzione di agente dovrebbe produrre e cosa va su ciascuno
  • Capire perché ogni attributo gen_ai.* porta ancora il badge 'Development' di OpenTelemetry — e come costruire dashboard che lo sopravvivano
  • Attivare l'esportazione OpenTelemetry di Claude Code con i nomi esatti delle variabili e leggere le metriche e gli eventi che emette
  • Catturare il request-id, l'unico identificatore che il supporto Anthropic può cercare
  • Evitare le tre trappole di privacy: l'identità non è oscurata, il controllo dei contenuti è per segnale e il tuo endpoint del collector è una via di esfiltrazione
  • Scegliere un budget di cardinalità prima che arrivi la fattura delle metriche

Perché rieseguire non è fare debug​

Quattro proprietà di un'esecuzione di agente rompono ciascuna un normale riflesso di debug:

ProprietàIl riflesso che rompe
Il campionamento è stocastico"Eseguilo di nuovo con lo stesso input." Lo stesso input non è la stessa esecuzione.
I risultati degli strumenti sono dal vivoAnche a temperatura 0, uno strumento che legge un database, un orologio o il web restituisce qualcosa di nuovo. La traiettoria diverge al passo 2.
La decisione sta nel contesto, non nel codiceIl tuo stack trace mostra callModel(). Non mostra i 180 KB di cronologia accumulata che hanno fatto scegliere al modello lo strumento sbagliato.
Il fallimento è spesso silenziosoUn agente rotto raramente solleva un'eccezione. Restituisce una risposta sbagliata e sicura di sé con HTTP 200. Non c'è nessuna eccezione da intercettare.

L'ultima merita enfasi perché inverte le normali priorità di strumentazione. In un servizio tipico si strumentano prima gli errori. In un agente, i fallimenti interessanti sono i successi — turni che si sono conclusi, sono costati denaro e hanno fatto la cosa sbagliata. Se registri solo gli span terminati con error.type, hai registrato la metà facile dei tuoi problemi.

I quattro span di un'esecuzione di agente​

Le convenzioni semantiche GenAI di OpenTelemetry definiscono le forme. Il modello mentale utile è quello di quattro livelli annidati, ciascuno con una domanda diversa a cui rispondere.

Le regole di naming sono specificate, e seguirle è ciò che rende le tue tracce leggibili in uno strumento che non hai scritto tu:

  • Span di inferenza — gen_ai.operation.name è chat (oppure embeddings, text_completion, generate_content); il nome dello span è {gen_ai.operation.name} {gen_ai.request.model}, quindi chat claude-sonnet-5. Span kind CLIENT.
  • Span dell'agente — create_agent {gen_ai.agent.name} e invoke_agent {gen_ai.agent.name}, con ripiego su invoke_agent semplice quando il nome non è disponibile. Esistono sia una variante client sia una interna, ed è così che un agente ospitato e il tuo loop restano distinguibili.
  • Span del workflow — invoke_workflow {gen_ai.workflow.name}, per l'orchestrazione multi-agente sopra gli agenti.
  • Span dello strumento — execute_tool, per l'esecuzione effettiva dello strumento.
  • Span MCP — una convenzione separata: nome dello span {mcp.method.name} {target} (o solo {mcp.method.name} quando non c'è un target a bassa cardinalità), con mcp.method.name obbligatorio e mcp.session.id raccomandato. Impostare anche gen_ai.operation.name su uno span di chiamata a strumento MCP è esplicitamente raccomandato, così i consumatori possono trattare le chiamate a strumenti MCP come qualsiasi altra chiamata a strumento.

Gli attributi per chiamata che valgono i byte: gen_ai.provider.name, gen_ai.request.model, gen_ai.response.model (questi due differiscono quando il fornitore fissa uno snapshot), gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_write.input_tokens, gen_ai.usage.reasoning.output_tokens, gen_ai.response.finish_reasons e, per lo streaming, gen_ai.response.time_to_first_chunk.

Per Anthropic in particolare esiste una convenzione del fornitore che prevale su quella generica: gen_ai.provider.name DEVE essere "anthropic" e DOVREBBE essere impostato al momento della creazione dello span anziché aggiunto alla fine. Questo dettaglio conta più di quanto sembri — il tail-based sampling e le regole di routing per fornitore possono agire solo su attributi che esistono quando lo span inizia.

La parte di cui nessuno ti avverte: le convenzioni non sono stabili​

Ecco il fatto che dovrebbe plasmare il tuo design, e che non troverai nella pagina commerciale di un vendor di osservabilità.

Ogni singolo attributo, span, metrica ed evento gen_ai.* è contrassegnato "Development" — il termine di OpenTelemetry per ciò che una volta si chiamava sperimentale. Nessuno è Stable. E nel 2026 le convenzioni si sono fisicamente spostate: il materiale GenAI ha lasciato il repository principale open-telemetry/semantic-conventions per un repository dedicato open-telemetry/semantic-conventions-genai, che non ha alcuna release taggata — zero tag, zero release, con commit che arrivano ogni giorno.

Leggi le tabelle degli attributi in quel repository e l'asimmetria è quasi comica. Su uno span di inferenza GenAI, gli attributi con il badge verde Stable sono error.type, server.address e server.port — i tre che non hanno nulla a che fare con GenAI. Tutto ciò che rende lo span riguardante un LLM è Development.

Quindi: non leggerlo come "non usare le convenzioni". Usale — un vocabolario condiviso che potrebbe cambiare vale comunque più di uno artigianale che di sicuro non viaggerà. Leggilo come non agganciare nulla di costoso direttamente a quei nomi. Tre abitudini lo rendono economico:

Guided walkthrough1 of 4
  1. Un modulo che possiede ogni chiave di attributo che la tua app scrive. Quando `gen_ai.usage.reasoning.output_tokens` viene rinominato, cambi una costante, non 60 punti di chiamata. È un file di venti righe ed è la cosa con il miglior ritorno di tutta questa pagina.

L'identificatore che risolve davvero gli incidenti: request-id​

Ogni risposta dell'API di Claude porta un header di risposta request-id, con un valore come req_018EeWyXxfu5pfWkrYcMdjWG. Lo stesso valore compare come campo request_id nei corpi delle risposte di errore. È l'identificatore che il supporto Anthropic ti chiede, e l'unico che permette a qualcuno dall'altra parte di guardare la tua chiamata specifica.

Mettilo sullo span. Costa 30 byte ed è la differenza tra "l'agente ha fallito alle 03:12 UTC, ecco 900 richieste candidate" e una sola ricerca.

Cattura l'ID di richiesta Anthropic sullo span corrente (Python)

import anthropic
from opentelemetry import trace

client = anthropic.Anthropic()
span = trace.get_current_span()

message = client.messages.create(
  model="claude-sonnet-5",
  max_tokens=1024,
  messages=[{"role": "user", "content": "Hello, Claude"}],
)

# The Python and TypeScript SDKs expose it directly on the response object.
span.set_attribute("anthropic.request_id", message._request_id)
span.set_attribute("gen_ai.usage.input_tokens", message.usage.input_tokens)
span.set_attribute("gen_ai.usage.output_tokens", message.usage.output_tokens)

Tre dettagli da conoscere prima di averne bisogno:

  • Python e TypeScript lo espongono come _request_id sugli oggetti di risposta di primo livello. C#, Go, Java e PHP lo espongono tramite i loro accessor di risposta grezza (with_raw_response / .withResponse() / .withRawResponse() / WithRawResponse / option.WithResponseInto / ->raw). Ruby richiede un middleware per richiesta, perché l'SDK non lo mostra sull'oggetto.
  • Gli errori lo portano comunque. Quando una chiamata fallisce, request_id è nel corpo JSON dell'errore — quindi la correlazione sopravvive proprio al caso in cui ti serve di più.
  • Su Claude Platform on AWS ci sono due ID, e quello primario non è di Anthropic. x-amzn-requestid è l'ID primario e quello indicizzato in CloudTrail; request-id è secondario. Usa quello AWS per le ricerche in CloudTrail e quello Anthropic per i ticket al supporto Anthropic. Registra entrambi, con etichetta, o passerai un pomeriggio a cercare nel sistema sbagliato.

Claude Code: l'unico agente che esporta già tutto​

Se il tuo agente è Claude Code — in CI, in una flotta o in un intero team — non devi strumentare nulla. Include un exporter OpenTelemetry. La maggior parte dei team che lo usa su larga scala non l'ha mai attivato, ed è un peccato, perché risponde direttamente a "quanto ci costa e funziona?".

Abilita la telemetria di Claude Code verso un collector OTLP locale

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-token"
claude

CLAUDE_CODE_ENABLE_TELEMETRY=1 è l'interruttore generale — senza di esso non viene esportato nulla, qualunque cosa tu imposti d'altro. Poi scegli gli exporter per segnale: OTEL_METRICS_EXPORTER accetta otlp, prometheus, console o none; OTEL_LOGS_EXPORTER accetta otlp, console, none. Le tracce sono separate e in beta: imposta anche CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1, poi OTEL_TRACES_EXPORTER. Gli intervalli di esportazione predefiniti sono 60.000 ms per le metriche e 5.000 ms per i log.

Le metriche sono la parte su cui vale la pena costruire una dashboard, perché quelle di costo sono attribuite con una finezza sufficiente a rispondere a domande reali:

MetricaA cosa risponde
claude_code.cost.usage (USD)Spesa, attribuibile per modello e per origine della query — agente principale vs subagente vs ausiliario
claude_code.token.usage (token)La stessa forma in token, che è ciò che ottimizzi davvero
claude_code.session.countAdozione. Sessioni stabili con costo in crescita è una storia diversa da entrambi in crescita.
claude_code.active_time.total (s)Tempo di lavoro effettivo, in contrapposizione a un terminale lasciato aperto
claude_code.lines_of_code.countVolume di modifiche — un denominatore, mai un obiettivo
claude_code.code_edit_tool.decisionIl segnale di qualità. Accettazione/rifiuto sugli strumenti di modifica. Rifiuti in aumento significano che l'agente propone lavoro peggiore.
claude_code.commit.count, claude_code.pull_request.countOutput che ha davvero lasciato la macchina

Gli eventi (esportati come log) sono il registro per azione: claude_code.user_prompt, claude_code.assistant_response, claude_code.tool_result, claude_code.tool_decision, claude_code.api_request, claude_code.api_error, claude_code.api_refusal, claude_code.api_retries_exhausted, claude_code.permission_mode_changed, claude_code.mcp_server_connection, claude_code.skill_activated, claude_code.plugin_installed, claude_code.plugin_loaded, claude_code.at_mention, claude_code.auth, claude_code.hook_registered, claude_code.managed_settings_resolved, claude_code.internal_error, più claude_code.api_request_body / claude_code.api_response_body per i payload grezzi.

Due di questi meritano una query salvata dal primo giorno. claude_code.api_retries_exhausted è la forma di un'interruzione che raggiunge i tuoi sviluppatori — scatta dopo che il backoff dell'SDK si è arreso. E claude_code.permission_mode_changed è una traccia di audit: è il modo per scoprire che la flotta ha girato in una modalità più permissiva di quella prevista dalla tua policy, senza leggere la trascrizione di nessuno.

Attribuzione multi-team, a basso costo​

Una sola variabile fa il lavoro di un'intera strategia di tagging. OTEL_RESOURCE_ATTRIBUTES viene esportata come etichette delle metriche (controllata da OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES, attiva per impostazione predefinita), quindi l'attribuzione per centro di costo è una riga in qualunque cosa fornisca i tuoi ambienti di sviluppo:

Attribuisci la spesa dell'agente a un team e a un centro di costo

export OTEL_RESOURCE_ATTRIBUTES="department=engineering,team.id=platform,cost_center=eng-123"

Le tre trappole di privacy​

È qui che i rollout di telemetria ben intenzionati vanno storti, e tutte e tre sono evitabili se le conosci prima che il collector sia attivo.

1. Il contenuto è oscurato per impostazione predefinita. L'identità no. Le impostazioni predefinite di Claude Code sono davvero prudenti sul contenuto: prompt, risposte dell'assistente, parametri degli strumenti e output degli strumenti sono tutti oscurati a meno che tu non scelga di attivarli per segnale (OTEL_LOG_USER_PROMPTS, OTEL_LOG_ASSISTANT_RESPONSES, OTEL_LOG_TOOL_DETAILS, OTEL_LOG_TOOL_CONTENT, OTEL_LOG_RAW_API_BODIES), e i nomi dei plugin di terze parti compaiono come segnaposto "third-party" finché non imposti OTEL_LOG_TOOL_DETAILS=1. Ma user.email — l'indirizzo reale dell'accesso, o delle credenziali di una sessione cloud — è incluso quando disponibile, e organization.id è sempre presente quando si è autenticati. Quindi la postura predefinita è "non sappiamo cosa hanno digitato, sappiamo esattamente chi sono".

È la scelta giusta per la maggior parte delle aziende e quella sbagliata per alcune. Vale la pena distinguerla dalla sua vicina: user.id è un identificatore anonimo casuale generato alla prima esecuzione e conservato in ~/.claude.json — è documentato come privo di informazioni personali e non derivato dal tuo account Claude. user.account_uuid e user.account_id sono quelli legati all'account, e si disattivano con OTEL_METRICS_INCLUDE_ACCOUNT_UUID. Decidi con cognizione quale di questi tre il tuo collector deve conservare, e dì ai tuoi sviluppatori quale è.

2. Attivare la cattura dei contenuti sposta i tuoi segreti in un secondo sistema. OTEL_LOG_TOOL_CONTENT=1 è una cosa ragionevole da volere — l'output degli strumenti è dove si vede la maggior parte dei bug degli agenti. Ma significa anche che ogni file letto dal tuo agente, ogni output di comando e qualunque cosa un .env contenesse ora sta nel tuo backend di osservabilità, sotto la sua policy di retention e i suoi controlli d'accesso anziché quelli del tuo repo. Considera l'attivazione come concedere al tuo vendor di osservabilità lo stesso nulla osta del tuo controllo di versione, perché è esattamente questo. Se ti serve solo durante un incidente, attivala per l'incidente.

3. L'endpoint del tuo collector è un canale di esfiltrazione. È la trappola con i denti. La telemetria si configura con variabili d'ambiente, e negli ambienti cloud queste sono leggibili da tutti gli utenti — quindi un OTEL_EXPORTER_OTLP_ENDPOINT impostato da uno sviluppatore può puntare la telemetria di un'intera sessione verso un host che non hai mai sentito nominare. L'indicazione stessa della documentazione è la soluzione: metti credenziali ed endpoint nelle impostazioni gestite dal server anziché nelle variabili d'ambiente ambientali, dove le credenziali gestite possono rimuovere le variabili impostate dagli sviluppatori proprio per impedire l'esportazione verso collector non autorizzati. Se hai attivato una qualsiasi cattura dei contenuti, questo smette di essere igiene e diventa il controllo che conta.

Per le credenziali ruotanti esiste un hook per header dinamici anziché un token statico: punta otelHeadersHelper a uno script che stampa JSON, e viene eseguito all'avvio e circa ogni 29 minuti dopo (regolabile con CLAUDE_CODE_OTEL_HEADERS_HELPER_DEBOUNCE_MS). Funziona su http/protobuf e http/json, non su grpc — un dettaglio che altrimenti ti costerà un'ora, dato che la guida rapida usa grpc.

Cardinalità: scegli un budget prima della fattura​

La telemetria degli agenti è insolitamente brava a generare etichette ad alta cardinalità, perché gli identificatori naturali sono per esecuzione. Un ID di sessione è unico per sessione per definizione; attaccalo a una metrica e hai fabbricato una serie temporale per sessione.

Claude Code espone esplicitamente il compromesso anziché nasconderlo, e i valori predefiniti ti dicono come i progettisti lo pesano:

VariabilePredefinitoAggiunge
OTEL_METRICS_INCLUDE_SESSION_IDtruesession.id, ccr.session.id — la cardinalità più alta dell'insieme
OTEL_METRICS_INCLUDE_ACCOUNT_UUIDtrueuser.account_uuid, user.account_id
OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTEStruequalunque cosa tu metta in OTEL_RESOURCE_ATTRIBUTES
OTEL_METRICS_INCLUDE_VERSIONfalseapp.version
OTEL_METRICS_INCLUDE_ENTRYPOINTfalseapp.entrypoint
OTEL_METRICS_INCLUDE_REPOSITORYfalseattributi vcs.* del repository

La regola portabile, oltre Claude Code: gli identificatori per esecuzione vanno su span ed eventi, non sulle etichette delle metriche. Gli span sono già uno per esecuzione, quindi un ID di sessione lì è gratis e rende la traccia collegabile. Su un contatore o un istogramma moltiplica il numero di serie per il tuo traffico. Se un numero ha senso solo per una singola esecuzione, non è una metrica.

L'ID di sessione è attivo per impostazione predefinita perché collegare metriche e tracce è davvero utile. Se il tuo backend fattura per serie, è il primo interruttore da spegnere — e la prima cosa da controllare quando la fattura ti sorprende.

Un piano di strumentazione di partenza​

Non un modello di maturità. L'ordine seguente è scelto perché ogni passo sia utile da solo, e perché tu ti possa fermare al punto che corrisponde a quanto i tuoi agenti possono costarti.

Guided walkthrough1 of 5
  1. Prima di qualsiasi dashboard. Input, output, lettura cache, scrittura cache, token di ragionamento, modello richiesto, modello rispondente e l'ID di richiesta del fornitore. Questo singolo record risponde alla maggior parte delle domande sui costi e rende ogni incidente riconciliabile con il fornitore. Se non fai altro di questa pagina, fai questo.

Errori comuni​

  • Strumentare la chiamata al modello e non il loop. I dati a livello di chiamata sembrano a posto fino a quando non ti accorgi che stai pagando 30 turni. Senza uno span padre non c'è nessun turns_per_task con cui accorgertene.
  • Conservare solo gli span falliti. Gli agenti falliscono con HTTP 200. Campiona anche i successi, o il tuo archivio di tracce contiene solo i bug che erano già facili.
  • Cablare i nomi gen_ai.* nelle dashboard. Sono tutti Development, in un repo senza release taggate, con commit quotidiani. Un modulo di mappatura, un attributo di risorsa con la versione di semconv.
  • Mettere ID di sessione o di esecuzione sulle etichette delle metriche. Una serie per esecuzione. Mettili sugli span, dove sono gratis e più utili.
  • Misurare il costo senza un segnale di qualità. Ogni leva che riduce il costo riduce anche il lavoro svolto. Un rapporto è un obiettivo; un numero grezzo di costo è una trappola.
  • Scartare il request-id. Sono 30 byte ed è l'unico identificatore che attraversa il confine verso i sistemi del fornitore.
  • Registrare il contenuto degli strumenti "per ora". La retention è immediata e reale, e "per ora" non ha una data di fine. Decidi, attiva per l'incidente, disattiva dopo.
  • Dare per scontato che il trasporto della guida rapida supporti tutto. Gli header di autenticazione dinamici funzionano su http/protobuf e http/json — non sul grpc che ti dà la guida rapida.
  • Trattare la telemetria dei costi come un controllo. claude_code.cost.usage si incrementa dopo il completamento di ogni richiesta. È un'osservazione eccellente e non ha mai bloccato nulla.

Verifica di realtà tra fornitori​

Le convenzioni sono neutre rispetto al fornitore per progetto — semantic-conventions-genai contiene documenti dedicati per Anthropic, OpenAI, AWS Bedrock e Azure AI Inference accanto a quelli generici, ed è ciò che rende gestibile una flotta mista. Due cose restano da normalizzare a tuo carico:

Il conteggio dei token non significa la stessa cosa ovunque. Input in cache, token di ragionamento e token multimodali sono prezzati e a volte contati in modo diverso da fornitore a fornitore, quindi gen_ai.usage.input_tokens sommato tra fornitori è un numero privo di significato. Converti in denaro al margine, usando la tariffa di ciascun fornitore, e aggrega il denaro. Il costo è l'unica unità davvero confrontabile.

L'identificatore di richiesta del fornitore non ha un attributo standard. Ogni fornitore ne ha uno, ciascuno lo chiama in modo diverso e le convenzioni non impongono dove finisca. Scegli una chiave — provider.request_id va bene quanto un'altra — e scrivici dentro quella di ogni fornitore, con gen_ai.provider.name già sullo span per disambiguare.

Check yourself

0/4
  1. Perché 'riproducilo in locale' non è una strategia di debug praticabile per un'esecuzione di agente?
  2. Su uno span di inferenza GenAI, quali attributi portano il badge 'Stable' di OpenTelemetry?
  3. Abiliti la telemetria di Claude Code con le impostazioni predefinite documentate. Cosa conserva il tuo collector?
  4. Dove va un identificatore per sessione?
Premi Invio o Spazio per girare la carta. Usa le frecce sinistra e destra per spostarti tra le carte.Termine mostrato.
1 / 18
Key takeaways
  • Non puoi riprodurre un'esecuzione di agente, quindi l'osservabilità non è uno strumento facoltativo — è l'unica registrazione che esisterà.
  • Registra i successi interessanti, non solo gli errori. Gli agenti falliscono con HTTP 200 e una risposta sbagliata e sicura di sé.
  • Ogni nome gen_ai.* è 'Development', in un repo senza release taggate. Usa le convenzioni; instrada le dashboard attraverso un livello di mappatura e metriche derivate.
  • Crea span per il loop, non solo per la chiamata. Il bug costoso sono undici turni per un lavoro da due, ed è invisibile a livello di chiamata.
  • Metti il request-id su ogni span. Sono 30 byte e l'unico identificatore che entra nei sistemi del fornitore.
  • Claude Code esporta già tutto questo — con una sola variabile d'ambiente. claude_code.code_edit_tool.decision è il denominatore di qualità che manca alla maggior parte dei team.
  • Il contenuto è oscurato per impostazione predefinita; l'identità no. Sappi quale fra user.id, user.email e user.account_uuid conserva il tuo collector, e dillo al tuo team.
  • Il tuo endpoint OTLP è una via di esfiltrazione. Gestiscilo lato server, soprattutto una volta attiva una qualsiasi cattura dei contenuti.
  • ID per esecuzione sugli span, mai sulle etichette delle metriche. La telemetria dei costi osserva; non ha mai bloccato una singola richiesta.

Prossimi passi​

  • Cache diagnostics — i campi per richiesta che spiegano perché una lettura dalla cache su cui contavi non è avvenuta
  • Hard spend caps — il livello di enforcement che questa pagina si limita a osservare: quali fornitori bloccano davvero la richiesta
  • Evaluating agents — trasformare i segnali di qualità che hai appena iniziato a registrare in un giudizio ripetibile

Fonti e approfondimenti​