Разбор на Habr: почему r.Context() не останавливает LLM и как строить AI-задачи на Go
Habr опубликовал разбор типичной ловушки Go-бэкендов с LLM. Привычная схема отменяет работу вместе с HTTP-запросом через r.Context(). Но если пользователь закрыл вкладку посреди анализа документа, провайдер может продолжать генерацию и выставлять счёт за токены. Автор предлагает выносить длительные AI-операции в фоновые задачи на PostgreSQL и отдельно проработать идемпотентность, retry и защиту от повторных списаний. • Контекст запроса не равен жизненному циклу AI-задачи. • Состояние задачи лучше хранить в PostgreSQL, а не в памяти процесса. • Retry без идемпотентности означает двойную оплату API.
AI-обработка оригинала Habr AI; редакция Hamidun News
Закрытая вкладка браузера отменяет HTTP-запрос, но не обязана останавливать модель на стороне провайдера. Автор свежего разбора на Habr показывает, как эта разница превращается в расходы и баги в обычном Go-бэкенде. Если пользователь запустил анализ документа и ушёл, сервис может остаться без результата, а счёт за токены всё равно придёт. Для длительных AI-операций автор предлагает фоновые задачи на PostgreSQL, строгую идемпотентность и осознанную политику повторов.
Где ломается привычная модель
Go-разработчики привыкли к простому правилу: работа живёт, пока жив `r.Context()`. Клиент оборвал соединение, контекст отменился, горутины завершились, ресурсы освободились. Для запроса к базе или внутреннему микросервису это удобно и правильно.
С LLM всё иначе. Вызов может длиться десятки секунд или минуты, особенно при разборе больших документов или цепочках из нескольких обращений к модели. Тарификация идёт за обработанные и сгенерированные токены, и отмена на вашей стороне не гарантирует, что провайдер немедленно прекратит работу. Это зависит от конкретного API и от того, как закрыто соединение.
Возникают сразу две проблемы:
- работа отменена, результата нет, а деньги потрачены;
- пользователь возвращается, нажимает кнопку ещё раз, и платная операция запускается повторно.
Фоновая задача как самостоятельная сущность
Основная идея разбора в том, что AI-операция не должна принадлежать HTTP-запросу. Обработчик принимает вход, создаёт задачу и сразу отвечает идентификатором. Дальше задача живёт собственной жизнью, а клиент узнаёт о ней через опрос, SSE или WebSocket.
В качестве хранилища автор берёт PostgreSQL. Для многих команд это разумный выбор: база уже есть в стеке, транзакции понятны, а отдельный брокер сообщений не нужен. Типичный набор для такой очереди выглядит так:
- таблица задач со статусом (создана, выполняется, завершена, ошибка, отменена);
- захват задач воркерами через `FOR UPDATE SKIP LOCKED`, чтобы два воркера не взяли одну и ту же;
- время последнего heartbeat, по которому зависшие задачи возвращаются в очередь;
- счётчик попыток и время следующей попытки;
- сохранённый результат и данные об использованных токенах.
Главный выигрыш в том, что состояние переживает рестарт сервиса. При деплое или падении пода задача не пропадает вместе с горутиной, а подбирается другим воркером.
Идемпотентность и деньги
Самое дорогое место в такой системе находится на стыке retry и биллинга. Повтор запроса к LLM стоит денег, поэтому его нельзя делать вслепую, как повтор обращения к кэшу.
Практические принципы, которые следуют из логики разбора:
- ключ идемпотентности создаётся на уровне пользовательского действия и защищает от двойного нажатия кнопки;
- у задачи есть уникальный идентификатор, по которому результат записывается один раз;
- retry разрешён только для ошибок, где повтор имеет смысл (таймаут, 429, 5xx), но не для невалидного ввода;
- списание средств у пользователя привязано к задаче, а не к отдельной попытке;
- отмена пользователем становится явным статусом, а не побочным эффектом разорванного соединения.
С чего начать
Если в вашем сервисе LLM-вызов выполняется прямо в обработчике, проверьте три вещи. Первое: что произойдёт с оплаченной генерацией, если клиент отключится на середине. Второе: может ли повторный запрос создать дубль платной операции. Третье: переживёт ли задача перезапуск процесса.
Если хотя бы на один вопрос ответ неудобный, нужна отдельная модель задачи. Минимальный шаг — таблица со статусами и один воркер, а `SKIP LOCKED`, heartbeat и политику повторов можно добавлять по мере роста нагрузки. Отдельный брокер имеет смысл подключать, когда PostgreSQL перестанет справляться с очередью. До этого момента он, как правило, только усложняет систему.
Материал не про новую модель или релиз, а про инженерную дисциплину вокруг LLM. Именно там сейчас теряется заметная часть бюджетов AI-продуктов: ошибки возникают в обвязке, а не в промптах.
Вопросы и ответы по материалу
Что известно по теме: где ломается привычная модель?
Go-разработчики привыкли к простому правилу: работа живёт, пока жив `r.Context()`. Клиент оборвал соединение, контекст отменился, горутины завершились, ресурсы освободились. Для запроса к базе или внутреннему микросервису это удобно и правильно. С LLM всё иначе. Вызов может длиться десятки секунд или минуты, особенно при разборе больших документов или цепочках из нескольких обращений к модели. Тарификация идёт за обработанные и сгенерированные токены, и отмена на вашей стороне не гарантирует, что провайдер немедленно прекратит работу. Это зависит от конкретного API и от тог
Что известно по теме: фоновая задача как самостоятельная сущность?
Основная идея разбора в том, что AI-операция не должна принадлежать HTTP-запросу. Обработчик принимает вход, создаёт задачу и сразу отвечает идентификатором. Дальше задача живёт собственной жизнью, а клиент узнаёт о ней через опрос, SSE или WebSocket. В качестве хранилища автор берёт PostgreSQL. Для многих команд это разумный выбор: база уже есть в стеке, транзакции понятны, а отдельный брокер сообщений не нужен. Типичный набор для такой очереди выглядит так: таблица задач со статусом (создана, выполняется, завершена, ошибка, отменена); захват задач воркерами через `FOR UPD
Что известно по теме: идемпотентность и деньги?
Самое дорогое место в такой системе находится на стыке retry и биллинга. Повтор запроса к LLM стоит денег, поэтому его нельзя делать вслепую, как повтор обращения к кэшу. Практические принципы, которые следуют из логики разбора: ключ идемпотентности создаётся на уровне пользовательского действия и защищает от двойного нажатия кнопки; у задачи есть уникальный идентификатор, по которому результат записывается один раз; retry разрешён только для ошибок, где повтор имеет смысл (таймаут, 429, 5xx), но не для невалидного ввода; списание средств у пользователя привязано к задаче,
Системное обучение работе с ИИ и AI-агентами
Образовательная платформа Vibe Academy (vibe-academy.ru) — практические курсы, промпт-инжиниринг, автоматизация процессов и прикладные AI-воркфлоу.
Главное из мира ИИ — раз в неделю
7 ключевых событий недели, отобранных вручную. Без шума, репостов и пресс-релизов.
Готово! Проверьте почту — мы отправили подтверждение.