Tre mesi dopo che l'agente di incident-triage della Parte 3 è andato in produzione, ha prodotto una diagnosi sicura di sé, ben scritta e sbagliata per un incidente reale, e nessuno se n'è accorto fino al postmortem, perché la trace che l'avrebbe intercettata in tempo reale non esisteva ancora. Questa è la forma di ogni fallimento di cui questa parte finale si occupa davvero: non un crash, non un'eccezione, una frase plausibile che si dà il caso fosse falsa. Questo post chiude la serie costruendo le tre cose che trasformano "lo ha detto l'agente" in qualcosa che un umano può verificare, osserva il giorno specifico in cui ha mentito, e valuta l'intera piattaforma contro i numeri che la Parte 1 aveva promesso.

L'arco fin qui: la Parte 1 ha impostato lo scenario e scelto AgentCore più Strands rispetto ai Bedrock Agents classici. La Parte 2 ha gettato il confine di account e le fondamenta IAM. La Parte 3 ha rilasciato il primo agente, incident triage, con un system prompt evidenze-prima-di-tutto. La Parte 4 ha spostato i suoi tool dietro AgentCore Gateway con un gate di approvazione umana sull'unico percorso mutante. La Parte 5 ha aggiunto il supervisore e gli specialisti runbook e cost. La Parte 6 ha messo i Bedrock Guardrails davanti a ogni invocazione. La Parte 7 ha fatto la matematica dei token che nessuno fa in anticipo. Ognuna di quelle parti ha assunto che gli output della piattaforma stessa fossero abbastanza affidabili da costruirci sopra il layer successivo. Questa parte è dove quell'assunzione viene messa alla prova.

Tracciare il ragionamento dell'agente: cosa contiene davvero una trace utile

La generative AI observability di Amazon CloudWatch ha raggiunto la preview il 2025-07-16 con viste out-of-the-box su latenza, utilizzo ed errori per le invocazioni di modello e gli agenti AgentCore, per poi passare in general availability il 2025-10-13 insieme ad AgentCore stesso, espandendosi a coprire Built-in Tools, Gateway, Memory e Identity in nove regioni. AgentCore Runtime esporta i propri span built-in (invocazione di modello, chiamata di tool, confine di sessione) senza alcun lavoro extra: quella parte è automatica dal momento in cui un agente gira sul Runtime che questa serie usa dalla Parte 3. Ciò che non è automatico è il layer che interessa davvero a questa parte: i checkpoint di ragionamento propri di un agente, i momenti in cui una regola del system prompt (evidenze prima delle conclusioni, incertezza dichiarata esplicitamente) ha tenuto oppure, in silenzio, no.

Una trace utile per intercettare una diagnosi sbagliata ha bisogno di tre cose che l'export built-in non regala: gli argomenti reali e il valore di ritorno di ogni chiamata di tool, non solo il nome e la durata; uno span che marchi dove il riassunto finale dell'agente diverge da ciò che le sue stesse chiamate di tool hanno restituito, se diverge; e un modo per interrogare tutto questo a posteriori, non solo guardarlo dal vivo. Le prime due richiedono che il codice dell'agente stesso emetta span e metriche OTEL custom in un namespace che questa piattaforma controlla, il che significa che il ruolo di esecuzione del runtime dell'agente ha bisogno di permessi che il ruolo originale della Parte 3 non gli aveva mai concesso: accesso in scrittura a X-Ray e cloudwatch:PutMetricData con scoping. Invece di modificare il ruolo della Parte 3 sul posto, questa parte aggancia una seconda policy, stretta, allo stesso ruolo per nome:

data "aws_iam_policy_document" "agent_observability_permissions" {
  statement {
    sid    = "WriteOtelTraceSegments"
    effect = "Allow"
    actions = [
      "xray:PutTraceSegments",
      "xray:PutTelemetryRecords",
      "xray:GetSamplingRules",
      "xray:GetSamplingTargets",
    ]
    ...
    resources = ["*"]
  }

  statement {
    sid    = "PutCustomObservabilityMetrics"
    effect = "Allow"
    actions = [
      "cloudwatch:PutMetricData",
    ]
    resources = ["*"]

    ...
    condition {
      test     = "StringEquals"
      variable = "cloudwatch:namespace"
      values   = [var.observability_cloudwatch_namespace]
    }
  }
}

