Урок курса

Граница no-code: когда хватает n8n, когда нужен код

AI Agents PRO: LangGraph, AutoGen и LLMOps в продакшне

Что умеет визуальный конструктор

Курс про то, как собирать агентов кодом, и самый лёгкий способ оправдать его существование — объявить визуальные конструкторы игрушкой для тех, кто не умеет программировать. Так делать не будем. На них работают вполне серьёзные вещи, и знать, где именно они перестают справляться, полезнее, чем отмахнуться.

No-code — сборка решения из готовых блоков в визуальном редакторе, без единой строки кода. Low-code — то же самое, но со вставками собственных скриптов там, где готовых блоков не хватило. Граница между ними размытая: почти каждая заметная платформа сегодня умеет и так, и так. Дальше будем говорить «конструктор», подразумевая оба варианта.

Задача, на которой будем разбираться

Возьмём один сценарий и пронесём его через весь урок. В интернет-магазин приходят письма в поддержку, и с ними нужно сделать три вещи: понять, о чём письмо; на вопрос по товару ответить самому, взяв факты из базы знаний; а проблему с оплатой не решать, а завести тикет в трекере и написать дежурному менеджеру в мессенджер.

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

Как это выглядит в n8n

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

Языковая модель подключается узлом AI Agent. Внутри него крутится ровно тот цикл, который мы разбирали: модель получает список инструментов и сама решает, какие звать и в каком порядке. Модель, память и инструменты цепляются к этому узлу отдельными под-узлами. Инструментом может быть другой узел холста — поиск в векторном хранилище, запрос к базе, вызов внешнего API.

Собранный сценарий выглядит так:

Холст n8n: письмо приходит в узел AI Agent, к которому подключены модель, память и инструменты; дальше маршрут расходится на ответ клиенту и заведение тикета

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

Когда конструктор — правильный выбор

Смотреть надо не на слово «продакшн», а на три вещи сразу.

Первое: маршрут известен заранее и меняется редко. Письмо пришло, классифицировали, отправили по одной из двух веток. Если завтра ветку добавят, её дорисуют мышкой за десять минут.

Второе: основная работа — это склейка чужих сервисов. Почта, трекер, мессенджер, таблица, 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 с темой обращения, получил номер PAY-4100 и ответил клиенту

Модель прочитала письмо, поняла, что это оплата, и позвала create_ticket — в базу знаний она не пошла, потому что искать там нечего. Ответ клиенту собран уже вокруг номера тикета.

Это тридцать с небольшим строк, и почти всё в них — описание задачи, а не обвязка. Но интереснее не длина, а что именно стало доступно по сравнению с холстом.

Инструмент — обычная функция

Внутри search_kb нет ничего особенного: вызов поиска и склейка результата. Именно поэтому туда можно дописать что угодно — отфильтровать документы по правам конкретного менеджера, отбросить совпадения ниже порога, записать в лог, что искали и что нашли. И проверить это тестом, не поднимая ни модель, ни платформу: функция принимает строку и возвращает строку.

Строка документации у функции — не украшение. Именно её текст уходит в модель как описание инструмента, и по нему модель решает, звать ли эту функцию. Так что описания инструментов — такая же часть промпта, как системное сообщение, и правятся они с той же осторожностью.

Лимит шагов — параметр вызова

recursion_limit обрывает цикл, если модель начала ходить по кругу. Восемь шагов на письмо в поддержку — это с запасом: обычно хватает двух-трёх. Без такого лимита зациклившийся агент выясняется по счёту от провайдера, а не по алерту.

Память — подключаемый компонент

InMemorySaver хранит переписку в оперативной памяти процесса, а thread_id связывает вызовы в один разговор: следующее письмо от того же клиента придёт с уже накопленным контекстом. Для продакшна вместо него ставят хранилище, которое переживает перезапуск, — и это замена одной строки, а не переделка сценария.

Что здесь важнее всего

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

Гибрид: конструктор снаружи, код внутри

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

Кто что делает лучше

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

Код дал ровно три вещи: возврат на предыдущий шаг, тесты с диффом промпта и права на уровне запроса. Заметьте, что ни одна из них не про интеграции.

Отсюда и деление. Всё, что связано с внешними сервисами, остаётся на холсте. Всё, что требует тестов, версий и трейсинга, уезжает в собственный сервис и вызывается с холста одним HTTP-запросом.

Слева блоки конструктора: приём письма, коннекторы, уведомления; справа собственный сервис с агентом, промптами и трейсингом; между ними 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. 1.Заменить узел на холсте вызовом HTTP Request
  2. 2.Неделю гонять старый и новый варианты на одном потоке писем
  3. 3.Собрать его логику отдельным сервисом за HTTP
  4. 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. Числа бывают большими, но перебирать исполнения по одному не нужно — ответ считается формулой.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 11

Возвраты в сценарии

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

Задание

Экспорт сценария приходит двумя списками: nodes — имена узлов, edges — пары (откуда, куда). Узел лежит на возврате, если из него можно по стрелкам вернуться в него же — за один шаг или за десять.

Реализуйте функцию find_loop_nodes(nodes, edges). Верните имена таких узлов, отсортированные обычным sorted. Если возвратов в сценарии нет, верните пустой список.

Пример

find_loop_nodes(
    ["Письмо", "Ответ", "Проверка", "Отправка"],
    [("Письмо", "Ответ"), ("Ответ", "Проверка"),
     ("Проверка", "Ответ"), ("Проверка", "Отправка")],
)  # ["Ответ", "Проверка"]

find_loop_nodes(
    ["Письмо", "Ответ", "Отправка"],
    [("Письмо", "Ответ"), ("Ответ", "Отправка")],
)  # []

В первом примере проверка возвращает управление на составление ответа, и оба этих узла оказываются на возврате. Приём письма и отправка в него не попадают: вернуться в них уже нельзя.

Примечание

Имена узлов уникальны, и каждое имя из edges обязательно встречается в nodes. Одна и та же пара может встретиться в edges несколько раз — на ответ это не влияет. Ребро узла в самого себя тоже считается возвратом. Узлы, до которых вообще нет стрелок, в списке присутствовать могут.

Разрешена только стандартная библиотека Python 3.12. Узлов бывает до 300, рёбер до 2000 — этого достаточно, чтобы для каждого узла отдельно проверить, можно ли из него вернуться обратно.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку