Урок 4. Логика без модели
Три предыдущих урока крутились вокруг модели. Этот — нет: в графе не будет ни одной LLM-ноды. Сообщение придёт на вход, нода проверит условие, две ветки соберут разный ответ, и обе сойдутся в один выход.
Главный аргумент урока — цена. Запуск воркфлоу стоит один кредит, а всё остальное в счёте — это вызовы моделей и платные внешние операции. Здесь их нет, поэтому каждый прогон стоит ровно один кредит из ста месячных, сколько бы раз вы его ни повторяли, пока отлаживаете условие. Для сравнения: один ответ бота из первого урока обходится примерно в два кредита. Разбор формулы — в «Кредитах».
Заодно это два навыка, на которых спотыкаются чаще всего: ветвление и шаблоны.
- Исполнение + данные
Что будет делать граф
Сценарий бытовой: посетитель пишет в чат, и мы хотим отвечать по-разному на срочное обращение и на обычный вопрос — без модели, по слову в тексте. Логика простая нарочно: цель урока — механика, а не сообразительность бота.
Сборка
-
В разделе Воркфлоу нажмите Новый воркфлоу, задайте имя. На пустом холсте поставьте Вход и Выход — они в разделе палитры
Input / Output, группаBoundaries. -
Перетащите ноду Если / Иначе — раздел
Logic & Flow, группаBranching. Соедините выход Входа с её входом. -
Откройте настройки ноды (выделите её и нажмите карандаш) и впишите в поле «Условие» одно выражение:
{{ 'срочно' in inputs.input | lower }}inputs.input— то, что пришло на её вход, то есть текст сообщения. Фильтр| lowerприводит его к нижнему регистру, чтобы «Срочно» и «СРОЧНО» тоже засчитались. -
Поставьте две ноды Шаблон — раздел
Data, группаValues & State. Одну соедините с выходомTrueноды Если / Иначе, вторую — с выходомFalse. -
В шаблоне первой напишите ответ для срочного обращения, во второй — для обычного. Внутрь можно подставить само сообщение:
Приняли как срочное: {{ inputs.input }}. Ответим в течение часа. -
Соедините выходы обеих нод Шаблон с входом Выхода.
-
Нажмите Проверить, затем Сохранить.
Условие: пишите одно выражение
Поле «Условие» — шаблонное, и от того, как вы его напишете, зависит результат.
Настройка ноды
Если / Иначе
Параметры
Вся строка — одно выражение: результат приходит как логическое значение.
Правило одно и оно общее для всех шаблонных полей: если вся строка состоит из одного
{{ ... }}, наружу приходит настоящее значение — число, список, да/нет. Если выражение
смешано с текстом, результат всегда строка. Подробно — в «Шаблонах».
Для условия это важно вот почему. Смешанную запись вида {{ inputs.input }} > 100 платформа
тоже понимает: она подставляет значение, а потом пытается прочитать получившееся как
сравнение двух чисел. Пока по обе стороны оказываются числа, всё честно. Но если слева
окажется текст — например, пришло «Здравствуйте» — сравнивать нечего, и строка целиком
считается непустой, то есть истинной. Ветка True будет срабатывать всегда, а выглядеть
это будет как «условие сломалось».
Полезные формы для этой ноды: {{ 'слово' in inputs.input | lower }} — вхождение подстроки;
{{ inputs.input | length > 200 }} — длина; {{ variables.plan == 'paid' }} — сравнение с
переменной.
Два способа сослаться на значение
В шаблонах есть два разных обращения, и путаница между ними — вторая по частоте причина пустых полей.
| Запись | Что читает |
|---|---|
{{ inputs.input }} |
Значение, которое доставил непосредственный предшественник на этот порт. Имя относительно каждой ноды: у второй ноды Шаблон это то, что пришло из Если / Иначе, а не из Входа. |
{{ nodes.if_1.output }} |
Значение любой ноды графа по её идентификатору. if_1 — тот самый id, который виден в её настройках рядом с названием. |
Рядом с output лежат остальные поля результата. У Если / Иначе это {{ nodes.if_1.branch }} —
строка true или false: удобно, когда в ответ хочется вписать, какой веткой он собран.
Кстати, о значении: Если / Иначе ничего не меняет, а пропускает вход дальше как есть. Поэтому
{{ inputs.input }} в обеих нодах Шаблон — это исходное сообщение пользователя.
Ссылка в шаблоне не запускает ноду
Это ключевая мысль, и она объясняет половину пустых результатов. {{ nodes.X.output }} —
это чтение из памяти запуска: там лежат выходы тех нод, которые уже отработали. Ссылка
никого не будит и очередь исполнения не меняет — ноду запускает только входящее ребро
(разбор — «Как исполняется граф»).
Значит, если сослаться на ноду из невыбранной ветки, читать будет нечего. Ошибки не будет:
неизвестное имя рендерится пустой строкой, и нода спокойно доработает с дырой в тексте.
Найти дыру можно: платформа записывает все пути, которые не разрешились, в отладку ноды под
ключом __undefined_refs__ — это единственный след опечатки в id. Где его смотреть,
написано в «Шаблонах».
Почему две ветки в один вход — это законно
Ветки исключают друг друга: if выбирает ровно одну ногу, второе ребро становится
мёртвым. Планировщик это понимает и запускает Выход, как только сработал хотя бы один
вход, а все оставшиеся мертвы. Выход отрабатывает один раз и получает значение той
ветки, которая сработала.
Предупреждение wire.andjoin_partial_payload, которое вы видели при проверке, ровно об
этом и говорит: значения второй ветки у выхода не будет. Обычно это и нужно, поэтому
подсказка, а не ошибка.
Разница ровно в вопросе «могут ли эти две ветки сработать в одном запуске». Взаимоисключающие — предупреждение, одновременные — ошибка.
Проверка
Откройте вкладку Тест и отправьте два сообщения: одно со словом «срочно», другое без. Ответы должны отличаться.
На холсте вкладки видно, какая нода отработала, а какая осталась серой. Кликните по пропущенной ноде Шаблон — в её карточке будет причина пропуска: условие выше выбрало другую ветку. Это штатная работа графа, а не сбой: запуск при этом считается завершённым, а не частичным. Полный список причин — в «Статусах».
Каждый такой прогон стоит один кредит и ни одного токена — можно спокойно перебрать формулировки условия.
Если не получилось
| Что видно | Почему | Что делать |
|---|---|---|
| Всегда срабатывает ветка True | Условие записано смешанной формой и после подстановки стало непустым текстом, а не сравнением | Занесите всё сравнение внутрь одних скобок: {{ inputs.input | length > 100 }} |
| Сработали обе ветки | Ветки идут не от одного условного узла, а параллельно — тогда они не исключают друг друга | Проверьте, что оба ребра выходят из одной ноды «Если / Иначе», с портов True и False |
| Валидатор требует барьер (wire.fanin_needs_barrier) | Ветви могут сработать в одном запуске — гонка за входной порт | Сведите их нодой «Дождаться всех» перед общим входом |
| Нода серая, помечена пропущенной | Условие выбрало другую ветку либо выше по графу оборвался путь | Откройте карточку ноды в запуске и прочитайте причину пропуска |
| В ответе дыра: часть текста пустая | Имя в шаблоне не разрешилось — опечатка в id или ссылка на ноду из невыбранной ветки | Смотрите __undefined_refs__ в отладке ноды; сравните id с тем, что написано в настройках |
| Фигурные скобки видны в ответе как есть | Поле не является шаблонным — подстановку делают только помеченные поля | Перенесите выражение в поле «Шаблон» ноды «Шаблон» |
| Предупреждение andjoin_partial_payload | Ожидаемо для этого графа: взаимоисключающие ветви сходятся в один вход | Ничего не делать — оно не блокирует ни сохранение, ни публикацию |
Что дальше
Как исполняется граф
Мёртвые рёбра, OR-join и барьер — почему нода запускается или нет.
Шаблоны
Все пространства имён, нативные значения и однократный рендер.
Циклы
Следующий шаг после ветвления: повторить действие для каждого элемента.
Что могло пойти не так
Разбор частых поломок и куда смотреть в первую очередь.