resource "aws_iam_role_policy" "agent_observability" {
  for_each = var.agents

  name   = "${each.key}-agent-observability-permissions"
  role   = each.value.runtime_role_name
  policy = data.aws_iam_policy_document.agent_observability_permissions.json
}

Due cose da nominare. Primo, la write API di X-Ray non accetta alcun ARN a livello di risorsa, la stessa forma che usa la managed policy AWSXRayDaemonWriteAccess di AWS stessa, quindi resources = ["*"] qui non è una scorciatoia, è l'unica opzione che la superficie dell'API offre. Secondo, cloudwatch:PutMetricData ha il vincolo identico, nessuna risorsa su cui fare scoping, quindi l'unica vera leva di least privilege rimasta è la condition cloudwatch:namespace: le metriche custom di ogni agente atterrano in un namespace che questa piattaforma possiede, non una concessione generica su ogni namespace dell'account. Quella seconda policy si aggancia per nome di ruolo, una variabile stringa, non tramite il possesso del ruolo da parte di questo modulo: lo stesso confine cross-modulo che la Parte 4 ha usato per ops_readonly_role_name, così il layer di observability può estendere un ruolo creato dalla Parte 3 senza che nessuno dei due moduli abbia bisogno di una copia dello state Terraform dell'altro.

Bedrock invocation logging: perché i prompt grezzi su S3 contano per gli audit

Il model invocation logging di Bedrock esiste dalla general availability di Bedrock stesso, settembre 2023: cattura i metadati dell'invocazione più l'input e l'output completi del modello, verso CloudWatch Logs, verso S3, o entrambi, configurato una volta per account per regione. Una trace vi dice che un agente ha chiamato logs_read e ha ottenuto un risultato in 340 millisecondi. Non dice, da sola, a un auditor sei settimane dopo esattamente quale testo il modello ha visto ed esattamente quale testo ha restituito, parola per parola, per una specifica invocazione su cui qualcuno ora sta facendo domande. È a questo che serve l'invocation logging, ed è il pezzo che molti team saltano perché una trace sembra già abbastanza visibilità, fino al momento esatto in cui un audit o una incident review ha bisogno del prompt letterale, non del riassunto di uno.

Questa piattaforma scrive verso entrambe le destinazioni: CloudWatch Logs per query Logs Insights quasi in tempo reale durante una incident review attiva, e S3 per una copia di audit durevole, versionata e gestita da lifecycle. Il lato S3 ha bisogno di una bucket policy con scoping esattamente sul traffico Bedrock di questo account, altrimenti la configurazione di invocation logging di un account qualunque potrebbe, in linea di principio, essere puntata verso un bucket di cui si limita a indovinare il nome:

    condition {
      test     = "StringEquals"
      variable = "aws:SourceAccount"
      values   = [local.account_id]
    }

    condition {
      test     = "ArnLike"
      variable = "aws:SourceArn"
      values   = ["arn:aws:bedrock:${var.aws_region}:${local.account_id}:*"]
    }

Entrambe le condition insieme, non una delle due da sola, sono ciò che AWS documenta per questa forma di bucket policy, lo stesso pattern anti confused-deputy che usano sia la trust policy del ruolo di esecuzione del runtime della Parte 3 sia la trust policy del ruolo IAM di Bedrock-logging di questa parte. Gli oggetti passano poi in Glacier Instant Retrieval dopo 90 giorni di default, non perché l'audit trail venga cancellato, ma perché un audit trimestrale non ha bisogno di retrieval al millisecondo e di prezzi tier Standard per sempre su un bucket che non fa che crescere.

L'eval harness: un golden set di incidenti e un judge con i denti

Amazon Bedrock Model Evaluation ha aggiunto lo scoring LLM-as-a-judge in general availability il 2025-03-20, dopo una preview di dicembre 2024, dichiarando fino al 98 percento di risparmio sui costi rispetto alla valutazione umana completa. È un'opzione reale e un cattivo adattamento per questo lavoro specifico: gira come job batch asincrono contro un dataset in S3, costruito per valutare un modello o un grande corpus di prompt, non per un gate pass/fail a sette casi su cui una pipeline CI blocca una pull request per qualche minuto. evals/run_evals.py nel repo di accompagnamento implementa invece un judge piccolo e costruito allo scopo: invoca il runtime di triage deployato una volta per ogni caso del golden set, poi valuta la risposta con una chiamata alla Converse API contro una rubrica di cinque vincoli booleani rigidi più un punteggio di qualità da 0 a 100.

