Перейти к содержимому

Антипаттерны

Антипаттерны — это повторяющиеся ошибки проектирования workflow, которые приводят к сломанным, неэффективным или неподдерживаемым workflow. Workflow Management Flow (WMF) автоматически проверяет их при создании и редактировании.

Директива говорит агенту собрать данные, затем inputSchema просит агента вернуть эти же данные как структурированный вывод. Агент работает как оператор ввода данных, копируя из вывода инструмента в поля схемы.

Обнаружение: inputSchema повторяет структуру данных, которые агенту было поручено собрать.

❌ Directive: "Get project info from package.json"
inputSchema: { project_name: string, version: string, description: string }
// Агент копирует данные из файла в поля схемы — бессмысленный кругооборот
✅ Use expression node to extract data, or capture agent's ANALYSIS in inputSchema
inputSchema: { architecture_assessment: string, risks_identified: string[] }

У узла нет inputSchema, но директива указывает агенту выполнить побочные эффекты (создать файлы, запустить команды). Никаких свидетельств о выполненном не сохраняется.

Обнаружение: Узел agent-directive без inputSchema, где директива описывает действия с наблюдаемыми результатами.

❌ Directive: "Create test files for auth module" (no inputSchema)
// Workflow не знает, что было создано и было ли выполнено успешно
✅ inputSchema: { files_created: string[], tests_passing: boolean }
// Свидетельства завершения сохраняются в контексте workflow

#3: Необъявленная переменная / Неявное продвижение

Заголовок раздела «#3: Необъявленная переменная / Неявное продвижение»

Ссылка на переменную по голому имени, которая не объявлена в variableRegistry workflow, или ожидание, что локальный вывод узла станет глобальной переменной за счёт совпадения имён. Глобальные переменные должны объявляться явно; узел записывает глобальную переменную только когда указывает её имя в inputSchema.globalInputs.

Обнаружение: Шаблонная ссылка по голому имени (не локальная node-id.name и не системная переменная вроде executionId), для которой нет соответствующей записи в variableRegistry. Или workflow, который полагается на продвижение возвращаемого ключа узла в глобальную переменную по совпадению имён.

❌ Директива ссылается на переменную по голому имени, не объявленную в variableRegistry
// Нет записи в реестре → значение undefined во время выполнения
❌ Узел возвращает ключ и ожидает, что он станет глобальным из-за совпадения имени
// Неявного продвижения нет — значение остаётся локальным выводом узла
✅ Объявляйте каждую глобальную переменную в variableRegistry
Узел записывает глобальную переменную только через inputSchema.globalInputs (указав имя)
Ссылайтесь на локальные выводы узла как node-id.name

Обратная связь при ревью передаётся через текстовые поля inputSchema вместо персистентных файлов. Агент теряет контекст между итерациями.

Обнаружение: Цикл ревью/исправления, где узел исправления получает обратную связь только через текст inputSchema, а не через персистентный файл.

❌ Review node → inputSchema: { feedback: string } → Fix node reads feedback from input
// Обратная связь эфемерна, теряется при повторе
✅ Review node writes review.md → Fix node reads review.md
// Результаты сохраняются между итерациями и повторами

Директива говорит «исправляй пока не будет готово» без явной структуры цикла в графе. Агент повторяет попытки внутренне без контроля workflow.

Обнаружение: Слова вроде «retry», «keep trying», «fix until» в директиве без соответствующего условного узла, создающего цикл в графе.

❌ Directive: "Keep fixing tests until they all pass"
// Нет структуры графа, агент зацикливается внутренне без контроля workflow
✅ [fix] → [run-tests] → condition(passed?) → [fix] / [next]
// Явный цикл в графе с видимостью каждой итерации для workflow

Циклы валидации без лимитов итераций или путей эскалации. Могут выполняться бесконечно, если условие никогда не будет выполнено.

Обнаружение: Цикл в графе без переменной-счётчика итераций или без условия проверки лимита итераций.

❌ Loop: [action] → [check] → [fix] → [action] (бесконечно, если качество никогда не пройдёт)
✅ Expression node increments counter → Condition: iterations > max → escalation path
// Агент не может застрять — после N попыток эскалация пользователю

