Архив рубрики ~Лента новостей~

Почему кросс-постинг в соцсети оказался задачей про state, idempotency и битую кириллицу

Почему кросс-постинг в соцсети оказался задачей про state, idempotency и битую кириллицу

В какой-то момент у меня появилась задача от маркетинга автоматизировать кросс-постинг статей в соц.сети и, в принципе, задача довольно понятная и здравая.

Запрос был такой — сделать короткий анонс из готовой статьи в блоге для размещения в соц.сети, подготовить рерайты для площадок, прикрепить картинку, поставить UTM-метки и опубликовать все по каналам. 

Т.к. задача рутинная, сразу возникла идея отдать ее агенту.
То есть, на входе статья в блоге, а на выходе Google Doc с короткими анонсами, рерайтами для Дзена и Spark, а далее публикация в Telegram и отложенные посты для остальных социальных сетей.

При первом же нормальном прогоне стало ясно что в этой задаче много подводных камней:

  • telegram-пост фактически ушел, но CLI вернул gateway timeout after 10000ms. 

  • ссылка была markdown-анкором, но визуально слиплась с текстом. 

  • исходный WebP весил 483 КБ, а после конвертации стал PNG на 4,7 МБ. 

  • VK уперся в антибот-проверку, OK потерял карточку предпросмотра.

  • в Google Doc с рерайтами попали битые символы U+FFFD.

Довольно быстро выяснилось, что кросс-постинг в данном случае это не действие, а пайплайн с несколькими независимыми зонами отказа — извлечение контента, редактура, подготовка медиа, доставка, платформенные особенности, проверка конечного состояния и отчетность. То есть нужно было прописать пайплайн с несколькими транспортами, разными форматами контента, внешними состояниями, медиа-ограничениями, редакционными правилами и проверками результата.

И заработало это все только когда накопившиеся ошибки начали превращаться в инварианты, проверки и стоп-факторы.

Короче, распишу все эти проблемы, может кому пригодится в работе, учитывая что маркетинг сейчас очень сильно хочет в ИИ.

Кросс-постинг не является одной операцией

На входе была статья из блога, а на выходе нужен был не один пост, а набор артефактов:

  • короткий анонс для соцсетей;

  • ссылка с корректными UTM-метками;

  • изображение, подходящее под ограничения площадок;

  • Google Doc с материалами;

  • отдельные рерайты для площадок, где нужен не анонс, а самостоятельный текст;

  • публикация в телегу;

  • планирование остальных коротких анонсов через сервис отложенного постинга;

  • проверка, что обязательные площадки действительно получили корректное состояние;

  • финальный отчет.

Если смешать все это в одну задачу, потом невозможно понять, где именно произошла ошибка. Агент пишет текст, скачивает картинку, открывает браузер, вставляет HTML, планирует посты, проверяет ссылки и в конце уверенно сообщает «готово». А внутри могло сломаться что угодно.

Поэтому первая нормальная архитектурная граница появилась скорее в голове, чем в коде — нужно разделить пакет, транспорт и QC — то есть я в принципе сначала разложил все на части, хотя вроде бы задача простая, но все равно важно и ее раскладывать на составные части.

Поэтому более полезная модель выглядит так:

source → content package → media resolution → transport → verification → report

Как только эти этапы разделены, становится возможным формализовать, за что отвечает каждый из них:

  • Пакет отвечает за материалы. 

  • Транспорт отвечает за доставку. 

  • QC отвечает за доказательства, что результат можно считать выполненным.

Timeout не означает, что публикации не было

Самый показательный ранний инцидент случился в телеге.

Агент отправлял пост через CLI, а команда вернула ошибку ожидания ответа от gateway:

gateway timeout after 10000ms

Обычная реакция автоматизации в такой ситуации — повторить операцию, однако пост то уже дошел до телеги и таймаут произошел не на стороне публикации, а на обратном пути подтверждения. И в итоге получается, что агент повторяет отправку и создает дубль, а дубль поста в соцсети не такая ошибка, которую можно тихо откатить транзакцией, особенно если соц.сети большие и активные.

Отсюда первый важный момент — ошибка клиента не всегда означает, что действие не произошло. Сеть могла оборваться после выполнения операции, gateway мог не дождаться ответа, CLI мог завершиться по таймауту, хотя API уже принял запрос.

Поэтому первое важное правило:
неуспешный transport response не является достаточным основанием для retry операции с побочным эффектом.