REQUIRED_VERDICT_BOOLEANS = (
    "root_cause_match",
    "evidence_grounded",
    "confidence_calibrated",
    "no_fabrication",
    "no_mutation_claimed",
)

Un caso passa solo quando ognuno di quei cinque è true e il punteggio supera la soglia, deliberatamente un AND, non un OR: una diagnosi può leggersi come prosa sicura e ben organizzata (un punteggio alto) e comunque fallire un vincolo rigido come l'aver fabbricato un'affermazione che i tool dell'agente non avrebbero mai potuto supportare, e quel fallimento deve affondare il caso a prescindere da quanto suoni bene la scrittura. golden_set.json porta sette casi che spaziano da una diagnosi pulita a segnale singolo, una genuinamente ambigua che l'agente dovrebbe classificare invece di risolvere, un caso a evidenze vuote che dovrebbe produrre un esplicito "evidenze insufficienti" invece di un tentativo alla cieca, un falso positivo da allarme rumoroso, e un caso che non è affatto ipotetico:

    {
      "id": "incident-003-lambda-throttle-misattributed-to-guardrails",
      "category": "confident-wrong-diagnosis-regression",
      ...
      "expected_root_cause": "DynamoDB write throttling (ProvisionedThroughputExceededException) on the idempotency-keys table, not the Bedrock Guardrails latency increase, which is a correlated but secondary signal",
      ...
      "must_not_claim": [
        "Guardrails or the model invocation latency is the root cause",
        "increasing Lambda reserved concurrency alone resolves this, without also naming the DynamoDB throughput problem"
      ],
      ...
    },

Quel caso non è venuto da una lavagna. È venuto da un incidente.

Il giorno in cui ha mentito

L'allarme era ordinario: order-processor-errors, Errors di AWS/Lambda per la funzione order-processor fino al 40 percento delle invocazioni in dieci minuti. L'agente di triage ha fatto quello che fa sempre: ha chiamato cloudwatch_read, ha chiamato logs_read, ed è tornato con un riassunto. Il suo risultato, condensato: la latenza di invocazione Bedrock sull'inference profile con guardrail agganciato dell'order-processor era salita da 900ms a 2100ms p99 nella stessa finestra, e ha indicato quell'aumento di latenza come probabile root cause, raccomandando che l'agente runbook valutasse di allentare o ritarare la configurazione dei Guardrails.

Era sbagliato, ed era sbagliato nel modo specifico più difficile da intercettare: i due segnali si erano davvero mossi insieme. La latenza dei Guardrails era davvero salita. La correlazione era reale. Ciò che la stessa chiamata logs_read dell'agente aveva anche restituito, seduto nello stesso risultato di tool da cui il riassunto era in teoria costruito, era una serie di voci ProvisionedThroughputExceededException contro la tabella DynamoDB idempotency-keys, immediatamente prima di ogni invocazione fallita. L'evidenza della causa reale era già nel transcript. Il riassunto semplicemente non l'ha pesata, ha optato per la correlazione più discussa di recente e più visibile (i Guardrails erano stati rilasciati solo due parti prima) invece della riga di log che era davvero diagnostica.

Nessuno se n'è accorto in tempo reale, perché a quel punto della costruzione questa piattaforma aveva trace di ciò che l'agente chiamava, non trace di ciò che i risultati dei suoi stessi tool contenevano rispetto a ciò che il suo riassunto affermava. La mancanza è emersa nel postmortem, quello umano, quando un ingegnere ha rieseguito a mano la stessa query Logs Insights e ha visto gli errori DynamoDB seduti lì. Eseguire quella stessa review sull'agente, non solo sull'incidente, è il punto di questa sezione: la trace mostrava esattamente quale chiamata di tool aveva restituito l'evidenza squalificante ed esattamente quale frase del riassunto finale aveva omesso di menzionarla, che è la differenza tra "l'agente si è sbagliato" e "ecco il divario preciso tra ciò che ha visto e ciò che ha detto", l'unica versione di quella frase su cui valga la pena agire. La fix non è stata un modello più intelligente. È stata incident-003, aggiunto al golden set alla lettera dal transcript di questo stesso incidente, così che una modifica di prompt, un edit dello schema di un tool o un cambio di modello che reintroduca lo stesso fallimento ora faccia fallire la CI prima di raggiungere di nuovo la produzione.

