golang

Как я собрал корпоративного AI‑ассистента в Mattermost для девопс‑задач

  • четверг, 23 июля 2026 г. в 00:00:11
https://habr.com/ru/articles/1061870/

Расскажу, как за несколько месяцев из простого пайплайна для решения багов с помощью LLM вырос полноценный AI‑ассистент, который живёт прямо в нашем корпоративном Mattermost, умеет сам ходить в десятки внешних систем (GitLab, Jira, Kubernetes, Prometheus и другие), и работает целиком внутри нашего контура — без обращений к внешним сервисам. Все запросы к моделям идут через LLM гейтвей за которым стоит несколько виртуалок с видеокартами для инференса. Сразу скажу что ассистент использовался при написании статьи.

С чего всё началось

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

Первая версия была простая: пайплайн, который шел по шагам:

получить тикет 
получить репу
проиндексировать репу в квадрант
найти релевантные куски кода
сгенерировать ответ

Со своей задачей худо‑бедно он справлялся, но хотелось большего — чтобы можно было что‑то уточнить или переспросить. А не попоробовать ли написать своего агента, подумал я.

Архитектура в общих чертах

Схема получилась довольно простая:

Пользователь в Mattermost
        │   (упоминание бота, личное сообщение, тред)
        ▼
Ассистент (Go)
        │
        ├──► LLM Gateway - свои виртуалки с видеокартами на которых крутятся модели
        │
        ├──► Postgres  - диалоги, история действий, права, статистика
        │
        ├──► Neo4j     - граф знаний: факты, связи между сущностями, человеческие знания
        │
        └──► Подключённые внешние системы через MCP:
                  GitLab, Jira, Kubernetes, Prometheus, Jaeger, OpenSearch,
                  чтение документов, работа в браузере, запуск задач и т.д.

Набор технологий: Go, Postgres, Neo4j и локальные модели, никаких внешних подписок. Всё крутится внутри корпоративного контура.

В основе — фреймворк eino от ByteDance. Если совсем коротко, он отвечает за reAct loop: модель решает, какой инструмент вызвать → инструмент вызывается → результат возвращается модели → и так пока задача не решена. Подробно про eino — это отдельная статья.

Все запросы к моделям — через один гейтвей

Начинал я с одной виртуалки с Ollama. Очень быстро выяснилось, что это неэффективно: ассистент думал над ответом по 10–15 минут — для живого диалога в чате это не годится.

Поэтому добавил вторую виртуалку и перешёл с Ollama на vLLM — на нем инференс гораздо быстрее. А чтобы не привязывать имя модели к конкретному серверу и не править настройки в нескольких местах, поставил перед виртуалками OpenAI‑совместимый гейтвей:

  • Ассистент обращается к стабильным именам моделей и не знает, на каком сервере они физически работают.

  • Гейтвей сам направляет запрос на свободную видеокарту.

  • Поменять модель или добавить железо теперь можно правкой конфига, без изменений в коде.

Два уровня: оркестратор и агенты

Главная идея в устройстве ассистента: не стоит сваливать всё на один разговор с моделью. Контекст быстро забивается, и вместе с ним плывёт внимание. Когда в одну инструкцию модели навалено три сотни тулзов и десяток правил, она начинает путаться даже на простых вопросах.

Поэтому в ассистенте два уровня:

  • оркестратор — его задача — понять запрос, разбить его на шаги и раздать их агентам. У него намеренно небольшой набор инструментов: запустить агента, сохранить что‑то в память, нарисовать схему и по мелочи дернуть пару тулзов.

  • агенты — создаются под конкретную подзадачу. Каждому даются только нужные мцп сервера (например, для задачи «найди в репозиториях функцию X» — только GitLab и поиск по коду) и жёсткие лимиты: сколько он может потратить времени и сколько раз вызвать инструменты. Когда лимит исчерпан, агент обязан коротко отчитаться, что успел сделать, и вернуть результат оркестратору.

Агентам могут быть назначены роли:

  • reader — быстро находит и достаёт данные, работает на лёгкой модели с небольшим лимитом;

  • investigator — копает глубоко и ищет первопричину, работает на тяжёлой модели с большим лимитом.

  • executor — запускает скрипты или cli тулзы в изолированном эфемерном поде в кубах

