Skip to content

RCA (causa raiz)

Quando um alerta dispara, o Ongrid dá spawn em um worker LLM que guia o agent ReAct de graph-kernel na persona incident-investigator, chama tools para coletar evidências, e escreve um report estruturado de volta em investigation_reports.

O report renderiza na página /alerts/incidents/:id do SPA ao lado da série disparando — o SRE humano nunca precisa começar "do prompt em branco".

HLD-013

O pipeline atual entrega Phase 1+2 do modelo causal do HLD-013. A abordagem ingênua "resumir o que disparou" (PR-2) foi substituída quando ficou claro que os operadores queriam o paciente zero — o processo / container / linha específico que iniciou a cascata — não um resumo do texto do alarme.

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 é a costura pública que alert.Usecase chama — veja usecase.go:301.

Gates

Três gates filtram antes mesmo de um worker ser spawned. Cada rejeição é persistida como uma linha status=skipped para que o SPA mostre ao operador uma razão em vez de "nunca iniciado".

GatePadrãoComportamento em miss
Severity floor (Config.MinSeverity)warningSkip silencioso, nenhuma linha escrita
In-process inflight (por incident_id)sempre onCoalesce silencioso
Cap de concorrência (Config.MaxConcurrent)5Linha skipped: concurrency limit reached (N workers in flight)

O cap de concorrência defende rate-limits do provider LLM e limita RAM quando 100 incidentes disparam ao mesmo tempo. Callers acima do cap recebem uma linha skipped explícita, não uma fila.

O worker

O runtime dá spawn em um worker com 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",
})

O prompt é renderizado por renderAlertPrompt e inclui:

  • Metadados do incidente (rule, severity, device_id, value, threshold, summary).
  • Uma instrução de início explícita: Start with correlate_incident to pull metrics + logs + traces + topology around the fire window.
  • Um budget rígido: máximo 10 chamadas de tool, precisa começar a escrever o report até a chamada #7. Esse budget vive na mensagem do usuário porque modelos non-frontier (GLM, DeepSeek) seguem restrições de user-message mais confiavelmente que de system-message — sem ele, o cap MaxStep do eino era atingido a cada run.
  • Uma diretiva de locale que sobrescreve o idioma implícito da persona com a locale da UI do operador (veja Modelos / Roteamento para como a locale se propaga).

Budget de tool + salvage

O grafo ReAct do eino limita o total de passos. Quando um worker esgota o cap sem escrever uma resposta final, o investigator salvaguarda a trilha parcial:

  1. MessageReader.ListMessages(sessionID, limit=100) puxa cada turn.
  2. Mensagens de assistant + tool são concatenadas em um markdown sintético "o que achamos".
  3. O salvage é alimentado pelo mesmo extractor Pass-2.
  4. O report é marcado ready com uma nota de baixa confiança prefixada: 工作器超出最大步数预算(exceeds max steps);以下为根据已收集工具结果的局部分析,置信度偏低。

Sem salvage o operador via status=failed sem dado útil — o worker tipicamente tinha chamado 10+ tools e reunido a resposta, só nunca tinha escrito o turn de síntese.

Extração estruturada (Pass 2)

A mensagem final assistant do worker é markdown — o SPA precisa de campos estruturados. Uma segunda chamada LLM barata extrai:

  • root_cause — TL;DR de um parágrafo.
  • affected_window — quando o span do sintoma começou / parou.
  • pinpointed_target — o processo / container / arquivo específico que mudou.
  • related_alerts — incidentes co-disparando (via RelatedAlertQuerier).
  • evidence — bullets com citações de fonte (saída PromQL, linhas de log, etc.).
  • suggested_actions — próximos passos rodáveis pelo operador.
  • confidence + confidence_factors.
  • tool_call_count — lido de volta de chat_messages para que a UI mostre quantas tools o worker realmente invocou (não um hardcoded 0).

Modelo + provider configuráveis via Config.SummarizerProvider / Config.SummarizerModel. Timeout padrão 30s (prompt curto, resposta curta, sem loop de tool).

Quando o extractor não está conectado ou erra, o fallback usa firstParagraphOneLine sobre o markdown do worker para preencher root_cause e envia o markdown inteiro verbatim como findings_md.

Armadilha do bold-header

firstParagraphOneLine (usecase.go:846) pula scaffolding markdown puro (headings, dividers, títulos de seção totalmente em negrito) para que root_cause leia como uma frase, não como **现象**. Um bug anterior só removia o ** da frente e deixava o par final — corrigido na mesma função.

Re-trigger manual

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

ForceEnqueue roda o caminho manual:

  1. Para qualquer worker rodando atualmente para esse incidente (best-effort — avisa e continua se o worker_id estiver stale pós-restart).
  2. Hard-delete da linha investigation_reports anterior (cobre linhas soft-deletadas de force-enqueues anteriores para que o índice unique incident_id não rejeite o próximo Create).
  3. Libera o guard de inflight.
  4. Chama EnqueueWith com a locale parseada de Accept-Language para que o report regenerado volte no idioma da UI do operador.

O severity floor ainda se aplica — triggers manuais em incidentes nível info retornam erros severity below floor.

Backfill em boot

Instalações novas batem em um chicken-and-egg: a cadeia RCA estruturada só conecta quando pelo menos um provider LLM está configurado, mas incidentes podem disparar antes do operador adicionar um provider. BackfillUnstartedIncidents roda no boot, percorre ListIncidentsWithoutReport(since, limit), e re-enqueua qualquer coisa que disparou na janela. Os gates normais continuam se aplicando, então incidentes resolvidos e acima do floor são pulados.

Veja cmd/ongrid/main.go para o wiring — roda no startup com uma janela de 24 horas.

Veja também

  • Alertas — o que dispara.
  • Topologia — o que expand_topology expõe para o walk de blast radius do investigator.
  • Skillscorrelate_incident, get_incident_detail, query_promql, search_logs: as tools que o worker chama.
  • Modelos / Roteamento — como o locale + provider por-investigação é resolvido.
  • Modelos / Budget — o cap global de tokens por dia que limita o custo de investigação.