Streamlit или Gradio: что выбрать для ИИ-приложения — вопрос модели событий и типа пользовательской работы. Аналитический экран с таблицами и фильтрами удобно собирать на Streamlit: его цикл перерисовывает скрипт целиком. Для интерфейса одной типизированной функции или модели с отдельными событиями начните с пилота на Gradio. Постоянный production-доступ — отдельное решение о хостинге и безопасности для обоих вариантов.
Аналитическое приложение и интерфейс функции: где проходит граница
Развилка видна на уровне задачи, которую решает пользователь. Аналитический экран предполагает таблицы, фильтры, несколько связанных виджетов и постоянную перерисовку сводки при изменении любого параметра. Интерфейс функции или модели предполагает один вызов: пользователь подаёт вход, получает результат, при необходимости меняет один параметр и запускает снова.
Документация Streamlit описывает базовый цикл так: по умолчанию, когда на экране что-то должно обновиться, Streamlit перезапускает весь Python-скрипт сверху вниз (docs.streamlit.io). Такая модель хорошо ложится на дашборд: любое изменение фильтра пересобирает всю картину, и разработчику не нужно вручную связывать отдельные блоки. Для интерфейса одной функции этот же цикл означает, что при каждом действии заново исполняется весь файл, включая код, не относящийся к текущему вызову.
Документация Gradio описывает пакет как открытый Python-пакет для быстрой сборки демо или веб-приложения под модель машинного обучения, API или произвольную Python-функцию (gradio.app). Класс Interface рассчитан на демо для моделей, которые принимают один или несколько входов и возвращают один или несколько выходов (gradio.app). Класс gr.Blocks даёт низкоуровневый подход к вёрстке с более настраиваемыми макетами и потоками данных (gradio.app).
Практический критерий прост. Если пользователь проводит в приложении десять минут, меняет фильтры и смотрит, как перестраивается сводка, — пилот на Streamlit. Если пользователь приходит за одним результатом и уходит, — пилот на Gradio. Матрица решения с результатом выглядит так:
| Тип работы пользователя | Первый пилот | Почему |
|---|---|---|
| Аналитический экран: таблицы, фильтры, сводки | Streamlit | Перерисовка всего скрипта пересобирает связанные блоки |
| Интерфейс одной типизированной функции или модели | Gradio | Явные inputs/outputs и отдельные события |
| Постоянный production-доступ | Отдельное решение | Хостинг и безопасность выбираются для обоих вариантов |
Граница проходит по числу связанных элементов на экране и по тому, сколько раз пользователь меняет вход. Аналитический экран живёт за счёт повторных перерисовок. Интерфейс функции живёт за счёт одного намеренного запуска.
Rerun, Session State и caching в Streamlit
Три механизма Streamlit решают разные задачи, и путать их дорого. Rerun — это модель исполнения. Session State — это память между перезапусками внутри сессии пользователя. Caching — это переиспользование тяжёлых объектов между перезапусками.
Rerun запускается при любом обновлении экрана. Документация Streamlit уточняет порядок: когда виджету передан колбэк через параметр on_change или on_click, колбэк всегда выполняется раньше остальной части скрипта (docs.streamlit.io). Это важно для валидации: проверку ввода можно повесить на колбэк и получить её результат до того, как отработает основная логика.
Session State хранит значения, которые должны пережить перезапуск. Документация Streamlit сообщает: каждый виджет с ключом автоматически добавляется в Session State (docs.streamlit.io). Отсюда практическое следствие: если полю ввода назначен ключ, его текущее значение доступно в состоянии без отдельного сохранения.
Caching отвечает за тяжёлые объекты. Документация Streamlit объясняет проблему: объекты создаются заново снова и снова, и их трудно сохранять между перезапусками или сессиями (docs.streamlit.io). Кэширование ускоряет приложение и помогает удерживать объекты между перезапусками (docs.streamlit.io). Здесь есть ловушка: по умолчанию кэшированные значения доступны всем пользователям приложения (docs.streamlit.io). Если результат должен быть виден только внутри сессии, документация предлагает передать scope=»session» в декоратор кэширования или использовать Session State (docs.streamlit.io).
Порядок работы с этими механизмами для учебного примера:
- Тяжёлую загрузку модели или справочника обернуть в кэш, чтобы она не повторялась при каждом rerun.
- Поля ввода снабдить ключами, чтобы их значения попадали в Session State.
- Валидацию отрицательной ставки повесить на колбэк изменения поля.
- Расчёт привязать к нажатию кнопки.
Четвёртый пункт напрямую связан с расходами. Если расчёт делает внешний платный вызов, привязка к нажатию кнопки отделяет намеренное действие от случайного изменения поля. Кэш для такого вызова стоит настраивать с учётом того, что по умолчанию его значения видны всем пользователям.
Interface, Blocks и события в Gradio
Gradio строит интерфейс вокруг функции. Класс Interface покрывает типовой случай: один или несколько входов, один или несколько выходов. Этого достаточно для демонстрации модели, когда нужно быстро показать результат.
Класс gr.Blocks нужен, когда макет и потоки данных требуют настройки. Документация Gradio приводит структуру блока: контекст with gr.Blocks() as demo, внутри — поле ввода с подписью, поле вывода и кнопка (gradio.app). Событие привязывается к кнопке явно, а список входов и выходов указывается в вызове.
Событийная модель Gradio отличается от перерисовки скрипта. Функция вызывается тогда, когда сработал привязанный слушатель. Это даёт контроль над числом вызовов: изменение поля само по себе не запускает расчёт, если слушатель повешен на кнопку.
| Механизм | Streamlit | Gradio |
|---|---|---|
| Модель исполнения | Перезапуск скрипта сверху вниз | Вызов функции по событию |
| Память между действиями | Session State | Состояние компонентов и сессии |
| Тяжёлые объекты | Кэш с областью видимости | Кэш средствами Python |
| Явные входы и выходы | Виджеты и вывод в потоке скрипта | inputs и outputs в слушателе |
| Настройка макета | Поток скрипта | gr.Blocks |
Для интерфейса одной функции таблица читается так: Gradio даёт явную привязку события к функции, Streamlit даёт перерисовку всего экрана. Выбор зависит от того, сколько раз пользователь меняет вход и сколько элементов на экране связано между собой.
Очередь событий — отдельная тема. Когда несколько пользователей нажимают кнопку одновременно, вызовы выстраиваются. Для демонстрации модели этого достаточно. Для нагрузки с внешними платными вызовами очередь стоит продумать заранее: она определяет, сколько запросов уйдёт наружу при пике.
Одна учебная функция в двух интерфейсах
Возьмём функцию, которая считает оценку по тексту и ставке: estimate(text, rate) = len(text.split()) * rate при rate >= 0. Функция и проверки ниже служат учебной иллюстрацией. Он показывает, как одна и та же логика выглядит в двух схемах.
Учебные проверки, которые стоит прогнать на обоих интерфейсах:
| Вход | Ставка | Ожидаемый результат |
|---|---|---|
| «кот спит дома» | 2 | 6 |
| пустая строка | 2 | 0 |
| «кот спит» | 3 | 6 |
| «кот спит» | отрицательная | ошибка валидации |
Схема Streamlit для этой функции: text_area для текста, number_input для ставки, button для запуска, вывод результата. Схема Gradio: TextArea, Number, привязка Button.click с явными inputs и outputs. Обе схемы описаны как учебные; реальный интерфейс в примере не запускался.
Собственная функция выглядит одинаково в обоих файлах:
def estimate(text: str, rate: float) -> float:
if not isinstance(text, str) or not isinstance(rate, (int, float)):
raise ValueError("Текст должен быть строкой, ставка — числом")
if rate < 0:
raise ValueError("Ставка должна быть неотрицательной")
return len(text.split()) * rate
Разница начинается на уровне обвязки. В Streamlit вызов происходит в потоке скрипта, и результат выводится виджетом. В Gradio вызов привязан к слушателю кнопки, а входы и выходы перечислены явно. Логика функции при этом остаётся общей, и её можно вынести в отдельный модуль, который импортируют оба приложения.
Практический вывод: пишите функцию так, чтобы она не знала об интерфейсе. Тогда смена UI сводится к переписыванию обвязки, а проверки остаются теми же. Это экономит время при переходе от демо к рабочему прототипу.
Пустой ввод, неверный тип и отрицательная ставка
Три проверки покрывают большинство ошибок на входе. Их стоит прогонять на обоих интерфейсах, потому что поведение виджетов различается.
Пустой ввод. Пустая строка даёт ноль слов и нулевую оценку при любой неотрицательной ставке. Это корректный результат, и его не нужно превращать в ошибку. Проверка: подать пустую строку и убедиться, что интерфейс показывает нулевой результат без ошибки.
Неверный тип. Поле числа в обоих фреймворках возвращает числовой тип, но при ручном вводе или программном вызове может прийти строка. Проверка: подать строку вместо числа и убедиться, что ошибка перехвачена и показана понятным текстом.
Отрицательная ставка. Этот вход должен завершаться ошибкой валидации. Проверка: подать отрицательное число и убедиться, что расчёт не выполняется, а пользователь видит сообщение.
Порядок проверок важен. Сначала тип, потом знак, потом расчёт. Если проверять знак до проверки типа, сравнение строки с нулём вызовет исключение.
В Streamlit валидацию удобно вешать на колбэк изменения поля: документация подтверждает, что колбэк выполняется раньше остальной части скрипта. В Gradio проверка живёт внутри функции или в отдельном слушателе, привязанном к тому же событию. В обоих случаях сообщение об ошибке должно быть видно рядом с полем.
Отдельная ловушка — молчаливый пропуск. Если функция возвращает None при ошибке, интерфейс покажет пустое поле вывода, и пользователь решит, что расчёт прошёл. Явное исключение с перехватом даёт понятное сообщение.
Состояние и повторные внешние вызовы при событиях
Главный риск интерфейса с внешними платными вызовами — незаметный повторный запрос. Он возникает, когда изменение поля само запускает платный расчёт.
Учебная последовательность: ввод текста, изменение ставки, нажатие расчёта. Разберём, какое событие что запускает.
| Шаг | Событие | Что запускается | Что сохраняется |
|---|---|---|---|
| Ввод текста | Изменение поля | Ничего | Значение поля в состоянии |
| Изменение ставки | Изменение поля | Валидация | Значение ставки в состоянии |
| Нажатие расчёта | Клик | Расчёт и внешний вызов | Результат в состоянии |
Контроль из учебного артефакта формулируется так: изменение поля само не должно незаметно создавать оплаченный запрос в проектируемой схеме. Проверять нужно привязку событий в конкретном приложении.
В Streamlit защита строится на двух приёмах. Первый: расчёт привязывается к кнопке. Второй: тяжёлый результат кэшируется, но с учётом того, что по умолчанию кэш виден всем пользователям приложения. Если результат зависит от пользовательских данных, документация предлагает scope=»session» или Session State.
В Gradio защита строится на явной привязке слушателя. Слушатель на кнопке даёт один вызов на одно нажатие. Слушатель на изменении поля даёт вызов на каждое изменение, и это стоит учитывать при платных запросах.
Отдельный вопрос — состояние между действиями. Если пользователь ввёл текст, потом изменил ставку, оба значения должны сохраниться до нажатия кнопки. В Streamlit за это отвечает Session State, куда попадают виджеты с ключами. В Gradio значения компонентов сохраняются в рамках сессии.
Проверка на повторные вызовы проста: добавьте счётчик обращений к внешнему сервису и прогоните сценарий с несколькими изменениями полей. Если счётчик растёт без нажатия кнопки, привязку событий стоит пересмотреть.
Локальное демо, доступ по ссылке и production
Локальный запуск и публичный доступ — разные задачи с разными обязанностями.
Локальное демо. Оба фреймворка запускаются на машине разработчика и открывают интерфейс в браузере. На этом этапе важны скорость итерации и простота отладки. Учебный пример из этой статьи рассчитан именно на такой сценарий.
Доступ по ссылке. Публичная ссылка даёт доступ к запущенному приложению. Документация Hugging Face описывает три варианта SDK для Spaces: Gradio, Docker и статический HTML (huggingface.co). Публичные Spaces полностью открыты: любой может посмотреть исходный код, открыть работающее приложение и клонировать репозиторий (huggingface.co). Приватные Spaces полностью приватны: исходный код и работающее приложение доступны владельцу и соавторам (huggingface.co).
Ссылка share сама по себе не доказывает приватность. Для приложения с пользовательскими данными отдельно настройте доступ и хранение секретов. Правило об открытом исходном коде выше относится к публичным Hugging Face Spaces.
Стоимость. Документация Hugging Face сообщает: создание Gradio и Docker Spaces обычно требует платного плана — PRO для личных аккаунтов, Team или Enterprise для организаций. При этом подходящие бесплатные личные аккаунты могут размещать до 2 Gradio Spaces на ZeroGPU (huggingface.co). Проверяйте применимость этого исключения к своему аккаунту и проекту.
Версия SDK. Документация Hugging Face уточняет: выбор Gradio в качестве SDK при создании Space инициализирует Space последней версией Gradio, прописывая свойство sdk в YAML-блоке README.md (huggingface.co). Для смены версии правится свойство sdk_version (huggingface.co). Это важно для воспроизводимости: версия фиксируется в файле конфигурации.
Обязанности production-развёртывания для обоих вариантов:
- Определить, кто имеет доступ к приложению и к исходному коду.
- Вынести секреты в переменные окружения и не выводить их в логи.
- Ограничить частоту внешних платных вызовов.
- Зафиксировать версии зависимостей.
- Продумать поведение при пиковой нагрузке и очередь событий.
Пункт про секреты стоит раскрыть. Ключи внешних сервисов храните в настройках секретов хостинга и передавайте приложению через переменные окружения. При публичном Space исходный код виден, поэтому ключ в файле становится доступен всем. Переменные окружения задаются в настройках хостинга и не попадают в репозиторий.
Пункт про частоту вызовов связан с моделью событий. В Streamlit защита строится на кнопке и кэше. В Gradio — на явной привязке слушателя. В обоих случаях стоит добавить счётчик и лимит.
Пункт про версии связан с воспроизводимостью. Файл конфигурации фиксирует версию SDK, и это позволяет повторить сборку позже.
Частые вопросы
Streamlit или Gradio: что выбрать для чат-бота?
Для чат-бота важна событийная модель: сообщение пользователя запускает один вызов модели. Gradio с явной привязкой слушателя к кнопке отправки даёт контроль над числом вызовов. Streamlit тоже подходит, но перерисовка всего скрипта требует аккуратной работы с Session State, чтобы история сообщений сохранялась между перезапусками. Начните с пилота на Gradio и перенесите логику в общий модуль.
Что лучше для LLM-приложения?
Зависит от типа работы. Для интерфейса одной модели с одним входом и одним выходом начните с пилота на Gradio. Аналитический экран с историей запросов, фильтрами и сводками удобнее на Streamlit. Внешние платные вызовы в обоих случаях привязывайте к намеренному действию и ограничивайте счётчиком.
Чем Streamlit отличается от Gradio по модели исполнения?
Streamlit перезапускает весь Python-скрипт сверху вниз при обновлении экрана. Gradio вызывает функцию по привязанному событию. Отсюда разница в состоянии: в Streamlit за память отвечает Session State, в Gradio значения компонентов сохраняются в рамках сессии.
Можно ли использовать один и тот же код функции в обоих фреймворках?
Да. Вынесите расчёт в отдельный модуль, который не знает об интерфейсе. Тогда обвязка Streamlit и обвязка Gradio импортируют одну функцию, а проверки остаются общими. Это упрощает смену UI и тестирование.
Где разворачивать приложение для постоянного доступа?
Документация Hugging Face описывает Spaces с тремя вариантами SDK: Gradio, Docker и статический HTML. Создание Gradio и Docker Spaces обычно требует платного плана; для подходящих бесплатных личных аккаунтов документация допускает до 2 Gradio Spaces на ZeroGPU. Публичные Spaces открывают исходный код и работающее приложение, приватные доступны владельцу и соавторам. Хостинг и безопасность выбираются отдельно от выбора фреймворка.
