Синхронные и асинхронные агентные пайплайны: чем платите
Один медленный вызов LLM в цикле может держать поток 20 секунд. Разбираем, когда агентный пайплайн стоит гонять синхронно, а когда переписать на async — с кодом на Python и ценой ошибки в задержке и деньгах.

Агент делает пять вызовов к модели подряд: планирование, три поиска в инструментах, финальный ответ. Каждый вызов к внешнему API — от 0,5 до 20 секунд ожидания сети, в течение которого процессор ничего не считает, а просто ждёт ответа. Если вы обрабатываете эти шаги синхронно и в одном потоке, вы платите суммой всех задержек. Если параллелите независимые шаги — платите задержкой самого медленного. Разница на реальном пайплайне легко доходит до кратной.
Вопрос не в том, что асинхронность «быстрее» — она не быстрее для одной операции. Вопрос в том, где у вас ожидание ввода-вывода, сколько его, и можно ли перекрыть одно ожидание другим. Ниже — где граница проходит на практике.
Что на самом деле различается
Термины путают, потому что за словом «асинхронный» прячутся две разные вещи: конкурентность внутри одного процесса (async/await, событийный цикл) и распределённость через очередь задач (worker, брокер сообщений). Для агентных пайплайнов важны обе, но решают они разные проблемы.
- Синхронный пайплайн — шаги идут строго по очереди, каждый блокирует следующий. Запрос пользователя висит открытым, пока агент не закончит. Просто отлаживается, легко читается стектрейс.
- Асинхронный в пределах процесса — независимые вызовы к модели и инструментам запускаются конкурентно через событийный цикл. Тот же запрос пользователя всё ещё висит открытым, но внутри работа перекрывается.
- Асинхронный через очередь — запрос сразу возвращает идентификатор задачи, а сам пайплайн выполняется в фоновом воркере. Пользователь опрашивает статус или получает вебхук. Единственный вариант, который переживает пайплайны на минуты и часы.
Смешивать их полезно: воркер, который внутри себя гоняет async-конкурентные вызовы к LLM, — типичная продакшн-конфигурация.
Асинхронность не ускоряет вычисление. Она перестаёт заставлять вас ждать того, что и так ждёт сеть.
Где синхронный пайплайн — правильный выбор
Соблазн «сделать всё асинхронным» дорого обходится на отладке. Синхронный код остаётся разумным выбором в нескольких случаях.
Шаги строго зависят друг от друга
Если шаг 2 не может стартовать без результата шага 1 — а в агентных цепочках это норма, потому что следующий вызов модели строится на предыдущем ответе — параллелить нечего. Async здесь добавит синтаксического шума и не даст ни миллисекунды выигрыша. Цепочка «планирование → действие → наблюдение → следующее действие» по своей природе последовательна.
Один пользователь, короткий цикл, прототип
Пока вы отлаживаете логику агента локально, синхронный код читается сверху вниз, ошибки видно в стектрейсе целиком, а точки останова работают предсказуемо. Переписывать на async имеет смысл, когда пайплайн доказал ценность и упёрся в задержку или в число одновременных пользователей.
Где асинхронность окупается
Выигрыш появляется там, где есть параллелимое ожидание. Три сценария, где это почти всегда так:
- Веерные вызовы (fan-out). Агенту нужно опросить пять источников и свести результаты. Синхронно — сумма пяти задержек. Конкурентно — задержка самого медленного источника.
- Много одновременных запросов. Один синхронный воркер на запрос упирается в память и число потоков. Событийный цикл держит сотни ожидающих корутин на одном потоке, пока они простаивают на сети.
- Долгие пайплайны. Если цепочка идёт минуту и дольше, держать HTTP-соединение открытым — плохая идея: таймауты балансировщика, ретраи клиента, повторный запуск дорогого пайплайна. Тут нужна очередь.
Как выглядит fan-out на async
Пример на Python: параллельно вызываем несколько инструментов агента и собираем результаты. asyncio.gather запускает корутины конкурентно, общее время — время самого медленного вызова, а не сумма.
import asyncio
async def call_tool(session, name, query):
# реальный сетевой вызов к инструменту или модели
async with session.post(f"/tools/{name}", json={"q": query}) as r:
return name, await r.json()
async def fan_out(session, query):
tools = ["search", "docs", "calendar"]
tasks = [call_tool(session, t, query) for t in tools]
# ждём все, но конкурентно; return_exceptions чтобы один сбой
# не ронял остальные
results = await asyncio.gather(*tasks, return_exceptions=True)
return {name: res for name, res in results
if not isinstance(res, Exception)}
Ключевое здесь — return_exceptions=True. Без него первое же исключение в любой из задач роняет весь gather, а конкурентно запущенные вызовы к платной модели вы всё равно оплатите. С флагом вы получаете частичный результат и сами решаете, деградировать или падать.
Сравнение по параметрам
| Параметр | Синхронный | Async в процессе | Очередь + воркеры |
|---|---|---|---|
| Независимые вызовы | сумма задержек | задержка максимума | задержка максимума |
| Одновременных запросов | ограничено потоками | сотни на поток | масштаб воркерами |
| Пайплайн на минуты | таймауты | таймауты | штатный режим |
| Сложность отладки | низкая | средняя | высокая |
| Инфраструктура | только приложение | только приложение | + брокер, + мониторинг |
Скрытые издержки асинхронности
Async не бесплатен, и счёт приходит не в момент, когда вы его пишете, а когда что-то ломается в проде.
- Отладка сложнее. Стектрейс корутины обрывается на событийном цикле, а не показывает всю цепочку вызовов. Нужны структурные логи с идентификатором задачи, иначе вы не свяжете строки лога между собой.
- Одна блокирующая операция убивает выигрыш. Синхронный вызов внутри async-функции блокирует весь событийный цикл, и все конкурентные задачи встают. Библиотека клиента к модели должна быть async целиком, иначе смысла нет.
- Лимиты провайдера. Запустив пятьдесят вызовов конкурентно, вы упрётесь в rate limit API модели и получите шквал ответов 429. Нужен семафор, ограничивающий число одновременных запросов.
- Стоимость при сбое. Конкурентные вызовы к платной модели, часть которых вы отбросите, оплачиваются полностью. При fan-out на десятки запросов это заметная строка в счёте.
Семафор в тот же пример добавляется так:
sem = asyncio.Semaphore(5) # не больше 5 одновременных вызовов
async def call_tool(session, name, query):
async with sem:
async with session.post(f"/tools/{name}",
json={"q": query}) as r:
return name, await r.json()
Практический порядок решения
Не выбирайте архитектуру заранее — выведите её из свойств пайплайна.
- Постройте пайплайн синхронно и измерьте, где реально уходит время. Часто окажется, что 90% задержки — один шаг, и параллелить остальное бессмысленно.
- Есть ли независимые вызовы, которые идут по очереди? Если да — переведите именно их на конкурентный async, остальное оставьте как есть.
- Пайплайн стабильно длиннее 20-30 секунд или таймаутит балансировщик? Выносите в очередь с воркерами и статусом задачи.
- Добавьте семафор на число конкурентных вызовов к модели раньше, чем поймаете первый 429 в проде.
Правильный ответ почти всегда гибридный: синхронная последовательная цепочка рассуждений агента, внутри которой параллелятся веерные вызовы инструментов, а весь пайплайн целиком крутится в фоновом воркере под очередью. Начинать с этой конструкции сразу — переусложнение; приходить к ней по мере роста нагрузки — норма.
Событийный цикл* — механизм в основе async: один поток по очереди выполняет готовые к работе корутины, а те, что ждут сеть, снимает с исполнения до прихода данных. Именно поэтому один поток держит сотни конкурентных ожиданий, но ни одна CPU-тяжёлая операция не должна его блокировать.
Prompt-инженер: Идеальные запросы для Midjourney, ChatGPT и других моделей.
Спросить за 15 ₽Источники: Python docs — asyncio: Coroutines and Tasks
Частые вопросы
Async действительно быстрее синхронного кода?
Можно ли параллелить шаги рассуждения агента?
Когда нужна очередь задач, а не async в процессе?
Почему конкурентные вызовы упираются в ошибки 429?
Стоит ли сразу писать пайплайн на async?
Оплачиваются ли отброшенные конкурентные вызовы к модели?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.