AI
AgentOps : borner timeout, retries et circuit breaker d'un outil MCP avant la production
Un runbook de production pour définir le budget de latence, l'idempotence, les retries, le circuit breaker, les traces et le rollback d'un outil MCP appelé par un agent IA.
Un agent d’exploitation appelle un outil MCP pour lire l’état d’un déploiement, préparer une action puis l’exécuter après approbation. En test, chaque appel aboutit. En production, une dépendance ralentit : l’agent attend, le client expire, le runtime retente et l’opérateur relance la conversation. Une seule intention peut alors devenir plusieurs appels concurrents, avec une réponse tardive et un état final difficile à expliquer.
Le problème n’est pas seulement la latence du modèle. Il se trouve à la frontière entre agent, transport MCP, serveur d’outils et backend métier. Ce runbook sert à borner cette frontière avant l’ouverture en production : définir un budget de temps, décider quels appels sont retentables, couper les cascades avec un circuit breaker, observer un parcours complet, puis valider ou rollbacker la politique de résilience.
Figer un parcours et son résultat attendu
Commencez par un seul parcours représentatif. Le cas fil rouge utilise un agent interne qui vérifie un job de déploiement et peut demander sa relance. La lecture est sans effet de bord ; la relance est une écriture approuvée. Ces deux appels ne doivent pas partager aveuglément la même politique de timeout et de retry.
journey:
name: inspect_then_restart_deployment_job
agent: ops-assistant-prod
user_deadline: 20s
tools:
- name: get_deployment_job
effect: read_only
expected_result: current job state
- name: restart_deployment_job
effect: write
approval: human_required
expected_result: one accepted operation or no operation
evidence:
- one conversation and intent identifier
- one tool call identifier per attempt
- backend operation identifier when accepted
- timeout owner and elapsed time
- retry reason and idempotency key
- circuit state at call time
- final validation or rollback decision Le délai utilisateur est un plafond pour le parcours, pas la valeur à recopier sur chaque couche. Si le backend peut consommer vingt secondes, le transport, l’agent et l’interface n’ont plus de marge pour signaler proprement l’échec ou proposer une reprise.
Classer les outils par effet, pas par protocole
MCP décrit une interface d’outil ; il ne rend pas l’opération idempotente. Classez chaque outil selon son effet réel et la capacité du backend à reconnaître une répétition.
Lecture deterministe
Exemple : obtenir l'etat d'un job par identifiant
Retry possible sur erreur transitoire si le budget restant le permet
Limiter tout de meme le nombre de tentatives et la concurrence
Recherche ou calcul couteux
Exemple : interroger une large fenetre de logs
Retry seulement avec une requete bornee et un budget backend connu
Preferer pagination, cache ou reprise explicite a la duplication
Ecriture idempotente
Exemple : demander une operation avec une cle d'idempotence stable
Retry possible si le backend garantit un resultat unique pour cette cle
Toujours relire l'etat de l'operation apres une reponse ambigue
Ecriture non idempotente
Exemple : relancer un job sans identifiant d'operation stable
Aucun retry automatique
Apres timeout, verifier le backend puis reprendre manuellement ou rollbacker Le point de contrôle doit vivre dans le catalogue d’outils ou la policy d’exécution, pas uniquement dans le prompt. Un modèle peut proposer un retry ; le runtime doit rester l’autorité qui l’autorise ou le bloque.
Construire un budget de timeout de l’extérieur vers l’intérieur
Un timeout utile indique quelle couche abandonne et laisse aux couches externes le temps de traiter ce résultat. Les valeurs suivantes illustrent un contrat, elles ne constituent pas des seuils universels.
journey_budget_ms: 20000
layers:
user_interface:
deadline_ms: 20000
agent_runtime:
deadline_ms: 15000
reserve_for_final_response_ms: 3000
mcp_transport:
timeout_ms: 10000
tool_handler:
backend_timeout_ms: 7000
rules:
- inner_timeout_must_expire_before_outer_timeout
- no_retry_without_remaining_budget
- cancellation_must_propagate_to_backend_when_supported
- late_results_must_not_trigger_a_second_write
- timeout_events_must_name_the_layer_that_expired Mesurez le p50, le p95 et la queue de distribution par outil, backend et résultat. Un seul timeout global masque la différence entre attente réseau, file d’exécution, throttling backend et traitement métier. Gardez aussi une réserve pour produire une réponse exploitable : l’agent doit pouvoir dire que l’état est ambigu et fournir l’identifiant de corrélation au lieu de disparaître à la dernière milliseconde.
Rendre le retry explicite et borné
Une politique de retry doit combiner classe d’erreur, budget restant, compteur de tentatives et idempotence. Un backoff exponentiel avec jitter réduit la synchronisation des clients, mais ne rend pas une écriture répétable.
{
"tool": "get_deployment_job",
"max_attempts": 3,
"retry_on": ["transport_unavailable", "backend_429", "backend_503"],
"do_not_retry_on": ["invalid_arguments", "policy_denied", "approval_required", "backend_4xx_other"],
"backoff": {
"strategy": "exponential_with_jitter",
"initial_ms": 250,
"max_ms": 1500
},
"constraints": {
"remaining_budget_required": true,
"same_intent_id": true,
"new_tool_call_id_per_attempt": true
}
} Pour une écriture, ajoutez une clé d’idempotence dérivée d’une intention stable, de la cible et de l’approbation, sans y placer de secret. Le backend doit persister la clé avec l’opération et renvoyer le même identifiant si la demande arrive de nouveau.
tool: restart_deployment_job
automatic_retry: false
idempotency:
key_components:
- intent_id
- target_job_id
- environment
- approval_id
backend_guarantee: one_operation_per_key
after_ambiguous_timeout:
- stop_agent_retry
- query_operation_by_idempotency_key
- compare_target_state
- resume_only_with_explicit_decision Si le backend ne sait pas rechercher l’opération par clé, le retry automatique reste désactivé. Le correctif consiste d’abord à améliorer le contrat d’outil ou l’API cible, pas à demander au modèle d’être plus prudent.
Ouvrir le circuit avant la cascade
Le circuit breaker protège le serveur MCP et sa dépendance lorsqu’une série d’échecs indique que les retries ne peuvent plus aider. Il doit être défini par outil et, si nécessaire, par cible ou backend. Un circuit global peut rendre indisponibles des lectures saines à cause d’une seule opération dégradée.
breaker:
scope: tool_and_backend
tool: get_deployment_job
backend: deployment-api-prod
window: 60s
minimum_calls: 20
open_when:
failure_ratio_gte: 0.50
slow_call_ratio_gte: 0.70
open_duration: 30s
half_open:
probe_calls: 3
concurrent_probes: 1
fallback:
mode: read_only_degraded_response
include:
- last_known_state_timestamp
- incident_reference
- no_write_action_performed Les seuils doivent provenir de mesures et d’exercices contrôlés. En état ouvert, l’agent ne doit pas contourner le breaker en choisissant un outil équivalent ou en multipliant les conversations. La réponse dégradée doit distinguer une donnée fraîche, une donnée en cache et une absence de preuve.
Tracer la tentative, pas seulement la conversation
Un tableau de bord agrégé par conversation cache les retries. Émettez un événement par tentative avec les identifiants d’intention, d’appel et d’opération backend, ainsi que la couche de timeout et l’état du circuit.
let StartTime = datetime(2026-08-02T08:00:00Z);
let EndTime = datetime(2026-08-02T10:00:00Z);
AgentToolCallEvents
| where TimeGenerated between (StartTime .. EndTime)
| where Protocol == "mcp"
| summarize
calls=count(),
intents=dcount(IntentId),
retries=countif(Attempt > 1),
timeouts=countif(Result == "timeout"),
breakerRejects=countif(CircuitState == "open"),
p50DurationMs=percentile(DurationMs, 50),
p95DurationMs=percentile(DurationMs, 95),
backendOperations=dcountif(BackendOperationId, isnotempty(BackendOperationId))
by bin(TimeGenerated, 5m), ToolName, Backend, Result
| extend CallsPerIntent = todouble(calls) / iif(intents == 0, 1.0, todouble(intents))
| order by TimeGenerated asc Adaptez la table à votre pipeline. Le signal critique est le rapport entre intentions, tentatives et opérations backend. Une hausse de CallsPerIntent sans hausse de trafic utilisateur révèle une amplification interne. Pour une écriture idempotente, plusieurs tentatives peuvent exister, mais une seule opération backend doit être créée.
Tester les défaillances avant le trafic réel
La validation doit injecter lenteur, timeout, réponse tardive, indisponibilité et résultat ambigu. Elle couvre aussi le comportement de l’agent : explique-t-il l’état, s’arrête-t-il quand le budget est épuisé et respecte-t-il l’approbation ?
cases:
- id: read_transient_503
tool: get_deployment_job
fault: backend_503_then_success
expected:
attempts: 2
backend_operations: 0
answer_contains_fresh_state: true
- id: write_response_lost_after_accept
tool: restart_deployment_job
fault: response_timeout_after_backend_accept
expected:
automatic_retry: false
operation_lookup_by_idempotency_key: true
duplicate_operations: 0
- id: sustained_dependency_failure
tool: get_deployment_job
fault: backend_503_for_90s
expected:
circuit_opens: true
probe_concurrency: 1
write_fallback: false
- id: budget_exhausted
tool: get_deployment_job
fault: slow_response
expected:
extra_retry: false
timeout_layer_reported: true
correlation_id_returned: true Exécutez ces cas en environnement isolé, puis avec un canary de production sans effet métier. Vérifiez que l’arrêt de la requête annule réellement le traitement lorsque le backend le permet ; sinon, documentez qu’une réponse tardive reste possible et interdisez toute seconde écriture automatique.
Déployer par étapes et préparer le rollback
Commencez en shadow mode : calculez la décision de retry et de breaker sans l’appliquer. Comparez-la au comportement actuel, puis activez-la pour un seul outil read-only. Les outils d’écriture viennent ensuite, uniquement avec idempotence backend, approbation bornée et recherche d’état après timeout ambigu.
Le rollback porte sur la policy de résilience, pas sur les opérations métier déjà acceptées. Versionnez la configuration, gardez la précédente et prévoyez un kill switch qui bloque les écritures tout en conservant les diagnostics read-only. Si une opération a été créée, son annulation ou sa compensation suit le runbook du backend.
Conservez la nouvelle policy seulement si le canary confirme : un budget respecté, aucune amplification incontrôlée, un breaker observable, aucune opération dupliquée et une réponse agent exploitable en mode dégradé. Rollbackez si les appels légitimes sont coupés, si la latence se déplace vers une autre couche, si les retries augmentent la charge ou si l’état d’une écriture reste impossible à prouver.
Conclusion
La résilience d’un outil MCP ne se résume pas à ajouter trois retries. Elle relie le budget utilisateur aux timeouts internes, classe les effets de bord, exige l’idempotence des écritures, ouvre le circuit avant la cascade et conserve une trace par tentative.
La décision de production tient en une règle : activer la policy seulement si un test de panne prouve qu’une intention reste bornée dans le temps et ne crée qu’un effet métier explicable. Dans le cas contraire, revenez à la configuration précédente, bloquez les écritures concernées et corrigez d’abord le contrat d’outil ou l’observabilité.