Batch API: пакетная обработка запросов к LLM

Как использовать Batch API для снижения стоимости LLM на 50%. OpenAI, Anthropic, Google Vertex AI — примеры и паттерны.

Batch API (пакетная обработка) — это способ отправить тысячи запросов к LLM одним пакетом и получить результаты через несколько часов вместо мгновенного ответа. Взамен провайдеры дают скидку 50% на все токены. Если у вас есть задачи, которые не требуют ответа в реальном времени — eval-пайплайны, разметка данных, генерация контента, массовый перевод — Batch API экономит половину бюджета без единой строчки оптимизации кода.

Представьте, что вы отправляете письма. Можно отправить каждое отдельно курьером (дорого, но быстро), а можно собрать все в мешок и отправить почтой (дешевле вдвое, но дойдёт через день). Batch API — это «почтовый мешок» для запросов к AI: вы собираете сотни или тысячи запросов, отправляете пакетом, а через несколько часов забираете готовые ответы. Половина стоимости — за терпение.

Суть в двух словах

Batch API позволяет отправить сотни или тысячи запросов к LLM одним пакетом и получить ответы в течение 24 часов. Главное преимущество — скидка 50% у OpenAI и Anthropic на все токены в пакете.

Когда использовать:

  • Eval-пайплайны — прогон тестовых наборов на тысячах примеров
  • Разметка и классификация данных — категоризация тикетов, отзывов, документов
  • Генерация контента — массовое создание описаний, переводов, суммаризаций
  • Эмбеддинги — векторизация больших коллекций документов
  • Синтетические данные — генерация обучающих примеров для fine-tuning

Как это работает:

Без техники
5 000 запросов классификации в реальном времени. Claude Sonnet 4 по $3/1M input + $15/1M output. Стоимость: ~$45/день.
С техникой
Те же 5 000 запросов через Batch API. Скидка 50%: $1.50/1M input + $7.50/1M output. Стоимость: ~$22/день. Экономия: $690/мес.
Стоимость 10 000 запросов: реальное время vs batch ($)
Claude Sonnet (realtime)90%
Claude Sonnet (batch)45%
GPT-4.1 (realtime)50%
GPT-4.1 (batch)25%
Claude Haiku (realtime)30%
Claude Haiku (batch)15%

Плюсы

  • Скидка 50% на все токены (input + output)
  • Выше rate limits — можно отправить десятки тысяч запросов
  • Идеально для ETL, eval, ночных обработок
  • Простой API — минимум кода для интеграции

Минусы

  • Результат через часы, не мгновенно (SLA — 24 часа)
  • Нет стриминга — только полные ответы
  • Нужна логика для мониторинга статуса и обработки ошибок
  • Ограничения на размер пакета (зависит от провайдера)

Зачем нужен Batch API

Большинство LLM-задач в продакшене не требуют мгновенного ответа. Eval-пайплайны, ежедневная классификация тикетов, генерация отчётов, разметка данных, создание эмбеддингов — всё это может подождать несколько часов. Провайдеры это понимают и предлагают сделку: вы отказываетесь от мгновенного ответа, а они снижают цену вдвое.

За сценой Batch API позволяет провайдерам более эффективно утилизировать GPU. Вместо того чтобы держать ресурсы для мгновенного ответа на каждый запрос, они могут группировать пакетные запросы и обрабатывать их в периоды низкой нагрузки. Выигрывают все: вы платите меньше, провайдер лучше загружает инфраструктуру.

Batch API — самый простой способ сэкономить. В отличие от prompt caching, model routing или prompt compression, здесь не нужно менять архитектуру приложения. Вы просто отправляете те же самые запросы пакетом и получаете 50% скидку.

Сценарии использования

Eval-пайплайны

Оценка качества LLM-приложения требует прогона сотен или тысяч тестовых примеров. Каждый пример — отдельный запрос к LLM (LLM-as-judge). При 2 000 примеров в eval-наборе и стоимости $0.01 за запрос обычный прогон стоит $20. Через Batch API — $10. На ежедневных прогонах это $300/мес экономии.

Разметка и обогащение данных

Классификация тикетов поддержки, извлечение сущностей из документов, категоризация отзывов — типичные offline-задачи. Массив из 50 000 записей, обработанный через batch, стоит в 2 раза дешевле.

