Почему одна модель — это риск

Когда ты строишь SaaS на одном API, ты зависишь от:

  • Доступности — упал API → упал твой сервис
  • Цены — подняли тариф → твоя маржа сжалась
  • Качества — поменяли модель → ответы стали хуже
  • Санкций/блокировок — запретили доступ из РФ → всё

Решение: федеративная архитектура. Несколько LLM работают как единая система. Если одна недоступна — запрос уходит на другую. Если задача простая — идёт на дешёвую модель. Сложная — на мощную.

Архитектура bbb2.ru: 4 модели, 1 endpoint

                    ┌─────────────────────┐
                    │   API Router        │
                    │   (LLMRouter.php)   │
                    └──────────┬──────────┘
                               │
            ┌──────────────────┼──────────────────┐
            │                  │                  │
            ▼                  ▼                  ▼
   ┌────────────────┐ ┌──────────────┐ ┌──────────────────┐
   │  DeepSeek v4   │ │  Alice LLM   │ │  YandexGPT 5.1   │
   │  (генерация    │ │  (тексты,    │ │  (извлечение     │
   │   сложного     │ │   SEO,       │ │   сущностей,     │
   │   контента)    │ │   валидация) │ │   промпты)       │
   └────────────────┘ └──────────────┘ └──────────────────┘
            │                  │                  │
            └──────────────────┼──────────────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │   Fallback Queue    │
                    │   (если основная    │
                    │    модель упала)    │
                    └─────────────────────┘

Код: роутер LLM

<?php
// LLMRouter.php — федеративный роутер запросов к разным LLM
class LLMRouter {
    private array $models = [
        "deepseek" => [
            "url" => "https://api.deepseek.com/v1/chat/completions",
            "key" => "DEEPSEEK_API_KEY",
            "cost_per_1k_tokens" => 0.014, // $
            "max_tokens" => 32000,
            "priority" => 1, // основной для сложных задач
        ],
        "alice" => [
            "url" => "https://ai.api.cloud.yandex.net/v1/responses",
            "key" => "YANDEX_API_KEY", 
            "cost_per_1k_tokens" => 0.025, // ₽
            "max_tokens" => 4000,
            "priority" => 2, // для текстов и SEO
            "folder_id" => "YANDEX_FOLDER_ID",
        ],
        "yandexgpt" => [
            "url" => "https://ai.api.cloud.yandex.net/v1/responses",
            "key" => "YANDEX_API_KEY",
            "cost_per_1k_tokens" => 0.015, // ₽
            "max_tokens" => 2000,
            "priority" => 3, // для технических задач
            "folder_id" => "YANDEX_FOLDER_ID",
        ],
    ];

    // Роутинг: выбираем модель по задаче
    function route(string $task, string $prompt, array $options = []): string {
        $model = $this->selectModel($task);
        return $this->callWithFallback($model, $prompt, $options);
    }

    private function selectModel(string $task): string {
        return match(true) {
            str_contains($task, "generate_business_plan") => "deepseek",
            str_contains($task, "seo_audit") => "alice",
            str_contains($task, "extract_entities") => "yandexgpt",
            str_contains($task, "validate") => "alice",
            str_contains($task, "simple_text") => "yandexgpt", // дешёвая модель для простого
            default => "deepseek",
        };
    }

    private function callWithFallback(string $primaryModel, string $prompt, array $options): string {
        $models = [$primaryModel];
        // Добавляем fallback-модели в порядке приоритета
        foreach ($this->models as $name => $cfg) {
            if ($name !== $primaryModel) {
                $models[] = $name;
            }
        }

        $lastError = "";
        foreach ($models as $modelName) {
            try {
                $result = $this->callModel($modelName, $prompt, $options);
                if ($result !== false) return $result;
            } catch (\Exception $e) {
                $lastError = $e->getMessage();
                error_log("LLM {$modelName} failed: {$lastError}, trying fallback...");
            }
        }

        return "Все модели недоступны: {$lastError}";
    }

    private function callModel(string $modelName, string $prompt, array $options): string|false {
        $cfg = $this->models[$modelName];
        $apiKey = getenv($cfg["key"]);
        if (!$apiKey) return false;

        if ($modelName === "deepseek") {
            return $this->callDeepSeek($cfg, $prompt, $options);
        } else {
            return $this->callYandex($cfg, $modelName, $prompt, $options);
        }
    }

    private function callDeepSeek(array $cfg, string $prompt, array $options): string|false {
        $body = json_encode([
            "model" => "deepseek-chat",
            "messages" => [["role" => "user", "content" => $prompt]],
            "max_tokens" => $options["max_tokens"] ?? 4000,
            "temperature" => $options["temperature"] ?? 0.7,
        ]);

        $ch = curl_init($cfg["url"]);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $body,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                "Content-Type: application/json",
                "Authorization: Bearer " . getenv($cfg["key"]),
            ],
            CURLOPT_TIMEOUT => 60,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        
        if ($httpCode !== 200) return false;
        
