Урок курса
Граница no-code: когда хватает n8n, когда нужен код
AI Agents PRO: LangGraph, AutoGen и LLMOps в продакшнеЧто умеет визуальный конструктор
Курс про то, как собирать агентов кодом, и самый лёгкий способ оправдать его существование — объявить визуальные конструкторы игрушкой для тех, кто не умеет программировать. Так делать не будем. На них работают вполне серьёзные вещи, и знать, где именно они перестают справляться, полезнее, чем отмахнуться.
No-code — сборка решения из готовых блоков в визуальном редакторе, без единой строки кода. Low-code — то же самое, но со вставками собственных скриптов там, где готовых блоков не хватило. Граница между ними размытая: почти каждая заметная платформа сегодня умеет и так, и так. Дальше будем говорить «конструктор», подразумевая оба варианта.
Задача, на которой будем разбираться
Возьмём один сценарий и пронесём его через весь урок. В интернет-магазин приходят письма в поддержку, и с ними нужно сделать три вещи: понять, о чём письмо; на вопрос по товару ответить самому, взяв факты из базы знаний; а проблему с оплатой не решать, а завести тикет в трекере и написать дежурному менеджеру в мессенджер.
Задача обычная до скуки, и именно поэтому на ней хорошо видно границу: первые версии таких сервисов почти всегда собирают в конструкторе, а через полгода половина из них переезжает в код.
Как это выглядит в n8n
n8n — открытая платформа автоматизации, которую можно поднять у себя или взять как облачный сервис. Работа идёт на холсте: узлы соединяются стрелками, каждый узел что-то делает — читает почту, отправляет HTTP-запрос, пишет строку в базу, шлёт сообщение в чат. Готовых узлов несколько сотен, и главная их ценность не в том, что они рисуются мышкой, а в том, что авторизация к внешним сервисам уже написана и отлажена. Подключить почту, трекер задач и мессенджер — это три формы с логином, а не три клиента к трём разным API со своими причудами в обновлении токена.
Языковая модель подключается узлом AI Agent. Внутри него крутится ровно тот цикл, который мы разбирали: модель получает список инструментов и сама решает, какие звать и в каком порядке. Модель, память и инструменты цепляются к этому узлу отдельными под-узлами. Инструментом может быть другой узел холста — поиск в векторном хранилище, запрос к базе, вызов внешнего API.
Собранный сценарий выглядит так:

Никакой хитрости в этом нет — вечер работы, и оно действительно отвечает клиентам. Стоит сказать прямо: это не прототип, который потом «по-настоящему перепишут». На таких сценариях компании годами держат внутренние процессы, и переписывать их незачем.
Когда конструктор — правильный выбор
Смотреть надо не на слово «продакшн», а на три вещи сразу.
Первое: маршрут известен заранее и меняется редко. Письмо пришло, классифицировали, отправили по одной из двух веток. Если завтра ветку добавят, её дорисуют мышкой за десять минут.
Второе: основная работа — это склейка чужих сервисов. Почта, трекер, мессенджер, таблица, CRM. Чем больше в задаче интеграций и чем меньше собственной логики, тем сильнее перевес конструктора: каждый готовый узел — это рабочий день, который вы не потратили на клиента к очередному API.
Третье: цена ошибки терпима. Неудачный ответ клиенту читает человек и может поправить. Не списание денег и не рассылка на всю базу.
Когда все три верны, спорить с конструктором нечем: он выигрывает по срокам с большим отрывом, и написанный вручную сервис будет делать то же самое, только через три недели и с вашей собственной обвязкой вокруг токенов Gmail.
Где проходит граница
Собранный на холсте сервис поддержки проработал месяц, и требования к нему подросли — как это всегда и бывает. Разберём их по одному: каждое само по себе выглядит мелочью, а вместе они и составляют границу, после которой холст перестаёт быть удобнее кода.
Возврат на предыдущий шаг
Первое требование пришло от юристов: агент не должен обещать сроки доставки, которых компания не гарантирует. Решение напрашивается — перед отправкой прогонять готовый ответ вторым вызовом модели, и если проверка не прошла, возвращаться назад и переформулировать. Не больше двух попыток, дальше письмо уходит человеку.
Внутри узла AI Agent цикл есть, но он свой: модель ходит по инструментам, пока не сочтёт задачу решённой. Цикл вокруг ваших собственных узлов — ответ, проверка, снова ответ — это стрелка назад на холсте. Нарисовать её можно, но счётчик попыток придётся держать в переменной и вручную проверять на каждом проходе, а схема после этого перестаёт читаться: линия идёт против общего потока, и глазами уже не видно, сколько раз выполнится каждый узел.

