The Daily Newsstand · Free, Always
Tuesday, September 15, 2026

Как AI‑ассистент вырос в production‑систему: путь от NLU к оркестрации и надёжной обработке сообщений

Translate

В 2025 году Volga AI Assistant начинался как отдельный Telegram‑бот. Пользователь отправлял сообщение, NLU определял намерение, после чего бот возвращал подготовленный ответ.

Архитектура первой версии почти полностью повторяла этот сценарий:

Первая версия Volga AI Assistant: прямой путь от пользовательского сообщения к NLU и обратно.

Первая версия Volga AI Assistant: прямой путь от пользовательского сообщения к NLU и обратно.

Для первого прототипа такой схемы было достаточно. Она позволяла проверить работу NLU, собрать основные intents и понять, насколько автоматизация типовых обращений вообще применима в этом контексте.

Но в начале 2026 года я увидел у проекта более практическое применение.

У организации уже были собственные официальные каналы коммуникации, в том числе сообщения официального сообщества в VK. Пользователю нет особого смысла искать отдельного Telegram‑бота только ради того, чтобы задать вопрос организации. Гораздо естественнее, если ассистент работает непосредственно там, где этот диалог уже происходит.

С этого момента идея проекта изменилась.

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

И именно это решение постепенно потянуло за собой почти всю дальнейшую архитектуру.

Отдельный бот стал частью общего диалога

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

Можно было построить отдельную систему обращений: создавать ticket, передавать его сотруднику и фактически выводить пользователя из автоматического сценария. Я рассматривал такой вариант, но в новой версии сразу пошёл по другой модели: автоматическая и ручная поддержка должны быть двумя состояниями одного диалога.

Ручная поддержка как состояние того же диалога.

Ручная поддержка как состояние того же диалога.

В Assistant Mode новое сообщение обрабатывает система. При переходе в Manual Support история и сам диалог остаются теми же, но автоматический ответ блокируется и управление получает сотрудник.

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

Первая версия рабочего процесса сотрудника при этом была довольно простой. Отдельный service bot уведомлял о запросе ручной поддержки и давал ссылку на нужный диалог официального сообщества в VK.

Первая версия рабочего процесса сотрудника: состояние диалога уже переключается внутри системы, но работа продолжается через интерфейс VK.

Первая версия рабочего процесса сотрудника: состояние диалога уже переключается внутри системы, но работа продолжается через интерфейс VK.

Эта версия решала сам handoff, но вскоре стала заметна следующая проблема: состояние уже принадлежало Volga, а рабочее место сотрудника всё ещё оставалось снаружи. Именно отсюда появился следующий крупный этап проекта — собственная панель управления.

Панель управления изменила требования ко всей системе

Когда официальных обращений становится больше, переходить по ссылкам в отдельные интерфейсы платформ неудобно. Мне хотелось дать сотруднику единое рабочее место: очередь обращений, историю переписки, текущий режим диалога и возможность ответить пользователю, не покидая систему. Так появилась собственная Admin UI с интеграцией через VK Mini App.

Единая очередь обращений в панели управления Volga AI Assistant.

Единая очередь обращений в панели управления Volga AI Assistant.

На первый взгляд это обычная продуктовая доработка: вместо перехода в оф. сообщество VK сделать собственный интерфейс. На практике она изменила гораздо больше.

Пока сотрудник работает непосредственно во внешней платформе, именно её интерфейс фактически является местом, которому он доверяет. Если сообщение пришло в оф. сообщество VK, сотрудник его там увидит.

Но если основной рабочей поверхностью становится собственная панель, ситуация другая. Если пользователь отправил сообщение, а оно потерялось между webhook, NLU и сохранением в базе, сотрудник его в панели не увидит. Если сотрудник отправил ответ через панель управления, а внешний API завершился ошибкой, интерфейс не должен создавать впечатление, что сообщение уже успешно доставлено. Получалось, что собственной панели недостаточно просто отображать данные внешних каналов.

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