        $data = json_decode($response, true);
        return $data["choices"][0]["message"]["content"] ?? false;
    }

    private function callYandex(array $cfg, string $modelName, string $prompt, array $options): string|false {
        $modelMap = ["alice" => "aliceai-llm/latest", "yandexgpt" => "yandexgpt-5.1/latest"];
        $model = $modelMap[$modelName] ?? "yandexgpt-5.1/latest";

        $body = json_encode([
            "model" => $model,
            "messages" => [["role" => "user", "text" => $prompt]],
            "max_tokens" => $options["max_tokens"] ?? 2000,
            "temperature" => $options["temperature"] ?? 0.3,
        ]);

        $ch = curl_init($cfg["url"]);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $body,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                "Content-Type: application/json",
                "Authorization: Api-Key " . getenv($cfg["key"]),
                "x-folder-id: " . getenv($cfg["folder_id"] ?? "FOLDER_ID"),
            ],
            CURLOPT_TIMEOUT => 60,
        ]);

        $response = curl_exec($ch);
        $data = json_decode($response, true);
        
        return $data["result"]["text"] ?? ($data["choices"][0]["message"]["content"] ?? false);
    }
}

// Использование
$router = new LLMRouter();

// Простой текст → YandexGPT (копейки)
$simple = $router->route("simple_text", "Напиши описание продукта в 2 предложениях");

// Сложная генерация → DeepSeek
$complex = $router->route("generate_business_plan", "Напиши бизнес-план кофейни...");

// SEO → Alice
$seo = $router->route("seo_audit", "Проанализируй страницу...");

Мониторинг: что работает, что упало

Роутер пишет логи каждого вызова. Раз в час cron проверяет health:

<?php
// llm_health_check.php — мониторинг доступности моделей
function healthCheck(): array {
    $router = new LLMRouter();
    $results = [];
    
    foreach (["deepseek", "alice", "yandexgpt"] as $model) {
        $start = microtime(true);
        $response = $router->route("simple_text", "Ответь одним словом: OK");
        $latency = round((microtime(true) - $start) * 1000);
        
        $results[$model] = [
            "status" => $response !== false ? "UP" : "DOWN",
            "latency_ms" => $latency,
        ];
    }
    
    // Если модель упала — алерт в Telegram
    foreach ($results as $model => $status) {
        if ($status["status"] === "DOWN") {
            sendTelegramAlert("🔴 {$model} упала! Latency: {$status["latency_ms"]}ms");
        }
    }
    
    return $results;
}

Экономика: сколько реально стоит SaaS на LLM

Типичный SaaS на LLM обрабатывает 10 000 запросов в месяц. Стоимость на разных моделях:

МодельЦена за 1K токеновНа 10K запросов*
ChatGPT 4o$0.0025 (~0.25 ₽)~5 000 ₽
DeepSeek v4$0.00014 (~0.013 ₽)~260 ₽
Alice LLM0.025 ₽~500 ₽
YandexGPT 5.10.015 ₽~300 ₽
Гибрид (роутер)~0.015 ₽ (среднее)~300 ₽

*при 2000 токенов на запрос (средний)

300 ₽ в месяц на API при 10 000 запросах. Это себестоимость SaaS, за который можно брать 990–4 990 ₽/мес с пользователя.

Модели монетизации SaaS на LLM

ПродуктТарифСебестоимость APIМаржа
Генератор бизнес-планов1 990–9 990 ₽~5 ₽ на план99.7%
SEO-аудитор990 ₽/мес~10 ₽/мес99%
AI-консультант (бот)1 500 ₽/мес~15 ₽/мес99%

Маржа 99% возможна потому, что API стоит копейки, а ценность продукта — в логике, архитектуре и результате.

Где взять клиентов на SaaS

Не надо таргета и бюджетов. Работает другое:

  1. Пишешь статью как эта — с кодом, схемами, цифрами. Публикуешь на vc.ru, Хабре, в Telegram-каналах
  2. В статье — CTA: «Если нужно готовое решение — вот ссылка на сервис»
  3. Первый клиент приходит не с рекламы, а со статьи, которую ты написал 3 месяца назад

Моя воронка на bbb2.ru: статья на vc.ru → 12 000 просмотров → 340 переходов на сайт → 8 заявок → 3 продажи.

Что это значит для вас: SaaS на LLM не требует команды, инвестиций и офиса. Нужен роутер (код выше), домен, VPS за 600 ₽ и понимание задачи клиента. 300 ₽ в месяц на API — и можно продавать продукт за 1 990–9 990 ₽. Маржа — 99%. Код открытый, архитектура рабочая. Берите и стройте.