Сначала нужно проверить факт публикации. Только после этого решать, требуется ли повтор.

В том же кейсе выяснилась еще одна мелочь, которая перестала быть мелочью после автоматизации. Markdown-ссылка стояла в одном абзаце с анонсом и визуально с ним сливалась.

Рабочий caption зафиксировали как контракт:

  • Короткий анонс.

  • Текст статьи

  • HTML-анкор через используемый путь отправки обрабатывался как обычный текст, поэтому выбор Markdown здесь тоже перестал быть стилистическим решением.

С этого момента формат сообщения описывался уже как набор проверяемых условий.

Браузерная автоматизация оказалась плохой

По поводу самих публикаций — сначала я их публиковал через браузер (noVNC).

  • VK антибот-проверка. Автоматический клик не засчитался, пришлось вмешиваться человеку, а загрузка изображений через браузер тоже оказалась нестабильной — файл есть, путь есть, а react uploader живет своей жизнью.

  • OK сломался иначе. Там нужно вставить текст и URL, дождаться карточки предпросмотра, затем удалить видимую ссылку и отправить пост. Один раз агент удалил URL слишком рано и карточка не успела подтянуться. В результате вышел текст без нормальной ссылки и без preview.

Так что браузер подходит как fallback и как способ исследовать поведение площадки, но держать его штатным транспортом для регулярного кросс-постинга опасно. И главная проблема даже не в том, что браузер часто ломается, а в том, что агент может выбрать старый браузерный путь, потому что «когда-то так уже получалось» и если в навыке сохранены исторические инструкции, но не указано, что они fallback, агент будет воспринимать их как равноправные варианты.

Поэтому рабочий маршрут пришлось отделить от исторических способов: 

  • Telegram остался прямой публикацией. 

  • VK, OK, Facebook и X ушли в сервис отложенного постинга. 

Браузерные сценарии остались как аварийный путь.

API уменьшает поверхность отказов, но не определяет Done

После перехода на LiveDune пайплайн стал заметно стабильнее, но возник другой риск — слишком оптимистичная трактовка ответа API.

HTTP 201 и scheduled подтверждают, что сервис принял объект и перевел его в определенное внутреннее состояние, однако они не доказывают, что конечная площадка получит публикацию в корректном виде. Картинка все еще может не пройти требования конкретной соцсети, а ссылка может потеряться. Отдельный канал может завершиться ошибкой уже после того, как upstream-сервис принял задачу. Поэтому критерий Done пришлось поднять на уровень выше.

Для каждой обязательной площадки должно существовать проверяемое конечное состояние. Например, для VK нужен идентификатор публикации и подтвержденный scheduled или published state. Если OK, Facebook и X прошли, а состояние VK не доказано, весь запуск нельзя маркировать как успешный — а такое реально часто происходило.

Это довольно важный сдвиг в модели агента — результатом является не успешно выполненный API call, а подтвержденный бизнес-эффект.

Картинка — это не URL

Отдельная боль была связана с изображениями.

По изображениям быстро вылезли следующие нюансы — один канал может принять WebP, другой нет, Google Drive-ссылка может открываться в браузере, но ломать preview или публикацию, HEAD-запрос может дать неполную картину, реальная загрузка через GET может показать другой MIME, редиректы или слишком большой размер, X может требовать более жесткое ограничение по весу, ну и VK через посредника лучше кормить прямым CDN URL в PNG или JPEG. Короче, довольно много мелочей, которые надо было исправить.

Например, исходный WebP весил 483 КБ, а после конвертации он превратился в PNG на 4,7 МБ и формально агент то подготовил картинку, но по факту он ухудшил asset почти в десять раз. После этого стало ясно, что og:image — не готовый файл для публикации, а только кандидат. То есть нужен отдельный image resolver и его задача была адаптировать publish-ready asset под ограничения транспорта.

И я, соответственно, сформировал правила проверки:

  • URL должен быть публично доступен;

  • формат должен подходить целевой площадке;

  • размер должен укладываться в лимиты;

  • MIME нужно проверять по реальной загрузке, а не только по расширению;

  • Google Drive не должен быть default-CDN;

  • WebP нельзя считать универсальным форматом;

  • если одна площадка не приняла картинку, нужно чинить конкретный target, а не пересобирать весь пакет.

Это как раз тот случай, где простая операция превращается в resolver с правилами выбора, валидации и fallback.

Самая неприятная ошибка была в тексте

