Как я перестал бояться evals: практический гайд по тестированию AI-фич для инди-хакера
· 6 мин чтения
Большинство инди-хакеров тестируют AI-функционал одинаково: меняют системный промпт, нажимают «Generate» три-четыре раза в песочнице, видят адекватный результат и пушат в прод.
В пет-проектах это работает. Но когда твой продукт — SaaS вроде VYUD Publisher, где шедулер по крону отправляет сгенерированные посты прямиком в реальные Telegram-каналы, группы VK или профили LinkedIn пользователей, такой подход превращается в мину замедленного действия. Рано или поздно LLM выдаст сломанный markdown в соцсеть, которая его не поддерживает, придумает несуществующий факт о бизнесе клиента или напишет 4000 знаков вместо лаконичного поста для X.
Enterprise-подход с тяжёлыми фреймворками и сотнями бенчмарков соло-разработчику не подходит: он съедает недели разработки и тысячи долларов на API. Ниже — система легковесных evals (оценок качества), которую можно развернуть за вечер и спокойно спать, пока фоновые воркеры публикуют контент.
1. Детерминированный слой: бесплатные проверки за 5 мс
Самая частая ошибка при внедрении AI — пытаться валидировать всё подряд через ещё одну LLM. Это дорого, медленно и недетерминированно. Большинство критических сбоев в продакшене отсекается обычным кодом ещё до того, как пост попадёт в базу со статусом scheduled.
В Publisher для этого используется комбинация Pydantic-схем и платформенных линтеров:
- Чёрные списки синтаксиса по платформам. Telegram и блог нормально переваривают чистый Markdown. Но если отдать звёздочки и решётки заголовков в VK, Instagram, X или Одноклассники, на выходе получится визуальная каша. Правило простое: изолировать пайплайны форматирования и проверять через словарь
NO_MARKDOWN_PLATFORMS. Любой Markdown перед отправкой в такие каналы либо вычищается стриппером вродеmarkdown_to_plain_text, либо бракуется на этапе генерации. - Длина и лимиты соцсетей. Если промпт просил пост на 280 символов для X, а модель увлеклась и выдала 350, пост упадёт с ошибкой API в момент публикации. Валидатор проверяет жёсткие лимиты посимвольно сразу после генерации.
- Запрещённые стоп-паттерны. Банальные регулярки на фразы-паразиты, выдающие бота с головой: «Конечно! Вот пост для вашего блога:», «Надеюсь, этот текст был полезен» или незаполненные плейсхолдеры вроде
[Вставьте ссылку].
Если результат не проходит детерминированный линтер, бэкенд даже не сохраняет черновик, а сразу делает ретрай с повышенной строгостью в системном сообщении.
2. Контент-планы: строгие схемы против галлюцинаций структуры
Генерация контент-плана на неделю или месяц вперёд — тяжёлая задача для модели. Модели любят терять дни недели, дублировать темы или внезапно менять формат с JSON на разметку списками.
Решение для быстрого запуска — Structured Outputs с жёстким контрактом схемы:
from pydantic import BaseModel, Field
from typing import List
class ContentPlanItem(BaseModel):
day_offset: int = Field(ge=1, le=30)
topic: str = Field(min_length=10, max_length=120)
hook: str = Field(description="Первая строчка поста, зацепка")
call_to_action: str
target_platform: str
class ContentPlan(BaseModel):
strategy_summary: str
schedule: List[ContentPlanItem] = Field(min_items=7, max_items=30)Eval для таких фич — синтетический прогон на 10 разных нишах, от локальной кофейни до B2B SaaS. Метрика бинарная: Schema Pass Rate, то есть процент ответов, которые без ошибок распарсились в валидный объект с первого раза. Если показатель ниже 98%, промпт уходит на доработку: снижается температура, убираются двусмысленные инструкции.
3. LLM-as-a-Judge: дешёвая оценка качества без академизма
Когда синтаксис и схема валидны, встаёт вопрос смысла: насколько текст живой, соответствует ли он редполитике пользователя и нет ли в нём откровенной ерунды.
Вместо субъективных шкал «оцените качество от 1 до 10», на которых путаются сами модели, лучше работает быстрый бинарный чеклист через дешёвую модель-судью (класса Haiku или mini).
Золотой датасет инди-хакера. Не нужно собирать тысячи примеров. Достаточно завести JSON-файл с 20–30 реальными кейсами, на которых генерация когда-то спотыкалась: сложные ниши, специфический tone-of-voice, длинный контекст.
Судье передаются исходный контекст, целевая аудитория, сгенерированный пост и список бинарных вопросов (True/False):
- Relevance: отвечает ли пост заявленной теме без ухода в абстрактную философию?
- Hallucination Check: есть ли в тексте конкретные факты или цифры, которых не было в предоставленном контексте?
- Tone Match: соответствует ли стиль заданной тональности (экспертный, провокационный, дружелюбный)?
Набор из 30 тестов перед деплоем прогоняется меньше чем за минуту и стоит несколько центов. Если после правки промпта доля «True» по чеклисту просела хотя бы на 5%, правка откатывается.
4. Генерация изображений: контроль на входе и выходе
Оценивать сгенерированные картинки программно сложнее всего. Пользователь не простит, если вместо иллюстрации к IT-статье получит абстрактную кашу с шестью пальцами на руке.
Вместо сложного CV-анализа готовых изображений я смещаю фокус на два этапа:
- Prompt Sanitization (pre-eval). Модель, которая пишет промпты для диффузионных сетей, проходит через фильтр на негативные стили, артефакты и стоп-слова. Если пользователь пишет «сделай стильный баннер про запуск продукта», промежуточная LLM должна расширить это в детальное техническое описание для генератора, жёстко зафиксировав соотношение сторон (16:9 для соцсетей, 1:1 для ленты) и цветовую гамму.
- Post-generation check. Базовая проверка метаданных: успешная загрузка в Supabase Storage (post-media), валидные заголовки ответа, ненулевой размер файла и корректный mime-type. Если генератор вернул битый URL или таймаут, шедулер переводит статус в retrying с экспоненциальным backoff и не ломает цепочку публикации.
Архитектура безопасного продакшена
Финальная защита от факапов заложена прямо в жизненный цикл сущностей в базе. В VYUD Publisher пост проходит строгую цепочку состояний:
[Prompt + Context]
│
▼
[Generation] ──(линтеры + Pydantic)──► [Draft / Scheduled]
│
(CAS-клейм шедулера)
│
▼
[Publishing]
│
┌────────────────────────┴────────────────┐
▼ ▼
[Успех в соцсети] [Partial / Full Error]
│ │
▼ ▼
[Published] [Retrying (до 3 раз)] ──► [Failed]- Draft и Scheduled разделены. Если уверенность модели или результат валидации оказались в серой зоне, пост не планируется на автоматическую публикацию. Он остаётся черновиком и ждёт одного клика подтверждения от пользователя.
- Атомарный CAS-клейм. Перед отправкой шедулер делает атомарный
UPDATE ... WHERE status = 'scheduled' RETURNING *. Даже при параллельных воркерах ни один пост не уйдёт в канал дважды. - Graceful degradation. Если внешний провайдер отвалился на вложении или тексте, ошибка пишется в
last_errorс понятным человеческим описанием, а не сырым стек-трейсом.
Тестировать AI-продукт в соло-режиме можно без академических метрик вроде BLEU и ROUGE. Хватает трёх вещей: жёсткие линтеры на входе, Pydantic-схемы посередине и 30 синтетических тестов через Claude или GPT на выходе. С ними можно спать спокойно, пока LLM ведут соцсети клиентов на автопилоте.