Асинхронные запросы

Когда запросов много, ждать каждый по очереди — расточительно. На помощь приходит асинхронность.

Асинхронный запрос — вызов, который не блокирует программу на время ожидания ответа: пока один запрос «висит» в сети, можно отправлять и обрабатывать другие.

Зачем это нужно

Запрос к LLM долгий — секунды. Если обрабатывать 100 текстов последовательно (отправил → дождался → следующий), общее время — сумма всех ожиданий. Асинхронно их можно запустить «внахлёст»: пока ждём ответ на первый, уже летят второй, третий и так далее. Время сокращается в разы.

Последовательно:  [запрос1][запрос2][запрос3]      — медленно
Параллельно:      [запрос1]
                  [запрос2]
                  [запрос3]                       — быстро (внахлёст)

Как выглядит асинхронный клиент (Claude)

SDK даёт асинхронную версию клиента. Запросы запускают как корутины и ждут все сразу через asyncio.gather.

import asyncio
import anthropic

client = anthropic.AsyncAnthropic()

async def ask(text):
    resp = await client.messages.create(
        model="claude-opus-4-8",
        max_tokens=256,
        messages=[{"role": "user", "content": text}],
    )
    return "".join(b.text for b in resp.content if b.type == "text")

async def main():
    questions = ["Столица Франции?", "Столица Японии?", "Столица Бразилии?"]
    # Запускаем все три запроса одновременно
    answers = await asyncio.gather(*(ask(q) for q in questions))
    for q, a in zip(questions, answers):
        print(q, "->", a)

asyncio.run(main())

Подводные камни

  • Rate limits. Запустить 1000 запросов разом — верный способ получить шквал 429. Ограничивайте параллелизм (например, семафором на N одновременных запросов).
  • Обработка ошибок. Часть запросов может упасть; gather по умолчанию роняет всё на первой ошибке — продумайте, собирать ли ошибки или прерываться.
  • Не для всего. Для огромных нелатентных объёмов лучше batch-режим (раздел 5): он дешевле и со своими лимитами.

Async или потоки?

Цель — занять время ожидания сети полезной работой. В Python для этого есть асинхронные клиенты (как выше) и пулы потоков. Async обычно эффективнее для большого числа сетевых запросов: нет накладных расходов на потоки, всё крутится в одном цикле событий. Главное — не путать async с настоящей параллельностью вычислений: тяжёлый CPU-код от asyncio быстрее не станет, выигрыш только на ожидании ввода-вывода (а запрос к LLM — это как раз ожидание сети).

Семафор: ограничиваем одновременность

sem = asyncio.Semaphore(5)   # не больше 5 запросов разом

async def ask_limited(text):
    async with sem:
        return await ask(text)

Итог

  • Async позволяет отправлять много запросов «внахлёст» и резко сократить общее время.
  • SDK дают асинхронные клиенты (AsyncAnthropic / async-методы OpenAI).
  • Ограничивайте параллелизм семафором, чтобы не словить 429; для массивов — batch.
Проверьте себя
1. В чём смысл асинхронных запросов к LLM при обработке многих текстов?
AОни делают ответы точнее
BЗапросы выполняются внахлёст, не дожидаясь друг друга, что сокращает общее время
CОни отключают rate limits
DОни не требуют ключа API
2. Что произойдёт, если запустить тысячи асинхронных запросов одновременно без ограничений?
AОни выполнятся мгновенно без последствий
BСкорее всего, вы упрётесь в rate limit и получите массу ошибок 429
CПровайдер автоматически поднимет лимиты
DЗапросы выполнятся по очереди
3. Что лучше использовать для массовой нелатентной обработки больших объёмов?
AТолько синхронные запросы
BBatch-режим — он дешевле и со своими лимитами
CУвеличить temperature
DОдин огромный запрос
Поддержать проект