Самое важное из мира AI — в канале MAX AgentLabОтдельные разборы, инструменты и практические схемы Читайте Agent Lab в Telegram Разборы, кейсы и новости о практических AI-агентах без лишней воды.
Практика 12 минут чтения Go

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

DALL·E, SDXL и Flux для каталога: сравнение моделей через единый API
Temporary fallback cover; replace in editorial pass.

Для каталога важна не «красота» одной удачной картинки, а стабильность: модель должна сохранить цвет, материал, форму, ракурс и ограничения бренда при предсказуемой задержке. В этой статье соберём единый 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.

Адаптер провайдера

Адаптер должен делать четыре вещи:

  1. Проверить нормализованный запрос до сетевого вызова.
  2. Преобразовать его в формат API провайдера.
  3. Передать ключ идемпотентности, если это поддерживается.
  4. Преобразовать ответ и ошибки в общий формат.
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. Проверьте нагрузку ступенчато

Начните с малого числа параллельных запросов и увеличивайте его ступенями. На каждом уровне фиксируйте:

Остановите тест, если растёт очередь, появляются неконтролируемые повторы или превышается бюджет эксперимента. Это безопаснее, чем пытаться «дожать» максимальную пропускную способность.

Практическая стратегия выбора

Обычно правильный ответ — не одна модель для всех сценариев, а маршрутизация:

Порог выбирайте из данных: например, отправлять результат на повторную генерацию, если не пройдена проверка обязательного атрибута. Такой контроль качества должен быть явным, а не скрытым в субъективной оценке «картинка выглядит хорошо».

Проверка результата

Перед интеграцией в каталог проверьте:

  1. При одинаковом JobID повтор запроса не создаёт неконтролируемый дубль.
  2. 429 и временные 5xx повторяются с ограничением числа попыток.
  3. Ошибки 4xx, связанные с запросом или авторизацией, завершаются сразу.
  4. Контекст отменяет ожидание и сетевой вызов.
  5. В логах нет токенов, исходных секретов и лишних персональных данных.
  6. Для каждого результата сохраняются модель, версия промпта, попытки и задержка.
  7. Тестовые изображения проверены человеком или согласованным автоматическим чекером.

Безопасные команды для локальной проверки

Команды ниже не отправляют запросы к внешнему API и не требуют секретов:

gofmt -w ./...
go test ./...
go vet ./...

Для сетевого теста задайте короткий таймаут и используйте отдельный тестовый проект провайдера. Не вставляйте ключ в командную строку: она может попасть в историю оболочки или список процессов. Перед запуском убедитесь, что endpoint и лимит запросов относятся к тестовой среде.

Типовые ошибки

Повтор любого сбоя

Так можно многократно отправить некорректный запрос и увеличить расходы. Разделяйте временные транспортные ошибки и постоянные ошибки валидации.

Идемпотентность только на уровне клиента

Если процесс упал после отправки запроса, локальный клиент может не знать, был ли результат создан. Храните состояние задания и уточните у провайдера, поддерживает ли он повторное использование ключа.

Сравнение разных промптов

Сильная редактура под одну модель делает эксперимент необъективным. Версионируйте шаблоны и фиксируйте все преобразования.

Стоимость без стоимости повторов

Считайте цену успешной карточки, а не только цену одного вызова. В формулу должны попасть повторы, неуспешные операции и ручная модерация, если они существенны.

Ограничения метода

Единый интерфейс упрощает эксплуатацию, но не делает модели взаимозаменяемыми. Они могут по-разному поддерживать редактирование, размеры, seed, негативные подсказки, прозрачность и форматы ответа. Поэтому адаптер обязан явно объявлять поддерживаемые возможности, а неподдерживаемые параметры должны приводить к понятной ошибке.

Кроме того, один набор карточек не описывает весь каталог. После пилота расширьте выборку редкими материалами, сложными ракурсами, мелким текстом, упаковкой и товарами с похожими оттенками. Результаты такого теста — решение для конкретного набора данных и условий, а не вечный рейтинг моделей.

Дополнительные материалы: гайды Agent Lab Journal и глоссарий терминов.