К содержимому
Завалищев
База знаний
Инструкции 8 мин чтения

Почему ваши агенты нестабильны: промпт — плохой исполнительный слой

Валидация, retry, маршрутизация живут в промпте — агент работает в 72% случаев. Выношу execution logic в код — 97%. Разбираю, что не должно быть в промпте.

У меня был агент на Claude Sonnet. Обработка клиентских заявок: классифицировать, извлечь данные, проверить в базе, ответить. Системный промпт — 2 800 токенов. Роль, порядок шагов, условия ветвления, формат ответа, retry, валидация, ограничения. Всё в одном тексте.

Стабильность: 72%.

Каждый третий запрос — сбой. Пропущенный шаг, сломанный JSON, бесконечный retry, неправильный маршрут. Три недели я переписывал промпт: уточнял формулировки, добавлял примеры, капслочил важное. Довёл до 81%. Потолок. Дальше промпт не тянул.

За один вечер вынес управление выполнением в Python. Граф шагов в LangGraph, Pydantic-валидация, retry в коде. Та же модель, те же данные — 97%.

Разница — не в промпте. А в том, кому поручено управление выполнением.

Промпт — рекомендация, не контракт

Промпт — текст на естественном языке. LLM интерпретирует его вероятностно. «Сначала классифицируй, потом извлеки, потом проверь» — для модели это не императивная программа, а рекомендация с весами.

В 82% запусков — следует порядку. В 13% — пропускает шаг или переставляет. В 5% — импровизирует. Не потому что модель плохая — потому что естественный язык не имеет гарантий исполнения.

Unknown block type "code", specify a component for it in the `components.types` option

18% ненадёжности — свойство среды, не баг промпта. LLM оптимизирует на «полезный ответ», а не на «точное следование инструкции».

Пять вещей, которые нельзя доверять промпту

1. Порядок выполнения

Промпт описывает последовательность словами. LLM решает, следовать ли. Граф в коде — гарантирует:

Unknown block type "code", specify a component for it in the `components.types` option

Агент физически не может пропустить validate. Переходы детерминированные. LLM работает внутри шага — но не контролирует переходы.

2. Валидация результатов

Промпт: «Убедись, что сумма — число, а дата в формате YYYY-MM-DD.»

LLM, который сгалюцинировал число, не поймает свою галлюцинацию при «проверке». Он уверен в ответе. Реальный случай: агент извлёк «15 000» как 150000. Промпт говорил проверить — модель «проверила» и подтвердила. Две заявки с десятикратно завышенной суммой ушли в работу.

Unknown block type "code", specify a component for it in the `components.types` option

Pydantic ловит 100% нарушений. Не «обычно» — каждый раз. При ошибке — retry с конкретным сообщением: модель получает обратную связь от кода, не от себя.

3. Retry-логика

Промпт: «Если ошибка — попробуй ещё раз с другими параметрами.» Реальность: LLM повторяет с теми же параметрами (формально «другими» — переставил пробел). Или входит в бесконечный цикл: промпт не задал лимит. Один запрос — 38 вызовов за 4 минуты. $12.

Unknown block type "code", specify a component for it in the `components.types` option

Три попытки. Backoff. Таймаут. Fallback на человека. Десять строк — и бесконечных циклов больше нет.

4. Маршрутизация с приоритетами

Промпт: «Жалобу — в качество. Срочное — эскалируй. Вопрос — в FAQ.» Заявка — одновременно жалоба, срочная и содержит вопрос. Промпт не описал приоритеты. LLM выбрал один маршрут, потерял два.

Unknown block type "code", specify a component for it in the `components.types` option

Детерминированно. Приоритет — явный. 20 строк unit-тестов покрывают все комбинации. Промпт нельзя unit-тестировать.

5. Управление состоянием

Промпт: «Помни, что клиент уже верифицирован. Не проверяй повторно.» На длинной цепочке из 15 tool calls системный промпт — далеко от текущей позиции. Правила теряют вес. Модель «забывает».

Unknown block type "code", specify a component for it in the `components.types` option

Bool — не «помнишь ли ты». Typed dict хранит состояние точно.

Что остаётся в промпте

После выноса execution logic промпт сжимается с 2 800 до 300-400 токенов.

Было (один промпт на всё):

Unknown block type "code", specify a component for it in the `components.types` option

Стало (промпт для одного шага):

Unknown block type "code", specify a component for it in the `components.types` option

Одна задача. Короткий промпт. LLM справляется на 95%+, а не на 72% — потому что задача проще.

Правило разделения

Если инструкцию можно выразить как if/else, цикл или Pydantic-схему — ей не место в промпте.

В промпте (LLM думает): понять суть заявки, извлечь сущности из текста, оценить тональность, сгенерировать ответ, принять нестандартное решение.
В коде (код выполняет): порядок шагов, валидация результатов, retry с backoff, маршрутизация, управление состоянием.

Результат

Стабильность: промпт = execution — 72% → 81% (потолок). Код = execution — 97%.
Промпт: 2 800 токенов → 300-400 на шаг.
Отладка edge case: 2-4 часа → 15-30 минут.
Добавить шаг: 1-2 часа → 30 минут.
Unit-тесты: невозможно → полное покрытие.

25 процентных пунктов. Без смены модели, без fine-tuning. Одно архитектурное решение: перестать просить LLM управлять выполнением.

Промпт — для мышления. Код — для выполнения. Каждый раз, когда в промпте пишете «сначала X, потом Y, если ошибка — повтори» — вы просите поэта работать диспетчером. Он справится. Иногда. Диспетчер — всегда.