Прототипирование интерфейсов: Streamlit и Telegram Bot

Курс посвящен созданию пользовательских интерфейсов для ИИ-решений. Вы научитесь собирать веб-панели на Streamlit и чат-ботов в Telegram, интегрируя их с контейнеризированным FastAPI и мульти-агентными системами на LangGraph.

1. Streamlit для ИИ-инструментов: реактивность, управление состоянием и визуализация графов

Streamlit для ИИ-инструментов: реактивность, управление состоянием и визуализация графов

Бэкенд мульти-агентной системы, упакованный в Docker-контейнеры и предоставляющий доступ через FastAPI, — это надежный фундамент. Однако cURL-запросы или Swagger UI не подходят для тестирования продукта реальными пользователями. Чтобы проверить гипотезы, собрать качественный датасет для LangSmith и доказать финансовую эффективность (ROI) пайплайна, системе необходимо «лицо». Разработка полноценного фронтенда на React или Vue требует времени и смещения фокуса с архитектуры ИИ на веб-технологии. Streamlit решает эту задачу, позволяя развернуть интерактивный пользовательский интерфейс исключительно средствами Python.

Реактивная модель выполнения: цена простоты

Классические фронтенд-фреймворки работают на основе манипуляций с объектной моделью документа (DOM) и обновления отдельных компонентов при изменении данных. Streamlit использует радикально иной подход: при любом взаимодействии пользователя с интерфейсом (нажатие кнопки, ввод текста, изменение положения слайдера) весь Python-скрипт выполняется заново, сверху вниз, от первой до последней строки.

Эта императивная модель (Rerun) делает код линейным и предсказуемым, но создает критическую проблему при интеграции с тяжелыми ИИ-моделями. Если на двадцатой строке скрипта происходит синхронный HTTP-запрос к LLM-шлюзу, то изменение любого UI-элемента на десятой строке спровоцирует повторный вызов нейросети. В условиях, когда генерация ответа занимает секунды, а стоимость вычисляется на основе обработанных токенов, неконтролируемые перезапуски скрипта приведут к исчерпанию лимитов API, перегрузке GPU-воркеров и разрушению пользовательского опыта.

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

Управление состоянием через st.session_state

Единственный способ сохранить данные между неизбежными перезапусками скрипта — использовать встроенный словарь st.session_state. Это глобальное хранилище, привязанное к конкретной вкладке браузера пользователя.

