Accueil / Articles

AegisOps : un agent SRE autonome doté de quatre couches de mémoire.

Comment un agent LangGraph ReAct résout les incidents Kubernetes, grâce à la mémoire épisodique pgvector, à des niveaux d’autonomie contrôlés par des politiques, à seize outils et à un suivi complet des audits.

Éditeur : REACTAPP.TOP12 min de lecture2723 mots

Sources: I Built an Autonomous SRE Agent That Resolves Production Incidents While I Sleep

Il est 3h14 du matin et PagerDuty est en ébullition. Prometheus a déclenché une alerte critique : le taux d’erreurs du service de paiement a bondi à 34 %, les pods meurent en raison d’OOMKills et la fenêtre de non-respect des SLA s’ouvre dans six minutes. Les options sont soit de se lever du lit pour se connecter par SSH à un cluster, soit de laisser un agent s’en occuper.

AegisOps est un agent SRE autonome conçu précisément pour de tels moments. Il diagnostique, agit et résout les incidents en production sans intervention humaine, tout en enregistrant chaque décision prise ainsi que les raisons qui la motivent. Ce n’est pas un chatbot intégré à kubectl : il dispose d’une véritable mémoire, de contrôles d’autonomie en plusieurs niveaux et d’un système permettant d’apprendre des incidents qu’il a déjà traités. L’objectif affiché n’a jamais été de remplacer l’ingénieur de garde, mais de lui fournir un intervenant inlassable qui connaît déjà les procédures, se souvient des dix derniers incidents et vérifie les politiques avant d’intervenir en production.

Pourquoi ReAct sur LangGraph

ReAct (Raisonnement plus Action) s’intègre naturellement à la réponse aux incidents. Une chaîne simple exécute les étapes de manière aveugle, tandis qu’un agent ReAct génère une pensée, choisit une action, observe ce qui s’est passé et itère. C’est l’habitude appropriée en cas de panne, où les questions utiles portent sur ce qui est connu, ce qui est observé et ce qu’il convient de tenter ensuite.

LangGraph offre une exécution de graphe à état. Chaque nœud a accès au AegisState complet, un TypedDict qui contient des messages, de la mémoire, des détails de l’incident, des décisions de politique, des appels d’outils et des résultats de vérification. Comme l’état est sauvegardé dans PostgreSQL après chaque étape, une exécution peut être pause, examinée et reprise.

Le graphe compte quatre nœuds, conçus délibérément de manière simple pour éviter une sur-ingénierie :

    • memory_node est traité en premier et charge tout ce dont l’agent aura besoin avant que toute réflexion ne commence.
  • agent_node représente la boucle de réflexion du LLM.
  • tools correspond au ToolNode de LangGraph, qui exécute ce que le modèle a demandé.
  • resolution_node clôture l’incident : il enregistre la mémoire, calcule le MTTR et envoie le message sur Slack.

Quatre niveaux de mémoire