На каждую роль — свои бюджеты и системные промты.

В коде запуск агента выглядит примерно так (упрощённо):

type AgentSpawnRequest struct {
    Role           SubagentRole  // reader / investigator / executor
    Task           string        // что нужно сделать
    Servers        []string      // какие мцп сервера дать
    TokenBudget    int           // лимит токенов
    ToolCallBudget int           // лимит вызовов тулзов
    Timtout      time.Duration   // лимит по времени
}

Что это даёт:

  1. Оркестратор остаётся с чистым контекстом. На нём только запрос, план и короткие итоги от агентов, а не простыня из десятков вызовов инструментов и размышлений.

  2. Параллельность. Оркестратор может запустить сразу несколько агентов и дождаться их всех разом. Наример, пока один лезет в Kubernetes, второй смотрит GitLab, третий читает трейсы, четвёртый — тикет в Jira. Разбор инцидента, который раньше занимал 10–20 минут (одна проверка за другой), теперь укладывается примерно во время самой долгой из них (2–3 мин).

  3. Надёжность. Зависший или зациклившийся агент не тянет за собой всю задачу — его останавливает лимит.

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

Как я подключил десятки внешних систем

MCP (Model Context Protocol) — это придуманный Anthropic общий стандарт, по которому AI‑ассистент дёргает внешние инструменты. На данный момент уже появилось много готовых мцп серверов: к GitLab, к Jira, к Kubernetes и так далее. Но у каждого свой набор инструментов, своя авторизация и свой способ общения.

Сам по себе eino спокойно работает с любым числом MCP‑серверов — сколько подключишь, столько и будет. Проблема в другом: когда серверов больше десятка, а у каждого свой набор инструментов (часто однотипных например дев и прод гитлаб), вываливать их всех скопом в оркестратора и в каждого агента — значит забить им контекст сотнями тулзов. Модель в них путается, а контекст пухнет ещё до того, как дойдёт до самой задачи.

Поэтому я написал свой mcp‑multiplexer — прослойку, которая собирает все мцп сервера за единым интерфейсом и позволяет раздавать однотипные мцп без дублирования описания тулзов. Такой подход заодно позволяет внедрять before и after хуки, для проверки прав пользователя на тулзы, проверку результатов тулзов кодом и тд. Работает так:

  • При старте ассистент подключается ко всем мцп серверам сразу и собирает их тулзы в один общий каталог.

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

  • Агент получает только нужные мцп с единым форматом вызова тулзов.

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

Проверка ответа перед отправкой

Выдумывать факты — болезнь любой нейросети, не какой‑то конкретной. Свежий показательный случай: на вопрос «сколько подов в неймспейсе» ассистент посмотрел жсон на 178 килобайт и ответил 31 вместо 32 — среди четырёх похожих имён одно потерял. Финальный ответ собирается из того, что нашли тулзы агентов, и на этом последнем шаге модель может переврать число, перепутать имя или дописать факт, которого в данных не было.

Чтобы лечить это в принципе, а не по одному случаю, я добавил критика (llm‑judge). Перед тем как ответ уйдёт пользователю, он сверяет каждое число и каждый факт из черновика с тем, что реально нашли в этой задаче. Что противоречит данным — исправляется; что не подтверждается — помечается как непроверенное.

По сути это правило — числа и факты берём из данных, а не из головы модели, распространённое на все ответы. Случай «31 вместо 32» при работающем критике уже невозможен: реальный список лежит в данных, и расхождение всплывает на сверке.

Безопасность: три рубежа

Раз ассистент работает внутри кконтура и ходит на прод через мцп (GitLab с продовым кодом, прод‑кластер Kubernetes, Jira), значит безопасность должна быть обеспечена в первую очередь. Защита устроена как три рубежа подряд:

Первый — кто вообще может пользоваться. Проверяю, что пользователь имеет право обращаться к ассистенту.