В контексте чат-ботов и мульти-агентных систем состояние выполняет три ключевые функции:

  • Хранение истории диалога. Streamlit не запоминает, какие сообщения уже были отрисованы. Массив сообщений должен храниться в st.session_state и итеративно перерисовываться при каждом Rerun-цикле.
  • Идентификация потока (Thread ID). Для корректной работы механизма Checkpointer в LangGraph фронтенд должен сгенерировать уникальный идентификатор сессии (например, UUIDv4) при первом открытии страницы, сохранить его в состоянии и передавать в FastAPI при каждом POST-запросе.
  • Блокировка повторных вызовов. Введение флагов (например, is_generating), которые отключают элементы ввода на время ожидания ответа от сервера, предотвращая состояние гонки при многократном нажатии кнопки отправки.
  • Паттерн инициализации состояния всегда располагается в начале скрипта:

    После инициализации скрипт отрисовывает исторические сообщения с помощью контейнеров st.chat_message, а затем ожидает нового ввода через st.chat_input. Как только пользователь отправляет текст, скрипт добавляет его в массив st.session_state.messages и уходит на перезапуск, отображая обновленную историю.

    Интеграция с FastAPI: потребление Server-Sent Events

    Ранее спроектированный эндпоинт FastAPI возвращает данные не единым монолитным JSON-ответом, а в виде потока Server-Sent Events (SSE). Это критически важно для снижения метрики Time-to-First-Token (TTFT). Streamlit предоставляет нативный метод st.write_stream для элегантной обработки таких потоков.

    !Архитектура взаимодействия Streamlit и FastAPI

    Поскольку Streamlit работает в синхронном режиме (несмотря на поддержку некоторых асинхронных функций в последних версиях), для потребления потока целесообразно использовать библиотеку requests с параметром stream=True или синхронный клиент httpx.

    Чтобы Streamlit мог отрисовывать токены по мере их поступления, необходимо обернуть сетевой запрос в функцию-генератор. Генератор читает байтовые строки из HTTP-соединения, отсекает префиксы data: , парсит JSON и через yield возвращает извлеченные текстовые фрагменты (дельты).

    Метод st.write_stream принимает этот генератор, автоматически блокирует выполнение скрипта, отрисовывает каждый полученный фрагмент с эффектом печатной машинки и, по завершении потока, возвращает склеенную итоговую строку. Эту строку необходимо сразу же сохранить в st.session_state.messages, чтобы при следующем взаимодействии с UI сгенерированный ответ не исчез с экрана.

    !Анимация потоковой генерации токенов

    Визуализация внутреннего состояния графа

    Современные ИИ-инструменты — это не просто генераторы текста, а сложные конвейеры, принимающие решения. Если пользователь задает вопрос, требующий поиска в Qdrant, выполнения SQL-запроса и валидации ответа через LLM-as-a-Judge, интерфейс не должен оставаться статичным. Непрозрачность процессов вызывает недоверие: пользователь не понимает, зависла ли система или выполняет сложную аналитику.

    Поток событий astream_events, который генерирует LangGraph и транслирует FastAPI, содержит не только токены финального ответа, но и метаданные о запускаемых узлах (Nodes) и вызываемых инструментах (Tools). Фронтенд на Streamlit должен уметь парсить эти метаданные и визуализировать их.

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

    Логика обработки усложняется: генератор, читающий SSE-поток, должен различать типы событий. Если приходит событие типа on_tool_start, Streamlit обновляет заголовок статуса (например, «Поиск в базе знаний...»). Если приходит on_tool_end, внутри контейнера статуса можно отрисовать st.json с аргументами вызова или извлеченными из Qdrant документами. Это реализует паттерн наблюдаемости (Observability) на стороне клиента.

    Когда граф переходит к финальной генерации ответа (событие on_chat_model_stream от финального узла), статус переводится в состояние complete, контейнер сворачивается, и начинается стандартная потоковая отрисовка текста в основном окне чата.

    Такой подход требует использования st.empty() — контейнеров-заполнителей. Поскольку Streamlit рисует элементы последовательно, мы заранее резервируем место под статус выполнения и место под финальный ответ. По мере парсинга SSE-потока скрипт направляет данные в нужный заполнитель, не нарушая визуальную структуру страницы.

    Разработка интерфейса на Streamlit замыкает цикл создания MVP. Имея контейнеризированную базу данных, асинхронный API, распределенные очереди задач и реактивный фронтенд, система готова к передаче тестовой группе. Собранные через этот интерфейс диалоги лягут в основу датасетов для LangSmith, а метрики использования позволят оптимизировать конфигурацию Kubernetes-кластера перед полноценным релизом.

    2. Telegram Bot API: асинхронное взаимодействие, обработка медиафайлов и Middleware для мессенджеров

    Telegram Bot API: асинхронное взаимодействие, обработка медиафайлов и Middleware для мессенджеров

    Пользователь отправляет 50-страничный финансовый отчет в Telegram-бот и ждет ответа. Через три секунды тишины он отправляет файл снова, затем отправляет знак вопроса, а затем нажимает «Очистить историю». В этот момент на сервере три параллельных процесса пытаются векторизовать один и тот же документ, исчерпывая лимиты GPU и блокируя пул соединений с базой данных. Мессенджеры диктуют жесткие паттерны UX: отсутствие визуального индикатора загрузки провоцирует пользователя на повторные действия, а асинхронная природа чата требует от архитектуры абсолютной устойчивости к хаотичному потоку входящих событий.

    Создание интерфейса ИИ-продукта в Telegram кардинально отличается от веб-разработки (например, на Streamlit). Здесь нет прямого контроля над DOM-деревом, а взаимодействие строится на обмене сериализованными JSON-объектами через серверы Telegram. Для интеграции мульти-агентной системы в мессенджер требуется слой адаптации, который свяжет асинхронный фреймворк (FastAPI) с библиотекой маршрутизации обновлений (aiogram).

    Архитектура доставки: от Long Polling к Webhooks

    Существует два способа получения обновлений от серверов Telegram: Long Polling и Webhooks.

    При Long Polling приложение само непрерывно опрашивает сервер: «Есть ли новые сообщения?». Это приемлемо для локальной разработки за NAT, но в production-среде создает избыточную сетевую нагрузку и задержки.

    Архитектурный стандарт для высоконагруженных ИИ-систем — Webhooks. Сервер Telegram выступает инициатором соединения и отправляет POST-запрос с JSON-нагрузкой на заранее зарегистрированный публичный эндпоинт вашего FastAPI-приложения. Это превращает бота из изолированного демона в органичную часть существующего REST API, позволяя переиспользовать общий Event Loop, пулы соединений к PostgreSQL и клиенты для вызова LLM.

    Интеграция aiogram 3.x в FastAPI реализуется через прямую передачу входящего запроса в диспетчер:

    Ключевой момент этой архитектуры заключается в ответе сервера. Telegram требует, чтобы вебхук вернул статус HTTP 200 OK в течение короткого времени. Если ответ не получен, Telegram считает попытку неудачной и начинает повторять отправку того же самого сообщения с экспоненциально возрастающей задержкой.

    Управление ожиданием при длительном инференсе

    Возникает архитектурное противоречие: генерация ответа от локальной Llama 3 или выполнение сложного графа LangGraph может занимать 15–30 секунд (). Если блокировать обработчик вебхука на время работы нейросети, Telegram оборвет соединение и пришлет дубликат запроса.

    Решение заключается в немедленном подтверждении получения обновления и выносе инференса в фоновый процесс. В контексте FastAPI и aiogram это реализуется комбинацией возврата HTTP 200 и запуска задачи через Celery или BackgroundTasks.

    Чтобы пользователь не чувствовал, что бот «завис», используется метод send_chat_action. Он транслирует в интерфейс статус «печатает...» или «отправляет документ...». Поскольку статус сбрасывается сервером Telegram каждые 5 секунд, для длительных задач требуется запускать асинхронный цикл, который периодически обновляет этот статус, пока фоновая задача не вернет результат.

    Middleware в Telegram: контроль потока и инъекция зависимостей

    Как и FastAPI, aiogram поддерживает паттерн Middleware, но его реализация адаптирована под специфику мессенджера. В aiogram 3.x конвейер обработки разделен на два слоя: Outer Middleware (Внешнее) и Inner Middleware (Внутреннее).

    Outer Middleware срабатывает до того, как диспетчер найдет подходящий обработчик (хэндлер) для сообщения. Это идеальное место для глобальных проверок: фильтрации спама, Rate Limiting или блокировки забаненных пользователей. Если Outer Middleware прерывает цепочку, диспетчер даже не пытается анализировать текст сообщения.

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

    Инъекция сессии базы данных

    Вместо того чтобы открывать соединение с PostgreSQL внутри каждого обработчика, создается Inner Middleware, которое получает сессию из пула SQLAlchemy и пробрасывает ее в аргументы хэндлера.

    В обработчике эта зависимость принимается как обычный аргумент: async def my_handler(message: Message, db_session: AsyncSession). Это обеспечивает чистую архитектуру и гарантирует корректное закрытие транзакций даже при возникновении исключений в бизнес-логике.

    Защита от флуда (Throttling)

    Для защиты тяжелых узлов LangGraph от спама используется Outer Middleware. Оно фиксирует время последнего обращения пользователя (например, в Redis) и отклоняет запросы, если . В отличие от HTTP API, где при превышении лимита возвращается код 429, в Telegram бот должен либо проигнорировать сообщение, либо отправить мягкое уведомление («Пожалуйста, подождите 5 секунд»).

    Обработка медиафайлов: голос, документы и ограничения API

    Telegram — мощный канал для мультимодальных ИИ-систем. Пользователи могут отправлять голосовые сообщения для Speech-to-Text (STT) моделей или PDF-документы для RAG-пайплайнов. Однако работа с файлами в Telegram Bot API имеет жесткую специфику.

    Когда пользователь отправляет файл, бот получает не сами байты, а объект с метаданными и уникальным строковым идентификатором — file_id. Этот идентификатор привязан к конкретному боту.

    Процесс скачивания файла состоит из двух этапов:

  • Запрос к API getFile(file_id) возвращает объект File, содержащий относительный путь file_path на серверах Telegram.
  • Формирование прямого URL вида https://api.telegram.org/file/bot<TOKEN>/<file_path> и скачивание байтов через httpx или встроенные методы aiogram (bot.download).
  • Ограничения размера файлов

    Публичный Telegram Bot API накладывает строгие ограничения: бот может скачать файл размером не более 20 МБ и отправить файл не более 50 МБ. Для корпоративных RAG-систем, где загружаются тяжелые сканы или архивы, это становится блокирующим фактором.

    Решение — развертывание локального Telegram Bot API сервера. Это официальное open-source приложение от Telegram, которое вы поднимаете в своей инфраструктуре (например, рядом с FastAPI в Kubernetes). При переключении бота на локальный сервер лимиты расширяются до 2000 МБ на скачивание и отправку, а файлы могут сохраняться напрямую на примонтированный жесткий диск (Local API mode), что полностью исключает этап сетевого скачивания файла из облака Telegram в ваш контейнер.

    Интеграция голосовых сообщений

    Голосовые сообщения в Telegram приходят в формате OGG с кодеком Opus. Большинство локальных моделей транскрибации (например, Whisper) требуют формат WAV или MP3.

    Пример архитектурного пайплайна обработки голоса:

  • Хэндлер перехватывает content_types=[ContentType.VOICE].
  • Бот скачивает file_id в объект io.BytesIO (в оперативную память, чтобы не изнашивать диск контейнера).
  • Байты передаются в фоновую задачу Celery.
  • Воркер Celery использует утилиту ffmpeg (установленную в Docker-образе) для конвертации OGG в WAV.
  • WAV-файл передается в модель STT.
  • Полученный текст маршрутизируется в LangGraph как обычный текстовый запрос.
  • Отправка результатов работы ИИ (например, сгенерированных графиков или отчетов) происходит через объекты FSInputFile. Если ИИ-агент сгенерировал Excel-таблицу в памяти, ее не обязательно сохранять на диск перед отправкой — достаточно обернуть BytesIO буфер в BufferedInputFile и передать методу send_document.

    Интеграция Telegram-интерфейса требует восприятия мессенджера не просто как средства отображения текста, а как распределенной асинхронной системы. Использование вебхуков, гранулярное управление состоянием через Middleware и правильная оркестрация долгих вызовов LLM позволяют создать иллюзию мгновенного и бесшовного диалога с ИИ, скрывая под капотом сложную машинерию очередей и векторных баз данных.

    3. Интеграция фронтенда с LangGraph: потоковая передача событий, Human-in-the-Loop и WebSockets

    Интеграция фронтенда с LangGraph: потоковая передача событий, Human-in-the-Loop и WebSockets

    Когда ИИ-агент выполняет сложный запрос — например, анализирует финансовый отчет, пишет SQL-запрос, исправляет в нем синтаксическую ошибку и формирует финальный ответ — процесс может занимать от 10 до 45 секунд. Если в течение этого времени пользовательский интерфейс остается статичным, возникает иллюзия зависания системы. Проблема усугубляется, когда графовая логика требует вмешательства человека (Human-in-the-Loop) для подтверждения деструктивного действия. Интерфейс должен не просто отображать финальный текст, но и уметь визуализировать «внутренний монолог» агента, а также динамически менять свое состояние от режима чтения к режиму ввода.

    Архитектура двунаправленного потока: WebSockets против SSE

    Ранее для трансляции потока токенов от языковой модели мы использовали Server-Sent Events (SSE). Эта технология идеально подходит для однонаправленной передачи данных (от сервера к клиенту). Однако интеграция с LangGraph, особенно при использовании паттерна Human-in-the-Loop (HITL), меняет архитектурные требования.

    Граф, остановленный перед критическим узлом (например, execute_sql), ожидает внешнего сигнала для возобновления работы. Если использовать SSE, клиенту придется открывать отдельное HTTP-соединение (POST-запрос) для отправки команды resume. В распределенных системах это создает проблему маршрутизации: POST-запрос должен попасть на тот же воркер, который удерживает состояние графа в памяти, либо требовать сложной синхронизации через базу данных (AsyncPostgresSaver) и брокер сообщений.

    Протокол WebSockets решает эту задачу элегантнее, устанавливая полнодуплексное постоянное TCP-соединение. Сервер непрерывно транслирует события графа клиенту, а клиент может в любой момент отправить управляющую команду (например, отмену генерации или подтверждение действия) через тот же открытый сокет.

    Для стандартизации обмена сообщениями поверх WebSockets применяется подход, вдохновленный спецификацией JSON-RPC. Каждое сообщение имеет строгий контракт:

    Трансляция событий LangGraph в UI-контракт

    Метод astream_events генерирует десятки внутренних событий LangChain, многие из которых избыточны для конечного пользователя. Задача интеграционного слоя (FastAPI WebSocket эндпоинта) — отфильтровать этот поток и преобразовать его в понятные для фронтенда сигналы.

    Обычно выделяют три критических типа событий для визуализации:

  • Генерация текста (on_chat_model_stream): извлечение дельт токенов для создания эффекта печатающегося текста.
  • Вызов инструментов (on_tool_start / on_tool_end): сигнал фронтенду для рендеринга спиннера, прогресс-бара или раскрывающегося списка (accordion) с названием выполняемого действия.
  • Остановка графа (on_custom_event или перехват GraphInterrupt): сигнал о том, что граф перешел в состояние ожидания пользовательского ввода.
  • Когда WebSocket-маршрутизатор FastAPI получает событие on_tool_start, он отправляет клиенту JSON. Фронтенд (например, React или Vue.js) реагирует на event_name: "on_tool_start", временно скрывая курсор печати текста и отрисовывая бейдж «Ищу в базе данных...». Как только приходит on_tool_end, бейдж окрашивается в зеленый цвет, и UI снова готовится принимать текстовые токены.

    Реализация Human-in-the-Loop на стороне клиента

    Самый сложный этап интеграции — обработка паузы в выполнении графа. Когда LangGraph достигает узла, перед которым установлен interrupt_before, он выбрабливает специальное исключение или возвращает состояние __interrupt__.

    На стороне сервера WebSocket-обработчик перехватывает это состояние и отправляет клиенту управляющий пакет:

    Получив такой пакет, фронтенд кардинально меняет UI:

  • Блокирует основное поле ввода чата, чтобы пользователь не мог отправить новый независимый запрос.
  • Отрисовывает контекстный виджет. В случае с SQL-запросом это может быть редактор кода с подсветкой синтаксиса, куда подставляется строка "DROP TABLE users;", и две кнопки: «Выполнить» и «Отменить».
  • Пользователь редактирует запрос (например, меняет на SELECT) и нажимает «Выполнить».
  • Фронтенд формирует ответный WebSocket-пакет и отправляет его на сервер:
  • Сервер принимает пакет, распаковывает данные и вызывает метод aupdate_state вместе с объектом Command(resume=data), после чего возобновляет итерацию по astream_events, продолжая транслировать события в тот же сокет.

    Специфика интеграции: гибридный подход для Streamlit

    Несмотря на преимущества WebSockets, их прямая реализация в Streamlit сопряжена с архитектурными трудностями. Реактивная модель Streamlit подразумевает полный перезапуск (Rerun) Python-скрипта при любом взаимодействии пользователя с интерфейсом. Поддержание долгоживущего асинхронного WebSocket-соединения внутри синхронного цикла перерисовки требует сложных оберток над asyncio и фоновых потоков, что часто приводит к утечкам памяти.

    Поэтому для Streamlit-прототипов применяется гибридный архитектурный паттерн:

  • Чтение (Downstream): Streamlit вызывает REST API эндпоинт FastAPI, который возвращает StreamingResponse (SSE). Функция st.write_stream отлично справляется с потреблением таких потоков, блокируя интерфейс до завершения генерации.
  • Запись (Upstream) и HITL: Когда поток SSE завершается специальным сигналом [INTERRUPT], Streamlit сохраняет это состояние в st.session_state. При следующем Rerun интерфейс отрисовывает форму редактирования (например, st.text_area). При отправке формы Streamlit делает обычный синхронный POST-запрос к REST API с командой resume, после чего снова открывает SSE-соединение для чтения результатов.
  • Этот подход жертвует элегантностью единого сокета ради стабильности реактивного движка, позволяя использовать стандартные виджеты без конфликтов с Event Loop.

    Управление состоянием в Telegram: проблема Rate Limits и Debouncing

    Взаимодействие с LangGraph через Telegram Bot API требует решения совершенно иного класса проблем. Мессенджер не поддерживает WebSockets для клиентов; бот может только отправлять новые сообщения или редактировать существующие.

    Для имитации потоковой генерации текста бот должен использовать метод edit_message_text. Однако серверы Telegram применяют жесткие ограничения (Rate Limits) на частоту редактирования одного и того же сообщения. Если пытаться обновлять сообщение при получении каждого токена от LLM, Telegram вернет ошибку HTTP 429 Too Many Requests, и бот будет временно заблокирован.

    Для решения этой проблемы применяется алгоритм Debouncing (устранение дребезга) с буферизацией.

    Пусть — скорость генерации модели (например, 30 токенов в секунду), а — минимально разрешенный интервал между редактированиями сообщения в Telegram (безопасное значение секунды). Размер текстового буфера , который накапливается между сетевыми запросами к API Telegram, вычисляется как:

    Вместо отправки каждого токена, Middleware или фоновая задача бота накапливает токены в памяти. Запускается асинхронный таймер: каждые 1.5 секунды таймер проверяет, изменился ли буфер. Если да, вызывается edit_message_text с текущим накопленным текстом, и таймер сбрасывается.

    Особого внимания требует отображение вызовов инструментов (on_tool_start). В Telegram нет спиннеров или динамических компонентов. Оптимальный UX достигается за счет форматирования: когда инструмент запускается, бот редактирует сообщение, добавляя курсивный текст: _⚙️ Выполняю поиск по корпоративной базе..._

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

    Для реализации Human-in-the-Loop в Telegram используются InlineKeyboardMarkup (инлайн-кнопки). Когда LangGraph приостанавливается, бот отправляет сообщение с описанием требуемого действия и кнопками «Подтвердить» / «Отменить». Нажатие на кнопку генерирует событие CallbackQuery. Обработчик этого события в aiogram извлекает thread_id из контекста, формирует команду resume и отправляет ее в API оркестратора, после чего удаляет кнопки из сообщения, чтобы предотвратить повторное нажатие.

    Таким образом, независимо от конечной платформы — будь то полноценное веб-приложение на WebSockets, реактивный дашборд на Streamlit или бот в мессенджере — интеграционный слой должен выступать в роли адаптера. Он преобразует высокочастотный, гранулярный поток событий LangGraph в тот формат и темп, который способен безопасно и наглядно переварить конкретный пользовательский интерфейс.

    4. Тестирование UI, бизнес-метрики и безопасность: от оценки UX до настройки CORS и Rate Limiting

    Тестирование UI, бизнес-метрики и безопасность: от оценки UX до настройки CORS и Rate Limiting

    Идеально спроектированный мульти-агентный граф на базе LangGraph, развернутый в отказоустойчивом кластере Kubernetes, становится абсолютно бесполезным, если браузер блокирует запросы к API, а нетерпеливый пользователь обрушивает базу данных, многократно нажимая кнопку отправки в Telegram. Интерфейс — это узкое горлышко восприятия продукта. Именно на этом слое технические метрики задержки (TTFT) превращаются в пользовательский опыт (UX), а архитектурные уязвимости открывают путь для отказа в обслуживании (DoS).

    Разработка прототипов на Streamlit и Telegram требует специфического подхода к тестированию, сбору продуктовой аналитики и защите периметра, поскольку эти каналы взаимодействия обладают принципиально разной природой: Streamlit работает в браузере под ограничениями Same-Origin Policy, а Telegram Bot API представляет собой серверную интеграцию.

    Специфика тестирования ИИ-интерфейсов

    Классическое тестирование пользовательских интерфейсов (E2E, Selenium, Cypress) опирается на детерминированность: нажатие на кнопку должно привести к появлению строго определенного текста. В ИИ-системах генерация вероятностна, а потоковая передача токенов делает состояние интерфейса крайне динамичным.

    При тестировании Streamlit-прототипов фокус смещается с проверки точного текста на проверку структурных элементов и состояний загрузки. Фреймворк предоставляет встроенный модуль AppTest, который позволяет симулировать взаимодействие пользователя с приложением без открытия реального браузера.

    Вместо того чтобы проверять, ответила ли модель «Париж», тест проверяет жизненный цикл компонента:

  • Симуляция ввода текста в st.chat_input.
  • Проверка появления контейнера st.status (индикация вызова инструмента поиска).
  • Проверка изменения состояния st.status на «завершено».
  • Проверка появления нового блока st.chat_message("assistant").
  • Для интерфейсов на базе Telegram (aiogram) критическим аспектом тестирования является проверка конечных автоматов (FSM) и обработки конкурентных сообщений. Поскольку Telegram асинхронен, пользователь может отправить новое сообщение, пока предыдущее еще обрабатывается агентом. Тестирование Outer Middleware должно гарантировать, что такие перекрывающиеся запросы корректно блокируются или ставятся в очередь, а пользователь получает уведомление (например, через всплывающее окно answer_callback_query), а не игнорирование.

    Бизнес-метрики на уровне интерфейса

    Технические метрики бэкенда, такие как Tokens Per Second (TPS) или задержка базы данных, не отражают реального качества прототипа. На уровне UI необходимо измерять показатели, напрямую влияющие на экономику продукта (ROI) и коэффициент отклонения (Deflection Rate).

    Коэффициент отказа от ожидания (Abandonment Rate)

    Авторегрессионная генерация требует времени. Если пользователь закрывает вкладку Streamlit или отправляет команду /cancel в Telegram до завершения ответа, вычислительные ресурсы GPU были потрачены впустую (Padding Waste на уровне сессии).

    Коэффициент отказа вычисляется как отношение прерванных сессий к общему числу запросов:

    Высокий показатель часто свидетельствует не о медленной модели, а о плохом UX: отсутствии индикаторов прогресса (send_chat_action в Telegram или st.status в Streamlit). Если пользователь не видит, что агент выполняет поиск в Qdrant или вызывает SQL-инструмент, он воспринимает систему как зависшую.

    Время разрешения Human-in-the-Loop (HITL Resolution Time)

    Для систем с участием человека (Human-in-the-Loop) критически важной метрикой становится время, которое оператор тратит на принятие решения. Если граф LangGraph приостанавливается для валидации SQL-запроса, интерфейс должен предоставить оператору весь необходимый контекст максимально наглядно.

    Если стабильно превышает несколько минут, это означает, что UI Streamlit спроектирован неэффективно: оператору приходится вручную искать метаданные, переключаться между вкладками или читать сырые JSON-структуры вместо удобных таблиц. Оптимизация этого показателя напрямую снижает стоимость обработки одного тикета.

    Безопасность браузерных прототипов: CORS и Same-Origin Policy

    Когда прототип Streamlit разворачивается на порту 8501, а FastAPI работает на порту 8000, они формально находятся на разных источниках (Origins). Браузеры реализуют механизм Same-Origin Policy (SOP), который блокирует выполнение JavaScript-запросов к другому домену, порту или протоколу из соображений безопасности.

    Чтобы разрешить Streamlit-клиенту обращаться к API, бэкенд должен явно указать браузеру, что этот источник доверенный, используя механизм Cross-Origin Resource Sharing (CORS).

    Процесс начинается с предварительного запроса (Preflight Request). Перед отправкой основного POST-запроса с телом (например, с вопросом пользователя), браузер отправляет легковесный запрос методом OPTIONS. Сервер FastAPI должен ответить специальными заголовками:

  • Access-Control-Allow-Origin — список разрешенных доменов.
  • Access-Control-Allow-Methods — разрешенные HTTP-методы (POST, GET).
  • Access-Control-Allow-Headers — разрешенные заголовки (например, Authorization для JWT).
  • Частой ошибкой при прототипировании является использование allow_origins=[""]. Это допустимо для публичных API без аутентификации. Однако, если ИИ-агент работает с корпоративными базами данных и требует передачи токенов доступа, спецификация CORS запрещает использование в сочетании с заголовком Access-Control-Allow-Credentials: true. Браузер немедленно заблокирует такой запрос.

    В FastAPI необходимо строго задавать адреса интерфейсов:

    Параметр max_age указывает браузеру кэшировать результаты Preflight-запроса на 600 секунд, что снижает сетевую задержку, избавляя от необходимости отправлять OPTIONS перед каждым сообщением в чате.

    Важно понимать: CORS защищает браузерных клиентов (Streamlit, React). Telegram Bot API является серверным приложением, которое обращается к FastAPI напрямую. Для Telegram концепция CORS не имеет смысла, так как запросы инициируются не из браузера пользователя, а с серверов Telegram.

    Защита от истощения ресурсов: Rate Limiting на уровне каналов

    В предыдущих модулях рассматривалась защита очередей GPU на уровне бэкенда. Однако блокировка на уровне API означает, что тяжелый запрос уже прошел через сеть, был десериализован и дошел до контроллера. Эффективная защита должна начинаться на уровне интерфейса.

    В Telegram-ботах нетерпеливые пользователи часто отправляют одно и то же сообщение несколько раз, если ответ не приходит мгновенно. Это приводит к дублированию задач в Celery и исчерпанию пула соединений PostgreSQL.

    Реализация Rate Limiting в Telegram строится на базе Outer Middleware в фреймворке aiogram. Поскольку Outer Middleware перехватывает апдейт (Update) до его парсинга и маршрутизации, это идеальное место для отсечения спама.

    Идентификатором для лимитирования здесь выступает не IP-адрес (он всегда принадлежит серверам Telegram), а user_id. Используя скользящее окно (Sliding Window) в Redis, Middleware проверяет, сколько сообщений пользователь отправил за последнюю минуту. Если лимит превышен, Middleware выбрасывает исключение CancelHandler, которое тихо прерывает цепочку обработки внутри aiogram. Запрос даже не доходит до функции-обработчика и не инициирует HTTP-вызов к FastAPI.

    Для Streamlit ситуация иная. Поскольку это веб-приложение, идентификатором может выступать IP-адрес клиента или уникальный идентификатор сессии в st.session_state. Если Streamlit-сервер развернут за Reverse Proxy (например, Nginx или Traefik), ограничение частоты запросов эффективнее настроить именно там, защищая сам процесс Streamlit от перегрузки.

    Особого внимания требует защита эндпоинтов, работающих с WebSockets (используемых для потоковой передачи событий astream_events). В отличие от REST, где каждый запрос изолирован, WebSocket удерживает постоянное TCP-соединение. Злоумышленник может открыть тысячи соединений, исчерпав файловые дескрипторы сервера. Защита таких эндпоинтов требует лимитирования не только по частоте сообщений внутри сокета, но и по количеству одновременно открытых соединений с одного IP-адреса.

    Интеграция надежных барьеров на уровне UI позволяет гарантировать, что дорогостоящие вычислительные ресурсы GPU и сложных графов LangGraph будут тратиться исключительно на полезную работу (Goodput), а метрики UX будут отражать реальное взаимодействие с ИИ, а не борьбу пользователя с зависающим интерфейсом.