Как я перестал бояться 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):

  1. Relevance: отвечает ли пост заявленной теме без ухода в абстрактную философию?
  2. Hallucination Check: есть ли в тексте конкретные факты или цифры, которых не было в предоставленном контексте?
  3. Tone Match: соответствует ли стиль заданной тональности (экспертный, провокационный, дружелюбный)?

Набор из 30 тестов перед деплоем прогоняется меньше чем за минуту и стоит несколько центов. Если после правки промпта доля «True» по чеклисту просела хотя бы на 5%, правка откатывается.

4. Генерация изображений: контроль на входе и выходе

Оценивать сгенерированные картинки программно сложнее всего. Пользователь не простит, если вместо иллюстрации к IT-статье получит абстрактную кашу с шестью пальцами на руке.

Вместо сложного CV-анализа готовых изображений я смещаю фокус на два этапа:

  1. Prompt Sanitization (pre-eval). Модель, которая пишет промпты для диффузионных сетей, проходит через фильтр на негативные стили, артефакты и стоп-слова. Если пользователь пишет «сделай стильный баннер про запуск продукта», промежуточная LLM должна расширить это в детальное техническое описание для генератора, жёстко зафиксировав соотношение сторон (16:9 для соцсетей, 1:1 для ленты) и цветовую гамму.
  2. 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]
  1. Draft и Scheduled разделены. Если уверенность модели или результат валидации оказались в серой зоне, пост не планируется на автоматическую публикацию. Он остаётся черновиком и ждёт одного клика подтверждения от пользователя.
  2. Атомарный CAS-клейм. Перед отправкой шедулер делает атомарный UPDATE ... WHERE status = 'scheduled' RETURNING *. Даже при параллельных воркерах ни один пост не уйдёт в канал дважды.
  3. Graceful degradation. Если внешний провайдер отвалился на вложении или тексте, ошибка пишется в last_error с понятным человеческим описанием, а не сырым стек-трейсом.

Тестировать AI-продукт в соло-режиме можно без академических метрик вроде BLEU и ROUGE. Хватает трёх вещей: жёсткие линтеры на входе, Pydantic-схемы посередине и 30 синтетических тестов через Claude или GPT на выходе. С ними можно спать спокойно, пока LLM ведут соцсети клиентов на автопилоте.

Теги:EvalsAI

← Ко всем статьям