Дисклеймер: в статье упоминается Meta — организация, признанная экстремистской и запрещённая на территории РФ. Статья — не прогрев и не реклама. Все логи, цифры и скрины — из реальных прогонов на тестовом аккаунте.

Все ключи, телефоны и названия в примерах изменены; код, схема, логика и скрины‑ из рабочего проекта заказчика, и строго с его разрешения! Статья не является рекламой/самопиаром и тому подобное.

Пролог: живая встреча, бэклог и бесплатная модель

Всем привет! Немного вступления: ИИ агент заказчика работает в проде уже четвёртый месяц: ночные заявки в НЕЛЬЗЯgram Direct, диалоги, анкеты, сделки в amoCRM. В прошлой статье — разбор Reasoning Lock и анонсы продолжений. Одна из договорённостей с заказчиком — ветка обработки комментариев под постами: человек пишет «+» или «сколько стоит?» под публикацией = агент отвечает ему приватным сообщением в Direct. Обещание висело в бэклоге и ждало своего часа.

Час настал сам. 16 сентября вечером на OpenRouter выкатили новую стелс‑модель: stealth/union‑alpha. Провайдер анонимен, релизный штамп на странице 16 сентября, то есть сутки стелсу к моменту публикации этой статьи. Факты про данную llm: мультимодальность, tool calling, GPQA Diamond 90.9%, и главное — цена ровно ноль (пока что).

Была и цифра, которая меня спугнула: контекст 262K. Вчетверо меньше, чем у прошлого стелса, которому я скормил 60 млн токенов — подробнее в статье https://habr.com/ru/articles/1076084/. Ту прошлую задачу — эксперимент не потянет... а вот задание, и уже реальный бэклог внедрения одного контура пожалуй справиться.

Сеголня с утра я приехал к заказчику в офис — живая встреча по стратегии дальнейшего развития проекта. Вернулся с согласованным бэклогом, сел за работу и решил продолжить метод, обкатанный на прошлом стелсе: бенчмарки не показывают главное, реальная задача — показывает. Не «прогони модель по тестам», а «пусть она сделает настоящую фичу из живого проекта».

Разница с прошлым разом как я сказал ранее, в масштабе и ставках. Работы для модели = одна ветка комментариев, не весь воркфлоу. Но рабочая папка проекта довольно таки полная: живой экспорт воркфлоу, тулзы, патчи, дневники, документы. Открываю Visual Studio Code с Cline — и понеслась.

И ещё одно решение, необходимое на мой взгляд и правильное: не трогать аккаунт и воркфлоу заказчика. Реальный воркфлоу остаётся в проде нетронутым, а эксперимент переезжает на тестовую среду = мой собственный тестовый аккаунт инсты.

Спойлер: именно это решение и подложило мне «палки в колёса», которую я не учёл. Но об этом чуть позже.)

ТЗ‑запрос: 28 200 токенов только на вход — роль, бэклоги и экспорт живого воркфлоу.

Что выдала модель за один заход

Выход llm = семь файлов, и это получается сборочная система:

logic.cjs: тут чистые функции: роутинг событий, расчёт допуска, форматирование ответа, валидация API. Вынесены из нод специально они чтобы тестировались без n8n;

build.cjs — это сам сборщик: читает живой экспорт воркфлоу, вшивает функции в сodeноды, добавляет 15 новых, пишет готовый JSON и держит резервную копию исходника (и отказывается перезаписывать её, если она отличается = защита от затирания);

test.cjs: тут 16 тестов на реальном пейлоаде вебхука;

README.md : тут уже файл «РАЗБОР ВЕТКИ КОММЕНТАРИЕВ» на 38 КБ, где модель сама себе вынесла недостатки.

Итоговый JSON: все исходные ноды без изменения + 15 новых. Импортируется неактивным, пути вебхуков сохранены.

Занятная фишка, за которую я готов простить модели многое = первый раздел её самоанализа называется «Главное и признание недостатков».

«....Вы правы насчёт промпта. Сейчас это копия общего Direct‑промпта с дополнением для комментариев. Полноценного отдельного сценария для „+“, цены и даты рождения я не сделал....» — признание самой LLM

Стелс‑модель, которая сама пишет себе список косяков до того, как её «аудитишь» на мой взгляд не хило подкупает. Осталось проверить, не врёт ли она в остальных шести файлах!