Вот та самая граница, и она проходит буквально по одной стрелке. Прямой маршрут — работа для холста. Появилось ребро назад — вы описываете алгоритм, просто мышкой.
Промпт — это код
Второе требование пришло от самих себя: после каждой правки промпта половина ответов ломалась в неожиданных местах. Значит, нужен набор из пары сотен реальных писем и прогон по нему перед каждым изменением — обычный регресс, только вместо ожидаемого результата проверяются свойства ответа.
Здесь конструктор упирается сразу в две вещи. Прогон двухсот писем — это двести исполнений, которые тарифицируются, а гонять их придётся на каждую правку формулировки. И сам текст промпта нужно хранить так, чтобы видеть дифф, проходить ревью и откатываться на прошлую версию; синхронизация с Git в n8n доступна на старших тарифах, а на младших история правок остаётся внутри платформы, без привычного разбора «кто и что поменял в четверг».
Что происходит внутри
Холст показывает исполнение хорошо: видно, какие узлы отработали и какие данные лежали на входе и выходе каждого. В коде так удобно не бывает, пока вы это не настроите.
А вот чего не видно: сколько токенов ушло на конкретный вызов модели, почему из трёх инструментов был выбран этот, и как менялась задержка на шаге проверки последние две недели. Всё это — вопросы к отдельным вызовам внутри одного исполнения, и отвечает на них трейсинг: каждый вызов модели, каждый вызов инструмента и каждый шаг агента записываются отдельным отрезком со своей длительностью, стоимостью и параметрами. Под-узел трейсинга к AI Agent подключить можно, но всё, что вокруг него на холсте, в такую картину не попадёт.
Права и деньги
Ещё два требования пришли, когда сервисом заинтересовались соседние отделы.
Учётные данные в конструкторе привязаны к подключению: узел ходит в трекер от одной технической учётки. Правило «менеджер видит обращения только своих клиентов» относится не к подключению, а к каждому отдельному запросу, и выразить его в настройках узла нечем — это условие, которое нужно подставлять в запрос.
А деньги считаются по исполнениям. Пока их несколько тысяч в месяц, платформа дешевле любого разработчика, и это не близко. Счёт растёт линейно с числом обращений, поэтому на десятках тысяч арифметику стоит пересчитать заранее, а не после первого неприятного инвойса. Заодно посмотрите, во что обойдётся переезд: экспорт сценария — это JSON конкретной платформы, и перенести его куда-то ещё нельзя, только пересобрать.
Сводка порогов
| Что появилось в требованиях | Что с этим на холсте |
|---|---|
| Возврат на предыдущий шаг по результату проверки | Ветвление плюс счётчик вручную, схема перестаёт читаться |
| Регрессионный прогон промпта на наборе примеров | Каждый прогон — оплаченные исполнения, набор примеров хранить негде |
| Дифф, ревью и откат промпта | Синхронизация с Git — на старших тарифах |
| Токены, выбор инструмента и задержка по шагам | Видно исполнение узла, не видно отрезков трейсинга вокруг него |
| Права на уровне отдельного запроса | Доступ привязан к подключению, а не к обращению |
| Десятки тысяч обращений в месяц | Тарификация по исполнениям, счёт растёт линейно |
Как этим пользоваться
Считайте не количество узлов и не список функций платформы, а три вещи: обратные рёбра, тесты и права. Одно обратное ребро — ещё терпимо, живут и с таким. Обратное ребро плюс требование прогонять регресс перед выкладкой — вы уже пишете программу, просто мышкой, и без диффа, тестов и трейсинга.
Важно, что ни один из порогов не звучит как «конструктор слабый». Все они звучат как «задача перестала быть склейкой сервисов и стала разработкой». Это разные утверждения, и второе честнее.
Тот же сценарий на коде
Соберём ту же поддержку заново, теперь кодом. Задача не изменилась: ответить на вопрос по товару из базы знаний, а проблему с оплатой не решать самому, а завести тикет.
from itertools import count
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_core.vectorstores.in_memory import InMemoryVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langgraph.checkpoint.memory import InMemorySaver
knowledge_base = InMemoryVectorStore(embedding=OpenAIEmbeddings())
knowledge_base.add_texts([
"Возврат товара принимаем в течение 14 дней с момента получения.",
"Доставка по Москве — на следующий день, по России — от трёх дней.",
])
@tool
def search_kb(query: str) -> str:
"""Найти в базе знаний магазина фрагменты, отвечающие на вопрос клиента."""
found = knowledge_base.similarity_search(query, k=2)
return "\n".join(doc.page_content for doc in found)
ticket_numbers = count(4100)
@tool
def create_ticket(subject: str) -> str:
"""Завести тикет в трекере на проблему с оплатой. Возвращает номер тикета."""
# на месте этой строки — вызов API трекера
return f"PAY-{next(ticket_numbers)}"
agent = create_agent(
model=ChatOpenAI(model="gpt-5.6-terra", temperature=0),
tools=[search_kb, create_ticket],
system_prompt=(
"Ты поддержка интернет-магазина. Вопрос по товару или доставке — "
"отвечай по базе знаний. Проблема с оплатой — заводи тикет. "
"Сроков, которых нет в базе знаний, не обещай."
),
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "letter-8241"}, "recursion_limit": 8}
result = agent.invoke(
{"messages": [{"role": "user", "content": "Оплатил вчера, деньги списали, заказ не появился."}]},
config,
)
for message in result["messages"]:
message.pretty_print()

