Когда пользователь видит безликое "Failed to fetch" после тридцати секунд ожидания ответа, магия исчезает. Искусственный интеллект, который секунду назад казался всезнающим цифровым оракулом, мгновенно превращается в кусок нестабильного кода. Хуже этого — только уверенно сгенерированный бред, обрывающийся на полуслове, или вывалившийся прямо в чат сырой, не распарсенный JSON с оторванной закрывающей скобкой.
Индустрия десятилетиями училась проектировать интерфейсы для детерминированных систем. Кнопка нажата — запись в базе создана — фронтенд отрендерил зеленую галочку. В мире больших языковых моделей (LLM) этот социальный контракт между системой и пользователем разорван. Ответ может занять секунду, а может минуту. Провайдер может отвалиться по таймауту. Модель может упереться во внутренний фильтр безопасности, поймать галлюцинацию или просто выдать структуру данных, ломающую ваш парсер.
Если ваш UI реагирует на эти сценарии стандартным красным тостом в правом верхнем углу экрана, вы теряете пользователей. Проектирование AI-интерфейсов требует перехода от парадигмы "успех или ошибка" к парадигме "непрерывной деградации и восстановления".
Анатомия провала
Чтобы правильно обрабатывать ошибки, нужно понимать их природу. В контексте взаимодействия с LLM мы сталкиваемся с четырьмя принципиально разными классами сбоев.
Первый — сетевой отказ до начала генерации. Вы отправили запрос, но API провайдера (будь то OpenAI, Anthropic или агрегатор) лежат. Вы получаете 502 Bad Gateway или 529 Overloaded. Второй — таймаут TTFT (Time To First Token). Соединение установлено, но модель "думает" слишком долго. Пользователь смотрит на статичный лоадер и решает, что вкладка зависла. Третий — обрыв стрима. Модель начала печатать ответ, выдала три абзаца полезного текста и внезапно закрыла соединение из-за внутренних лимитов или сетевого сбоя. Четвертый — логический сбой. Стрим завершился успешно, HTTP 200, но внутри — галлюцинация, нарушение формата вывода (например, Markdown вместо запрошенного JSON) или отказ отвечать из-за цензуры.
Каждый из этих сценариев требует своего UI-паттерна. Сваливать их в единый catch (e) и показывать "Что-то пошло не так" — инженерное преступление.
UI-паттерны выживания
Главное правило AI-интерфейсов: сохраняйте контекст пользователя любой ценой. Худшее, что делает плохой UI при ошибке генерации — очищает поле ввода. Пользователь формулировал сложный многосоставной промпт пять минут. Если сеть моргнула, промпт обязан остаться в поле ввода, а уже сгенерированный кусок ответа — сохраниться в истории как черновик.
Когда стрим обрывается на середине, не прячьте уже полученный текст за экраном ошибки. Пользователь уже потратил время на его чтение. Оставьте текст на экране, визуально отделите место обрыва (например, градиентным затуханием или иконкой разрыва связи) и дайте кнопку "Продолжить генерацию с этого места". Бэкенд может взять сгенерированный кусок, добавить его в историю как assistant и отправить запрос заново.
Для борьбы с долгим TTFT используйте прогрессивные индикаторы состояния. Статичный спиннер убивает конверсию. Интерфейс должен разговаривать с пользователем: "Подключаемся к модели..", "Анализируем контекст..", "Генерируем ответ..". Даже если это просто заглушки, сменяющиеся по таймеру, они снижают когнитивное напряжение ожидания.
Если модель выдала битый JSON (что часто бывает при вызове инструментов), UI не должен падать с белым экраном. Парсер должен уметь извлекать частичные данные. Используйте AST-парсеры или регулярные выражения для извлечения валидных фрагментов. Если сломалась структура списка, покажите те элементы, которые удалось распарсить, а остальное предложите перегенерировать точечно.
Уровень инфраструктуры: Graceful Fallback и RouterAPI
Элегантная обработка ошибок в UI невозможна без умного бэкенда. Если фронтенд вынужден сам справляться с нестабильностью OpenAI, архитектура уже проиграла. Здесь на сцену выходят шлюзы маршрутизации, такие как RouterAPI.
Вместо того чтобы жестко привязывать фичу к конкретному провайдеру, мы внедряем абстракцию. Допустим, пользователь запрашивает сложный анализ кода. Основная модель — Claude 3.5 Sonnet. Запрос уходит в бэкенд, который маршрутизирует его через RouterAPI. Anthropic возвращает 429 Too Many Requests.
Глупый бэкенд прокинет 429 на фронтенд, и пользователь увидит ошибку. Умный бэкенд с RouterAPI перехватывает отказ "на лету". Он знает, что у нас есть резервный канал через резервный маршрут RouterAPI. RouterAPI прозрачно перенаправляет запрос туда. Если и там затор — происходит даунгрейд до GPT-4o или другой модели со схожими характеристиками.
Как это отражается в интерфейсе? Пользователь не видит красных крестов. Вместо этого индикатор генерации мягко пульсирует желтым, и появляется микро-текст: "Claude 3.5 перегружен. Переключаем на резервную модель..".
Мы превращаем сбой инфраструктуры в демонстрацию надежности нашего продукта. Пользователь видит, что система борется за его запрос, а не сдается при первой проблеме. Это вызывает доверие. Более того, при использовании нормализаторов стоимости (например, система тарификации RouterAPI), система может автоматически компенсировать разницу в цене между провайдерами, не перекладывая финансовые скачки на пользователя в реальном времени.
Искусство извиняться текстом
Текст ошибки — это полноценный элемент интерфейса. Забудьте фразы "Произошла непредвиденная ошибка" или "Сервер не отвечает". Они не дают пользователю точки опоры и вызывают лишь раздражение.
Ошибка должна объяснять, что случилось, и предлагать конкретное действие. Вместо "Ошибка генерации" пишите "Модель не смогла обработать такой объем текста. Попробуйте разбить документ на две части". Вместо "Таймаут сети" — "Провайдер отвечает слишком долго. Повторить запрос или переключиться на более быструю модель?".
Дайте пользователю контроль над ситуацией. Если запрос тяжелый и висит уже 20 секунд, покажите кнопку отмены. Позвольте человеку прервать процесс и изменить промпт, не дожидаясь системного таймаута.
Проектирование для хаоса
Разработка AI-продуктов заставляет инженеров принять хаос как данность. Мы больше не контролируем весь пайплайн исполнения. Мы зависим от черных ящиков с миллиардами параметров, которые работают на серверах, контролируемых чужими корпорациями.
Ошибки будут происходить. Стримы будут рваться. Модели будут галлюцинировать. Задача Staff-инженера — не пытаться построить идеальную систему, которая никогда не падает. Задача — спроектировать систему, которая падает так мягко, что пользователь воспринимает это как часть естественного диалога с машиной.
Интерфейс должен амортизировать удары инфраструктуры. Когда вы перестаете прятать нестабильность AI за глупыми заглушками и начинаете честно, но элегантно коммуницировать с пользователем, продукт переходит на новый уровень зрелости. Вы перестаете извиняться за технологии и начинаете управлять ожиданиями.