Генерация контента

Массовое создание описаний товаров, мета-тегов, переводов. 10 000 описаний через реальное время — $150, через batch — $75.

Создание эмбеддингов

Векторизация большой коллекции документов для RAG-системы. OpenAI Batch API поддерживает эмбеддинг-запросы — те же 50% скидка.

Синтетические данные для fine-tuning

Генерация обучающих примеров через сильную модель (Opus, GPT-4.1) для дальнейшего дообучения дешёвой. 100 000 примеров — серьёзная экономия.

Сравнение провайдеров

OpenAI Batch API

ПараметрЗначение
Эндпоинт/v1/batches
Формат входаJSONL-файл
Скидка50%
SLA24 часа
Поддерживаемые API/v1/chat/completions, /v1/embeddings, /v1/completions
Макс. запросов50 000 на пакет
Макс. размер файла200 МБ
МоделиВсе GPT-4.1, GPT-4o, GPT-4o-mini, o3, o4-mini

Процесс:

  1. Загрузить JSONL-файл через Files API (/v1/files)
  2. Создать batch (POST /v1/batches)
  3. Поллить статус (GET /v1/batches/{id})
  4. Скачать результат через Files API

Anthropic Message Batches

ПараметрЗначение
Эндпоинт/v1/messages/batches
Формат входаJSON-массив в теле запроса
Скидка50%
SLA24 часа
Поддерживаемые APIMessages API
Макс. запросов100 000 на пакет
МоделиClaude Opus 4, Sonnet 4, Haiku 3.5

Процесс:

  1. Отправить массив запросов в POST /v1/messages/batches
  2. Поллить статус (GET /v1/messages/batches/{id})
  3. Итерировать результаты (GET /v1/messages/batches/{id}/results)
У Anthropic не нужно отдельно загружать файл — запросы передаются прямо в теле POST-запроса как JSON-массив. У OpenAI файл загружается отдельно через Files API, а в batch передаётся input_file_id.

Google Vertex AI Batch Prediction

ПараметрЗначение
Формат входаJSONL в Cloud Storage
Скидка50%
SLA24 часа
МоделиGemini Pro, Gemini Flash

Vertex AI использует batch prediction jobs. Входные данные — JSONL-файл в Google Cloud Storage, результаты пишутся туда же.

Когда использовать batch, а когда реальное время

КритерийBatch APIRealtime API
Время ответаЧасы (до 24ч)Секунды
Стоимость50% скидкиПолная цена
СтримингНетДа
Rate limitsВышеСтандартные
RetryАвтоматическийНужен свой
Пользователь ждёт?НетДа
Не пытайтесь использовать Batch API для интерактивных сценариев, где пользователь ждёт ответ. Чат-боты, поиск, автодополнение — всё это только realtime. Batch — для фоновых задач.

Сравнение стоимости по объёму

Месячная стоимость: realtime vs batch (Claude Sonnet 4, $)
1K req/day realtime270%
1K req/day batch135%
10K req/day realtime2700%
10K req/day batch1350%
50K req/day realtime13500%
50K req/day batch6750%

При 50 000 запросов в день Batch API экономит $6 750/мес ($81 000/год) только на одном переключении режима.

Паттерны интеграции

Batch + Webhook

Вместо поллинга статуса используйте webhook-уведомления. При завершении пакета провайдер отправляет POST-запрос на ваш URL.

Batch в CI/CD

Eval-прогон через Batch API в pull request pipeline. Дешевле и не блокирует CI — результат приходит асинхронно, а PR-check обновляется через webhook или polling job.

Ночные batch-задачи

Классическая схема: cron-задача в 02:00 собирает накопленные за день запросы, отправляет batch, утром результаты готовы. Идеально для отчётов, классификации, обогащения данных.

Chunking больших датасетов

При 100 000+ запросов разбивайте на чанки по 10 000-50 000 и отправляйте несколько пакетов параллельно. Это повышает надёжность (сбой одного пакета не теряет все данные) и ускоряет обработку.

Best practices

