AI-функции, которые выдерживают реальные данные
Для демо нужен один хороший ответ. Функции в продакшене нужен разумный результат на любой ввод, который пришлют реальные пользователи. Вот что я понял, встраивая AI-функции в продукты, которыми пользуются люди.
За последние несколько лет я встроил AI-функции в продукты, которыми реальные люди пользуются каждый день. Как senior-инженер на контракте в FreeLogo.com (откроется в новой вкладке) — AI-платформе для создания логотипов и бренда из семейства HostPapa — я сделал AI-генерацию логотипов и векторный редактор, который работает рядом с ней. До этого в GWIN AI я работал над кроссплатформенным финансовым приложением с ассистентом, который наполняет виджеты в приложении живыми данными.
Ни то ни другое — не проекты Syncra. Оба продукта принадлежат компаниям, в которых я работал, и их внутреннее устройство остаётся у них. Поделиться я могу инженерной практикой, которую теперь применяю к каждой AI-функции, которую мы делаем в Syncra. Ничего экзотического в ней нет. По большей части это обычная инженерная дисциплина, применённая к компоненту, который работает медленно, ведёт себя вероятностно и время от времени уверенно ошибается.
Чем демо отличается от функции
Демо работает на входных данных, которые вы выбрали сами. Продакшен работает на всём, что придёт: текст, скопированный из PDF с поломанными переносами строк, запрос сразу на трёх языках, пустое поле, пользователь, дважды нажавший кнопку, кто-то, кто пишет «игнорируй свои инструкции». Модель, которая блестяще выглядит на ваших десяти примерах, столкнётся со всем этим в первую же неделю.
Поэтому вопрос никогда не звучит как «умеет ли модель это делать?». Вопрос в другом: что делает продукт, когда модель не справляется? Каждый урок ниже — вариация этого вопроса.
1. Считайте ответ модели недоверенным вводом
Ответ модели — это пользовательский ввод, который просто приходит с вашего собственного сервера. Валидируйте его на границе — точно так же, как валидировали бы отправку формы.
Там, где результат идёт в код, а не к читателю-человеку, запрашивайте структурированный вывод и проверяйте его по схеме, прежде чем к нему прикоснётся что-либо ещё:
const ChartRequest = z.object({
metric: z.enum(["revenue", "orders", "visitors"]),
period: z.enum(["week", "month", "year"]),
});
const parsed = ChartRequest.safeParse(modelOutput);
if (!parsed.success) return showFallback();Заранее решите, что происходит, если валидация не прошла: одна повторная попытка с ошибкой валидации в промпте, затем заранее определённое резервное состояние, которое UI умеет отрисовать. «Модель вернула что-то странное» должно быть обработанным случаем с продуманным экраном, а не исключением в логах.
2. Модель выбирает, факты поставляет ваш код
Когда ответ ассистента попадает в интерфейс — в график, карточку или виджет, — чётко разделите работу. Модель решает, что показать и с какими параметрами. Ваш код берёт реальные цифры из ваших собственных данных и отрисовывает их.
Модель никогда не должна набирать число, которое уже знает ваша база данных. Одно это правило даёт очень многое:
- Корректность. Цифры берутся из источника истины, а не из догадки модели о нём.
- Актуальность. Виджет показывает данные такими, какие они сейчас, а не такими, какими они были в промпте.
- Права доступа. Ваш слой данных уже знает, что этому пользователю можно видеть. Модель без прямого доступа к данным не может слить то, чего никогда не получала.
- Тестируемость. Можно отдельно проверять, что «модель выбрала правильный виджет» и что «виджет показывает правильные данные».
С вызовом инструментов (tool calling) это получается само собой. Дайте модели небольшой набор хорошо описанных инструментов со строгими параметрами и считайте, что её задача — маршрутизация и формулировки, а не арифметика.
3. Спроектируйте ожидание
Генерация занимает секунды, а не миллисекунды, и время от запроса к запросу разное. Относитесь к ней как к фоновой задаче, а не как к вызову функции:
- Запускайте работу, сразу возвращайте ответ и честно показывайте прогресс.
- Дайте людям возможность отменить операцию и перестаньте платить за работу, которую никто не ждёт.
- Сохраняйте результат, чтобы обновление страницы или обрыв соединения его не выбросили.
- Делайте повторные попытки идемпотентными, чтобы двойной клик не запускал — и не оплачивал — одну и ту же генерацию дважды.
Стримьте текст, когда частичный результат полезно читать. Для изображений и других результатов по принципу «всё или ничего» уделите состоянию ожидания настоящее дизайнерское внимание. Это часть функции, а в медленный день — почти всё, что видит пользователь.
4. Отдавайте результат туда, где его можно поправить
Результат работы AI редко оказывается ровно тем, чего хотел человек. Если единственный выход — «сгенерировать заново», пользователи в итоге бросают кости, пока не выпадет что-то достаточно близкое.
Паттерн надёжнее — помещать результат туда, где его можно редактировать: сгенерированный логотип открывается как редактируемые векторные фигуры, черновик ответа попадает в текстовое поле, предложенные значения заполняют форму, которую пользователь всё равно отправляет сам. Генерация быстро даёт человеку хорошую отправную точку. Доводит результат он уже в редакторе, и тот же редактор страхует в случаях, когда модель промахнулась.
Это влияет и на формат вывода. Если люди будут править результат, просите у модели что-то структурированное и редактируемое, а не «сплющенный» финальный артефакт.
5. Опирайте ответы на собственный контент
Многие проблемы вида «AI всё выдумал» на самом деле — проблемы поиска контекста (retrieval). Если ответ должен опираться на вашу документацию, каталог или записи, сначала найдите релевантные фрагменты и передайте модели только их.
Большую часть этой работы делают эмбеддинги и семантический поиск, и для старта им не нужно много инфраструктуры: векторной колонки в Postgres, который у вас уже работает (например, pgvector), многим продуктам хватает надолго, прежде чем понадобится что-то специализированное. Силы стоит тратить на то, что попадает в индекс. Режьте контент на чанки по его реальной структуре, храните вместе с каждым чанком его источник, чтобы ответы могли на него ссылаться, и переиндексируйте при изменении контента.
6. Соберите набор для оценки из реальных входных данных
До запуска соберите набор реальных или реалистичных входных данных и намеренно включите в него неудобные: пустые, очень длинные, на другом языке, неоднозначные, враждебные. Для каждого запишите не точную ожидаемую строку, а свойства, которыми должен обладать хороший ответ: проходит валидацию по схеме, упоминает нужную сущность, отказывает, когда должен.
Прогоняйте набор при каждом изменении промпта и каждой смене модели. Промпты — это код. Версионируйте их, проводите по ним ревью и никогда не правьте промпт в продакшене только потому, что один пример стал выглядеть лучше. Обновления модели заслуживают того же: новая модель может быть лучше в среднем и при этом хуже именно на тех случаях, от которых зависит ваш продукт.
7. Готовьтесь к плохому дню у провайдера
У провайдеров бывают таймауты, срабатывают рейт-лимиты, случаются сбои, а модели выводятся из эксплуатации. Спрячьте провайдера за собственным небольшим интерфейсом, чтобы остальной код никогда не обращался к SDK вендора напрямую. Выставьте таймауты. Определитесь с фолбэком: другая модель, закешированный результат или понятное сообщение с возможностью повторить попытку.
Логируйте достаточно для отладки: версию промпта, модель, задержку, результат валидации. Не пускайте в эти логи персональные данные или маскируйте их: в промпты попадает именно та информация, которую люди предпочли бы нигде не хранить. И следите за стоимостью одного запроса так же, как за задержкой. Она меняется с каждой правкой промпта.
Как это выглядит в Syncra
Мы занимаемся прикладным AI: ассистенты, семантический поиск, поиск контекста (retrieval) и рабочие процессы на базе AI, подключённые к собственным данным и инструментам клиента. Модели мы не обучаем. Каждая AI-функция, которую мы выпускаем, строится на описанной выше практике: валидированный вывод, факты из ваших систем, продуманное ожидание, редактор там, где он уместен, набор для оценки и фолбэк на тот день, когда провайдер подведёт.
Ничто из этого не делает демо эффектнее. Но именно от этого зависит, будет ли функция работать через месяц после запуска.