Хранение динамических данных, генерируемых в ходе выполнения workflow (содержимое файлов, ответы API, HTML-вывод, результаты извлечения) в переменных workflow. Эти переменные внедряются в каждую директиву через шаблоны, раздувая контекст потенциально неограниченным содержимым.

Обнаружение: Переменные, получающие динамический контент через inputSchema во время выполнения, особенно контент >1KB или контент, растущий неограниченно.

Область применения: Этот антипаттерн относится к данным, передаваемым между шагами workflow — НЕ к статической конфигурации, хранящейся как значение default в variableRegistry. См. паттерн Статическая конфигурация workflow для понимания различия.

❌ inputSchema: { html_content: string }
Next directive: "Publish {{step.html_content}}"
// Целая HTML-страница хранится в переменной, внедряется в каждую последующую директиву
❌ inputSchema: { extraction_results: object }
Next directive: "Analyze {{extraction_results}}"
// Потенциально большие данные извлечения передаются туда-обратно через переменные
✅ Directive: "Save HTML to {{workspace_path}}/report.html"
inputSchema: { file_path: string }
Next directive: "Publish file at {{step.file_path}}"
// В переменной хранится только путь, агент читает файл по необходимости

Исключение: Статический инструктивный контент, хранящийся как значение default в variableRegistry (правила, стандарты, чек-листы), — это правильный паттерн, а не злоупотребление переменными. Они намеренно размещены, чтобы агент видел их на каждом соответствующем шаге. См. Статическая конфигурация workflow.

Системное соображение. systemReminder статичен и применяется ко ВСЕМ шагам. Инструкции для конкретных шагов должны быть в директивах узлов, а не в systemReminder.

Агент вручную отслеживает индексы массивов, счётчики или пагинацию через inputSchema. Подвержено ошибкам и ломается при повторе.

Обнаружение: inputSchema с полями вроде current_index, next_item_number или значениями счётчиков, которые агент должен вычислять.

❌ inputSchema: { current_index: number, next_item: string }
// Агент выполняет арифметику, подвержен ошибкам
✅ Expression node: current_index = {current_index} + 1
Directive: "Process {{items[{current_index}]}}"
// Движок workflow обрабатывает арифметику детерминированно

Передача всего JSON workflow через параметры MCP-инструмента для больших workflow. Упирается в лимиты размера.

Обнаружение: manage({ action: "create", workflow: <large JSON> }) с workflow >50KB.

❌ manage({ action: "create", workflow: <50KB JSON> })
// Может упереться в лимиты размера параметров
✅ token({ action: "upload" }) → HTTP PUT with file content
// Загрузка по токену обрабатывает любой размер

Копирование информации, уже имеющейся в контексте агента, в переменные workflow. Избыточно и раздувает контекст.

Обнаружение: inputSchema, захватывающая данные, которые агент уже имеет в контексте разговора (например, списки директорий, информация о проекте из предыдущего анализа).

❌ variable: project_structure = "<output of tree command>"
// Агент уже имеет это в контексте; повторное хранение тратит токены
✅ Let agent re-read current data when needed. Store only stable references (paths, IDs).

#12: Вынесение критических инструкций в файлы

Заголовок раздела «#12: Вынесение критических инструкций в файлы»

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

Обнаружение: Директива говорит «прочитай правила из файла X» для контента, который должен быть стабильно доступен. Инструкции, критичные для поведения агента, хранятся вне workflow.

❌ Directive: "Read planning standards from ./standards.md before creating plan"
// Агент может пропустить чтение, вспомнить из обучающих данных или прочитать выборочно
// Дополнительная операция ввода-вывода на каждом шаге, требующем этих правил
✅ Declare as a variableRegistry default: planning_standards = "1. Each step must... 2. Tests built-in..."
Directive: "Create plan following {{planning_standards}}"
// Движок workflow гарантирует доставку; нет ввода-вывода; нет риска галлюцинаций

Агент видит одну и ту же директиву ревью/валидации дважды и сообщает пользователю, что workflow зациклился или застрял, после чего останавливается.