Архитектура за 60 секунд

```
Webhook (Direct и Comments приходят в один поток Meta)
  └─ IG: Route Events (Code)     — раздаёт igEventKind: direct / comment
      ├─ direct → старая Direct-ветка (не тронута ни строчкой)
      └─ comment → One At A Time (цикл, пачка по 1)
            → Prepare (допуск: вкл/выкл, тест-гейт, чёрный список, окно 7 суток)
            → Eligible? → Claim (Redis INCR — атомарный захват comment_id)
            → First Claim? → Record Pending (журнал в Redis)
            → AI Agent (модели и тулзы общие с Direct, память отдельная поднода)
            → Prepare Reply (чистка тегов, фолбэк, лимит 900 символов)
            → Send Private Reply → Validate API Result → Record Accepted
            → назад в цикл за следующим комментарием
```

Хочу отдельно пояснить три решения, которые на мой взгляд являются самыми сильные в данной сборке:

1. Private Reply адресуется по комментарию, а не по пользователю. Тело запроса:

```json
{
  "recipient": { "comment_id": "18120835172314431" },
  "message": { "text": "Ответ клиенту" }
}
```

Один комментарий = один приватный ответ, окно 7 суток, никакого публичного ответа под постом. Продолжение беседы — только когда человек сам напишет в директ, и это уже отдельное исполнение старой ветки, а не хитрый переход между агентами.

2. Дедупликация через Redis INCR, а не GET‑SET. Если на один комментарий одновременно набегут два исполнения, оба увидят «ключа нет» и оба отправят ответ. INCR атомарен: 1 — мой, 2+ — уже взяли.

3. Философия «не отправим дважды» важнее «обязательно отправим». Ошибка после захвата комментария блокирует автоповтор: ключ в Redis остался — значит второй раз агент не напишет, разбирайся руками. Осознанный обмен гарантии доставки на гарантию отсутствия дублей, так сказать= для агента ателье это правильный обмен.

Кстати приятный бонус, который подтвердился живым прогоном: ID автора комментария и ID отправителя в Direct один и тот же)). Значит, ключ памяти ХХХХ_ig:{userId} у обеих веток общий: комментарий ‑приватный ответ ‑человек пишет в директ — агент помнит, о чём был комментарий. Об этом подробнее в тестовом прогоне 1.1, где я едва не похоронил эту красивую схему собственным выводом.

Аудит: где прятались скрытые «косяки»

Я не поверил ни одному зелёному значку — скепсис или недоверие, да и банально страх:‑) Просмотрел пересборку из исходников (совпала с выданным JSON байт в байт, кроме случайных ID), 16 тестов, синтаксис всех Code‑нод а, главное, прогон функции прямо из JSON с имитацией входов n8n. Результаты: сборка честная, все исходные ноды не тронуты. Но находки были:

Находка (номер)

В чём «подвох»

1

Промпт ветки = копия Direct‑промпта на 12 000 символов с тегами VERDICT/TASK/STAGE Отдельный сценарий «+»/цена/дата рождения лежит в РАЗБОРЕ (!), но в JSON не внесён; Агент комментарием отвечал бы «как в директе», включая технические теги

2

При выключенном тест‑режиме пользователи из чёрного списка Direct получали бы ответы под постами так как нода «Чёрный список» на новом маршруте не выполняется.. что означало забаненный в директе клиент получает заботу в комментах

3

Чистка ответа ловит [TASK:NONE], но пропускает [END_DIALOG] (тег без двоеточия) Технический мусор приплывёт клиенту в личку

4

Private Reply выставлен на Graph API v25.0, а в воркфлоу заказчика директ ноды живут на доказанном v22.0 (как бы мелочь, но всё же)

5

Захардкоженный ID бизнес‑аккаунта взят из тестовой и не совпал с реальным из живого вебхука = с этой константой молча выкинул бы 100% реальных комментариев

6

Allowlist собран из ID ноды «Тест Аккаунт» директа, а реальный тестюзер в него не входил; в фикстуре модель склеила username с чужим ID, и в итоге было бы так: агент молча игнорирует комментарий = и ни одна галочка не покраснеет

