Skip to main content
Glama

ML On-Call Agent

«Почему вчерашний прогон регрессировал?» — мультиагентная система, которая отвечает на этот вопрос, сопоставляя отчёт о дрейфе, eval-прогон и журнал деплоя, и цитирует каждое своё утверждение.

Доступно через MCP, поэтому инструменты работают из любого MCP-клиента.

Статус: 65 тестов. Диагноз вычисляется детерминированно на основе взвешенных свидетельств, поэтому каждое число ниже — это утверждение, а не демонстрация.


Настоящая проблема

ML-система деградирует незаметно. Когда кто-то наконец замечает, улики разбросаны по трём местам, которые не общаются друг с другом:

  • отчёт о дрейфе — сдвинулось ли распределение входных данных?

  • eval-прогон — ухудшились ли офлайн-метрики, и на каких срезах?

  • журнал деплоя — выпускал ли кто-нибудь что-нибудь?

Сопоставление их — это работа, и люди делают её плохо в 3 часа ночи, потому что это значит держать в голове три JSON-файла одновременно.

Эти три артефакта не выдуманы для этого репозитория. Это то, что уже производят model-drift-monitor, llm-eval-pipeline и ai-code-review-bot. Читатель был написан на основе реального артефакта, а не документации.


Related MCP server: RAG Regression Gate

Результат

python -m oncall.cli evaluate
 scenario         truth           verdict          conf  margin  steps  action
 healthy          healthy         healthy          0.57     2.0      9  no action
 data_shift       data_shift      data_shift       0.71     8.0      9  retrain on recent data
 bad_deploy       bad_deploy      bad_deploy       0.66     8.5      9  roll back
 concept_drift    concept_drift   concept_drift    0.81     9.0      9  retrain with fresh labels
 pipeline_break   pipeline_break  pipeline_break   0.62     5.0      9  fix the upstream pipeline
 flaky_eval       noise           noise            0.62     3.0      9  rerun the evaluation

task success    : 100%
action accuracy : 100%
mean steps      : 9.0

Истина известна по построению, поэтому агент оценивается, а не восхищается. «Агент выдал правдоподобный отчёт об инциденте» — это не результат: правдоподобие — это то, что делают языковые модели, в том числе когда свидетельства ничего не подтверждают.

Сценарий, который является сутью

concept_drift: входные данные статистически идентичны, ничего не было задеплоено, а ранжирование модели инвертировалось (AUC 0.729 → 0.326, хуже случайного).

Не на что указать. Ответ получается из комбинации двух отрицаний и одного положительного — нет дрейфа, нет деплоя, ранжирование рухнуло — именно то, что упустило бы резюме любого отдельного артефакта. Это также слепое пятно, которое model-drift-monitor документирует о себе, диагностированное на уровень выше.


Ключевое проектное решение

Диагноз детерминирован. Языковая модель только повествует.

Очевидная реализация — вставить три JSON-файла в промпт и спросить, что пошло не так. Это каждый раз даёт связный текст — в том числе когда свидетельства ничего не подтверждают — и это нетестируемо, потому что нельзя делать утверждения о прозе или отличить правильный ответ от случайного.

Поэтому рассуждение — это обычный Python:

  • каждый специалист читает один артефакт и выдаёт Finding-и

  • каждый finding несёт цитату — файл, поле, значение. Finding требует её, поэтому утверждение без источника невозможно сконструировать

  • каждый finding указывает, какие корневые причины он поддерживает, а какие исключает

  • diagnose() суммирует веса — чистая функция, покрытая юнит-тестами против истины