Обнаружение: Обратная связь повторной валидации к идентичному узлу без счётчика раундов, без подсказки «ожидаемо, не баг» и зачастую без ограничения.

❌ [validate] → condition(issues?) → true → [fix] → [validate]
// Та же директива validate появляется снова; агент сообщает «workflow застрял в цикле»
✅ expression(validation_round = validation_round + 1) → [validate] (рендерит счётчик и подсказку)
condition(validation_round < max_validation_rounds) → true → [fix] → expression(...) → [validate]
→ false → [ask-user-limit]

Директива входа в цикл рендерит счётчик раундов и сообщает, что цикл ожидаем:

Re-validation pass {{validation_round}} of {{max_validation_rounds}} — a normal quality loop,
expected to converge. This is NOT a bug and NOT a stuck flow; do not report a loop to the user.

Счётчик инкрементируется узлом expression (автоматически потоком), объявляется в variableRegistry с числовым default (счётчик по умолчанию 0, max по умолчанию, например, 5). Счётчик, инкрементируемый агентом, сам является антипаттерном (см. #9).

{{var}} рендерится как [[UNDEFINED_VARIABLE]] во время выполнения, хотя var объявлена в variableRegistry; выражение вроде var = var + 1 даёт NaN.

Обнаружение: Объявление в variableRegistry без default, где переменная не инициализирована в start initialData и не записывается ни одним вышестоящим узлом через globalInputs.

❌ variableRegistry: { validation_round: { type: "number" } } // нет default
Directive: "Pass {{validation_round}} ..." // рендерится [[UNDEFINED_VARIABLE]]
Expression: validation_round = validation_round + 1 // NaN
✅ variableRegistry: { validation_round: { type: "number", default: 0 } }
// Счётчик начинается с реального значения; инкременты и шаблоны разрешаются

Задавайте default каждому счётчику и флагу. Валидатор выдаёт предупреждение «объявлена без default», когда переменная, на которую ссылаются, не имеет default и нигде не записывается выше по потоку.

{{name}}, который агент записывает в данные, возвращаемые через step(), рендерится как [[UNDEFINED_VARIABLE]], когда это значение позже интерполируется в директиву.

Обнаружение: Возвращаемые значения inputSchema, содержащие синтаксис шаблона {{...}} по голому имени.

❌ step() returns: { summary: "Report for {{project_name}}" }
Next directive: "Publish {{summary}}" // {{project_name}} рендерится [[UNDEFINED_VARIABLE]]
✅ step() returns: { summary: "Report for acme-api" }
// Подставленные значения данных считаются литеральным текстом, не парсятся как шаблоны

Никогда не записывайте голый {{...}} в данные step(); шаблоны принадлежат только статическим полям узла (directive, completionCondition, message). Движок считает подставленные значения данных литеральными.

Недоверенный ввод, содержащий синтаксис шаблона, интерполированный в директиву, может попытаться выгрузить набор переменных ({{context.variables}}), выполнить хелпер each/if или обойти область видимости другого узла через node-id.field.

Обнаружение: Директива интерполирует переменную, значение которой исходит из недоверенного ввода, способного содержать синтаксис {{...}}.

❌ Directive: "Summarize the user's note: {{user_note}}"
где user_note = "{{context.variables}}" // попытка выгрузить все переменные
✅ Интерполированные ЗНАЧЕНИЯ литеральны — движок нейтрализует синтаксис фигурных скобок,
исходящий из подставленных данных, поэтому {{context.variables}} в user_note рендерится как
обычный текст. Не отражайте недоверенный синтаксис шаблона в директивах; предпочитайте явные
именованные переменные полным выгрузкам {{context.variables}}.

Интерполированные значения никогда не парсятся повторно как шаблоны. Шаблоны живут в статических полях, контролируемых автором.

Правильный способ итерации по элементам массива:

1. Store array: items = ["a", "b", "c"]
2. Store index: current_index = 0
3. Directive: "Process item {{items[{current_index}]}}"
4. Expression: current_index = {current_index} + 1
5. Condition: {current_index} < items.length → loop / continue
[implement] → [review-to-file] → [check-issues] → [fix-from-file] → [review-to-file]
no issues → [next]