La plupart des systèmes d’agents omettent l’architecture de mémoire ou se contentent de deux niveaux. Un SRE humain apporte quatre types de mémoire lors d’un incident, et AegisOps les reproduit tous.

  1. Mémoire épisodique (pgvector, similarité cosinus) stocke les incidents résolus par le passé sous forme d’embeddings. Lorsqu’un nouvel incident survient, les trois cas les plus similaires sont récupérés et intégrés dans le prompt du système, accompagnés de leur cause racine, des actions entreprises et du résultat obtenu.
  2. Mémoire sémantique (RAG sur une base de connaissances via pgvector) contient des manuels opérationnels fragmentés, des documents d’architecture et des analyses post-mortem. L’outil retrieve_runbook effectue des recherches sémantiques à leur sein, fournissant à l’agent les connaissances institutionnelles de l’équipe.
  • Mémoire procédurale (politiques YAML, moteur de règles et Redis) couvre les règles strictes définissant ce que l’agent peut faire à chaque niveau d’autonomie. Elles sont chargées depuis params.yml dans Redis au démarrage du graphe et évaluées avant toute action susceptible de modifier quelque chose. Considérez-la comme le réflexe acquis de l’agent pour des opérations sûres.
  • Mémoire de travail (l’état AegisOps et le point de contrôle LangGraph) gère l’état en cours d’exécution : messages, pensées actuelles, appels d’outils, résultats des vérifications de santé et comptages de tentatives. Le fait de créer des points de contrôle à chaque nœud permet au contexte de survivre aux redémarrages.
  • Mémoire épisodique en pratique

    Lors de l’entrée, memory_node appelle retrieve_similar_incidents en fournissant la liste des symptômes de l’incident actuel. Un encodeur de phrases à 768 dimensions incorpore ces symptômes, et la distance cosinus de pgvector les compare avec les données de la table IncidentMemory. Les incidents antérieurs dont la similarité dépasse le seuil de 0,72 sont formatés et intégrés dans le prompt du système, avant la première réflexion de l’agent.

    Après une résolution, resolution_node appelle save_incident_memory, permettant ainsi à l’agent d’enregistrer ses propres mémoires. L’incorporation des symptômes s’effectue en les associant à l’hypothèse de cause racine, de sorte que les incidents ultérieurs présentant des symptômes similaires retrouveront celui-ci.

    Une base de données vectorielle dédiée telle que Pinecone ou Weaviate était une option possible. Conserver tout dans PostgreSQL éliminait une dépendance en matière d’infrastructure et assurait une cohérence transactionnelle entre les enregistrements d’incidents et leurs embeddings. À cette échelle, pgvector est plus que suffisant.

    Niveaux d’autonomie de L0 à L3

    C’était la question de conception la plus difficile : comment donner à un agent un véritable pouvoir sur l’environnement de production sans risquer une catastrophe ? La réponse réside dans un modèle d’autonomie en couches dans lequel chaque action modificatrice est soumise à des règles de politique avant d’être exécutée.

    Chaque outil d’action (restart_deployment, scale_deployment, update_deployment_image) commence par une appel à retrieve_policy. Le moteur de politiques évalue l’action, le service, le degré de gravité ainsi que le niveau d’autonomie actuel par rapport aux règles contenues dans params.yml. Si la réponse est négative, l’outil renvoie immédiatement un refus et l’agent doit trouver une autre solution, généralement une escalade.

    Le champ requires_approval mérite une attention particulière. Même au niveau L3, certaines actions destructrices, comme le retrait d’une image sur un service de paiement critique, nécessitent encore une approbation humaine explicite. L’agent envoie un message sur Slack et attend. Cela rassure l’équipe de garde sur le fait que les opérations les plus risquées font intervenir un humain.

    Les seize outils

    AegisOps propose seize outils répartis en quatre catégories. Six d’entre eux se distinguent :

    1. get_logs est l’outil de collecte des preuves. Il prend les 500 dernières lignes du fichier de journal d’un service, conserve les niveaux demandés et supprime de chaque ligne les adresses IP, les UUID ainsi que les nombres simples. Deux occurrences provenant de hôtes différents sont donc considérées comme un seul motif. Un Counter de Python classe ces motifs et retourne ceux qui apparaissent le plus fréquemment. Les lecteurs de terminal ainsi que la boucle de raisonnement de l’agent reçoivent le même résultat structuré, sans aucune implication d’un cluster Elasticsearch, uniquement un fichier de journal et quelques lignes de code Python.
    2. get_pod_status demande à l’API CoreV1Api de Kubernetes les pods portant l’étiquette app={deployment} et indique les redémarrages, le niveau de prêt ainsi que l’état des conteneurs. Lorsque le cluster est inaccessible, il fonctionne en mode dégradé et envoie l’agent vers get_logs à la place.
  • restart_deployment s’occupe de la majeure partie du travail. La mise à jour de l’annotation kubectl.kubernetes.io/restartedAt déclenche un redémarrage progressif, puis attend que chaque réplique soit prête avant d’exécuter une vérification de santé. Une étape facultative de déconnexion empêche le trafic d’arriver pendant le redémarrage.
  • update_deployment_image constitue la solution de retrait d’urgence. Elle réécrit l’image du conteneur dans la spécification, attend que le déploiement soit terminé, puis vérifie l’état des pods. Étant la solution la plus puissante du kit d’outils, son utilisation en cas d’incidents critiques en production nécessite une autonomie de niveau L3.
  • check_service_health combine l’état de prêt des pods fourni par Kubernetes avec le taux d’erreurs en temps réel fourni par Prometheus. Elle renvoie True uniquement lorsque tous les pods sont prêts et que les réponses de type 5xx restent inférieures à 5 %.
  • retrieve_runbook recherche les runbooks, les analyses post-mortem et les notes d’architecture selon leur contenu, puis retourne les quatre meilleurs extraits présentant une similarité de 0,75 ou plus, qui sont affichés dans le prompt du système sous l’intitulé "RUNBOOK CONTEXT".
  • Un incident, du début à la fin

    Voici ce que l’agent a fait pour le problème OOMKill du service de paiement à 3h14 du matin :

    [MEMORY] Loading episodic context for payment-service...
    [MEMORY] Found 2 similar past incidents (similarity: 0.89, 0.76)
    [MEMORY] Loaded 3 procedural policies for payment-service/critical
    
    [THOUGHT] Incident: payment-service OOMKill, 3 pods CrashLooping.
    Past incident INC-2801 had identical symptoms — root cause was
    a memory leak in the gRPC connection pool after a deploy.
    Starting with get_logs to confirm before acting.
    
    [ACTION] Calling tool: get_logs with {'service_name': 'payment-service', 'log_level': 'error'}
    → top_errors: [{"pattern": "out of memory: kill process", "count": 47},
    {"pattern": "grpc connection pool exhausted", "count": 23}]
    
    [THOUGHT] OOM confirmed. gRPC pool exhaustion matches INC-2801 pattern.
    Retrieve runbook before acting — want the fix procedure.
    
    [ACTION] Calling tool: retrieve_runbook with {'query': 'payment service grpc oom memory leak fix'}
    → [runbooks / payment-service-oom.md] Recommended action: rolling restart
    resets connection pool. If recurs within 1h, rollback to previous image.
    
    [THOUGHT] Runbook confirms rolling restart. Checking policy for autonomy L2...
    
    [ACTION] Calling tool: restart_deployment with {'service': 'payment-service',
    'deployment': 'payment-worker', 'namespace': 'production',
    'severity': 'critical', 'autonomy_level': 'L2'}
    → policy: ALLOWED (L2 ≥ min_level L2 for restart_deployment)
    → draining pods... restarting... polling rollout...
    → health_check: healthy=True, pods_ready=3/3, error_rate=0.008
    
    [THOUGHT] Restart succeeded. Error rate 0.8% — below 5% threshold.
    Verifying with check_service_health before closing.
    
    [ACTION] Calling tool: check_service_health with {'service': 'payment-service',
    'deployment': 'payment-worker', 'namespace': 'production'}
    → healthy=True, pods_total=3, pods_ready=3, error_rate=0.007
    
    [THOUGHT] Service fully recovered. Writing audit log and resolving.
    
    [ACTION] Calling tool: write_audit_log ...
    → audit_log written: restart_deployment for incident INC-2847
    
    RESOLVED: payment-service recovered after rolling restart. Root cause: gRPC
    connection pool OOM — matched pattern from INC-2801. All 3 pods healthy,
    error rate 0.7%. MTTR: 187 seconds.
    
    [MEMORY] Saving incident INC-2847 to episodic memory...
    [NOTIFY] Sending resolution to Slack channel: #all-aegisops ✅
    

    Tout le processus a duré 187 secondes, du déclenchement de l’alerte à sa résolution en un peu plus de trois minutes, sans que personne ne soit réveillé. C’est le temps désormais atteint de manière régulière pour les incidents similaires aux précédents. L’agent a commencé par examiner ce qui avait fonctionné la dernière fois, comme le ferait un ingénieur senior, ce qui relève du fonctionnement de la mémoire épisodique.

    Cette trace a été reconstituée pour plus de clarté. Une exécution réelle sur une configuration locale modeste, avec un seul cluster Kind et pas de Prometheus, donne un résultat différent. L’incident a été déclenché par une simple appel webhook via PowerShell :

    Invoke-RestMethod -Uri "http://localhost:8000/webhook/incident" `
      -Method Post `
      -ContentType "application/json" `
      -Body '{
        "source": "webhook",
        "severity": "critical",
        "service": "payment-service",
        "symptoms": ["High 500 errors", "DB connection timeouts"],
        "autonomy_level": "L2"
      }'
    

    L’agent a exécuté le processus de bout en bout. Il a récupéré des incidents similaires et a émis l’hypothèse qu’une augmentation soudaine du trafic dépassait la capacité des pods, ce qui correspondait à un cas antérieur en mémoire. Il a appelé la fonction get_pod_status et a constaté que 2 des 5 pods étaient prêts ; il a alors augmenté le nombre de répliques à 4, vérifié la situation avec check_service_health et a clôturé l’incident en le marquant comme RESOLU. Un message Slack est arrivé dans le canal #all-aegisops et l’incident a été enregistré à nouveau dans la mémoire épisodique.

    C’est moins dramatique qu’une panne due à un manque de mémoire sous gRPC à 3 heures du matin, mais le cycle reste identique : le même cycle de Réflexion, Action, Observation, la même vérification des politiques avant l’ajustement et la même écriture dans la mémoire à la fin. Seuls les contextes diffèrent. L’agent ne peut pas distinguer un cluster de ordinateurs portables d’une flotte de 50 nœuds. En modifiant le kubeconfig et en orientant Prometheus vers des métriques réelles, le même code fonctionne sur le système réel.

    Slack en tant que canal de sortie

    Les actions importantes envoient un message sur Slack. Lorsqu’un incident est résolu, le nœud de résolution écrit dans #all-aegisops avec la couleur indiquant le niveau de gravité, le MTTR ainsi que le résumé complet de la résolution, ce qui signifie que l’ingénieur de garde se réveille face à une coche verte et non à une alerte par pager.

    La stack technique

    Tout cela fonctionne grâce à une stack modeste : PostgreSQL avec pgvector gère la mémoire épisodique et les points de contrôle de LangGraph. Redis stocke les politiques et est réservé à des fonctionnalités de limitation de débit futures. Prometheus fournit des taux d’erreur réels à check_service_health, tandis que Grafana propose des tableaux de bord.

    version: "3.9"
    
    services:
      postgres:
        image: ankane/pgvector:latest
        ports:
          - "5445:5432"
        environment:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: admin123
          POSTGRES_DB: aegisops
        volumes:
          - postgres_data:/var/lib/postgresql/data
    
      redis:
        image: redis:alpine
        ports:
          - "6380:6379"
    
      app:
        build: .
        ports:
          - "8000:8000"
        env_file: .env
        depends_on:
          - postgres
          - redis
    
      prometheus:
        image: prom/prometheus:latest
        ports:
          - "9090:9090"
        volumes:
          - ./infra/prometheus.yml:/etc/prometheus/prometheus.yml
    
      grafana:
        image: grafana/grafana:latest
        ports:
          - "3000:3000"
    
    volumes:
      postgres_data:
    

    L’application est basée sur FastAPI via uvicorn. Les incidents arrivent sur une endpoint webhook qui crée un IncidentEvent et appelle run_agent(). L’exécution est asynchrone, ce qui permet de traiter plusieurs incidents en parallèle sans blocage. Le client Kubernetes utilisé est kubernetes_asyncio, ce qui garantit que les appels au cluster ne bloquent pas l’exécution.

    Un point important : get_pod_status et check_service_health chargent la configuration Kubernetes au moment de l’import du module, et non à l’intérieur de la fonction asynchrone. Si le cluster n’est pas accessible au démarrage, ces fonctions échouent silencieusement et renvoient des messages d’avertissement ; assurez-vous donc que KUBECONFIG ou le compte de service du cluster est configuré avant le démarrage de l’application.

    Le journal d’audit

    Chaque appel d’outil est enregistré dans un TypedDict ToolInvocation : ce qui a été exécuté, avec quels paramètres, ce qui a été retourné, si l’opération a réussi, combien de temps elle a duré, quand elle a eu lieu, et quelle décision de politique l’a autorisée. Ces enregistrements s’accumulent dans state["tool_invocations"] au cours de l’exécution et sont sauvegardés dans la mémoire épisodique à la fin du processus. Un outil distinct write_audit_log génère une ligne JSON structurée pour chaque action notable.

    Grâce à cela, tout incident peut être reproduit ultérieurement : la séquence des étapes, les permissions en vigueur et la raison derrière chacune d’elles. C’est précisément ce type d’enregistrement que demandent les équipes de conformité.

    Un choix de conception influence tous les autres. Chaque action de mutation renvoie la décision de politique ainsi que le résultat du contrôle de santé, permettant ainsi à l’agent d’apprendre en même temps trois choses : si l’appel a fonctionné, s’il était autorisé et si le service est actuellement en bon état. Ces trois éléments influencent sa prochaine décision.

    Qu’est-ce qui manque encore

    Tout ce qui a été décrit fonctionne de bout en bout. Les progrès futurs étaient limités uniquement par une machine locale qui avait du mal à exécuter Ollama en même temps que le système complet. Avec plus de ressources, trois lacunes se font immédiatement sentir.

    Le flux d’approbation représente le problème le plus urgent. approval_status existe déjà dans l’état et requires_approval est déjà vérifié, pourtant rien ne s’arrête réellement. Le plan consiste à envoyer un bouton Slack, à interrompre le graphe et à le reprendre uniquement après qu’une personne ait cliqué, avant que toute action de niveau 3 ne soit exécutée. LangGraph peut suspendre un graphe en cours d’exécution grâce à ses points de contrôle, il ne reste donc plus qu’à organiser les flux logiques, sans avoir besoin de redessiner l’ensemble du système.

    La comparaison des métriques avant et après constitue le deuxième problème. check_service_health ne fournit qu’une capture d’écran à un instant donné après la correction. Ce dont l’agent a besoin, c’est d’une différence mesurée par Prometheus : le taux d’erreurs cinq minutes avant et cinq minutes après, afin d’être sûr que quelque chose a bien été corrigé et non simplement que la situation s’est améliorée grâce au hasard du moment.

    L’autogénération des guides opérationnels est l’objectif à long terme. Actuellement, les deux sources de connaissances, les incidents passés et les guides opérationnels, ne s’alimentent jamais l’une l’autre. Après une correction inhabituelle, l’agent devrait écrire un nouveau fragment de guide opérationnel et l’ajouter à la base de connaissances, afin que retrieve_runbook puisse le restituer la fois suivante. Cela permettrait de relier les deux niveaux de mémoire.

    Ce que le projet a appris

    Le problème majeur avec les agents d’infrastructure autonomes réside en fait non pas dans le raisonnement du modèle, mais dans la structure de sécurité qui l’entoure. Les niveaux de mémoire, un moteur de politiques, un journal d’audit et des mécanismes d’approbation sont ce qui incite une équipe à laisser un outil fonctionner seul à 3 heures du matin.

    AegisOps adopte une position claire : une machine à états structurée avec des limites de permissions bien définies est supérieure à un agent libre qui improvise. Cette approche s’est avérée efficace jusqu’à présent, grâce à un journal des incidents plus complet et à un agent qui s’améliore à chaque nouveau cas. Le système de points de contrôle dans LangGraph, la séparation nette entre outils de diagnostic et outils d’action, ainsi que l’utilisation de TypedDict pour représenter l’état du système facilitent son inspection, sa débogage et son audit en cas de problème, afin de comprendre pourquoi l’agent a choisi telle ou telle action.

    Si vous développez quelque chose de similaire, investissez tôt dans une architecture de mémoire adaptée. Un agent doté d’une bonne mémoire vaut dix fois plus qu’un agent disposant simplement d’un prompt légèrement amélioré. Les Transformers nécessitaient de l’attention, tandis que les agents dans le monde réel ont besoin de contexte.

    Lectures complémentaires

  • Choisir une architecture RAG : naïve, avancée, CRAG, GraphRAG, agente — Une comparaison pratique de six architectures RAG avec leurs avantages, inconvénients et cas d’usage, ainsi qu’un processus de décision en quatre étapes et les situations où RAG n’est pas l’outil adapté.
  • Notes pratiques : J’ai donné à un agent IA une URL JSON. Il a créé une entreprise fonctionnelle — Guide pas à pas de Notes pratiques : J’ai donné à un agent IA une URL JSON. Il a créé une entreprise fonctionnelle : contrats, vérifications et emplacements de code prêts à l’emploi pour les équipes souhaitant mettre en œuvre ce modèle.
  • Passer à Claude Opus 5.5 : cinq changements majeurs liés à l’ID du modèle — Un mécanisme de transition vers Claude-Opus-5-5 : une réflexion en mode permanent, un choix d’outil forcé, des blocs de réflexion liés au modèle, des outils d’utilisation informatique et un flux de progression silencieux.
  • De la version naïve au pipeline RAG en production pour les déclarations SEC 10-K : segmentation en chunks, recherche hybride — Pourquoi un pipeline RAG classique a échoué avec des documents 10-K de 300 pages, et les solutions : segmentation en chunks tenant compte des sections, recherche hybride, réclassement par encodage croisé, validation et citations de métadonnées.
  • Engineering d’Agent Harness : budgets, permissions et évaluations pour les agents en production — La plupart des agents échouent en production à cause d’un système de gestion insuffisant, et non du modèle lui-même. Découvrez le mécanisme de boucle, les niveaux de risque, les budgets, la superposition des contextes et les évaluations de sécurité qui permettent de protéger les agents.
  • Comment un agent IA pour corriger les documents s’est trompé lui-même : faux positifs dans un environnement de test Docker — Un agent LangGraph conçu pour réparer des codes README endommagés a d’abord proposé une solution inutile, puis a échoué avec les packages src-layout. Voici l’erreur de l’environnement de test ainsi que la vraie solution pour corriger les documents.
  • Créer un serveur MCP pour un journal de travail consultable en 190 lignes de TypeScript — Un petit serveur MCP basé sur des fichiers Markdown : quatre outils fonctionnant selon des intentions, des descriptions au style prompt, un client de démonstration stdio servant également de test, ainsi que des pièges réels à éviter.
  • Fonctionnement interne de RAG : segmentation en chunks, embeddings, recherche vectorielle et réclassement — Une explication pas à pas de la génération augmentée par la récupération d’informations : segmentation en chunks, embeddings, bases de données vectorielles, assemblage des prompts, réclassement, limites liées aux hallucinations, ainsi qu’une brève note sur KAG.