Когда LLM-агент начинает вести себя непредсказуемо, первый импульс — дописать правило в промпт. Это работает два-три раза, а потом система превращается в клубок противоречий. Проблема не в промпте — в том, что никто не решил, что вообще делает код, а что модель.
Почему «пусть модель разберётся» — плохая архитектура
Возьмём типичный text2sql-агент или аналитический чат-бот над базой данных. Агенту поручают всё сразу: определить тип запроса, сформировать SQL, выбрать форму вывода (строка, таблица, график), решить, нужно ли предупредить об отклонении.
На десятке тестовых вопросов система работает отлично. Потом приходят реальные пользователи с разными формулировками — и один и тот же вопрос вдруг обрабатывается по-разному. Модель иначе интерпретировала фразу, и результат пришёл в другом формате без объяснений. Или отклонение то получает пояснение, то возвращается пустым.
Каждый раз разработчик добавляет новое правило в промпт. Через несколько итераций новые правила начинают противоречить старым. Нестабильность никуда не уходит — она просто прячется в порядке применения конфликтующих инструкций.
Корень проблемы сформулирован точно: граница между тем, что делает код, и тем, что делает модель, — это не деталь реализации, а отдельный архитектурный объект. Если её не зафиксировать явно, она сложится сама — и почти всегда не в пользу устойчивости системы.
Что должен делать код, а что — модель
Anthropic описала базовое разграничение ещё в декабре 2024 года в «Building Effective Agents»: в workflow поток управляется заранее написанным кодом, в агенте — моделью. Практическое следствие простое: всё, что код сделает точнее и надёжнее модели, должен делать код.
Выбор формы вывода (строка, таблица, график) — это не семантическая задача, это десять строк обычного кода на основе типа данных в ответе. Проверка синтаксиса SQL — тоже код. Логирование категории запроса — код. Модели остаётся то, где нужно работать в концептуальном пространстве: понять намерение пользователя, построить запрос, сформулировать объяснение отклонения.
Самый известный паттерн на этой границе — cascade routing: сначала дешёвая эвристика или лёгкая модель классифицирует запрос, потом по порогу уверенности происходит эскалация к более дорогой обработке. Важно понимать: порог уверенности классификатора показывает, насколько модель убеждена в своей интерпретации, — а не насколько задача проста на самом деле. Это разные вещи.
Правило асимметрии: категория может только расти
Первое жёсткое правило для удержания границы — асимметрия пересмотра категории.
Классификатор отнёс запрос к одной категории. В процессе обработки выяснилось, что запрос сложнее. Пересмотр обязателен — но только в одну сторону: категория может подняться до более строгой и дорогой обработки, понизиться она не может никогда.
Почему это важно: ошибка повышения и ошибка понижения — несопоставимые вещи.
| Тип ошибки | Что происходит | Обнаруживаемость |
|---|---|---|
| Ошибка повышения | Потрачено больше ресурсов, чем нужно | Видна в метриках стоимости |
| Ошибка понижения | Основание ответа молча подменяется более слабым | Не видна — ответ выглядит как любой другой |
Ошибка понижения опасна именно тем, что задним числом её нечем проверить. Ответ звучит так же уверенно, как и любой другой. Пользователь не знает, что под ним слабое основание.
Решение о повышении категории принимает код — простым правилом по факту обнаруженной сложности. Модели доверяют только сигнал «сложность выше ожидаемой», но не право самой понизить категорию. Если сделать хотя бы одно исключение, асимметрия перестаёт быть правилом и превращается в рекомендацию, которую легко обойти в следующей версии промпта.
def maybe_escalate(current_category: str, complexity_signal: bool) -> str:
"""
Категория может только расти. Понижение запрещено кодом, не промптом.
"""
category_order = ["simple", "moderate", "complex", "critical"]
if complexity_signal:
current_idx = category_order.index(current_category)
if current_idx < len(category_order) - 1:
return category_order[current_idx + 1]
return current_category
# Понижение здесь невозможно структурно — нет ветки для негоКаждое повышение нужно записывать в лог с изначальной и фактической категорией. Это не только аудит отдельного ответа — это метрика качества классификатора. Если доля повышений для одного типа запросов растёт от релиза к релизу, определение категории устарело.
{
"request_id": "req_8821",
"initial_category": "simple",
"final_category": "complex",
"escalation_reason": "join_complexity_detected",
"timestamp": "2026-09-16T18:24:09Z"
}Необнаруживаемая ошибка: когда всё прошло без сбоев, но результат неверный
Правило асимметрии не закрывает второй класс проблем. Запрос выполнился, план прошёл проверку, технических ошибок нет — а число всё равно не то. Соединение построено не по тому ключу, или фильтр применён не тот, который имел в виду пользователь.
Это необнаруживаемая ошибка: ответ выглядит корректным, модель выдала его уверенно, пайплайн не зафиксировал сбоя. Именно здесь граница между кодом и моделью критична в другом смысле: код должен делать верифицируемые шаги явными.
Практически это означает: шаги рассуждения модели, влияющие на результат, должны быть структурированными артефактами, которые код сохраняет отдельно от финального ответа. Не потому что так красиво, а потому что иначе отладка невозможна.
Где это ломается
Промпт как единственный контракт. Если граница между кодом и моделью описана только в системном промпте, она исчезнет при первом же обновлении промпта. Контракт должен быть зафиксирован в коде — структурно, а не текстом.
Уверенность модели как прокси простоты. Высокий confidence классификатора не означает, что задача простая. Это означает только, что модель уверена в своей интерпретации. Позволить высокой уверенности понижать категорию — значит поставить систему, которая уверена, на место системы, которая права.
Дрейф порога при обновлении модели. Cascade routing настроен под конкретную версию модели. После обновления модели распределение уверенности меняется, и порог эскалации нужно перекалибровывать. Если этого не делать, система тихо деградирует.
Атаки на роутер. Специально составленный запрос может намеренно занизить уверенность классификатора и спровоцировать нежелательную эскалацию или, наоборот, остаться в дешёвой категории. Это отдельная поверхность атаки, которую стоит учитывать в продакшне.
Что попробовать дальше
Зафиксируйте в коде список решений, которые модель принимать не может — например, выбор формы вывода или понижение категории. Сделайте это структурно: не правилом в промпте, а отсутствием соответствующей ветки в коде.
Настройте логирование повышений категорий как отдельную метрику и смотрите на её динамику по релизам. Это даст сигнал о том, когда определения категорий перестали соответствовать реальным запросам.
При следующем обновлении модели явно перепроверьте пороги cascade routing — не предполагайте, что они остались валидными.
FAQ
Чем граница код/модель отличается от обычного разделения ответственности?
Обычное разделение ответственности — это про то, кто что делает. Граница код/модель — про то, что это решение нужно принять явно и зафиксировать структурно в коде, а не описать в промпте. Промпт можно изменить случайно; код — намеренно.
Почему нельзя просто написать подробный системный промпт с правилами?
Правила в промпте конфликтуют между собой при росте их числа, и модель применяет их в непредсказуемом порядке. Код детерминирован: одни и те же входные данные дают один и тот же результат. Для всего, что требует предсказуемости, нужен код.
Что такое cascade routing и когда его использовать?
Cascade routing — паттерн, при котором запрос сначала обрабатывается дешёвой моделью или эвристикой, и только при недостаточной уверенности эскалируется к более дорогой обработке. Используется, когда нужно балансировать стоимость и качество при разнородных запросах.
Как обнаружить необнаруживаемую ошибку в SQL-агенте?
Логировать промежуточные решения модели: какой ключ выбран для JOIN, какой фильтр применён, какая таблица использована. Финальный SQL-запрос этого не показывает. Только структурированный лог шагов рассуждения позволяет восстановить, где ответ разошёлся с намерением пользователя.
Нужно ли перенастраивать пороги cascade routing после обновления модели?
Да, обязательно. Распределение уверенности у разных версий модели отличается, и пороги, откалиброванные под старую версию, после обновления могут давать систематически неверные эскалации или их отсутствие там, где они нужны.