Отдельная честность про себя: после прогона 1 я выдвинул одну гипотезу: ID комментатора и ID в директ живут в разных пространствах, общая память не сработает. прогон 1.1 её убил за пять минут: я написал в директ и в комменты с одного аккаунта, и, как итог: ID совпали до последней цифры. Гипотеза отозвана. Мораль серии: константы из фикстур и красивые теории сверяются только живым прогоном и это верно и для модели, и для аудитора.

Код: что в итоге поехало в «бой»

Роутер событий — это самая первая нода после вебхука. Компактный вариант боевой версии:

```js
const accountId = String(entry.id);
const chatId = String(v.from?.id || '');
const commentId = String(v.id || '');

// Только наш бизнес-аккаунт — РЕАЛЬНЫЙ ID из живого вебхука, а не из фикстуры
if (accountId !== '17841479739340657') continue;
// Эхо-петля (сам себе не отвечаем) и пустой автор
if (!chatId || chatId === accountId) continue;
// ID — только строки из цифр: JSON-число > 2^53 тихо округляется
if (!/^\d+$/.test(chatId) || !/^\d+$/.test(commentId)) continue;

// Живой текст, публикации FEED/REELS, не вложенный ответ, не удаление
if (typeof v.text !== 'string' || !v.text.trim()) continue;
if (!v.media || !['FEED', 'REELS'].includes(v.media.media_product_type)) continue;
if (v.parent_id || v.verb === 'remove' || v.verb === 'delete') continue;
```

Допуск (Prepare, боевая версия)‑ конфиг, который закрыл находки 1, 2, 6 одним движением:

```js
const config = {
  commentsEnabled: true,
  testMode: false,                     // боевой режим: отвечаем всем
  allowedUserIds: ['936850522443774', '826426153486930'],      // тест-гейт на будущее
  blacklistedUserIds: ['1344359104565035', '1686784379107626'], // чёрный список из Direct
  activePrompt: КОРОТКИЙ_ПРОМПТ_ВЕТКИ  // ~1.3K символов вместо копии на ~12K
};
```

А внутри prepare три строчки, которые стоят всей системы допуска:

```js
const allowed = config.commentsEnabled === true &&
  (!config.testMode || config.allowedUserIds.includes(j.chatId)) &&
  !(config.blacklistedUserIds || []).includes(j.chatId);
```

Чистка ответа перед отправкой = модель умеет markdown, а уж НЕЛЬЗЯgram обычный текст:

```js
text = text.replace(/```[\s\S]*?```/g, '')                    // блоки кода
  .replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '$1: $2')   // [текст](url) → текст: url
  .replace(/\[[A-Z_]{2,}(?::[^\]]*)?\]/gi, '')                // [TASK:NONE] и [END_DIALOG]
  .replace(/\*\*(.*?)\*\*/g, '$1').trim();
if (!text || Array.from(text).length > 900) text = ЗАГЛУШКА;  // лимит Instagram
```

Да, кстати, последняя строчка является та самая защита, которая выстрелила в прогоне 2: агент упал, а мне в директ всё равно пришла вежливая заглушка вместо тишины или технического мусора.

Три прогона

Прогон 1. Всё зелёное... НО...ноль результата

Импортировал, оставил комментарий под рилсом в тестовом аккаунте. Исполнение зелёное, ноды с галочками, но закончилось всё на Comments: Eligible?. Ответа мне в личку нет.

Ну блин, так бывает то в n8n: исполнение не упало = оно честно отработало замысел, который не совпал с намерением. Читаю вход Eligible?, а там готовый вердикт: skipReason: DISABLED_OR_NOT_ALLOWLISTED. Тест‑гейт модели был собран из ID ноды «Тест Аккаунт» директа = мой реальный тестовый юзер в этом списке не жил — находка номер 6 таблицы выше). Заодно всплыла находка 5 таблицы: захардкоренный ID аккаунта из фикстуры не равен реальный из вебхука = гвард с той константой ронял бы вообще всё.

Лечится не отладкой, а чтением одной строки skipReason и правкой конфига (Prepare v2 выше). Полчаса примерно)

Прогон 1.1. Пять минут, которые убили мою гипотезу

Пишу в директ и в комменты с одного аккаунта. Смотрю аутпут роутера: sender.id в Direct = 1385828923330271, chatId коммента = 1385828923330271. Идеальное совпадение. Гипотеза о «разных ID‑пространствах» мертва, общая память легитимна, архитектура модели оказалась точнее вывода её «аудитора». Супер).

