Skip to content

RCA (cause racine)

Quand une alerte part, Ongrid spawne un worker LLM qui pilote l'agent ReAct à graph kernel sur la persona incident-investigator, appelle des outils pour rassembler des preuves, et écrit un rapport structuré en retour dans investigation_reports.

Le rapport se rend sur la page /alerts/incidents/:id de la SPA à côté de la série en feu — le SRE humain ne doit jamais commencer « depuis un prompt vide ».

HLD-013

Le pipeline actuel atterrit les Phase 1+2 du modèle causal de HLD-013. L'approche naïve « résume ce qui est parti » (PR-2) a été remplacée une fois qu'il est devenu clair que les opérateurs voulaient le patient zéro — le processus / conteneur / ligne spécifique qui a démarré la cascade — pas un résumé du texte d'alarme.

Cycle de vie

text
incident.fire (isNew=true)
    └─ alert.Usecase.RecordFiring
        └─ Investigator.InvestigateAsync(incident)
            └─ Enqueue → Gate 1-3 (severity, inflight, semaphore)
                └─ repo.Create(pending row)   ← UI shows "investigating…"
                └─ go run(reportID, incident, dedupKey, locale)
                    ├─ spawner.SpawnWorker(incident-investigator persona)
                    ├─ worker drives the ReAct loop with tools
                    ├─ Pass-2 structured extraction (cheap model)
                    └─ repo.MarkReady(report fields)  ← UI shows ready

InvestigateAsync est le seam public qu'appelle alert.Usecase — voir usecase.go:301.

Gates

Trois gates filtrent avant qu'un worker ne soit jamais spawné. Chaque rejet est persisté comme ligne status=skipped pour que la SPA montre à l'opérateur une raison plutôt que « pas démarré pour toujours ».

GateDéfautComportement en miss
Plancher de sévérité (Config.MinSeverity)warningSkip silencieux, aucune ligne écrite
Inflight in-process (par incident_id)toujours actifCoalesce silencieux
Plafond de concurrence (Config.MaxConcurrent)5Ligne skipped: concurrency limit reached (N workers in flight)

Le plafond de concurrence défend les rate-limits du provider LLM et borne la RAM quand 100 incidents partent en même temps. Les appels au-dessus du plafond reçoivent une ligne skipped explicite, pas une file.

Le worker

Le runtime spawne un worker avec chatruntime.SpawnRequest :

go
// internal/manager/biz/alert/investigator/usecase.go:571
worker, err := uc.spawner.SpawnWorker(ctx, chatruntime.SpawnRequest{
    AgentName:   uc.cfg.AgentName, // "incident-investigator"
    Prompt:      prompt,
    Background:  false,
    SessionKind: "investigation",
})

Le prompt est rendu par renderAlertPrompt et inclut :

  • Métadonnées d'incident (rule, severity, device_id, value, threshold, summary).
  • Une instruction de démarrage explicite : Start with correlate_incident to pull metrics + logs + traces + topology around the fire window.
  • Un budget dur : 10 appels d'outils max, doit commencer à écrire le rapport à l'appel #7. Ce budget vit dans le message utilisateur parce que les modèles non-frontière (GLM, DeepSeek) suivent les contraintes du message utilisateur plus fiablement que celles du message système — sans, le plafond MaxStep d'eino était atteint un run sur deux.
  • Une directive de locale qui surcharge la langue implicite de la persona avec la locale d'UI de l'opérateur (voir Modèles / Routing pour comment la locale propage).

Budget d'outils + salvage

Le graphe ReAct d'eino plafonne les étapes totales. Quand un worker épuise le plafond sans écrire une réponse finale, l'investigator récupère la piste partielle :

  1. MessageReader.ListMessages(sessionID, limit=100) tire chaque tour.
  2. Les messages assistant + outil sont concaténés en un markdown synthétique « ce qu'on a trouvé ».
  3. Le salvage passe par le même extracteur Pass-2.
  4. Le rapport est marqué ready avec une note de faible confiance préfixée : 工作器超出最大步数预算(exceeds max steps);以下为根据已收集工具结果的局部分析,置信度偏低。

Sans salvage, l'opérateur voyait status=failed sans données utiles — le worker avait typiquement appelé 10+ outils et rassemblé la réponse, il n'avait juste jamais écrit le tour de synthèse.

Extraction structurée (Pass 2)

Le message d'assistant final du worker est du markdown — la SPA a besoin de champs structurés. Un second appel LLM bon marché extrait :

  • root_cause — TL;DR d'un paragraphe.
  • affected_window — quand le span de symptôme a-t-il démarré / arrêté.
  • pinpointed_target — le processus / conteneur / fichier spécifique qui a changé.
  • related_alerts — incidents co-firants (via RelatedAlertQuerier).
  • evidence — citations sources à puces (sortie PromQL, lignes de logs, etc.).
  • suggested_actions — prochaines étapes exécutables par opérateur.
  • confidence + confidence_factors.
  • tool_call_count — relu depuis chat_messages pour que l'UI montre combien d'outils le worker a réellement invoqué (pas un 0 hardcodé).

Modèle + provider configurables via Config.SummarizerProvider / Config.SummarizerModel. Timeout de 30s par défaut (prompt court, réponse courte, pas de boucle d'outils).

Quand l'extracteur n'est pas câblé ou erre, le fallback utilise firstParagraphOneLine sur le markdown du worker pour remplir root_cause et livre tout le markdown verbatim comme findings_md.

Piège du bold-header

firstParagraphOneLine (usecase.go:846) saute l'échafaudage markdown pur (titres, séparateurs, titres de section entièrement en gras) pour que root_cause se lise comme une phrase, pas comme **现象**. Un bug précédent ne retirait que le ** initial et laissait une paire en fin — corrigé dans la même fonction.

Re-déclenchement manuel

http
POST /v1/alerts/incidents/{id}/investigation
Accept-Language: en

ForceEnqueue exécute le chemin manuel :

  1. Stoppe tout worker en cours pour cet incident (best-effort — warn et continue si le worker_id est stale post-restart).
  2. Hard-delete la ligne investigation_reports précédente (couvre les lignes soft-deletées d'anciens force-enqueues pour que l'index unique incident_id ne rejette pas le prochain Create).
  3. Relâche le garde d'inflight.
  4. Appelle EnqueueWith avec la locale parsée depuis Accept-Language pour que le rapport régénéré revienne dans la langue d'UI de l'opérateur.

Le plancher de sévérité s'applique toujours — les déclenchements manuels sur des incidents de niveau info retournent des erreurs severity below floor.

Backfill au démarrage

Les installs fraîches touchent un problème de l'œuf et de la poule : la chaîne RCA structurée ne se câble que lorsqu'au moins un provider LLM est configuré, mais des incidents peuvent partir avant que l'opérateur n'ajoute un provider. BackfillUnstartedIncidents tourne au démarrage, parcourt ListIncidentsWithoutReport(since, limit), et ré-enqueue tout ce qui est parti dans la fenêtre. Les gates normaux s'appliquent toujours, donc les incidents résolus et au-dessus du plancher sont sautés.

Voir cmd/ongrid/main.go pour le câblage — il tourne au démarrage avec une fenêtre de 24h.

Voir aussi

  • Alertes — ce qui part.
  • Topologie — ce que expand_topology expose pour la marche de blast-radius de l'investigator.
  • Skillscorrelate_incident, get_incident_detail, query_promql, search_logs : les outils que le worker appelle.
  • Modèles / Routing — comment la locale + provider par investigation est résolue.
  • Modèles / Budget — le plafond global de tokens par jour qui borne le coût d'investigation.