И здесь задача перестала быть только задачей интерфейса. Пришлось менять сам жизненный цикл сообщения.

Что значит «мы приняли сообщение»

До этого обработка могла выглядеть примерно так:

webhook → NLU → бизнес-логика → ответ → API платформы

Для небольшой версии приложения это нормальная схема. Но теперь возник более строгий вопрос:

в какой момент сообщение можно считать действительно принятым системой?

Если отвечать внешней платформе только после прохождения всей цепочки, успешность webhook начинает зависеть от NLU, AI‑провайдера, прикладной логики и других внешних сервисов. Я решил изменить сам контракт.

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

Так появился Inbox.

Inbox: сначала сохранить, потом обрабатывать

После получения webhook Channel Adapter приводит событие платформы к внутренней модели сообщения. Затем само сообщение и Inbox job записываются в PostgreSQL одной транзакцией. Только после успешного COMMITвнешний канал получает подтверждение.

Inbox отделяет факт надёжного приёма сообщения от дальнейшей обработки.

Inbox отделяет факт надёжного приёма сообщения от дальнейшей обработки.

После COMMIT сообщение уже является частью внутреннего состояния системы. Его может отобразить панель управления, а worker — обработать независимо от webhook‑запроса. Если Dialogflow или другой внешний сервис временно недоступен, это уже не влияет на сам факт получения обращения.

И здесь довольно естественно возник второй вопрос: если входящее сообщение сначала надёжно фиксируется внутри системы, что делать с ответом в обратную сторону?

Тот же принцип для исходящих сообщений

Сообщения из панели управления и автоматические ответы проходят один и тот же путь доставки.

Представим простую ситуацию: система сформировала ответ, записала его в PostgreSQL и затем должна отправить во внешний канал. Если процесс завершится между сохранением и API‑вызовом, в истории сообщение уже существует, но пользователь его ещё не получил.

Поэтому на исходящей стороне я применил тот же принцип, что и для Inbox: сначала зафиксировать намерение отправить сообщение внутри системы, а саму доставку выполнять отдельно. Ответ и Outbox job создаются в одной транзакции.

Outbox: исходящее сообщение сначала фиксируется внутри системы, после чего отдельный worker выполняет доставку.

Outbox: исходящее сообщение сначала фиксируется внутри системы, после чего отдельный worker выполняет доставку.

Здесь Channel Router не принимает никаких продуктовых решений. Он получает уже готовое исходящее сообщение и по платформе назначения выбирает зарегистрированный Channel Adapter.

Таким образом, Inbox и Outbox решают одну общую задачу с двух сторон:

платформа → Inbox → внутренняя обработка → Outbox → платформа

Для собственной панели это особенно важно. Она работает с внутренней историей диалога и не обязана полагаться на то, что каждый внешний API‑вызов завершится успешно с первой попытки.

Когда каналов стало несколько

Параллельно с развитием панели расширялись и сами каналы взаимодействия. После VK появился MAX, при этом сохранялась интеграция с Telegram. И здесь задача была уже не про надёжность, а про границы приложения.

Каждая платформа имеет собственный формат входящих событий, идентификаторы, вложения и API отправки. Если учитывать всё это непосредственно в основной логике, с каждым новым каналом она будет всё сильнее зависеть от конкретных платформ. Поэтому платформенные особенности были вынесены в Channel Adapters, которые раннее были видны в схемах, но сейчас разберу их подробнее.

Channel Adapters приводят события разных платформ к единой внутренней модели.

Channel Adapters приводят события разных платформ к единой внутренней модели.

На входе adapter проверяет и нормализует событие. На выходе тот же слой знает, как доставить подготовленное сообщение обратно через API конкретной платформы. Внутри же приложения сообщение остаётся сообщением диалога, независимо от того, через какой канал оно пришло.

