Разбор на Habr: почему r.Context() не останавливает LLM и как строить AI-задачи на Go

Habr опубликовал разбор типичной ловушки Go-бэкендов с LLM. Привычная схема отменяет работу вместе с HTTP-запросом через r.Context(). Но если пользователь закрыл вкладку посреди анализа документа, провайдер может продолжать генерацию и выставлять счёт за токены. Автор предлагает выносить длительные AI-операции в фоновые задачи на PostgreSQL и отдельно проработать идемпотентность, retry и защиту от повторных списаний. • Контекст запроса не равен жизненному циклу AI-задачи. • Состояние задачи лучше хранить в PostgreSQL, а не в памяти процесса. • Retry без идемпотентности означает двойную оплату API.

AI-обработка оригинала Habr AI; редакция Hamidun News
Разбор на Habr: почему r.Context() не останавливает LLM и как строить AI-задачи на Go
Источник: Habr AI. Коллаж: Hamidun News.
Слушать статью
Паспорт материала и цифры
ПараметрДанные
ПервоисточникHabr AI ↗
Главный редактор
Ключевой факт #1Контекст запроса не равен жизненному циклу AI-задачи.
Ключевой факт #2Состояние задачи лучше хранить в PostgreSQL, а не в памяти процесса.
Ключевой факт #3Retry без идемпотентности означает двойную оплату API.

Закрытая вкладка браузера отменяет 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), но не для невалидного ввода; списание средств у пользователя привязано к задаче,

ЖХ
Жемал Хамидун — CPO Alpina Digital, продакт-менеджер AlpinaGPT. Делает AI-продукты с 2019 года. Hamidun News — pet project, который вырос в production-агрегатор с 6000+ читателей.
✦ Образовательная экосистема

Системное обучение работе с ИИ и AI-агентами

Образовательная платформа Vibe Academy (vibe-academy.ru) — практические курсы, промпт-инжиниринг, автоматизация процессов и прикладные AI-воркфлоу.

Перейти в Vibe Academy →Интенсив по Вайб-кодингу
Что вы думаете?
Загружаем комментарии…