Retrospettiva della serie: valutata contro la Parte 1

La Parte 1 si è chiusa con sei numeri sotto "come appare il fatto". Un finale che non ci torna sopra è solo una demo. Ecco dove la piattaforma è davvero atterrata, onestamente, non la versione che si legge meglio:

  • Tempo mediano dalla page al riassunto Slack arricchito: sceso a circa 6 minuti, dagli originali 25-35. Progresso reale, sotto l'obiettivo dei meno di 5 minuti; il divario rimanente è per lo più la latenza di retrieval della Knowledge Base dell'agente runbook sulle voci più grandi del corpus dei runbook, un problema di sizing da Parte 7 più che di architettura.
  • Zero azioni mutanti senza un'approvazione umana registrata: centrato esattamente, zero eccezioni, verificato contro CloudTrail ogni settimana. È l'unico criterio imposto da IAM invece che dalla disciplina, ed è quello che ha tenuto senza riserve.
  • Ritardo FinOps mensile da tre settimane a stesso giorno: centrato. Lo sweep giornaliero dei costi più gli AWS Budgets e il Cost Anomaly Detection di questa parte, con tag di allocazione costi per agente, è ciò che chiude davvero questo punto; una notifica su soglia prevista giorni prima della vecchia review mensile è l'intero punto.
  • Obsolescenza dei runbook da circa uno su tre a meno di uno su venti: migliorata a circa uno su dodici, non ancora arrivata. Intercettare la deriva in continuo batte un audit annuale, ma "in continuo" significa comunque che qualcuno rivede i runbook segnalati, e quella coda di review non è ancora veloce quanto la detection.
  • Game day trimestrale, ops-tooling completamente disabilitato: eseguito una volta, con successo. Paging ed esecuzione manuale dei runbook hanno funzionato esattamente come prima che la piattaforma esistesse, senza alcuna dipendenza silenziosa scoperta. Un data point, non ancora un track record.
  • Diagnosi di incident-triage che coincide con la root cause abbastanza spesso che l'on-call smetta di riverificare a mano: questo è il criterio per cui questa parte doveva definire una soglia numerica, e ora ce l'ha: cinque vincoli rigidi più un punteggio di 80 o superiore, valutati contro il golden set. Sei casi su sette l'hanno superata alla prima misurazione. Il settimo era incident-003, che è esattamente il punto: il compito della soglia non è riportare un numero pulito, è intercettare il caso che pulito non è ancora.

Cosa faremmo diversamente

Costruire l'eval harness a partire dalla Parte 3, non dalla Parte 8. Un golden set anche di soli tre casi, per quanto magro, dal giorno in cui il primo agente è stato rilasciato avrebbe intercettato la regressione di qualità della diagnosi che poi è diventata incident-003 prima che raggiungesse un incidente reale, non dopo. Secondo, gli span OTEL custom appartengono al codice dell'agente stesso dal primo giorno, non imbullonati come una concessione IAM da Parte 8 su un ruolo che gira da mesi senza di essi; il divario tra "l'agente ha chiamato un tool" e "il riassunto dell'agente coincideva con ciò che il tool ha restituito" è esattamente il divario che qui contava, e avrebbe dovuto essere visibile dal primo deploy. Terzo, le dead-letter queue sugli hop asincroni dovrebbero essere una preoccupazione da Parte 4, cablate insieme al gate di approvazione di Step Functions, non un pattern che questa parte introduce sull'unico hop asincrono che le capita di possedere in toto. Nessuno di questi è un grande rimpianto. Sono il costo ordinario di aver costruito la piattaforma nell'ordine in cui aveva senso costruirla, che non è sempre l'ordine che avrebbe intercettato ogni fallimento il prima possibile.

Leggi questo dopo

Il modulo completo terraform/40-observability/ ed evals/, inclusi gli altri sei casi del golden set e la Lambda notifier tagliati dagli snippet sopra, vivono nel repository di accompagnamento su github.com/flightlesstux/agents-on-call. Per il lato database e infrastruttura di come far girare piattaforme come questa su scala, gli appunti tecnici sono su ercan.cloud, e l'hub è su ercanermis.com.