Второй — кому что можно. На каждый инструмент заведены права. Инструменты делятся на «только чтение», «изменение» и «опасные» (удаление, выполнение команд). Изменяющие и опасные по умолчанию запрещены и требуют явного разрешения.

Третий — защита от подмены инструкций. Тут интереснее, и работает и на входе и на выходе:

  • На входе. Известные приёмы атак (вроде «забудь все предыдущие инструкции и…») заранее загружены в базу. Каждое входящее сообщение сверяется с ними по смыслу, и слишком похожее на атаку блокируется ещё до обращения к модели.

  • На выходе из внешних систем. То же для ответов сторонних сервисов. Если инструмент вернул что‑то подозрительно не по теме (например, кто‑то спрятал вредную инструкцию в описание тикета Jira), это обезвреживается до того, как попадёт обратно в модель.

Это не панацея, и я не утверждаю, что ассистент непробиваем. Но базовая защита от типовых атак есть, а любое нарушение правил записывается в историю для разбора.

Скилы: готовые сценарии под повторяющиеся задачи

Отдельная подсистема — скилы, как и в большинстве агенто — заранее описанные сценарии для частых задач:

  • Пользовательские скилы. Любой может создать или обновить свой скил в диалоге (!!skill-new, !!skill-run). Хранятся в зашифрованном виде.

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

  • Составные скилы позволяют собирать сложные сценарии из простых.

Что ещё внутри (коротко)

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

  • Работа с документами. Ассистент достаёт текст из PDF, Word, Excel, PowerPoint и текстовых файлов — можно кинуть ему документ и поспрашивать по нему вопросы.

  • Картинки. Приложил скриншот ошибки — он прогоняется через отдельную модель, которая видит изображения, и её описание подмешивается в запрос.

  • Схемы. Если в ответе модели появляется описание диаграммы, отдельный тул рисует из неё картинку и прикладывает к сообщению в Mattermost, но можно и просто попросить нарисовать схему чего либо.

  • Браузер. Агент умеет ходить в браузер — но только по заранее разрешённому списку сайтов (по умолчанию запрещены все).

  • Изолированный запуск. Часть инструментов (запуск чекалок для мров, выполнение скриптов) исполняется в изолированных эфемерных подах, а не внутри самого контейнера ассистента.

  • Поиск по коду. Специальный тул (греп аст, кстати тут на хабре про него писали) ищет по коду, вытаскивая сразу целые функции а не только пару строчек.

  • Память внутри задачи. Данные, добытые во время диалога, не теряются между сообщениями: их можно переиспользовать и не дёргать одно и то же дважды.

  • История действий. В Postgres пишется весь ход работы: входящее сообщение, каждый вызов инструмента, каждый ответ агента, реакции пользователей. Старые записи при уезжают в архив.

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

Что получилось в проде

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

  • Разобранные инциденты: несколько прод‑аварий разобрано с прямой помощью ассистента. Человек описывал симптомы и подсказывал подозрительные сервисы, а ассистент сам шёл по репозиториям, читал код, находил ошибку и предлагал, как чинить.

  • Время ответа: порядка 30 секунд на простые вопросы, до нескольких минут на сложные задачи с походами во внешние системы (120–160 токенов в секунду).

Несколько вещей, которые в начале были неочевидны:

  1. «Один большой ассистент с тремястами инструментов» — плохая идея. Несколько узкоспециализированных агентов с жёсткими лимитами надёжнее и дешевле. А если запускать их сразу несколько — длинный разбор инцидента превращается из «суммы всех проверок» во «время самой долгой».

  2. Лимитов должно быть несколько уровней. Ограничивать каждого агента по отдельности мало — ассистент умеет идти вразнос «размазанно». Общий лимит на всю задачу закрывает этот случай.

  3. MCP — вполне рабочий инструмент. Он уже экономит недели работы на интеграциях с разными системами.

  4. Финальный ответ нужно проверять. Дешёвая сверка фактов с собранными данными повышает точность ответов

Дальше

Планирую серию статей. Возможные темы:

  • MCP multiplexer: код и устройство отдельной библиотеки.

  • Проверка ответа: как сверять факты с данными и не выдумывать.

  • Слежение за инфраструктурой.

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