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
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 readyInvestigateAsync 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".
| Gate | Default | Comportamiento al fallar |
|---|---|---|
Suelo de severidad (Config.MinSeverity) | warning | Skip silencioso, no se escribe fila |
Inflight en proceso (por incident_id) | siempre on | Coalesce silencioso |
Tope de concurrencia (Config.MaxConcurrent) | 5 | Fila 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:
// 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:
MessageReader.ListMessages(sessionID, limit=100)jala cada turno.- Los mensajes assistant + tool se concatenan en un markdown sintético "qué encontramos".
- El salvage se alimenta a través del mismo extractor Pass-2.
- El informe se marca como
readycon 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íaRelatedAlertQuerier).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 desdechat_messagespara 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
POST /v1/alerts/incidents/{id}/investigation
Accept-Language: enForceEnqueue corre la ruta manual:
- Detiene cualquier worker corriendo actualmente para este incidente (best-effort — advierte y continúa si el worker_id está obsoleto post-restart).
- Hard-delete la fila previa de
investigation_reports(cubre filas soft-deleted de force-enqueues anteriores para que el índice únicoincident_idno rechace el siguiente Create). - Libera el guard inflight.
- Llama a
EnqueueWithcon el locale parseado deAccept-Languagepara 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_topologypara el recorrido de blast-radius del investigator. - Skills —
correlate_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.