Skip to content

RCA (корневая причина)

Когда срабатывает алерт, Ongrid порождает LLM-воркера, который ведёт graph-kernel ReAct-агента на персоне incident-investigator, вызывает инструменты для сбора доказательств и пишет структурированный отчёт обратно в investigation_reports.

Отчёт рендерится на странице SPA /alerts/incidents/:id рядом со срабатывающей серией — человеку-SRE никогда не приходится начинать «с пустого промпта».

HLD-013

Текущий конвейер приземляет Phase 1+2 причинной модели HLD-013. Наивный подход «суммировать что сработало» (PR-2) был заменён, как только стало ясно, что операторы хотели нулевого пациента — конкретный процесс / контейнер / строку, которая запустила каскад — а не пересказ текста алерта.

Lifecycle

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 — публичный шов, который вызывает alert.Usecase — см. usecase.go:301.

Гейты

Три гейта фильтруют до того, как воркер вообще порождается. Каждый отказ сохраняется как строка status=skipped, так что SPA показывает оператору причину вместо «не начато никогда».

ГейтПо умолчаниюПоведение при промахе
Severity floor (Config.MinSeverity)warningМолчаливый пропуск, строка не пишется
In-process inflight (per incident_id)всегда вклМолчаливое объединение
Concurrency cap (Config.MaxConcurrent)5Строка skipped: concurrency limit reached (N workers in flight)

Concurrency cap защищает rate-limit провайдера LLM и ограничивает RAM, когда 100 инцидентов срабатывают одновременно. Over-cap вызывающие получают явную skipped-строку, не очередь.

Воркер

Рантайм порождает воркера с 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",
})

Промпт рендерится renderAlertPrompt и включает:

  • Метаданные инцидента (rule, severity, device_id, value, threshold, summary).
  • Явную стартовую инструкцию: Start with correlate_incident to pull metrics + logs + traces + topology around the fire window.
  • Жёсткий бюджет: 10 вызовов инструментов максимум, должен начать писать отчёт к вызову #7. Этот бюджет живёт в user-сообщении, потому что не-frontier модели (GLM, DeepSeek) следуют user-message ограничениям надёжнее, чем system-message — без этого предел eino MaxStep попадался на каждом втором прогоне.
  • Locale-директива, которая переопределяет неявный язык персоны локалью UI оператора (см. Models / Routing, как локаль распространяется).

Бюджет инструментов + salvage

ReAct-граф eino ограничивает общие шаги. Когда воркер исчерпывает предел без написания финального ответа, investigator вытаскивает частичный след:

  1. MessageReader.ListMessages(sessionID, limit=100) тянет каждый ход.
  2. Сообщения assistant + tool конкатенируются в синтетический markdown «что мы нашли».
  3. Salvage пропускается через тот же Pass-2 extractor.
  4. Отчёт помечается ready с предваряющей low-confidence заметкой: 工作器超出最大步数预算(exceeds max steps);以下为根据已收集工具结果的局部分析,置信度偏低。

Без salvage оператор видел status=failed без полезных данных — воркер обычно вызвал 10+ инструментов и собрал ответ, он просто никогда не написал synthesis-ход.

Структурное извлечение (Pass 2)

Финальное assistant-сообщение воркера — markdown — SPA нужны структурированные поля. Второй, дешёвый LLM-вызов извлекает:

  • root_cause — однопараграфный TL;DR.
  • affected_window — когда span симптома начался / закончился.
  • pinpointed_target — конкретный процесс / контейнер / файл, который изменился.
  • related_alerts — co-firing инциденты (через RelatedAlertQuerier).
  • evidence — буллетированные исходные цитаты (вывод PromQL, лог-строки и т.д.).
  • suggested_actions — operator-runnable следующие шаги.
  • confidence + confidence_factors.
  • tool_call_count — считывается обратно из chat_messages, так что UI показывает, сколько инструментов воркер реально вызвал (не зашитый 0).

Настраиваемая модель + provider через Config.SummarizerProvider / Config.SummarizerModel. По умолчанию 30с таймаут (короткий промпт, короткий ответ, без tool-loop).

Когда extractor не подключён или ошибается, fallback использует firstParagraphOneLine поверх markdown воркера, чтобы заполнить root_cause и отгрузить весь markdown дословно как findings_md.

Ловушка bold-заголовка

firstParagraphOneLine (usecase.go:846) пропускает чистый markdown scaffolding (headings, dividers, полностью bold-заголовки секций), так что root_cause читается как предложение, а не как **现象**. Прежний баг срезал только ведущий ** и оставлял хвостовую пару — исправлено в той же функции.

Ручной re-trigger

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

ForceEnqueue запускает ручной путь:

  1. Остановить любой текущий воркер для этого инцидента (best-effort — предупреждает и продолжает, если worker_id устарел после рестарта).
  2. Hard-delete предыдущей строки investigation_reports (покрывает soft-deleted строки от ранних force-enqueue, чтобы уникальный индекс incident_id не отверг следующий Create).
  3. Освободить inflight guard.
  4. Вызвать EnqueueWith с локалью, распарсенной из Accept-Language, так что регенерированный отчёт приходит обратно на языке UI оператора.

Severity floor всё ещё применяется — ручные триггеры на info-level инцидентах возвращают ошибки severity below floor.

Boot-time backfill

Свежие установки попадают в курицу-и-яйцо: цепочка структурного RCA подключается только когда сконфигурирован хотя бы один LLM provider, но инциденты могут сработать до того, как оператор добавил provider. BackfillUnstartedIncidents запускается при загрузке, обходит ListIncidentsWithoutReport(since, limit) и переочередит всё, что сработало в окне. Нормальные гейты всё ещё применяются, так что resolved-инциденты и over-floor инциденты пропускаются.

См. cmd/ongrid/main.go для подключения — он запускается при старте с 24-часовым окном.

См. также

  • Алерты — что срабатывает.
  • Топология — что выставляет expand_topology для blast-radius walk investigator.
  • Skillscorrelate_incident, get_incident_detail, query_promql, search_logs: инструменты, которые вызывает воркер.
  • Models / Routing — как per-investigation locale + provider резолвится.
  • Models / Budget — глобальный per-day token cap, который ограничивает стоимость расследования.