| finding                                                    | source                     |
|------------------------------------------------------------|----------------------------|
| input drift is 'none': the scored population is             | `drift:severity=none`      |
| statistically the same as training                          |                            |
| roc_auc is 0.326 - WORSE THAN RANDOM. The model's ranking   | `evals:metrics.roc_auc     |
| has inverted, which is a changed relationship               |  =0.326`                   |
| 1 change(s) landed but none touch model behaviour           | `changes:changes[].files`  |

test_the_narration_does_not_change_the_diagnosis утверждает, что вердикт идентичен с моделью и без неё. Если он когда-нибудь упадёт, значит, модель начала выполнять рассуждение — и рассуждение перестаёт быть тестируемым в тот момент, когда это происходит.

Отрицательные свидетельства — это источник большей части диагностической силы. «Нет дрейфа» и «ничего не было задеплоено» — это findings с весами. LLM, резюмирующая JSON дрейфа, пропустила бы их, потому что ничего не произошло.


Граф

   SUPERVISOR ──► drift ──┐
        ▲   ├──► evals ───┤   one specialist per artifact, each consulted once
        │   └──► changes ─┤
        │                 ▼
        │             DIAGNOSE          sum the weighted findings
        │                 ▼
        └──────────────  CRITIC         "is this conclusion supported?"
           (bounded)      ▼
                        REPORT

Два свойства, которых нет у линейного конвейера:

Критик может отправить работу обратно. Если вердикт опирается на два артефакта, а третий так и не был прочитан, управление возвращается супервизору, а не публикуется вывод, сделанный на основе половины свидетельств.

Цикл ограничен. MAX_REVISIONS = 2 ограничивает его, а recursion_limit ловит всё, что вырвалось. Агент, который может зациклиться, — это агент, который может зациклиться навсегда, а вышедший из-под контроля on-call агент генерирует страницы вместо того, чтобы отвечать на них.

Он работает без LLM и без API-ключа — супервизор маршрутизирует по простому правилу Python — поэтому маршрутизация, делегирование и обратная связь критика покрыты юнит-тестами офлайн. test_the_graph_and_the_plain_loop_agree утверждает, что прогон LangGraph и простой цикл for достигают идентичных вердиктов на каждом сценарии, что доказывает: граф добавляет оркестрацию, а не рассуждение.


Сколько на самом деле стоит каждый артефакт

python -m oncall.cli ablate

свидетельства

успех задачи

точность действий

все три

100%

100%

без дрейфа

67%

67%

без eval

67%

67%

без изменений

100%

100%

Честный отрицательный результат, и он зафиксирован, чтобы его нельзя было тихо забыть. Удаление журнала изменений ничего не стоит на этом наборе сценариев — bad_deploy уже отделим по свидетельствам дрейфа и eval. Журнал изменений оправдывает своё место тем, что называет коммит — это то, что нужно человеку для действий, а не изменением диагноза.

test_the_change_log_currently_changes_no_verdicts фиксирует это. Если будущий сценарий сделает его критически важным, тест упадёт, и эту таблицу придётся изменить. Именно для этого и существует утверждение.

Абляция — единственный способ обнаружить, что источник, на интеграцию которого вы потратили неделю, не меняет ни одного вердикта.

И когда артефакт пропадает

Джобы падают, бакеты пусты, пути меняются. Интересный вопрос не в том, ответит ли агент — он ответит — а в том, заметит ли он:

  missing drift     -> verdict bad_deploy   flagged=True
  missing evals     -> verdict bad_deploy   flagged=True
  missing changes   -> verdict bad_deploy   flagged=True

Агент, который тихо диагностирует по двум из трёх файлов, хуже того, кто отказывается, потому что никто не знает, что ему нельзя доверять.


MCP-сервер

python -m oncall.mcp_server                 # stdio, for Claude Desktop et al
python -m oncall.mcp_server --http --port 8931

Семь инструментов — list_incidents, get_drift_report, get_eval_run, get_changes, investigate_incident, compare_incidents, list_specialists — плюс ресурс и шаблонный ресурс.

Инструменты возвращают свидетельства, а не прозу. Каждый возвращает артефакт или структурированный диагноз, поэтому модель клиента рассуждает над данными с цитатами, а не над абзацем, который кто-то уже резюмировал. Резюме — это место, где умирают детали.

Написано на mcp 2.0.0, который является ломающим переписыванием

Стоит упомянуть, потому что почти всё опубликованное — это 1.x и не работает. Каждый из этих пунктов был проверен запуском:

1.x

2.0.0

from mcp.server.fastmcp import FastMCP

исчезло — ModuleNotFoundError. Используйте MCPServer из mcp.server.mcpserver

@server.list_tools() / @server.call_tool()

исчезло — низкоуровневый Server принимает колбэки в конструкторе

stdio_client + ClientSession вручную

Client(server_or_url_or_transport)

tool.inputSchema

tool.input_schema — snake_case на модели, camelCase на проводе

Ещё два, которые стоили реального времени:

  • @server.tool() должен вызываться. Голый @server.tool вызывает TypeError, который об этом говорит.

  • Голый -> dict вообще не даёт structuredContent. Текстовый контент всё ещё там, поэтому в чат-клиенте выглядит нормально, но молча ломает любой клиент, читающий структурированное поле. Каждый инструмент здесь возвращает параметризованный дженерик (Dict[str, Any]) по этой причине, и есть тест.

Тестирование протокольного сервера без подпроцесса

Client(server) принимает объект сервера напрямую, поэтому весь протокольный цикл выполняется в процессе — без подпроцесса, без порта, без флаки-тестов из-за них. Именно эта возможность — единственная причина, по которой протокольные тесты здесь дёшевы.


Быстрый старт

git clone https://github.com/kanishqtanwar35-hub/ml-oncall-agent
cd ml-oncall-agent
pip install -r requirements.txt
export PYTHONPATH=src

python -m oncall.cli incidents                  # what can be investigated
python -m oncall.cli investigate concept_drift  # the full report
python -m oncall.cli evaluate                   # score it against truth
python -m oncall.cli ablate                     # what each artifact is worth
pytest -q                                       # 65 tests

Укажите на реальные артефакты:

python -m oncall.cli write ./artifacts --scenario bad_deploy
# then: Evidence.load("./artifacts") reads a real pipeline's output unchanged

Всё работает без API-ключа. investigate --narrate добавляет написанный моделью вступительный абзац, если задан GEMINI_API_KEY, и больше ничего не меняет.


Баги, о которых стоит прочитать

Жёстко заданный допуск проглотил случай с флаки-eval. regressions() использовал порог 0.02, поэтому сдвиг на 0.006 никогда не регистрировался, и проверка шума не запускалась — агент говорил healthy, тогда как истина была noise. Это ровно та ошибка фольклорного порога, против которой выступает model-drift-monitor, воспроизведённая на один репозиторий дальше. Обнаружение и значимость — два разных вопроса: нижний порог теперь 0.005 («всё, что показал бы дашборд»), а различение выполняет измеренный самим харнессом noise_std.

Тест восстановления не мог упасть. Он имитировал пропущенного специалиста, заранее заполнив consulted — но критик сравнивает доступные с проконсультированными, поэтому пометка чего-то как проконсультированного скрывала разрыв от самой проверки, которую тестировали. Он сообщал recovered: False и выглядел как сломанный критик. Ошибку нужно внедрять в маршрутизацию, а не в учёт; build_graph(skip_first_pass=...) делает это, и критик теперь демонстративно ловит разрыв и отправляет супервизора обратно.


Ограничения, изложенные прямо

  • Сценарии синтетические. Намеренно — реальные инциденты не приходят с размеченной корневой причиной, именно поэтому их диагностика сложна. Числа характеризуют метод, а не какую-либо продакшн-систему.

  • Шесть сценариев — это маленький набор. 100% на шести — это не 100% в общем случае, и следующий добавленный сценарий скорее сломает его, чем подтвердит.

  • Калибровка здесь неизмерима. Без неправильных ответов не с чем сравнивать уверенность. CLI печатает n/a и объясняет почему, а не выдумывает цифру — test_calibration_is_honestly_unmeasurable_here фиксирует это.

  • Веса заданы вручную. Они кодируют моё суждение о том, сколько стоят свидетельства, и больший размеченный набор инцидентов позволил бы подогнать их вместо этого — с отложенной выборкой, потому что подгонка на шести сценариях была бы запоминанием.

  • Точность выбора инструментов тривиально равна 1.0 при трёх всегда присутствующих артефактах. Метрика здесь потому, что она начинает иметь значение на четвёртом инструменте, а добавление её задним числом — это способ обнаружить, что агент месяцами вызывал всё подряд.

  • Нет живых интеграций. Он читает артефакты с диска. Подключение к реальному хранилищу, CI-системе и git-хосту — это работа по деплою, а не по рассуждению.

  • Критик проверяет полноту и поддержку, а не корректность. Он не может сказать вам, что веса неверны, только что свидетельства были скудными.

Дорожная карта

  1. Больший размеченный набор инцидентов и подгонка весов на отложенной выборке.

  2. Больше специалистов — задержка обслуживания, учёт затрат, свежесть feature-store — именно здесь точность выбора инструментов начинает что-то значить.

  3. Подключение MCP-сервера к реальным источникам артефактов вместо генератора сценариев.

  4. Корреляция нескольких инцидентов: одновременная регрессия трёх сервисов — это один инцидент, а не три.

Лицензия

MIT.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    MCP server that diagnoses RAG pipeline regression by detecting which failure modes (e.g., retrieval miss, hallucination) significantly regressed, not just score drops, using statistical methods to avoid false alarms.
    -
  • A
    license
    B
    quality
    D
    maintenance
    MCP server for achieving peak inference performance by detecting drift between code and runtime behavior, comparing against benchmarks, and providing optimization templates.
    8
    5 npm
    Apache 2.0