Проблемы с браузером и API были хотя бы ожидаемыми, а вот более неприятной оказалась ошибка с кириллицей.

В какой-то момент в рерайтах и Google Doc появились символы замены Unicode — U+FFFD. Это тот самый символ � который означает, что исходный символ уже потерян при декодировании или записи. Я нашел несколько вхождений уже после подготовки документа, то есть агент собрал markdown, записал Google Doc, прошел дальше и нигде не остановился.

Проблема в таком дефекте не в том, что текст выглядит некрасиво, хотя это тоже. А в том, что U+FFFD означает, что исходный символ на некотором этапе уже был потерян. После этого автоматическое исправление кодировки становится невозможно, потому что четко определить что находилось на месте replacement character не получится.

В моем случае источник проблемы находился в пути записи файлов с кириллицей. После этого для промежуточных текстовых артефактов закрепил запись через Python write_bytes или safe-write и отдельную проверку результата.

QC стал выполняться в нескольких точках:

  1. после локальной сборки Markdown;

  2. после записи или экспорта Google Doc;

  3. в pre-flight перед публикацией;

  4. перед формированием финального отчета.

Проверяется не только сам U+FFFD, но и последовательность байтов EF BF BD, а также характерные mojibake-паттерны.

Принципиально важно, что такая ошибка не исправляется глобальным replace(). Если встречается replacement character, нужно восстановить исходный контекст, исправить конкретное место и повторно прогнать проверку. С этого момента отсутствие encoding corruption стало не пожеланием к редактуре, а hard gate — если QC обнаруживает подобный дефект, transport stage не запускается.

И это один из самых полезных выводов из всего кейса — в агентном workflow текст это не просто контент — это данные, которые проходят через преобразования и эти данные можно испортить так же, как JSON, CSV или бинарный файл.

Полноценные рерайты источника

На площадках по типу Dzen и Spark — нужны были самостоятельные материалы. Они должны быть близки по смыслу к исходной статье, но не превращаться в рекламный мостик «перейдите читать оригинал». Сначала было искушение брать данные из API или JSON — это удобно для discovery. Можно быстро получить заголовок, ссылку, метаданные, иногда текстовые фрагменты.

Но JSON оказался плохим источником для редакционного материала. Вместе с основным текстом туда могли попадать CTA, баннеры, product cards и другие элементы страницы. Агент при этом просто переписывал тот контент, который получил. 

Поэтому источником рерайта стал полный публичный HTML с последующим sanitation step:

HTML → удаление CTA / banners / product inserts → нормализованный source → rewrite

После очистки уже можно проверять:

  • структура сохранилась или нет;

  • есть ли нормальные H2/H3;

  • не превратился ли текст в один сплошной абзац;

  • не укоротил ли агент материал слишком сильно;

  • не утащил ли он CTA из оригинальной страницы;

  • не вставил ли ссылку на оригинал там, где нужен самостоятельный материал;

  • не превысил ли лимит ссылок.

И если не закрепить это в процедуре, то агент будет выбирать самый удобный источник, а удобный источник не всегда правильный.

Одна свежая статья за запуск

Еще одна неочевидная граница появилась в автоматической проверке блога.

Можно сделать так, чтобы агент находил несколько новых статей и обрабатывал их. Технически это вроде понятно, но на практике это плохая идея для регулярного workflow. Если блог за один период выпустил несколько материалов, агент может начать создавать несколько Google Docs, несколько наборов рерайтов, несколько публикационных пакетов и несколько задач в сервисе отложенного постинга, а любая ошибка начинает размножаться и потом сложнее понять, какой пост относится к какой статье. Растет риск дублей.

Поэтому в навыке закрепил ограничение — за автоматический запуск обрабатывать ровно одну свежую неопубликованную статью. Остальные можно перечислить как skipped candidates, но не начинать параллельную обработку без команды. Это звучит как ограничение производительности, но на самом деле это ограничение области ответственности — в агентных задачах bounded work часто важнее скорости. 

Финальный отчет нельзя собирать из памяти агента

Проблема тут была в том, что если workflow многошаговый, финальный отчет сам становится частью системы и он должен строиться из состояния запуска.

Иначе появляются типовые ошибки:

  • агент пишет, что все опубликовано, хотя один канал был skipped;

  • пишет, что VK запланирован, хотя есть только успешный ответ по OK и Facebook;

  • сообщает, что создал документ, хотя документ был создан частично или с битой кодировкой;

  • считает рерайты готовыми, хотя не проверил экспорт из Google Docs;

  • повторно создает документ после ошибки и плодит дубли.

