Agents on Call, partie 8. Production : observabilité, évals, et le jour où elle ment
Traces OTEL dans CloudWatch, invocation logging Bedrock vers S3, un harnais d'évals avec jeu d'incidents de référence, et le jour où l'agent de triage a menti.

Trois mois après la mise en service de l'agent de triage d'incidents de la Partie 3, il a produit un diagnostic assuré, bien écrit et faux pour un incident réel, et personne ne l'a attrapé avant le postmortem, parce que la trace qui l'aurait attrapé en temps réel n'existait pas encore. C'est la forme de chaque défaillance dont cette partie finale traite réellement : pas un crash, pas une exception, une phrase plausible qui se trouvait être fausse. Ce billet clôt la série en construisant les trois choses qui transforment « l'agent l'a dit » en quelque chose qu'un humain peut vérifier, observe le jour précis où il a menti, et note la plateforme entière contre les chiffres que la Partie 1 avait promis.
L'arc jusqu'ici : la Partie 1 a posé le scénario et choisi AgentCore plus Strands face aux Bedrock Agents classiques. La Partie 2 a coulé la frontière de compte et les fondations IAM. La Partie 3 a livré le premier agent, le triage d'incidents, avec un system prompt qui place les preuves d'abord. La Partie 4 a déplacé ses outils derrière AgentCore Gateway avec une porte d'approbation humaine sur l'unique chemin mutant. La Partie 5 a ajouté le superviseur et les spécialistes runbook et coûts. La Partie 6 a mis des Bedrock Guardrails devant chaque invocation. La Partie 7 a fait le calcul de tokens que personne ne fait en amont. Chacune de ces parties a supposé que les propres sorties de la plateforme méritaient assez de confiance pour bâtir la couche suivante par-dessus. Cette partie est celle où cette hypothèse est mise à l'épreuve.
Tracer le raisonnement de l'agent : ce que contient réellement une trace utile
L'observabilité IA générative d'Amazon CloudWatch a atteint la preview le 16 juillet 2025 avec des vues prêtes à l'emploi de latence, d'usage et d'erreurs pour les invocations de modèles et les agents AgentCore, puis est passée en disponibilité générale le 13 octobre 2025 en même temps qu'AgentCore lui-même, en s'étendant pour couvrir Built-in Tools, Gateway, Memory et Identity dans neuf régions. AgentCore Runtime exporte ses propres spans intégrés (invocation de modèle, appel d'outil, frontière de session) sans aucun travail supplémentaire : cette partie-là est automatique dès qu'un agent tourne sur le Runtime que cette série utilise depuis la Partie 3. Ce qui n'est pas automatique est la couche dont cette partie se soucie réellement : les propres points de contrôle de raisonnement d'un agent, les moments où une règle du system prompt (les preuves avant les conclusions, énoncer l'incertitude explicitement) a tenu, ou discrètement n'a pas tenu.
Une trace utile pour attraper un diagnostic faux exige trois choses que l'export intégré ne vous donne pas gratuitement : les arguments réels et la valeur de retour de chaque appel d'outil, pas seulement son nom et sa durée ; un span marquant l'endroit où le résumé final de l'agent diverge de ce que ses propres appels d'outils ont retourné, s'il diverge ; et un moyen d'interroger tout cela après coup, pas seulement de le regarder en direct. Les deux premières exigent que le propre code de l'agent émette des spans et des métriques OTEL personnalisés dans un namespace que cette plateforme contrôle, ce qui signifie que le rôle d'exécution du runtime de l'agent a besoin de permissions que le rôle d'origine de la Partie 3 ne lui a jamais accordées : l'accès en écriture X-Ray et un cloudwatch:PutMetricData scopé. Plutôt que de modifier le rôle de la Partie 3 en place, cette partie attache une seconde politique étroite au même rôle par son nom :
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
}Deux choses à nommer. D'abord, l'API d'écriture de X-Ray ne prend aucun ARN au niveau ressource, la même forme que la propre politique managée AWSXRayDaemonWriteAccess d'AWS utilise, donc resources = ["*"] ici n'est pas un raccourci, c'est la seule option que la surface d'API offre. Ensuite, cloudwatch:PutMetricData a la contrainte identique, aucune ressource à scoper, si bien que le seul vrai levier de moindre privilège restant est la condition cloudwatch:namespace : les métriques personnalisées de chaque agent atterrissent dans un namespace que cette plateforme possède, pas un droit général sur chaque namespace du compte. Cette seconde politique s'attache par le nom du rôle, une variable chaîne, sans que ce module possède le rôle : la même frontière inter-modules que la Partie 4 a utilisée pour ops_readonly_role_name, si bien que la couche d'observabilité peut étendre un rôle créé par la Partie 3 sans qu'aucun des deux modules n'ait besoin d'une copie du state Terraform de l'autre.
L'invocation logging de Bedrock : pourquoi les prompts bruts dans S3 comptent pour les audits
Le model invocation logging de Bedrock existe depuis la propre disponibilité générale de Bedrock en septembre 2023 : il capture les métadonnées d'invocation plus l'entrée et la sortie complètes du modèle, vers CloudWatch Logs, vers S3, ou les deux, configuré une fois par compte et par région. Une trace vous dit qu'un agent a appelé logs_read et obtenu un résultat en 340 millisecondes. Elle ne dit pas, à elle seule, à un auditeur six semaines plus tard exactement quel texte le modèle a vu et exactement quel texte il a retourné, mot pour mot, pour une invocation précise sur laquelle quelqu'un pose désormais des questions. C'est à cela que sert l'invocation logging, et c'est la pièce que beaucoup d'équipes sautent parce qu'une trace donne déjà l'impression d'une visibilité suffisante, jusqu'au moment où un audit ou une revue d'incident a besoin du prompt littéral, pas du résumé d'un prompt.
Cette plateforme écrit vers les deux destinations : CloudWatch Logs pour des requêtes Logs Insights quasi temps réel pendant une revue d'incident active, et S3 pour une copie d'audit durable, versionnée et gérée par cycle de vie. Le côté S3 exige une politique de bucket scopée exactement au trafic Bedrock du propre compte, sans quoi la configuration d'invocation logging de n'importe quel compte pourrait, en principe, être pointée vers un bucket dont elle ne fait que deviner le nom :
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}:*"]
}Les deux conditions ensemble, pas l'une ou l'autre seule, sont ce qu'AWS documente pour cette forme de politique de bucket, le même schéma anti-confused-deputy que la politique de confiance du rôle d'exécution du runtime de la Partie 3 et la politique de confiance du rôle IAM de logging Bedrock de cette partie utilisent tous deux. Les objets transitionnent ensuite vers Glacier Instant Retrieval après 90 jours par défaut, non parce que la piste d'audit se fait supprimer, mais parce qu'un audit trimestriel n'a pas besoin d'une récupération à la milliseconde ni du tarif Standard pour toujours sur un bucket qui ne fait que grossir.
Le harnais d'évals : un jeu d'incidents de référence et un juge qui a du mordant
Amazon Bedrock Model Evaluation a ajouté la notation LLM-as-a-judge en disponibilité générale le 20 mars 2025, après une preview de décembre 2024, en rapportant jusqu'à 98 pour cent d'économies par rapport à une évaluation humaine complète. C'est une vraie option et un mauvais choix pour cette tâche précise : il s'exécute comme un job batch asynchrone contre un dataset dans S3, conçu pour noter un modèle ou un grand corpus de prompts, pas pour une porte pass/fail de sept cas sur laquelle une pipeline CI bloque une pull request pendant quelques minutes. evals/run_evals.py dans le dépôt compagnon implémente à la place un petit juge sur mesure : invoquer le runtime de triage déployé une fois par cas du jeu de référence, puis noter la réponse avec un appel à l'API Converse contre une grille de cinq contraintes booléennes dures plus un score de qualité de 0 à 100.
REQUIRED_VERDICT_BOOLEANS = (
"root_cause_match",
"evidence_grounded",
"confidence_calibrated",
"no_fabrication",
"no_mutation_claimed",
)Un cas ne passe que quand chacune de ces cinq est vraie et que le score franchit le seuil, délibérément un AND, pas un OR : un diagnostic peut se lire comme une prose assurée et bien organisée (un score élevé) tout en échouant à une contrainte dure comme fabriquer une affirmation que les outils de l'agent n'auraient jamais pu soutenir, et cet échec doit couler le cas quelle que soit la qualité de l'écriture. golden_set.json porte sept cas couvrant un diagnostic propre à signal unique, un diagnostic réellement ambigu que l'agent doit classer plutôt que trancher, un cas à preuves vides qui doit produire un « preuves insuffisantes » explicite plutôt qu'une supposition, un faux positif d'alarme bruyante, et un cas qui n'a rien d'hypothétique :
{
"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"
],
...
},Ce cas ne vient pas d'un tableau blanc. Il vient d'un incident.
Le jour où elle a menti
L'alarme était ordinaire : order-processor-errors, les Errors AWS/Lambda de la fonction order-processor jusqu'à 40 pour cent des invocations sur dix minutes. L'agent de triage a fait ce qu'il fait toujours : appelé cloudwatch_read, appelé logs_read, et il est revenu avec un résumé. Son constat, condensé : la latence d'invocation Bedrock sur l'inference profile avec guardrail de l'order-processor était passée de 900 ms à 2 100 ms au p99 dans la même fenêtre, et il a nommé cette hausse de latence comme cause racine probable, en recommandant que l'agent runbook regarde comment desserrer ou re-régler la configuration des Guardrails.
Il avait tort, et il avait tort de la façon précise qui est la plus dure à attraper : les deux signaux bougeaient réellement ensemble. La latence des Guardrails avait réellement monté. La corrélation était réelle. Ce que le propre appel logs_read de l'agent avait aussi retourné, assis dans le même résultat d'outil dont le résumé était censé être construit, était une série d'entrées ProvisionedThroughputExceededException contre la table DynamoDB idempotency-keys, immédiatement avant chaque invocation échouée. La preuve de la cause réelle était déjà dans la transcription. Le résumé ne l'a simplement pas pondérée, il a saisi la corrélation la plus récemment discutée, la plus visible (les Guardrails avaient été livrés à peine deux parties plus tôt) au lieu de la ligne de log qui était réellement diagnostique.
Personne ne l'a attrapé en temps réel, parce qu'à ce stade de la construction cette plateforme avait des traces de ce que l'agent appelait, pas des traces de ce que les propres résultats d'outils de l'agent contenaient face à ce que son résumé affirmait. Le raté a émergé au postmortem, celui des humains, quand un ingénieur a relancé à la main la même requête Logs Insights et vu les erreurs DynamoDB posées juste là. Rejouer cette même revue sur l'agent, pas seulement sur l'incident, est tout l'objet de cette section : la trace montrait exactement quel appel d'outil avait retourné la preuve disqualifiante et exactement quelle phrase du résumé final avait omis de la mentionner, ce qui est la différence entre « l'agent avait tort » et « voici l'écart précis entre ce qu'il a vu et ce qu'il a dit », la seule version de cette phrase qui vaille d'être actionnée. Le correctif n'était pas un modèle plus intelligent. C'était incident-003, ajouté au jeu de référence mot pour mot depuis la transcription de cet incident, si bien qu'un changement de prompt, une modification de schéma d'outil ou un changement de modèle qui réintroduit la même défaillance fait désormais échouer la CI avant d'atteindre à nouveau la production.
Rétrospective de la série : notée contre la Partie 1
La Partie 1 se fermait sur six chiffres sous « à quoi ressemble fini ». Un finale qui n'y revient pas n'est qu'une démo. Voici où la plateforme a réellement atterri, honnêtement, pas la version qui se lit le mieux :
- Temps médian entre la page et le résumé Slack enrichi : descendu à environ 6 minutes, contre 25 à 35 à l'origine. Un vrai progrès, en deçà de la cible de moins de 5 minutes ; l'écart restant tient surtout à la latence de récupération de la Knowledge Base de l'agent runbook sur les plus grosses entrées du corpus de runbooks, un problème de dimensionnement façon Partie 7 plus qu'un problème d'architecture.
- Zéro action mutante sans approbation humaine enregistrée : atteint exactement, zéro exception, vérifié contre CloudTrail chaque semaine. C'est le seul critère appliqué par IAM plutôt que par la discipline, et c'est celui qui a tenu sans réserve.
- Décalage FinOps mensuel de trois semaines à jour même : atteint. Le balayage de coûts quotidien plus les AWS Budgets et Cost Anomaly Detection de cette partie, avec des tags d'allocation de coûts par agent, est ce qui ferme réellement celui-ci ; une notification à seuil prévisionnel des jours avant l'ancienne revue mensuelle est tout l'intérêt.
- Obsolescence des runbooks d'environ un sur trois à moins d'un sur vingt : améliorée à environ un sur douze, pas tout à fait au but. Attraper la dérive en continu bat un audit annuel, mais « en continu » signifie toujours que quelqu'un passe en revue les runbooks signalés, et cette file de revue n'est pas encore aussi rapide que la détection.
- Game day trimestriel, ops-tooling entièrement désactivé : exécuté une fois, avec succès. Le paging et l'exécution manuelle des runbooks ont fonctionné exactement comme avant l'existence de la plateforme, sans aucune dépendance silencieuse découverte. Un point de donnée, pas encore un historique.
- Diagnostic de triage correspondant à la cause racine assez souvent pour que l'astreinte cesse de revérifier à la main : c'est le critère pour lequel cette partie devait définir un seuil numérique, et elle en a désormais un : cinq contraintes dures plus un score de 80 ou plus, évalués contre le jeu de référence. Six cas sur sept l'ont franchi à la première mesure. Le septième était
incident-003, ce qui est exactement le point : le travail du seuil n'est pas de rapporter un chiffre propre, c'est d'attraper le cas qui n'est pas encore propre.
Ce que nous ferions différemment
Construire le harnais d'évals dès la Partie 3, pas la Partie 8. Un jeu de référence, même mince, même de trois cas, présent dès le jour où le premier agent a été livré aurait attrapé la régression de qualité de diagnostic devenue plus tard incident-003 avant qu'elle n'atteigne jamais un incident réel, pas après. Ensuite, les spans OTEL personnalisés appartiennent au propre code d'un agent dès le premier jour, pas boulonnés en Partie 8 comme un droit IAM sur un rôle qui tourne depuis des mois sans eux ; l'écart entre « l'agent a appelé un outil » et « le résumé de l'agent correspondait à ce que l'outil a retourné » est exactement l'écart qui a compté ici, et il aurait dû être visible dès le premier déploiement. Troisièmement, les dead-letter queues sur les sauts asynchrones devraient être une préoccupation de la Partie 4, câblées en même temps que la porte d'approbation Step Functions, pas un schéma que cette partie introduit sur le seul saut asynchrone qu'elle se trouve posséder en propre. Rien de tout cela n'est un grand regret. C'est le coût ordinaire de construire la plateforme dans l'ordre qui avait du sens pour la construire, qui n'est pas toujours l'ordre qui aurait attrapé chaque défaillance au plus tôt.
À lire ensuite
- Partie 7, Dimensionnement : le calcul de tokens que personne ne fait en amont, pour le calcul de débit et de coût que les Budgets et Cost Anomaly Detection de cette partie surveillent désormais en production au lieu de l'attraper avec trois semaines de retard.
- Partie 1, Le scénario : pourquoi une équipe ops embauche des agents, là où cette série a commencé et où les six chiffres notés ci-dessus ont d'abord été couchés par écrit.
- Monitoring EC2 Disk Space with a Simple Bash Script and Slack Alerts sur ercan.cloud, la même forme alerte-vers-Slack vers laquelle le topic SNS et la Lambda de notification de cette partie construisent, depuis un script mono-hôte au lieu d'une plateforme multi-agents.
Le module complet terraform/40-observability/ et evals/, y compris les six autres cas du jeu de référence et la Lambda de notification retirés des extraits ci-dessus, vivent dans le dépôt compagnon à github.com/flightlesstux/agents-on-call. Pour le volet base de données et infrastructure de faire tourner des plateformes comme celle-ci à grande échelle, les notes de terrain sont sur ercan.cloud, et le hub se trouve sur ercanermis.com.
Plus d'Ercan
Deux autres sites, même auteur, terrain différent.
Cloud, AWS, EKS, Terraform, plateforme.
Notes de terrain de systèmes de production. EKS, IAM, Terraform à l'échelle organisation, observabilité, optimisation des coûts.
Visiter ercan.cloud →Le hub. À propos, conseil, contact.
Hub personnel pour les deux pistes d'écriture. Qui je suis, comment fonctionne le conseil, comment me joindre.
Visiter ercanermis.com →