Skip to content

RCA (causa raíz)

Cuando una alerta dispara, Ongrid lanza un worker LLM que conduce el agente ReAct con kernel de grafo en la persona incident-investigator, llama tools para recolectar evidencia, y escribe un informe estructurado de vuelta a investigation_reports.

El informe se renderiza en la página /alerts/incidents/:id de la SPA junto a la serie que está disparando — el SRE humano nunca tiene que empezar "desde un prompt en blanco".

HLD-013

El pipeline actual aterriza el modelo causal Phase 1+2 de HLD-013. El enfoque ingenuo "resume lo que disparó" (PR-2) fue reemplazado una vez quedó claro que los operadores querían el patient zero — el proceso / contenedor / línea específicos que iniciaron la cascada — no una recapitulación del texto de la alarma.

Ciclo de vida

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 es el seam público que llama alert.Usecase — ver usecase.go:301.

Gates

Tres gates filtran antes de que se lance un worker. Cada rechazo se persiste como una fila status=skipped para que la SPA muestre al operador una razón en vez de "no iniciado para siempre".

GateDefaultComportamiento al fallar
Suelo de severidad (Config.MinSeverity)warningSkip silencioso, no se escribe fila
Inflight en proceso (por incident_id)siempre onCoalesce silencioso
Tope de concurrencia (Config.MaxConcurrent)5Fila skipped: concurrency limit reached (N workers in flight)

El tope de concurrencia defiende los rate-limits del provider LLM y acota la RAM cuando 100 incidentes disparan a la vez. Los callers por encima del tope obtienen una fila skipped explícita, no una cola.

El worker

El runtime lanza un worker con 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",
})

El prompt lo renderiza renderAlertPrompt e incluye:

  • Metadata del incidente (regla, severidad, device_id, value, threshold, summary).
  • Una instrucción de arranque explícita: Start with correlate_incident to pull metrics + logs + traces + topology around the fire window.
  • Un presupuesto duro: máximo 10 llamadas de tool, debe empezar a escribir el informe en la llamada #7. Este presupuesto vive en el mensaje de usuario porque los modelos no-frontier (GLM, DeepSeek) siguen las restricciones del user-message con más fiabilidad que las del system-message — sin él, el tope MaxStep de eino se tocaba en cada otra ejecución.
  • Una directiva de locale que sobreescribe el idioma implícito de la persona con el locale de UI del operador (ver Modelos / Routing para cómo se propaga el locale).

Presupuesto de tools + salvage

El grafo ReAct de eino topa el total de pasos. Cuando un worker agota el tope sin escribir una respuesta final, el investigator rescata el trail parcial:

  1. MessageReader.ListMessages(sessionID, limit=100) jala cada turno.
  2. Los mensajes assistant + tool se concatenan en un markdown sintético "qué encontramos".
  3. El salvage se alimenta a través del mismo extractor Pass-2.
  4. El informe se marca como ready con una nota de baja confianza antepuesta: 工作器超出最大步数预算(exceeds max steps);以下为根据已收集工具结果的局部分析,置信度偏低。

Sin salvage el operador veía status=failed sin data útil — el worker típicamente había llamado a 10+ tools y reunido la respuesta, solo nunca escribió el turno de síntesis.

Extracción estructurada (Pass 2)

El mensaje final del assistant del worker es markdown — la SPA necesita campos estructurados. Una segunda llamada LLM barata extrae:

  • root_cause — TL;DR de un párrafo.
  • affected_window — cuándo empezó / paró el span del síntoma.
  • pinpointed_target — el proceso / contenedor / archivo específico que cambió.
  • related_alerts — incidentes co-disparando (vía RelatedAlertQuerier).
  • evidence — citas fuente con bullets (salida PromQL, líneas de log, etc.).
  • suggested_actions — próximos pasos ejecutables por el operador.
  • confidence + confidence_factors.
  • tool_call_count — leído de vuelta desde chat_messages para que la UI muestre cuántas tools invocó realmente el worker (no un 0 hardcoded).

Modelo + provider configurables vía Config.SummarizerProvider / Config.SummarizerModel. Timeout default 30s (prompt corto, respuesta corta, sin loop de tools).

Cuando el extractor no está cableado o erra, el fallback usa firstParagraphOneLine sobre el markdown del worker para rellenar root_cause y envía el markdown entero verbatim como findings_md.

Trampa de header negrita

firstParagraphOneLine (usecase.go:846) salta el scaffolding markdown puro (headings, dividers, títulos de sección completamente en negrita) para que root_cause se lea como una oración, no como **现象**. Un bug previo solo eliminaba el ** inicial y dejaba un par al final — arreglado en la misma función.

Re-trigger manual

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

ForceEnqueue corre la ruta manual:

  1. Detiene cualquier worker corriendo actualmente para este incidente (best-effort — advierte y continúa si el worker_id está obsoleto post-restart).
  2. Hard-delete la fila previa de investigation_reports (cubre filas soft-deleted de force-enqueues anteriores para que el índice único incident_id no rechace el siguiente Create).
  3. Libera el guard inflight.
  4. Llama a EnqueueWith con el locale parseado de Accept-Language para que el informe regenerado vuelva en el idioma de UI del operador.

El suelo de severidad sigue aplicando — triggers manuales en incidentes a nivel info devuelven errores severity below floor.

Backfill al boot

Instalaciones nuevas pegan el huevo y la gallina: la cadena RCA estructurada solo se cablea cuando al menos un provider LLM está configurado, pero los incidentes pueden disparar antes de que el operador añada un provider. BackfillUnstartedIncidents corre al boot, recorre ListIncidentsWithoutReport(since, limit) y re-encola todo lo que disparó en la ventana. Los gates normales siguen aplicando, así que los incidentes resueltos y por debajo del suelo se saltan.

Ver cmd/ongrid/main.go para el cableado — corre al arranque con una ventana de 24h.

Ver también

  • Alertas — qué dispara.
  • Topología — qué expone expand_topology para el recorrido de blast-radius del investigator.
  • Skillscorrelate_incident, get_incident_detail, query_promql, search_logs: las tools que llama el worker.
  • Modelos / Routing — cómo se resuelve el locale
    • provider por-investigación.
  • Modelos / Budget — el tope global de tokens por día que acota el costo de investigación.