Нормальный state для такого запуска должен хранить хотя бы:

Скрытый текст

{

  «article»: {

    «url»: «…»,

    «title»: «…»,

    «source_status»: «html_cleaned»

  },

  «package»: {

    «announcement»: «ready»,

    «dzen_rewrite»: «qc_passed»,

    «spark_rewrite»: «qc_passed»,

    «google_doc»: «created»

  },

  «media»: {

    «source»: «cdn»,

    «mime»: «image/jpeg»,

    «size_bytes»: 412000,

    «qc»: «passed»

  },

  «targets»: {

    «telegram»: {

      «status»: «published»,

      «evidence»: «message_id»

    },

    «vk»: {

      «status»: «published «,

      «evidence»: «post_id»

    },

    «ok»: {

      «status»: «published «,

      «evidence»: «post_id»

    },

    «facebook»: {

      «status»: «published «,

      «evidence»: «post_id»

    },

    «x»: {

      «status»: «published «,

      «evidence»: «post_id»

    }

  },

  «blockers»: []

}

Это не обязательно должен быть именно такой JSON. Просто важно закрепить, что финальный ответ должен рендериться именно из фактов. Если состояния нет, агент начинает полагаться на свою краткосрочную память, а память модели плохой источник фактов для production-отчета.

Стоп-факторы

На ранних этапах агент часто пытался “додавит” задачу: 

  • не прошел VK — открыть браузер. 

  • не загрузилась картинка — сконвертировать. 

  • получил timeout — повторить. 

  • не получилось через один формат — попробовать другой.

И вот такая стандартная реакция агента довольно опасная вещь, которая по итогу тянет за собой много проблем. У агента обязательно должны быть стоп-факторы.

В моем случае каждая неприятная ошибка превратилась либо в запрет, либо в проверку, либо в стоп-фактор.

  • ссылка слиплась с текстом — появился фиксированный формат caption. 

  • timeout после отправки — появилась проверка факта публикации. 

  • OK потерял preview — появилась проверка карточки перед удалением URL. 

  • WebP и Drive URL стали проблемой — появились правила по MIME, размеру и CDN. 

  • В Google Doc попал U+FFFD — появилась байтовая проверка. 

  • JSON оказался плохим источником рерайта — источником стал очищенный публичный HTML. 

  • API принял объект, но это не доказывало результат — появилась проверка состояния по каналам.

То есть именно эти ограничения в итоге привели к тому, что кросс-постинг стал нормальным. Конечно, можно было и заранее продумать все эти “мелочи”, но заранее прописывать пул стоп-факторов тоже то еще удовольствие, поэтому все фиксировалось чисто из практики.   

В какой момент промпт превращается в скилл

После всех итераций рабочая процедура стала выглядеть примерно так:

  1. выбрать ровно одну новую неопубликованную статью;

  2. получить полный HTML;

  3. очистить его от нерелевантных блоков;

  4. собрать content package: анонс и два рерайта;

  5. проверить структуру, объём и ссылки;

  6. прогнать encoding QC;

  7. разрешить и проверить media asset;

  8. сформировать Google Doc;

  9. опубликовать Telegram напрямую;

  10. создать публикации для остальных каналов через LiveDune;

  11. проверить состояние каждой обязательной площадки;

  12. завершить запуск ошибкой, если какое-либо требуемое состояние не доказано.

Последний пункт здесь важнее большинства остальных.

Основные выводы из кейса

Кросс-постинг оказался хорошим примером задачи, которая выглядит простой только на уровне пользовательской формулировки. На уровне системы это multi-target workflow с частичными отказами, неидемпотентными внешними действиями, разными транспортными контрактами, нестабильными медиа, преобразованиями текста и финальной отчетностью.

После этого кейса у меня осталось несколько правил, которые я бы применял к любому агенту, работающему с внешними публикациями.

  1. Разделять пакет, транспорт и QC — пока все смешано, любая ошибку найти намного труднее.

  1. Не считать внешний API-ответ конечным результатом — он доказывает только то, что внешний сервис ответил именно так в этот момент.

  1. Делать операции устойчивыми к неопределенному результату — timeout после внешнего действия требует проверки, а не автоматического повтора.

  1. Считать медиа отдельным объектом в workflow — URL картинки, publish-ready asset и файл, подходящий конкретной площадке — разные вещи.

  1. Проверять текст как данные — особенно если есть кириллица, Google Docs, markdown, HTML, clipboard и несколько этапов записи.

  1. Хранить состояние запуска — финальный отчет должен строиться из state и evidence, а не из уверенного пересказа модели.

  1. Явно описывать stop conditions — агенту нельзя оставлять право “как нибудь доделать”, если он потерял доказательства результата.