Прогон 2. Bad request — и «бомюа», которую я сам и подложил

Второй комментарий пишу. Гейт пройден, ключи захвачены, агент отработал, отправка...и... вот блин:

```text
Failed to send private reply to comment 18120835172314431
for account 17841479739340657: Bad request - please check your parameters
```

Две поломки в одном исполнении.

Первая сам ИИ агент. В коде ветки был включён режим фолбэкмодели, а саму вторую модель никто не создал и соответсвенно поднода не подключена. Агент упал, и сработала защита: в Prepare Reply пустой и нестандартный ответ заменяется заглушкой. Мне в директ ушла вежливая «Здравствуйте!✨ Спасибо за Ваш комментарий...» — вместо пустоты или сырого JSON. Защита сделала ровно то, для чего её писали.

Вторая — отправка. Meta отклонила сам запрос. Community‑нода глотает сырой ответ Graph API, показывая только «Bad request» — даже и диагностировать нечего, иду curl'ом. А причина оказалась смешнее любого бага: в ноде лежал кредитнаил заказчика, а тестировал я на своём тестовом аккаунте. Токен заказчика физически не может отвечать на комментарии моего аккаунта = Meta увидела «чужака» и вежливо послала меня с 400.

Вот она, та самая «безопасная песочница» которую я выбрал в начале, чтобы ничего не сломать у клиента. Решение было правильным, а вот его исполнение — нет: перенеся сборку в изоляцию, я забыл перенести контекст — credential должны были переехать вместе с ней. Первая «бомба» которую я не учёл, заложена моими же руками. ИИ тут ни при чём.

Прогон 3. Победа)

Правильный credential в ноду, фолбэк‑модель подключена — и цепочка прошла насквозь: допуск — захват — журнал — агент — отправка — валидация — запись результата. Приватный ответ пришёл в директ.

Самая шикарная проверка — второй комментарий под другим постом. Новый comment_id, новый claim‑ключ... и память, которая подтянула первый комментарий: агент отвечает уже с учётом истории общения. Логика складывается под бизнес заказчика: один юзер = сколько угодно комментариев (в зависимости скока контекст в redis выставить)= один непрерывный контекст.

Сухой остаток

Стелсу сутки от роду — и он за один заход собрал ветку с дедупликацией, журналированием, фулбэком и честным самоанализом. Контекста 262K хватило: ветка = это не 127 нод, тут важнее точность, чем объём.

Что отдавать стелсу можно: изолированную ветку с чистым контрактом (вебхук на входе, API на выходе), где ошибка не рушит прод. Что нельзя: отдавать константы (ID аккаунтов, allowlistы — они приехали из фикстур и были неверными) и верить зелёным галочкам без живого боя.

Экономика: модель стоит (пока что) 0 у.е.. Весь пайплайн проверки — пересборка, 18 тестов, аудит, три прогона время и внимание. Дешевле любого часа разработчика. «Стелс‑окно» — редкий шанс поймать фронтир бесплатно, пользуйтесь, пока открыто.

Две самые дорогие ошибки дня — человеческие: кредитнеил не оттуда и фолбэк модель без модели. ИИ‑ошибки (промпт‑копия, константы) ловятся аудитом и тестами за минуты. Человеческие — потому и человеческие, что аудитор их изначально оправдывает: «я же тестирую, что может случиться».

Мой главный инструмент этого цикла — второй аудит. Нашлось то, что модель честно скрыть не пыталась, и то, что она скрыть пыталась бы, если бы умела. А его собственные гипотезы умерли об живой вебхук. Проверяющий тоже нуждается в проверке.

В статье про прошлый стелс я закончил фразой: «Заменит не ИИ. Заменит человек, который научился им пользоваться». Сутки спустя у меня прибавилось: и который умеет отвечать за результат — за каждый credential, каждую константу и каждый бой.

Моя прошлая статья осталась с крючком: «один мозг, три входа» то есть. Instagram Direct, Facebook Direct и виджет на сайте. Теперь у этого мозга появился четвёртый вход: комментарии под постами. Он уже отвечает в тестовой среде и помнит каждого человека. Конечно дорабатывать много осталось, и донести его до продакшена заказчика, тесты уже в реальном аккаунте... Но об этом и о другом расскажу в следующих статьях.

Всем добра! И спасибо за внимание:‑)