Перейти к содержимому
Экосистема AI Vibe
← Назад к AI Vibe News

Редакция 4 августа 2026 г.

Разборы

Retry в промпте агента может опубликовать статью дважды: кейс MCP и DEV.to

Retry в промпте агента может опубликовать статью дважды: кейс MCP и DEV.to

Если вы ведёте соло-пайплайн «агент пишет и выкладывает за меня» через MCP и scheduled task, инструкция «повторить при ошибке» в task prompt опаснее, чем кажется: таймаут после успешного POST на сервере неотличим от «запрос не дошёл», и второй вызов создаёт дубликат. Разбор enjoy_kumawat показывает, как retry для 429 и для URLError — разные классы риска, и как закрыть дыру проверкой уже опубликованного title — без переписывания промпта агенту.

Retry зашит в task prompt, а не в код скрипта

Типичный agentic workflow: scheduled task дважды в сутки генерирует материал и запускает публикацию. В шаге 4 инструкций задачи явно прописано: при ответе API 429 подождать 35 секунд и повторить попытку. Автор следовал этому правилу десятки раз — retry живёт в промпте агента, а не в publish_devto.py.

Скрипт до фикса при любой HTTPError или URLError завершался через sys.exit. Таймаут urlopen — 30 секунд. Агент между отдельными invocations задачи не видит, создал ли сервер статью до обрыва соединения.

Промпт говорит «retry» — а код молча выходит. Caller решает повторить. Именно здесь ломается идемпотентность.

Два входа в один API: MCP-tool и publish-скрипт

Один репозиторий — small MCP server — обслуживает два независимых пути к POST https://Dev.to/api/articles:

  1. publish_devto.py — то, что реально вызывает scheduled task («go live»).
  2. create_article в server.py — для MCP-клиентов вроде Claude Desktop.

Один класс бага в двух местах: и скрипт, и MCP-tool до фикса делали «слепой» POST без проверки, опубликовано ли уже. Retry после timeout или dropped connection у MCP-клиента попадает в ту же ловушку, что и повторный запуск задачи по инструкции из промпта.

Почему Dev.to не страхует от слепого retry

API Dev.to не поддерживает idempotency-key: нет заголовка Idempotency-Key, нет client-supplied request ID для «уже обработанного» write. POST /api/articles либо создаёт статью, либо нет — режима «выполни только один раз» нет.

Для 429 повтор логичен: запрос отклонён до обработки, на сервере ничего не появилось. URLError при таймауте шире — DNS, обрыв, timeout. Caller не знает, успел ли первый POST пройти server-side.

Дилемма простая: не retry — рискуете потерять публикацию; retry — рискуете второй копией с тем же title и body, если первый POST уже прошёл.

Автор намеренно не гонял таймаут против production API — «risking an actual duplicate live article». Вместо этого воспроизвёл сценарий offline.

Offline repro: один publish, две записи

Fake urlopen на POST записывает статью в server_articles, затем бросает URLError("timed out"):

  • Первый вызов unfixed-скрипта: exit с timeout, в server_articles уже одна запись.
  • Второй вызов (имитация retry из task instructions): снова exit, в списке две записи с идентичным title — «one intended publish, two live articles».
# Упрощённая логика repro из поста
# POST → статья на «сервере» → URLError("timed out")
# Retry → второй POST с тем же title/body → дубликат

Фикс: live-список как общий source of truth

Перед POST при published=True скрипт вызывает already_published: GET https://Dev.to/api/articles/me/published?per_page=30, сравнение title. Совпадение — вывод ALREADY PUBLISHED (skipped duplicate), возврат URL без второго POST.

Лимит per_page=30 намеренный: покрывает окно «retry вскоре после timeout», не претендует на reuse старого title месяцами спустя.

Та же проверка в create_article в server.py: при совпадении title — already_published: True без POST. Stub-тест с title уже в списке подтверждает, что POST не вызывается.

После фикса при повторном запуске repro server_articles остаётся с одной записью.

Автор сознательно не добавлял client-side idempotency key, локальный кэш «уже пытался» и встроенный retry/backoff в скрипт. Аргумент: retry происходит между отдельными invocations scheduled task без гарантированного shared state; проверка live-списка на Dev.to — общий якорь для всех попыток, включая MCP и агента.

Инструкция «retry on 429» в task prompt не менялась — расширили безопасность для failure modes, которые caller может перепутать с «безопасным» retry.

Практика для соло-workflow с агентами и MCP

Мотивация автора — пост в MCP-теге про idempotent agent writes; сначала казалось «не про меня» (нет distributed queue), затем проверил свой single write path с explicit retry в промпте.

Вывод для тех, кто собирает похожий пайплайн:

  • Разделяйте классы ошибок в голове агента и в guardrails кода. 429 и timeout — не одно и то же; промпт не заменяет проверку «уже опубликовано».
  • Один API — один класс защиты на всех entry points. MCP-tool и cron-скрипт должны сходиться на одном source of truth — здесь published list по title.
  • Не полагайтесь на idempotency API, если платформа её не даёт: стабильный ключ (title + список опубликованных) лучше, чем надежда на «retry безопасен».

Если в вашем цикле агент сам себе задаёт retry в task prompt — заложите такую проверку до POST, а не после N-го «успешного» таймаута.

Источники