Входящий бриф от клиента почти никогда не приходит в виде аккуратной формы. Это письмо на полторы страницы, голосовое в расшифровке, таблица из трёх несвязанных вкладок или сообщение в духе «нам нужна автоматизация, бюджет обсудим». Раньше первичный разбор такого брифа и сборка чернового коммерческого предложения были ручной работой: кто-то садился, вычитывал письмо или таблицу, выуживал требования и перекладывал их в шаблон предложения. Сейчас этим занимается воркфлоу в n8n: он принимает сырой бриф, извлекает из него требования LLM-нодой, раскладывает их по структуре и собирает черновик КП, который человек дорабатывает, а не пишет с нуля.
Это не «магический промпт». Это оркестрация из полутора десятков нод, где у LLM ровно две задачи — извлечение и генерация, а всё, что между ними и вокруг, делают детерминированные ноды. В этой статье — разбор архитектуры, конкретные конфиги нод и, что важнее, грабли, на которые мы наступили в проде. Названий клиентов и волшебных цифр про «экономию 80%» здесь не будет.
Что вообще считать «агентом» в n8n
Сначала договоримся о термине, потому что вокруг него много шума.
AI-агент в n8n — это не один вызов модели, а оркестрация нод, где LLM отвечает за извлечение и генерацию текста, а детерминированные ноды — за валидацию, маршрутизацию и идемпотентность. Разница принципиальная: если вы положите всю логику в один большой промпт («прочитай бриф и сразу выдай готовое КП»), вы получите систему, которую невозможно отладить, протестировать по шагам и защитить от мусора на входе. Как только извлечение требований и генерация документа разнесены по разным нодам с явным контрактом данных между ними, у вас появляется то, что можно логировать, валидировать и чинить точечно.
Поэтому дальше я почти не буду говорить про промпты. Самое интересное — не в них, а в нодах между ними.
Что на входе: почему бриф — это «грязные данные»
Прежде чем проектировать ноды, надо честно описать вход. Наши брифы приходят как минимум в трёх формах:
-
структурированная форма с лендинга (редкий, идеальный случай);
-
свободный текст в теле webhook’а — письмо, пересланное через интеграцию, или текст из формы с одним большим полем «опишите задачу»;
-
частичный бриф — половина полей пустая, бюджет не указан, сроки «вчера».
С точки зрения инженера это классические грязные данные: schema-less, с пропусками, иногда с откровенным мусором («тест», «asdf», дубли одного и того же брифа от нетерпеливого клиента). Любой production-воркфлоу, который этого не учитывает, развалится на второй неделе. Поэтому большая часть нод в нашем флоу — это не «ИИ», а валидация, нормализация и защита от повторов.
Архитектура потока
Верхнеуровнево граф выглядит так:
Тот же граф текстом — для копирования, поиска и доступности:
Webhook (POST /brief-intake, responseMode: responseNode) │ ▼Code: Нормализация входа │ ── парсит body, приводит к единой форме, отсекает явный мусор │ ▼IF: бриф валиден? ├── false → Respond 422 + Telegram-уведомление «битый бриф» │ └── true ▼Code: Дедуп-ключ (idempotency key) │ ── хеш от нормализованного содержания брифа │ ▼HTTP/DB: бриф уже обработан? ├── да → Respond 200 (возвращаем прошлый результат, ничего не пишем) │ └── нет ▼LLM-нода: Извлечение требований → structured output (JSON по схеме) │ ▼Code: Валидация извлечённого JSON │ ── проверка обязательных полей, типов, диапазонов │ ▼Code: Маппинг в шаблон КП │ ── раскладываем требования по секциям предложения │ ▼LLM-нода: Генерация черновика КП → Markdown/HTML │ ▼DB/Sheets: Запись результата (с idempotency key) │ ▼Telegram: Уведомление менеджеру + Respond to Webhook 200Error Trigger → Telegram: Notify Error (параллельная ветка на весь флоу)
Схема выше — обобщённый граф потока в обезличенном виде: внутренние URL, ID воркфлоу и креды намеренно опущены. Это не «один промпт», а полтора десятка нод, где у LLM ровно две задачи — извлечь и сгенерировать, — а всё остальное делают детерминированные ноды.
Дальше — по нодам, с акцентом на то, что неочевидно.
Нода за нодой
1. Webhook: приём брифа
-
Тип:
n8n-nodes-base.webhook -
Параметры:
POST /brief-intake,responseMode: responseNode
responseMode: responseNode — важная деталь. Он означает, что ответ клиенту отдаёт отдельная нода Respond to Webhook в конце флоу, а не сам webhook сразу. Это даёт два эффекта: во-первых, мы можем вернуть осмысленный код (200/422) в зависимости от результата; во-вторых, отправитель брифа дожидается реального завершения обработки, а не получает «принято» до того, как мы вообще посмотрели на данные. Минус — нужно следить за таймаутом: если LLM-ноды отвечают долго, webhook может отвалиться по времени у клиента. Для тяжёлых случаев мы выносим генерацию в асинхронную ветку и отвечаем 202 Accepted сразу, но для брифа объёмом в страницу синхронного режима хватает.
2. Code: нормализация входа
-
Тип:
n8n-nodes-base.code
Здесь мы достаём $input.first().json.body и приводим разные формы брифа к одной структуре: { raw_text, source, contact, received_at }. Тут же отсекается явный мусор по простым эвристикам (пустое тело, длина текста ниже порога, стоп-слова вроде «тест»). Никакого ИИ — обычный JavaScript. Чем больше мусора вы отфильтруете детерминированно до LLM, тем меньше заплатите за токены и тем предсказуемее поведение.
Маленькая, но регулярная грабля: данные из webhook лежат именно в body, а не в корне json. Половина «почему у меня undefined» в n8n — это обращение к $json.email вместо $json.body.email.
3. IF: бриф валиден?
-
Тип:
n8n-nodes-base.if
Развилка на основе флагов из ноды нормализации. Невалидный бриф уходит в ветку, которая возвращает 422 и шлёт в Telegram короткое сообщение менеджеру: «пришёл битый бриф, посмотри». Это дешёвая страховка — лучше человек один раз глянет на пограничный случай, чем модель сгенерирует уверенное КП на основе слова «asdf».
4. Code: ключ идемпотентности
-
Тип:
n8n-nodes-base.code
Вот это — самая дорогая грабля, которую мы оплатили на другом воркфлоу, и я вынес её сюда осознанно. На пайплайне AI-переводов у нас не было уникального ключа на паре «контент + локаль», и при повторных запусках/ретраях в базу падали дубли одного и того же результата. Воркфлоу «отрабатывал успешно» — и тихо плодил мусор.
Вывод, который мы зашили в стандарт: идемпотентность обеспечивается в самом флоу, а не надеждой на то, что источник не пришлёт повтор. Здесь мы считаем стабильный хеш от нормализованного содержания брифа:
// Code node — формируем idempotency keyconst crypto = require('crypto');const text = $json.raw_text.trim().toLowerCase().replace(/\s+/g, ' ');const key = crypto.createHash('sha256').update(text).digest('hex');return [{ json: { ...$json, idempotency_key: key } }];
5. Проверка «уже обработан?»
Перед любой дорогой работой (а вызовы LLM — дорогие и по деньгам, и по времени) лезем в хранилище по idempotency_key. Если запись есть — отдаём прошлый результат и выходим. Если нет — идём дальше. На уровне БД это страхуется UNIQUE-индексом по ключу, чтобы даже гонка двух параллельных запусков не создала два КП.
6. LLM-нода: извлечение требований
-
Тип: LLM-нода (Gemini / OpenAI / Anthropic) к API модели
Первая из двух точек, где работает модель. Задача узкая: прочитать сырой текст и вернуть строго структурированный JSON — без прозы, без «вот ваше предложение». Промпт требует только извлечение, и мы фиксируем схему ответа:
{ "client_goal": "string", "pain_points": ["string"], "scope": ["string"], "integrations": ["string"], "budget": { "amount": "number|null", "currency": "string|null" }, "deadline": "string|null", "missing_fields": ["string"]}
Два приёма, без которых эта нода нестабильна:
-
Structured output / JSON mode. Просим модель отвечать строго валидным JSON по схеме. Если нода/модель это поддерживает — включаем нативно; если нет — оборачиваем парсинг в
try/catchи при невалидном JSON делаем один retry с ужесточённым промптом. Даже с включённым JSON mode часть ответов приходит с лишней прозой вокруг объекта или обрезается по лимиту токенов — поэтому обёртка с парсингом и retry обязательна, а не опциональна. -
Поле
missing_fields. Мы явно просим модель перечислить, чего в брифе не хватило, вместо того чтобы она это домысливала. Это превращает галлюцинацию («бюджет 500 000 ₽», которого в брифе не было) в честный сигнал («бюджет не указан»). Колоссальная разница для документа, который пойдёт клиенту.
7. Code: валидация извлечённого
-
Тип:
n8n-nodes-base.code
LLM вернула JSON — это ещё не значит, что он корректен. Здесь детерминированно проверяем: все обязательные поля на месте, типы совпадают, budget.amount — число в разумном диапазоне, массивы не пустые там, где это критично. Если проверка падает — либо ещё один контролируемый retry извлечения, либо эскалация человеку через Telegram. Модель предлагает, детерминированный код располагает — ни одно её утверждение не идёт дальше без проверки.
8. Code: маппинг в шаблон КП
Чистая трансформация без ИИ: раскладываем извлечённые требования по секциям нашего шаблона предложения (контекст, объём работ, архитектура решения, этапы, ориентир по срокам). Где данных нет — подставляем явный маркер [уточнить у клиента], а не выдумываем. Этот маркер потом видит менеджер и видит клиент — и это правильно.
9. LLM-нода: генерация черновика КП
-
Тип: LLM-нода (Gemini / OpenAI / Anthropic) к API модели
Вторая и последняя точка работы модели. На вход — уже структурированные и провалидированные данные, на выход — связный текст черновика в Markdown. Важно, что модель здесь не принимает решений о фактах: цифры, сроки и объём уже зафиксированы предыдущими нодами, её работа — превратить структуру в читаемый документ. Чем меньше свободы у генерирующей ноды по части фактов, тем меньше потом правок у человека.
10. Запись результата
Пишем черновик в хранилище вместе с idempotency_key, ссылкой на исходный бриф и метаданными (модель, версия промпта, стоимость вызова в токенах). Логирование стоимости — не блажь: без него вы не узнаете, сколько реально стоит один черновик, пока не придёт счёт.
11. Telegram + Respond to Webhook
Менеджеру падает уведомление со ссылкой на готовый черновик и списком missing_fields («модель не нашла бюджет и сроки — уточни»). Webhook отвечает 200. Человек открывает черновик и доводит его до отправки — публикует КП всегда человек, не воркфлоу.
12. Error Trigger — отдельная ветка
-
Тип:
n8n-nodes-base.errorTrigger→n8n-nodes-base.telegram
Параллельная ветка, которая ловит ошибку в любой ноде всего флоу и шлёт в Telegram имя воркфлоу, упавшую ноду и текст ошибки. Это та самая разница между «воркфлоу тихо упал три дня назад, а мы не знали» и «через минуту после сбоя инженер уже смотрит лог». Никаких тихих падений — это у нас жёсткий стандарт.
Грабли, за которые мы заплатили
Самое ценное в любом таком разборе — не идеальная схема, а места, где она ломалась.
1. Идемпотентность — не опция. Повторю урок из п.4 в общем виде: webhook вам пришлют дважды. Клиент нажмёт кнопку повторно, интеграция сделает retry, n8n перезапустит ноду после сбоя. Если в пайплайне нет уникального ключа и проверки «уже обработан?», вы будете тихо плодить дубли КП и записей в базе. Лечится UNIQUE-индексом в хранилище плюс ранней проверкой по хешу до дорогих нод.
2. HTTP-ноды не привязывают credential автоматически. Когда вы выносите вызов к API (модели, CRM, базы) в httpRequest-ноду, n8n не подставляет в неё credential сам — в отличие от нативных нод, где он привязан к типу ноды. Несколько раз мы ловили 401/403 ровно потому, что нода была сконфигурирована, а credential к ней не прикреплён в выпадающем списке Authentication. Отдельная боль — когда в инстансе несколько похожих кред (например, разные окружения БД), легко прикрепить не ту и потом долго искать, почему данные «уходят не в ту базу». Проверяйте привязку credential в каждой HTTP-ноде явно и держите окружения именованно различимыми.
3. Мусорные и частичные брифы. Без раннего фильтра модель уверенно генерирует КП на основе «тест тест» — и это худший вид ошибки, потому что выглядит правдоподобно. Двойная защита: детерминированный фильтр до LLM (длина, стоп-слова, пустота) и поле missing_fields от самой модели, превращающее домысел в честный пропуск.
4. Невалидный JSON от модели. Даже с JSON mode часть ответов прилетает с лишней прозой или обрезанной по лимиту токенов. try/catch вокруг парсинга, один контролируемый retry с ужесточённым промптом, и только потом — эскалация человеку. Никогда не передавайте сырой текст модели в следующую ноду как будто это гарантированно валидный объект.
5. Стоимость и таймауты. Два вызова LLM на бриф — это деньги и секунды. Деньги контролируются тем, что мусор отсекается до модели, а повторы — идемпотентностью. Секунды — тем, что при тяжёлых брифах генерация уходит в асинхронную ветку с ответом 202, иначе синхронный webhook рискует отвалиться по таймауту у отправителя.
Наивный против production-варианта
|
Аспект |
Наивный «один промпт» |
Production-флоу |
|---|---|---|
|
Логика |
один большой промпт «бриф → КП» |
разнесённые ноды: extract → validate → generate |
|
Грязный вход |
модель «как-нибудь разберётся» |
детерминированный фильтр + |
|
Галлюцинации фактов |
домысливает бюджет и сроки |
факты фиксируются кодом, |
|
Повторы |
дубли в базе |
idempotency key + |
|
Невалидный ответ |
падение следующей ноды |
|
|
Сбои |
тихое падение |
Error Trigger → Telegram |
|
Отладка |
«почему-то плохо» |
логи по шагам, стоимость на вызов |
Разница между колонками — это не «больше нод ради нод». Это разница между демкой, которую приятно показать, и системой, которая работает по понедельникам.
Что бы я сделал иначе
Если бы собирал этот флоу заново: вынес бы извлечение и генерацию в отдельные суб-воркфлоу с собственными версиями промптов и тестами, а не держал в одном полотне; с первого дня логировал бы стоимость в токенах на каждый вызов; и завёл бы небольшой набор «золотых» брифов (успех, частичный, мусор) как регрессионный тест перед каждым изменением промпта. Базовая идея, к которой всё сводится: LLM — это ненадёжный, но полезный исполнитель внутри надёжной детерминированной обёртки. Чем больше ответственности за факты, повторы и сбои вы снимаете с модели и отдаёте обычным нодам, тем спокойнее живёте.
Зачем это бизнесу за пределами «у нас красивый воркфлоу» — отдельный разговор; если интересно, как такие точечные автоматизации складываются в общую картину внедрения ИИ, я разбирал это в тексте про ИИ как новое электричество. Здесь же мне важнее было показать инженерную изнанку: ноды, контракты данных и грабли.
Владимир Нагин — основатель LeadUp AI, агентства AI-автоматизации бизнес-процессов на базе AI-агентов. Пишет о нейросотрудниках и автоматизации бизнеса в блоге blog.leadup.guru. Автор курсов по n8n и Make.com.
ссылка на оригинал статьи https://habr.com/ru/articles/1043148/