На этом все. Если у вас были подобные кейсы в работе, буду рад обратной связи в комментах, можем обсудить. А так, если маркетинг к вам придет с подобной “легкой задачкой”, крайне рекомендую адекватные дедлайны ставить.

Спасибо за внимание!

Источник: habr.com

Оцените материал:

Поделиться
Понравилась статья? Расскажите другим
ВКонтакте
Читайте также
Новости робототехники Роботы на Уолл-стрит: нетрадиционные пути выхода на публичные рынки для робототехнических компаний Новости робототехники Универсальный железнодорожный робот заменит людей на опасных участках Архив рубрики ~Коротко из Telegram~ ☀️ Видео KodeCloud разбирает квантизацию — почему модель на 70… Архив рубрики ~Коротко из Telegram~ Батарейки из водорослей Учёные из Кембриджского университета вместе с биодизайнером… Архив рубрики ~Коротко из Telegram~ «Пятёрочка» научилась напоминать, что у вас скоро скиснет В приложении… Архив рубрики ~Коротко из Telegram~ Nebius: $582 млн выручки и $5,7 млрд капекса Голландская компания… Архив рубрики ~Коротко из Telegram~ CXMT обошла Tencent и стала самой дорогой компанией Китая Китайский… Архив рубрики ~Коротко из Telegram~ ↗️ Билайн отчитался за I полугодие 2026 г.: выручка выросла… Архив рубрики ~Коротко из Telegram~ VK вышел в плюс Выручка VK во втором квартале 2026… Архив рубрики ~Коротко из Telegram~ ☁️ Yandex Cloud отчитался за первое полугодие: выручка +30% Ознакомились… Архив рубрики ~Коротко из Telegram~ Ошибка выжившего: джун Крупнейшие AI-лаборатории начали нанимать философов. Перед нами… Архив рубрики ~Коротко из Telegram~ Российских чипов все меньше По данным АРПЭ, в 2025 году… Архив рубрики ~Коротко из Telegram~ Шпионы плетут нейросети Технологическая война США и КНР отвергает джентльменские… Архив рубрики ~Коротко из Telegram~ 🤖 Билайн запустил внутреннюю среду для работы с нейросетями Интересный… Новости робототехники Роботы на Уолл-стрит: нетрадиционные пути выхода на публичные рынки для робототехнических компаний Новости робототехники Универсальный железнодорожный робот заменит людей на опасных участках Архив рубрики ~Коротко из Telegram~ ☀️ Видео KodeCloud разбирает квантизацию — почему модель на 70… Архив рубрики ~Коротко из Telegram~ Батарейки из водорослей Учёные из Кембриджского университета вместе с биодизайнером… Архив рубрики ~Коротко из Telegram~ «Пятёрочка» научилась напоминать, что у вас скоро скиснет В приложении… Архив рубрики ~Коротко из Telegram~ Nebius: $582 млн выручки и $5,7 млрд капекса Голландская компания… Архив рубрики ~Коротко из Telegram~ CXMT обошла Tencent и стала самой дорогой компанией Китая Китайский… Архив рубрики ~Коротко из Telegram~ ↗️ Билайн отчитался за I полугодие 2026 г.: выручка выросла… Архив рубрики ~Коротко из Telegram~ VK вышел в плюс Выручка VK во втором квартале 2026… Архив рубрики ~Коротко из Telegram~ ☁️ Yandex Cloud отчитался за первое полугодие: выручка +30% Ознакомились… Архив рубрики ~Коротко из Telegram~ Ошибка выжившего: джун Крупнейшие AI-лаборатории начали нанимать философов. Перед нами… Архив рубрики ~Коротко из Telegram~ Российских чипов все меньше По данным АРПЭ, в 2025 году… Архив рубрики ~Коротко из Telegram~ Шпионы плетут нейросети Технологическая война США и КНР отвергает джентльменские… Архив рубрики ~Коротко из Telegram~ 🤖 Билайн запустил внутреннюю среду для работы с нейросетями Интересный…

Оставить комментарий