Ограничения Batch API

  • Нет стриминга — ответы приходят только целиком после обработки всего пакета
  • SLA 24 часа — нет гарантии быстрой обработки, хотя обычно готово за 1-6 часов
  • Лимиты на размер — до 50 000 запросов (OpenAI) или 100 000 (Anthropic) на пакет
  • Expiration — незавершённые пакеты удаляются через 24 часа
  • Нельзя отменить отдельные запросы — только весь пакет целиком

Реализация Batch API

OpenAI Batch API — полный пример

import json
import time
from openai import OpenAI

client = OpenAI()


def prepare_batch_file(
    requests: list[dict],
    model: str = "gpt-4.1-mini",
) -> str:
    """Подготовка JSONL-файла для batch."""
    lines = []
    for i, req in enumerate(requests):
        lines.append(json.dumps({
            "custom_id": req.get("id", f"req-{i}"),
            "method": "POST",
            "url": "/v1/chat/completions",
            "body": {
                "model": model,
                "max_tokens": req.get("max_tokens", 512),
                "messages": req["messages"],
            },
        }))
    return "\n".join(lines)


def run_openai_batch(
    requests: list[dict],
    model: str = "gpt-4.1-mini",
    description: str = "batch job",
) -> dict:
    """Полный цикл OpenAI Batch API."""
    # 1. Подготовить JSONL
    jsonl_content = prepare_batch_file(requests, model)

    # 2. Загрузить файл
    with open("/tmp/batch_input.jsonl", "w") as f:
        f.write(jsonl_content)

    input_file = client.files.create(
        file=open("/tmp/batch_input.jsonl", "rb"),
        purpose="batch",
    )
    print(f"File uploaded: {input_file.id}")

    # 3. Создать batch
    batch = client.batches.create(
        input_file_id=input_file.id,
        endpoint="/v1/chat/completions",
        completion_window="24h",
        metadata={"description": description},
    )
    print(f"Batch created: {batch.id}")

    # 4. Поллить статус
    while batch.status not in ("completed", "failed", "expired", "cancelled"):
        time.sleep(30)
        batch = client.batches.retrieve(batch.id)
        counts = batch.request_counts
        print(
            f"Status: {batch.status}, "
            f"completed: {counts.completed}/{counts.total}, "
            f"failed: {counts.failed}"
        )

    if batch.status != "completed":
        raise RuntimeError(f"Batch failed: {batch.status}, errors: {batch.errors}")

    # 5. Скачать результаты
    output_file = client.files.content(batch.output_file_id)
    results = {}
    for line in output_file.text.strip().split("\n"):
        item = json.loads(line)
        custom_id = item["custom_id"]
        if item["response"]["status_code"] == 200:
            body = item["response"]["body"]
            results[custom_id] = {
                "content": body["choices"][0]["message"]["content"],
                "usage": body["usage"],
            }
        else:
            results[custom_id] = {"error": item["response"]["body"]}

    return results


# Пример: классификация 1000 тикетов
tickets = [
    "Не могу войти в аккаунт после смены пароля",
    "Хочу оформить возврат товара, заказ #12345",
    "Ваше приложение вылетает при открытии камеры",
    # ... ещё 997 тикетов
]

requests = [
    {
        "id": f"ticket-{i}",
        "messages": [
            {
                "role": "system",
                "content": (
                    "Classify support ticket into one of: "
                    "billing, technical, account, shipping, other. "
                    "Reply with JSON: {\"category\": \"...\", \"priority\": \"low|medium|high\"}"
                ),
            },
            {"role": "user", "content": ticket},
        ],
        "max_tokens": 64,
    }
    for i, ticket in enumerate(tickets)
]

results = run_openai_batch(requests, model="gpt-4.1-mini", description="ticket classification")
for custom_id, result in list(results.items())[:3]:
    print(f"{custom_id}: {result.get('content', result.get('error'))}")

Anthropic Message Batches — полный пример

import time
import anthropic

client = anthropic.Anthropic()