Так появилась ещё одна полезная граница: платформа отвечает за транспорт, а состояние и обработка диалога принадлежат самой системе.

От NLU‑ответа к выполнению сценариев

В первой версии NLU практически напрямую определял результат:

message → intent → fulfillment → reply

Но по мере развития системы одного intent стало недостаточно.

После интерпретации запроса могло потребоваться изменить состояние диалога, выполнить прикладное действие, обратиться к отдельному AI‑модулю или перевести пользователя в Manual Support.

Здесь я решил разделить две ответственности. Dialogflow ES занимается интерпретацией запроса и возвращает intentaction, параметры и при необходимости fulfillment. А уже приложение решает, как выполнить этот action. И для этого появился Action Dispatcher.

Dialogflow интерпретирует запрос, а Action Dispatcher выбирает способ его выполнения.

Dialogflow интерпретирует запрос, а Action Dispatcher выбирает способ его выполнения.

Это разделение оказалось полезным ещё до активного использования LLM.

NLU не должен знать, каким образом внутри backend реализован конкретный бизнес‑сценарий. Он только сообщает приложению результат интерпретации.

А когда позже появились AI‑модули, для них уже существовало естественное место в архитектуре.

Где в этой схеме оказался AI

В августе 2026 года систему начали дополнять отдельные AI‑сценарии: свободный fallback, работа с управляемой базой знаний и подбор мероприятий. При этом я не стал менять существующую архитектуру вокруг LLM. Если Action Dispatcher уже умеет выбирать способ выполнения запроса, AI можно встроить как один из этих способов.

AI-модули являются одним из исполнителей внутри существующей оркестрации.

AI‑модули являются одним из исполнителей внутри существующей оркестрации.

Такое разделение позволило не передавать модели решения, которые обычный код уже может принять однозначно.

Если нужно изменить режим диалога — меняется состояние. Если нужно выполнить известный action — работает прикладная логика. LLM вызывается там, где действительно требуется работа со свободным естественным языком или недетерминированный поиск ответа.

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

Во что в итоге сложился путь сообщения

Упрощённый жизненный цикл сообщения в текущей архитектуре.

Упрощённый жизненный цикл сообщения в текущей архитектуре.

Здесь я намеренно оставляю за пределами схемы часть production‑механизмов: Privacy Gateway, retry‑политики, дедупликацию, quotas и некоторые детали AI‑контекста. Они важны технически, но основная эволюция системы хорошо видна и без них.

Сначала архитектура, потом стек

В итоге проект использует Python и FastAPI для backend, PostgreSQL для долговечного состояния, Redis для временных данных, Dialogflow ES для NLU, Next.js и React для операторской панели. Отдельные AI‑сценарии работают через внешние LLM‑провайдеры. Но гораздо лучше стек объясняется через ответственность компонентов, чем через список технологий.

PostgreSQL хранит сообщения, состояния диалогов и Inbox/Outbox. Redis используется для временных механизмов вроде locks, quotas и sessions. Dialogflow интерпретирует пользовательский запрос. Action Dispatcher связывает этот результат с прикладным сценарием. Channel Adapters изолируют внешние платформы. Получается, что технологии подбирались уже под появляющиеся архитектурные границы, а не наоборот.

Вместо заключения

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

NLU отвечает за интерпретацию. Action Dispatcher — за выбор сценария. LLM используется в тех сценариях, где действительно нужна. Сотрудник подключается внутри того же диалога. А Inbox/Outbox позволяют внутренней системе сохранять согласованную историю независимо от временных проблем внешних сервисов.

Именно такой переход — от небольшого NLU‑бота к системе с собственной оркестрацией и надёжной обработкой сообщений — для меня и стал главным результатом развития Volga AI Assistant.

Более подробное описание текущей архитектуры, reliability‑механизмов, privacy‑контура и отдельных компонентов проекта собрано в техническом case study на GitHub.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.