DALL·E, SDXL и Flux для каталога: сравнение моделей через единый API

Для каталога важна не «красота» одной удачной картинки, а стабильность: модель должна сохранить цвет, материал, форму, ракурс и ограничения бренда при предсказуемой задержке. В этой статье соберём единый Go-интерфейс для разных провайдеров и спроектируем тест, который позволит сравнить DALL·E, SDXL и Flux на одинаковых входных данных.
Названия моделей здесь обозначают семейства и варианты развёртывания, а не один неизменный продуктовый endpoint. Конкретные параметры, лимиты, форматы ответа и тарифы зависят от провайдера. Поэтому код ниже — рабочий каркас адаптера: адрес API, схема запроса и парсинг ответа должны быть настроены под выбранный сервис.
Что именно выбирать для каталога
Сначала разделите задачу на два класса:
- Генерация по тексту: создать сцену или предмет по описанию.
- Редактирование изображения: изменить фон, цвет или композицию, сохранив исходный товар.
Для каталога второй класс часто ближе к бизнес-задаче: исходная форма товара уже известна, а от модели требуется аккуратно изменить окружение. Если же исходных фотографий нет, тестируйте текстовую генерацию, но отдельно измеряйте соответствие атрибутам.
| Критерий | Что измерять | Почему это важно |
|---|---|---|
| Точность атрибутов | Цвет, материал, число деталей, форма, надписи | Ошибки превращаются в возвраты и ручную модерацию |
| Задержка | p50, p95 и доля таймаутов | Определяет UX и размер очереди |
| Стоимость | Стоимость успешного изображения, включая повторы | Один дешёвый запрос не равен дешёвому результату |
| Нагрузка | Пропускная способность, rate limit, ошибки 429 и 5xx | Важна при пакетной генерации и сезонных пиках |
Единый контракт
Клиент приложения не должен знать, как конкретный провайдер называет размер изображения или где находится URL результата. Он передаёт нормализованный запрос, а адаптер отвечает за преобразование.
В примере ниже идемпотентность означает, что повторная отправка одного логического задания не должна незаметно создавать дубликат. Реальная поддержка ключа зависит от API-провайдера; если провайдер её не гарантирует, ключ всё равно можно использовать в собственной базе заданий.
package catalogimage
import (
"context"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"strconv"
"time"
)
type GenerateRequest struct {
Model string
Prompt string
Negative string
Width int
Height int
JobID string
SourceImage []byte
}
type GenerateResponse struct {
ID string
ImageURL string
Provider string
Model string
Latency time.Duration
}
type Provider interface {
Name() string
Generate(context.Context, GenerateRequest) (GenerateResponse, error)
}
type RetryClient struct {
Client *http.Client
MaxAttempts int
BaseDelay time.Duration
}
func (c RetryClient) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
if c.Client == nil {
c.Client = http.DefaultClient
}
if c.MaxAttempts < 1 {
c.MaxAttempts = 1
}
if c.BaseDelay <= 0 {
c.BaseDelay = 250 * time.Millisecond
}
var lastErr error
for attempt := 0; attempt < c.MaxAttempts; attempt++ {
if attempt > 0 {
delay := c.BaseDelay * time.Duration(1<<(attempt-1))
delay += time.Duration(rand.Int63n(int64(c.BaseDelay)))
timer := time.NewTimer(delay)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
}
cloned := req.Clone(ctx)
resp, err := c.Client.Do(cloned)
if err != nil {
lastErr = err
if ctx.Err() != nil {
return nil, ctx.Err()
}
continue
}
if !retryable(resp.StatusCode) {
return resp, nil
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
lastErr = fmt.Errorf("retryable HTTP status: %d", resp.StatusCode)
}
return nil, lastErr
}
func retryable(status int) bool {
return status == http.StatusRequestTimeout ||
status == http.StatusTooManyRequests ||
status == http.StatusBadGateway ||
status == http.StatusServiceUnavailable ||
status == http.StatusGatewayTimeout
}
func idempotencyKey(jobID string) (string, error) {
if jobID == "" {
return "", errors.New("job id is required")
}
return "catalog-" + jobID, nil
}
func retryAfter(resp *http.Response) time.Duration {
value := resp.Header.Get("Retry-After")
seconds, err := strconv.Atoi(value)
if err != nil || seconds < 0 {
return 0
}
return time.Duration(seconds) * time.Second
}
В боевом клиенте добавьте конкретный HTTP-метод, URL, авторизацию из переменных окружения и декодирование ответа. Не помещайте секреты в исходный код, логи или URL. Для каждого провайдера создайте отдельный тип, реализующий Provider.
Адаптер провайдера
Адаптер должен делать четыре вещи:
- Проверить нормализованный запрос до сетевого вызова.
- Преобразовать его в формат API провайдера.
- Передать ключ идемпотентности, если это поддерживается.
- Преобразовать ответ и ошибки в общий формат.
type HTTPProvider struct {
NameValue string
Endpoint string
Token string
Retry RetryClient
}
func (p HTTPProvider) Name() string {
return p.NameValue
}
func (p HTTPProvider) Generate(ctx context.Context, in GenerateRequest) (GenerateResponse, error) {
if in.Model == "" || in.Prompt == "" {
return GenerateResponse{}, errors.New("model and prompt are required")
}
if in.JobID == "" {
return GenerateResponse{}, errors.New("job id is required for idempotency")
}
key, err := idempotencyKey(in.JobID)
if err != nil {
return GenerateResponse{}, err
}
// Здесь формируется JSON-схема конкретного провайдера.
// Не копируйте это место между API без проверки их документации.
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
p.Endpoint,
nil,
)
if err != nil {
return GenerateResponse{}, err
}
req.Header.Set("Authorization", "Bearer "+p.Token)
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Content-Type", "application/json")
started := time.Now()
resp, err := p.Retry.Do(ctx, req)
if err != nil {
return GenerateResponse{}, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return GenerateResponse{}, fmt.Errorf("%s: HTTP status %d", p.Name(), resp.StatusCode)
}
// Декодируйте JSON ответа конкретного API и проверьте наличие результата.
return GenerateResponse{
Provider: p.Name(),
Model: in.Model,
Latency: time.Since(started),
}, nil
}
Обратите внимание: повторять безопасно только временные ошибки. Ошибки валидации запроса, неподдерживаемый размер, неверную авторизацию и недостаточный баланс повторять не следует. Для ответа 429 учитывайте Retry-After, если он присутствует, и ограничивайте общее время операции контекстом.
Как сравнить DALL·E, SDXL и Flux честно
Не сравнивайте модели по трём случайным промптам. Составьте фиксированный набор карточек, где каждое требование можно проверить независимо.
1. Подготовьте датасет
Для каждой карточки сохраните:
- идентификатор задания;
- текстовое описание товара;
- обязательные атрибуты и запрещённые изменения;
- исходное изображение, если тестируете редактирование;
- целевой размер и формат;
- версию промпта и конфигурации.
Не меняйте промпт между моделями без отдельной причины. Если API требуют разные поля, держите общий смысл постоянным и записывайте каждое преобразование.
2. Зафиксируйте матрицу запуска
Для каждого задания запускайте все модели в одинаковом порядке или в перемешанном порядке, чтобы время суток и состояние очереди не были систематическим преимуществом одной модели. Используйте одинаковое число повторов и одинаковые ограничения по таймауту.
Минимальная запись результата может выглядеть так:
{
"job_id": "chair-0042-run-02",
"provider": "example-provider",
"model": "example-model",
"prompt_version": "catalog-v3",
"started_at": "2026-01-01T12:00:00Z",
"duration_ms": 8420,
"http_status": 200,
"attempts": 1,
"success": true,
"output_bytes": 183920,
"attribute_score": null,
"cost": null
}
Поля attribute_score и cost оставлены пустыми намеренно: их нельзя правдоподобно заполнить без ваших проверок и тарифных данных.
3. Оцените результат отдельно от транспорта
Для атрибутов используйте чек-лист или слепую оценку двумя проверяющими. Например, отдельно отметьте соответствие цвета, формы, материала и количества объектов. Средний балл полезен, но сохраняйте и ошибки по категориям: модель с хорошим средним результатом может регулярно ошибаться именно в критичном для бизнеса поле.
Для задержки считайте p50 и p95 по успешным запросам, а также долю таймаутов и повторов. Для стоимости используйте фактический тариф выбранного провайдера на дату запуска и включайте стоимость неуспешных попыток, если они тарифицируются.
4. Проверьте нагрузку ступенчато
Начните с малого числа параллельных запросов и увеличивайте его ступенями. На каждом уровне фиксируйте:
- успешные ответы;
- 429, 5xx и сетевые ошибки;
- p95 задержки;
- среднее число попыток;
- размер очереди и время до начала обработки.
Остановите тест, если растёт очередь, появляются неконтролируемые повторы или превышается бюджет эксперимента. Это безопаснее, чем пытаться «дожать» максимальную пропускную способность.
Практическая стратегия выбора
Обычно правильный ответ — не одна модель для всех сценариев, а маршрутизация:
- быстрый и недорогой вариант — для черновиков и низкорисковых карточек;
- модель с лучшим сохранением атрибутов — для публикации;
- отдельный режим редактирования — когда нужно сохранить исходный товар;
- резервный провайдер — при превышении лимитов или временной недоступности.
Порог выбирайте из данных: например, отправлять результат на повторную генерацию, если не пройдена проверка обязательного атрибута. Такой контроль качества должен быть явным, а не скрытым в субъективной оценке «картинка выглядит хорошо».
Проверка результата
Перед интеграцией в каталог проверьте:
- При одинаковом
JobIDповтор запроса не создаёт неконтролируемый дубль. - 429 и временные 5xx повторяются с ограничением числа попыток.
- Ошибки 4xx, связанные с запросом или авторизацией, завершаются сразу.
- Контекст отменяет ожидание и сетевой вызов.
- В логах нет токенов, исходных секретов и лишних персональных данных.
- Для каждого результата сохраняются модель, версия промпта, попытки и задержка.
- Тестовые изображения проверены человеком или согласованным автоматическим чекером.
Безопасные команды для локальной проверки
Команды ниже не отправляют запросы к внешнему API и не требуют секретов:
gofmt -w ./...
go test ./...
go vet ./...
Для сетевого теста задайте короткий таймаут и используйте отдельный тестовый проект провайдера. Не вставляйте ключ в командную строку: она может попасть в историю оболочки или список процессов. Перед запуском убедитесь, что endpoint и лимит запросов относятся к тестовой среде.
Типовые ошибки
Повтор любого сбоя
Так можно многократно отправить некорректный запрос и увеличить расходы. Разделяйте временные транспортные ошибки и постоянные ошибки валидации.
Идемпотентность только на уровне клиента
Если процесс упал после отправки запроса, локальный клиент может не знать, был ли результат создан. Храните состояние задания и уточните у провайдера, поддерживает ли он повторное использование ключа.
Сравнение разных промптов
Сильная редактура под одну модель делает эксперимент необъективным. Версионируйте шаблоны и фиксируйте все преобразования.
Стоимость без стоимости повторов
Считайте цену успешной карточки, а не только цену одного вызова. В формулу должны попасть повторы, неуспешные операции и ручная модерация, если они существенны.
Ограничения метода
Единый интерфейс упрощает эксплуатацию, но не делает модели взаимозаменяемыми. Они могут по-разному поддерживать редактирование, размеры, seed, негативные подсказки, прозрачность и форматы ответа. Поэтому адаптер обязан явно объявлять поддерживаемые возможности, а неподдерживаемые параметры должны приводить к понятной ошибке.
Кроме того, один набор карточек не описывает весь каталог. После пилота расширьте выборку редкими материалами, сложными ракурсами, мелким текстом, упаковкой и товарами с похожими оттенками. Результаты такого теста — решение для конкретного набора данных и условий, а не вечный рейтинг моделей.
Дополнительные материалы: гайды Agent Lab Journal и глоссарий терминов.