# Промпт не виноват: как зафиксировать границу между кодом и LLM-агентом
Каноническая страница: https://plainews.ru/posts/llm-agent-code-model-boundary-architecture
Опубликовано: 2026-09-17T07:01:24.702Z

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

## Почему «пусть модель разберётся» — плохая архитектура

Возьмём типичный text2sql-агент или аналитический чат-бот над базой данных. Агенту поручают всё сразу: определить тип запроса, сформировать SQL, выбрать форму вывода (строка, таблица, график), решить, нужно ли предупредить об отклонении.

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

Каждый раз разработчик добавляет новое правило в промпт. Через несколько итераций новые правила начинают противоречить старым. Нестабильность никуда не уходит — она просто прячется в порядке применения конфликтующих инструкций.

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

## Что должен делать код, а что — модель

Anthropic описала базовое разграничение ещё в декабре 2024 года в «Building Effective Agents»: в workflow поток управляется заранее написанным кодом, в агенте — моделью. Практическое следствие простое: всё, что код сделает точнее и надёжнее модели, должен делать код.

Выбор формы вывода (строка, таблица, график) — это не семантическая задача, это десять строк обычного кода на основе типа данных в ответе. Проверка синтаксиса SQL — тоже код. Логирование категории запроса — код. Модели остаётся то, где нужно работать в концептуальном пространстве: понять намерение пользователя, построить запрос, сформулировать объяснение отклонения.

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

## Правило асимметрии: категория может только расти

Первое жёсткое правило для удержания границы — асимметрия пересмотра категории.

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

Почему это важно: ошибка повышения и ошибка понижения — несопоставимые вещи.

| Тип ошибки | Что происходит | Обнаруживаемость |
| --- | --- | --- |
| Ошибка повышения | Потрачено больше ресурсов, чем нужно | Видна в метриках стоимости |
| Ошибка понижения | Основание ответа молча подменяется более слабым | Не видна — ответ выглядит как любой другой |

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

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

```python
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
    # Понижение здесь невозможно структурно — нет ветки для него
```

Каждое повышение нужно записывать в лог с изначальной и фактической категорией. Это не только аудит отдельного ответа — это метрика качества классификатора. Если доля повышений для одного типа запросов растёт от релиза к релизу, определение категории устарело.

```json
{
  "request_id": "req_8821",
  "initial_category": "simple",
  "final_category": "complex",
  "escalation_reason": "join_complexity_detected",
  "timestamp": "2026-09-16T18:24:09Z"
}
```

## Необнаруживаемая ошибка: когда всё прошло без сбоев, но результат неверный

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

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

> **Важно.** Если модель строит SQL-запрос, код должен логировать не только финальный запрос, но и промежуточные решения: какой ключ выбран для JOIN, какой фильтр применён. Это единственный способ постфактум восстановить, где именно ответ разошёлся с намерением пользователя.

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

## Где это ломается

**Промпт как единственный контракт.** Если граница между кодом и моделью описана только в системном промпте, она исчезнет при первом же обновлении промпта. Контракт должен быть зафиксирован в коде — структурно, а не текстом.

**Уверенность модели как прокси простоты.** Высокий confidence классификатора не означает, что задача простая. Это означает только, что модель уверена в своей интерпретации. Позволить высокой уверенности понижать категорию — значит поставить систему, которая уверена, на место системы, которая права.

**Дрейф порога при обновлении модели.** Cascade routing настроен под конкретную версию модели. После обновления модели распределение уверенности меняется, и порог эскалации нужно перекалибровывать. Если этого не делать, система тихо деградирует.

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

## Что попробовать дальше

Зафиксируйте в коде список решений, которые модель принимать не может — например, выбор формы вывода или понижение категории. Сделайте это структурно: не правилом в промпте, а отсутствием соответствующей ветки в коде.

Настройте логирование повышений категорий как отдельную метрику и смотрите на её динамику по релизам. Это даст сигнал о том, когда определения категорий перестали соответствовать реальным запросам.

При следующем обновлении модели явно перепроверьте пороги cascade routing — не предполагайте, что они остались валидными.

---

## FAQ

### Чем граница код/модель отличается от обычного разделения ответственности?

Обычное разделение ответственности — это про то, кто что делает. Граница код/модель — про то, что это решение нужно принять явно и зафиксировать структурно в коде, а не описать в промпте. Промпт можно изменить случайно; код — намеренно.

### Почему нельзя просто написать подробный системный промпт с правилами?

Правила в промпте конфликтуют между собой при росте их числа, и модель применяет их в непредсказуемом порядке. Код детерминирован: одни и те же входные данные дают один и тот же результат. Для всего, что требует предсказуемости, нужен код.

### Что такое cascade routing и когда его использовать?

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

### Как обнаружить необнаруживаемую ошибку в SQL-агенте?

Логировать промежуточные решения модели: какой ключ выбран для JOIN, какой фильтр применён, какая таблица использована. Финальный SQL-запрос этого не показывает. Только структурированный лог шагов рассуждения позволяет восстановить, где ответ разошёлся с намерением пользователя.

### Нужно ли перенастраивать пороги cascade routing после обновления модели?

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

## Источники

- Habr AI: Граница код | модель как архитектурный объект
