Расскажу, как за несколько месяцев из простого пайплайна для решения багов с помощью 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 // лимит по времени}
Что это даёт:
-
Оркестратор остаётся с чистым контекстом. На нём только запрос, план и короткие итоги от агентов, а не простыня из десятков вызовов инструментов и размышлений.
-
Параллельность. Оркестратор может запустить сразу несколько агентов и дождаться их всех разом. Наример, пока один лезет в Kubernetes, второй смотрит GitLab, третий читает трейсы, четвёртый — тикет в Jira. Разбор инцидента, который раньше занимал 10–20 минут (одна проверка за другой), теперь укладывается примерно во время самой долгой из них (2–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 токенов в секунду).
Несколько вещей, которые в начале были неочевидны:
-
«Один большой ассистент с тремястами инструментов» — плохая идея. Несколько узкоспециализированных агентов с жёсткими лимитами надёжнее и дешевле. А если запускать их сразу несколько — длинный разбор инцидента превращается из «суммы всех проверок» во «время самой долгой».
-
Лимитов должно быть несколько уровней. Ограничивать каждого агента по отдельности мало — ассистент умеет идти вразнос «размазанно». Общий лимит на всю задачу закрывает этот случай.
-
MCP — вполне рабочий инструмент. Он уже экономит недели работы на интеграциях с разными системами.
-
Финальный ответ нужно проверять. Дешёвая сверка фактов с собранными данными повышает точность ответов
Дальше
Планирую серию статей. Возможные темы:
-
MCP multiplexer: код и устройство отдельной библиотеки.
-
Проверка ответа: как сверять факты с данными и не выдумывать.
-
Слежение за инфраструктурой.
Если что‑то из этого интересно — напишите в комментариях, расскажу подробнее о том, на что больше запросов.
ссылка на оригинал статьи https://habr.com/ru/articles/1061870/