Модель прочитала письмо, поняла, что это оплата, и позвала create_ticket — в базу знаний она не пошла, потому что искать там нечего. Ответ клиенту собран уже вокруг номера тикета.
Это тридцать с небольшим строк, и почти всё в них — описание задачи, а не обвязка. Но интереснее не длина, а что именно стало доступно по сравнению с холстом.
Инструмент — обычная функция
Внутри search_kb нет ничего особенного: вызов поиска и склейка результата. Именно поэтому туда можно дописать что угодно — отфильтровать документы по правам конкретного менеджера, отбросить совпадения ниже порога, записать в лог, что искали и что нашли. И проверить это тестом, не поднимая ни модель, ни платформу: функция принимает строку и возвращает строку.
Строка документации у функции — не украшение. Именно её текст уходит в модель как описание инструмента, и по нему модель решает, звать ли эту функцию. Так что описания инструментов — такая же часть промпта, как системное сообщение, и правятся они с той же осторожностью.
Лимит шагов — параметр вызова
recursion_limit обрывает цикл, если модель начала ходить по кругу. Восемь шагов на письмо в поддержку — это с запасом: обычно хватает двух-трёх. Без такого лимита зациклившийся агент выясняется по счёту от провайдера, а не по алерту.
Память — подключаемый компонент
InMemorySaver хранит переписку в оперативной памяти процесса, а thread_id связывает вызовы в один разговор: следующее письмо от того же клиента придёт с уже накопленным контекстом. Для продакшна вместо него ставят хранилище, которое переживает перезапуск, — и это замена одной строки, а не переделка сценария.
Что здесь важнее всего
Каждая из трёх вещей — поиск, лимит, память — это отдельный кусок, который можно подменить, обернуть, залогировать и протестировать по отдельности. Разница с холстом не в том, что кода «больше возможностей». Разница в том, что у решения появились швы, по которым его можно разбирать. Именно они дают дифф, тесты и трейсинг, которых не хватало на холсте.
Гибрид: конструктор снаружи, код внутри
Из двух предыдущих шагов легко сделать вывод, что выбор бинарный: либо холст, либо код. На практике так почти не бывает. Ни одна команда не переписывает работающий сценарий целиком за один заход — переезд идёт частями, и промежуточное состояние часто оказывается конечным.
Кто что делает лучше
Посмотрим ещё раз, за что мы платили в каждом варианте. Конструктор дал готовую авторизацию к почте, трекеру и мессенджеру, приём входящих по расписанию и вебхукам, интерфейс, в котором видно каждое исполнение. Всё это пришлось бы писать самим, и это была бы неделя работы без единой строчки, относящейся к задаче.
Код дал ровно три вещи: возврат на предыдущий шаг, тесты с диффом промпта и права на уровне запроса. Заметьте, что ни одна из них не про интеграции.
Отсюда и деление. Всё, что связано с внешними сервисами, остаётся на холсте. Всё, что требует тестов, версий и трейсинга, уезжает в собственный сервис и вызывается с холста одним HTTP-запросом.