def run_anthropic_batch(
    requests: list[dict],
    model: str = "claude-haiku-3.5-20241022",
) -> dict:
    """Полный цикл Anthropic Message Batches."""
    # 1. Подготовить запросы
    batch_requests = []
    for i, req in enumerate(requests):
        batch_requests.append({
            "custom_id": req.get("id", f"req-{i}"),
            "params": {
                "model": model,
                "max_tokens": req.get("max_tokens", 512),
                "messages": req["messages"],
                "system": req.get("system", ""),
            },
        })

    # 2. Создать batch
    batch = client.messages.batches.create(requests=batch_requests)
    print(f"Batch created: {batch.id}, status: {batch.processing_status}")

    # 3. Поллить статус
    while batch.processing_status != "ended":
        time.sleep(30)
        batch = client.messages.batches.retrieve(batch.id)
        counts = batch.request_counts
        print(
            f"Status: {batch.processing_status}, "
            f"succeeded: {counts.succeeded}, "
            f"errored: {counts.errored}, "
            f"processing: {counts.processing}"
        )

    # 4. Получить результаты
    results = {}
    for entry in client.messages.batches.results(batch.id):
        custom_id = entry.custom_id
        if entry.result.type == "succeeded":
            msg = entry.result.message
            results[custom_id] = {
                "content": msg.content[0].text,
                "usage": {
                    "input_tokens": msg.usage.input_tokens,
                    "output_tokens": msg.usage.output_tokens,
                },
            }
        else:
            results[custom_id] = {"error": str(entry.result)}

    return results


# Пример: eval-прогон с LLM-as-judge
eval_samples = [
    {
        "id": f"eval-{i}",
        "system": (
            "You are an evaluator. Score the AI response on a scale 1-5 "
            "for accuracy, helpfulness, and safety. "
            "Reply with JSON: {\"accuracy\": N, \"helpfulness\": N, \"safety\": N}"
        ),
        "messages": [
            {
                "role": "user",
                "content": f"Question: {sample['question']}\n\nAI Response: {sample['response']}",
            },
        ],
        "max_tokens": 128,
    }
    for i, sample in enumerate(eval_dataset)
]

results = run_anthropic_batch(eval_samples, model="claude-sonnet-4-20250514")

Chunking больших датасетов

from typing import Iterator


def chunk_requests(
    requests: list[dict],
    chunk_size: int = 10_000,
) -> Iterator[list[dict]]:
    """Разбивка на чанки для надёжности."""
    for i in range(0, len(requests), chunk_size):
        yield requests[i : i + chunk_size]


def run_chunked_batch(
    requests: list[dict],
    model: str = "gpt-4.1-mini",
    chunk_size: int = 10_000,
) -> dict:
    """Batch-обработка с чанкингом и retry."""
    all_results = {}
    failed_chunks = []

    for chunk_idx, chunk in enumerate(chunk_requests(requests, chunk_size)):
        print(f"Processing chunk {chunk_idx} ({len(chunk)} requests)...")
        try:
            results = run_openai_batch(
                chunk,
                model=model,
                description=f"chunk-{chunk_idx}",
            )
            all_results.update(results)
        except RuntimeError as e:
            print(f"Chunk {chunk_idx} failed: {e}")
            failed_chunks.append((chunk_idx, chunk))

    # Retry неудачных чанков
    for chunk_idx, chunk in failed_chunks:
        print(f"Retrying chunk {chunk_idx}...")
        try:
            results = run_openai_batch(chunk, model=model)
            all_results.update(results)
        except RuntimeError:
            print(f"Chunk {chunk_idx} failed again, skipping")

    return all_results


# 100 000 запросов, чанками по 10 000
all_results = run_chunked_batch(large_dataset, chunk_size=10_000)

Batch в CI/CD (GitHub Actions)

# .github/workflows/eval-batch.yml
name: LLM Eval (Batch)

on:
  pull_request:
    paths: ["prompts/**", "src/llm/**"]

jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run batch eval
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          python scripts/submit_eval_batch.py \
            --input eval/test_cases.jsonl \
            --model gpt-4.1-mini \
            --output /tmp/batch_id.txt

      - name: Wait for results
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          python scripts/poll_batch.py \
            --batch-id $(cat /tmp/batch_id.txt) \
            --timeout 3600 \
            --output eval/results.json

      - name: Check quality threshold
        run: |
          python scripts/check_eval_threshold.py \
            --results eval/results.json \
            --min-score 0.85

Batch + Cron (ночные задачи)

# scripts/nightly_batch.py
import schedule
import time
from datetime import datetime


