Дисклеймер: в статье упоминается 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 в ноду, фолбэк‑модель подключена — и цепочка прошла насквозь: допуск — захват — журнал — агент — отправка — валидация — запись результата. Приватный ответ пришёл в директ.

№1 - заглушка из прогона 2; №2 - ответ на первый коммент, №3  ответ на второй коммент
№ 1 — заглушка из прогона 2; № 2 — ответ на первый коммент, № 3 ответ на второй коммент
Экспортированная ветка комментов
Экспортированная ветка комментов
Проверка памяти через Termius: один юзер - несколько комментариев = контекст всегда с нами))
Проверка памяти через Termius: один юзер — несколько комментариев = контекст всегда с нами))
Ключи в Redis: claim и result по каждому комментарию!)
Ключи в Redis: claim и result по каждому комментарию!)

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

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

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

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

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

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

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

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

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

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