Как выглядит граница
Сценарий на холсте после такого разделения становится короче, чем был: триггер на новое письмо, узел HTTP Request с вызовом вашего сервиса, ветвление по полю в ответе, отправка письма или создание тикета. Узла AI Agent в нём больше нет — вся работа с моделью переехала за HTTP.
Сервис на другом конце — обычный веб-обработчик поверх того же агента. Он принимает текст письма и идентификатор клиента, а возвращает готовый ответ и решение, что с ним делать:
@app.post("/support/answer")
def answer(letter: Letter) -> dict:
config = {
"configurable": {"thread_id": letter.client_id},
"recursion_limit": 8,
}
result = agent.invoke(
{"messages": [{"role": "user", "content": letter.text}]},
config,
)
return {"reply": result["messages"][-1].content, "action": decide(result)}
Что здесь важно: thread_id — идентификатор клиента, поэтому переписка продолжается между письмами, а холст об этом ничего не знает и знать не должен. Ему возвращают текст и поле action, по которому он расходится на ветки. Вся неопределённость, ради которой мы вообще брали агента, осталась по одну сторону границы.
С чего начинать переезд
Переносить стоит не то, что сложнее всего выглядит, а то, что чаще всего ломается. Обычно это один узел — тот, вокруг которого больше всего ветвлений и переменных со счётчиками. Его выносят в сервис первым, на холсте на его месте появляется HTTP Request, а остальные тридцать узлов остаются на месте и продолжают работать.
Проверить, что перенос удался, можно до переключения: пустить оба варианта параллельно на одном и том же потоке писем и сравнить ответы за неделю. Расхождения почти наверняка найдутся, и почти всегда они окажутся не ошибкой нового кода, а поведением старого сценария, о котором никто не помнил.
И обратный случай стоит держать в голове: если после разделения в собственном сервисе остался один вызов модели без циклов, проверок и тестов — переносить было нечего, и правильным ответом был холст.
Задача 1
Команда за вечер собрала на n8n сценарий, который читает почту, заводит задачи в трекере и пишет в мессенджер. Что дало основной выигрыш в скорости?
Задача 2
Сценарием поддержки на холсте заинтересовались соседние отделы, и появилось правило: менеджер видит обращения только своих клиентов. Почему его не выходит выполнить настройками подключения?
Задача 3
Появилось требование: готовый ответ прогоняется через проверку, и если она не прошла — ответ переформулируется, но не больше двух раз. Почему это требование считают переломным для холста?
Задача 4
Инженер возражает: «На холсте видно каждое исполнение и данные на всех узлах — трейсинг нам не нужен». В чём он ошибается?
Задача 5
В разобранном коде поддержки за каждую роль отвечает своя часть вызова. Сопоставьте роль с тем, чем она задаётся.
Сопоставьте элементы
Задача 6
Сценарий поддержки на холсте решили переносить в собственный сервис не целиком, а по частям. С какого узла разумно начать?
Задача 7
В прогоне агент ни разу не заглянул в базу знаний, хотя инструмент search_kb был ему доступен. Почему так вышло?
Задача 8
Другая команда делит по той же схеме свой сценарий — не поддержку, а обработку заявок с сайта. По какому признаку проводить границу между холстом и собственным сервисом?
Задача 9
Сценарий из тридцати узлов переводят на гибридную схему. Расставьте шаги переезда в порядке выполнения.
Расположите элементы в правильном порядке
- 1.Заменить узел на холсте вызовом HTTP Request
- 2.Неделю гонять старый и новый варианты на одном потоке писем
- 3.Собрать его логику отдельным сервисом за HTTP
- 4.Выбрать узел, вокруг которого больше всего ветвлений и счётчиков
Задача 10
Порог перехода на свой хостинг
Счёт за конструктор растёт вместе с числом обращений, и пока их немного, спорить не о чем. Интересно другое: где именно арифметика переворачивается. Это тот случай, когда лучше посчитать заранее, а не после первого неприятного инвойса.
Задание
Тариф платформы устроен так: base рублей в месяц, в которые уже включено quota исполнений. Каждый начатый пакет из pack_size исполнений сверх квоты стоит ещё pack_price рублей — пакет оплачивается целиком, даже если из него использовано одно исполнение. Свой хостинг обходится в self_hosted рублей в месяц при любом объёме.
Реализуйте функцию switch_point(base, quota, pack_size, pack_price, self_hosted). Верните наименьшее целое число исполнений в месяц, при котором платформа обходится строго дороже своего хостинга.
Пример
switch_point(2000, 5000, 1000, 500, 9000) # 19001
switch_point(12000, 5000, 1000, 500, 9000) # 0
В первом примере на 19 000 исполнений оплачены 14 пакетов сверх квоты, и счёт составит ровно 9000 — столько же, сколько свой хостинг. Строго дороже становится со следующего исполнения: оно открывает пятнадцатый пакет.
Во втором примере абонентская плата сама по себе выше стоимости хостинга, поэтому порог достигнут уже на нулевом объёме.
Примечание
Все аргументы — целые неотрицательные числа, при этом pack_size и pack_price строго больше нуля, так что порог существует всегда. Значение quota может быть нулевым: тогда пакеты считаются с первого же исполнения.
Разрешена только стандартная библиотека Python 3.12. Числа бывают большими, но перебирать исполнения по одному не нужно — ответ считается формулой.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 11
Возвраты в сценарии
Мы договорились, что граница между холстом и кодом проходит по обратному ребру: пока маршрут идёт вперёд, его удобно рисовать мышкой, а как только появился возврат — это уже алгоритм. Глазами такое ребро заметно на схеме из четырёх узлов и совершенно теряется на схеме из сорока. Значит, искать его должен код.
Задание
Экспорт сценария приходит двумя списками: nodes — имена узлов, edges — пары (откуда, куда). Узел лежит на возврате, если из него можно по стрелкам вернуться в него же — за один шаг или за десять.
Реализуйте функцию find_loop_nodes(nodes, edges). Верните имена таких узлов, отсортированные обычным sorted. Если возвратов в сценарии нет, верните пустой список.
Пример
find_loop_nodes(
["Письмо", "Ответ", "Проверка", "Отправка"],
[("Письмо", "Ответ"), ("Ответ", "Проверка"),
("Проверка", "Ответ"), ("Проверка", "Отправка")],
) # ["Ответ", "Проверка"]
find_loop_nodes(
["Письмо", "Ответ", "Отправка"],
[("Письмо", "Ответ"), ("Ответ", "Отправка")],
) # []
В первом примере проверка возвращает управление на составление ответа, и оба этих узла оказываются на возврате. Приём письма и отправка в него не попадают: вернуться в них уже нельзя.
Примечание
Имена узлов уникальны, и каждое имя из edges обязательно встречается в nodes. Одна и та же пара может встретиться в edges несколько раз — на ответ это не влияет. Ребро узла в самого себя тоже считается возвратом. Узлы, до которых вообще нет стрелок, в списке присутствовать могут.
Разрешена только стандартная библиотека Python 3.12. Узлов бывает до 300, рёбер до 2000 — этого достаточно, чтобы для каждого узла отдельно проверить, можно ли из него вернуться обратно.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