def nightly_classification():
    """Ночная классификация накопленных тикетов."""
    # 1. Забрать необработанные записи из БД
    unprocessed = db.fetch_unprocessed_tickets(limit=50_000)
    if not unprocessed:
        print("No tickets to process")
        return

    print(f"Processing {len(unprocessed)} tickets at {datetime.now()}")

    # 2. Подготовить batch-запросы
    requests = [
        {
            "id": f"ticket-{t['id']}",
            "messages": [
                {"role": "system", "content": CLASSIFICATION_PROMPT},
                {"role": "user", "content": t["text"]},
            ],
            "max_tokens": 64,
        }
        for t in unprocessed
    ]

    # 3. Отправить batch (50% скидка)
    results = run_openai_batch(requests, model="gpt-4.1-mini")

    # 4. Записать результаты в БД
    for ticket_id, result in results.items():
        db_id = int(ticket_id.split("-")[1])
        if "content" in result:
            db.update_ticket_classification(db_id, result["content"])
        else:
            db.mark_ticket_error(db_id, str(result["error"]))

    print(f"Done. Processed: {len(results)}")


# Запуск каждый день в 02:00
schedule.every().day.at("02:00").do(nightly_classification)

while True:
    schedule.run_pending()
    time.sleep(60)

Мониторинг batch-задач

from dataclasses import dataclass, field
from datetime import datetime
import logging

logger = logging.getLogger(__name__)


@dataclass
class BatchMonitor:
    """Мониторинг batch-задач с алертами."""
    batches: dict = field(default_factory=dict)
    stale_threshold_hours: float = 12.0

    def track(self, batch_id: str, total_requests: int):
        self.batches[batch_id] = {
            "submitted_at": datetime.now(),
            "total": total_requests,
            "status": "submitted",
        }

    def update(self, batch_id: str, status: str, completed: int, failed: int):
        if batch_id not in self.batches:
            return
        entry = self.batches[batch_id]
        entry["status"] = status
        entry["completed"] = completed
        entry["failed"] = failed
        entry["last_update"] = datetime.now()

        # Алерт на зависший batch
        elapsed = (datetime.now() - entry["submitted_at"]).total_seconds() / 3600
        if elapsed > self.stale_threshold_hours and status not in ("completed", "failed"):
            logger.warning(
                f"Batch {batch_id} stale: {elapsed:.1f}h elapsed, "
                f"status={status}, completed={completed}/{entry['total']}"
            )

    def report(self) -> dict:
        """Сводка по всем batch-задачам."""
        total_cost_saved = 0
        for batch_id, entry in self.batches.items():
            if entry["status"] == "completed":
                # Примерная экономия: 50% от стоимости realtime
                total_cost_saved += entry["total"] * 0.005  # ~$0.005 на запрос
        return {
            "active_batches": sum(
                1 for b in self.batches.values()
                if b["status"] not in ("completed", "failed")
            ),
            "total_batches": len(self.batches),
            "estimated_savings": round(total_cost_saved, 2),
        }
ПромптOpenAI Batch API
POST /v1/batches
{
  "input_file_id": "file-abc123",
  "endpoint": "/v1/chat/completions",
  "completion_window": "24h",
  "metadata": {
    "description": "nightly ticket classification"
  }
}
Ответ модели

{ "id": "batch_abc123", "status": "validating", "request_counts": { "total": 5000, "completed": 0, "failed": 0 } }

ПромптAnthropic Message Batches
POST /v1/messages/batches
{
  "requests": [
    {
      "custom_id": "eval-001",
      "params": {
        "model": "claude-sonnet-4-20250514",
        "max_tokens": 256,
        "messages": [{"role": "user", "content": "..."}]
      }
    }
  ]
}
Ответ модели

{ "id": "msgbatch_abc123", "processing_status": "in_progress", "request_counts": { "processing": 5000, "succeeded": 0, "errored": 0 } }

Комбинируйте Batch API с prompt caching и дешёвыми моделями. Например: batch на Haiku с кэшированным системным промптом — это 50% скидка (batch) + 90% экономия на input (cache) + дешёвая модель. Тройная экономия.

Проверьте себя

Проверьте себя

1. Какую скидку даёт Batch API у OpenAI и Anthropic?

2. Какой сценарий НЕ подходит для Batch API?

3. Зачем ставить custom_id на каждый запрос в batch?

Связанные темы