---
title: "Не каждый запрос заслуживает агента"
canonical: https://maxtokens.ai/ru/posts/not-every-request-needs-an-agent/
date: 2026-06-18
tags: [llm, agents, inference, cost, routing]
description: "Перед агентом — дешёвый классификатор-триаж: тривиальное идёт в шаблон, простое в прямой вызов, сложное в полный цикл. Тир модели — это маршрутное решение."
---
Я долго гонял каждое сообщение пользователя через полный агентный цикл: планирование, tool-calling, большой контекст и топовую модель. Так проще: один путь, одна архитектура, меньше решений в коде.

Проблема проявилась не там, где я ожидал. Счёт и латентность росли не только на сложных задачах, а на самых обычных сообщениях: «спасибо», «можешь короче», «а что ты умеешь», «привет». Запускать многошагового агента на такой запрос — всё равно что вызывать автокран, чтобы поднять ложку.

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

Дорогой путь должен включаться только там, где он действительно нужен.

## Наивная архитектура: один путь для всего

Самый простой вариант выглядит так:

```text
request → agent_loop → response
```

Один маршрут. Всегда топовая модель. Всегда tool-calling. Всегда большой системный промпт и растущий контекст.

Но реальный трафик устроен иначе. В нём лежит всё подряд:

- приветствия;
- благодарности;
- smalltalk;
- короткие уточнения вроде «да» или «сделай короче»;
- FAQ-вопросы;
- оффтоп;
- запросы, которым нужен ровно один известный инструмент.

Для всего этого полный агентный цикл часто избыточен.

Цена наивного пути на простом запросе складывается из трёх вещей. Сначала токены: системный промпт агента, описания инструментов и история диалога — на «спасибо» это сотни или тысячи токенов впустую. Дальше латентность: агентный цикл это несколько ходов модели и round-trip'ы к инструментам, и даже на простой задаче пользователь ждёт как на сложной. И качество: чем больше лёгких мусорных запросов проходит через дорогую модель, тем сильнее загрязняется её контекст, и на действительно трудных задачах это начинает мешать.

Главный сдвиг в голове такой: тир модели — это не константа приложения, а маршрутное решение.

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

## Воронка стоимости

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

<img src="/posts/not-every-request-needs-an-agent/funnel-ru.svg" alt="Воронка стоимости: весь трафик идёт в дешёвый классификатор, тот маршрутизирует запрос в шаблон, прямой вызов инструмента или полный агентный цикл" width="760" height="360" loading="lazy" decoding="async" />

Минимально полезных маршрутов три:

1. **Trivial** — приветствие, благодарность, smalltalk, простая формулировка. Можно ответить шаблоном или маленькой моделью без инструментов.
2. **Single tool** — запрос требует одного понятного действия. Инструмент можно вызвать напрямую, без планирования агентом.
3. **Agent** — задача неоднозначная, многошаговая или требует нескольких инструментов. Здесь нужен полный цикл.

Начать можно даже с двух маршрутов: `cheap` и `agent`. Главное — перестать отправлять весь трафик в самый дорогой путь.

## Почему это работает

Распределение запросов почти всегда тяжёлохвостое. Большая часть трафика оказывается простой, меньшая часть — действительно сложной.

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

Разница в стоимости между маршрутами обычно составляет порядок или два. Классификатор — это один короткий вызов маленькой модели с маленьким ответом: enum и confidence. Агентный проход — это несколько вызовов модели, растущий контекст и tool round-trip'ы.

Кэш тоже помогает, но его не надо описывать как «почти бесплатно». Системный промпт классификатора стабилен, поэтому большая часть input-токенов может покрываться prompt cache. Но это не ноль: вы всё равно платите за output, за запись кэша и теряете скидку после истечения TTL.

Короткий классификатор с кэшированным префиксом — действительно дешёвый. Просто не бесплатный.

Среднюю стоимость можно прикинуть так:

```text
avg = P(trivial)·c_cheap + P(single_tool)·c_tool + P(agent)·c_agent
```

`c_agent` доминирует, потому что он на порядки больше остальных. Если хотя бы половину трафика вывести из `agent` в `cheap`, средняя стоимость падает не на проценты, а в разы.

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

## Сам классификатор

Классификатор не должен отвечать пользователю. Его задача — принять маршрутное решение.

Полезно классифицировать не только «просто / сложно», а несколько признаков:

- категория интента;
- нужен ли инструмент;
- какой инструмент нужен;
- уровень сложности;
- язык;
- safety-флаг;
- нужно ли вообще отвечать.

Часто это не один label, а небольшая схема.

Классификатор можно сделать маленькой моделью, embedding-классификатором или гибридом правил и модели. В любом случае `temperature = 0`: здесь нужен детерминизм, а не красота текста.

Ответ лучше сразу заставить быть структурированным. Парсить прозу хрупко, а схему легко тестировать.

```python
from enum import Enum
from pydantic import BaseModel

class Route(str, Enum):
    TRIVIAL = "trivial"          # приветствие, спасибо, smalltalk
    SINGLE_TOOL = "single_tool"  # одно известное действие без планирования
    AGENT = "agent"              # неоднозначная или многошаговая задача

class Triage(BaseModel):
    route: Route
    tool: str | None = None
    confidence: float            # 0..1

async def classify(message: str, recent: list[str]) -> Triage:
    return await small_model.structured(
        system=CLASSIFIER_PROMPT,
        input={
            "message": message,
            "recent": recent[-2:],
        },
        schema=Triage,
        temperature=0,
    )
```

Дальше начинается самое важное: ошибки классификатора стоят по-разному.

Если сложный запрос ошибочно отправить в дешёвый путь, пользователь получит плохой ответ. Если простой запрос ошибочно отправить в агента, вы просто переплатите. Первое ломает доверие к продукту. Второе ломает только экономику.

<img src="/posts/not-every-request-needs-an-agent/misroute-ru.svg" alt="Асимметрия ошибок: сложный запрос в дешёвом пути даёт плохой ответ и ломает доверие; простой запрос в агенте — лишь переплата" width="760" height="360" loading="lazy" decoding="async" />

Поэтому при низкой уверенности нужно эскалировать вверх — к способности, а не вниз, к дешевизне.

```python
triage = await classify(msg, recent)

# При сомнении выбираем способность, а не экономию.
if triage.confidence < 0.6:
    return await run_agent(msg)

match triage.route:
    case Route.TRIVIAL:
        return template_reply(msg)

    case Route.SINGLE_TOOL if triage.tool is not None:
        return await call_tool(triage.tool, msg)

    case Route.AGENT:
        return await run_agent(msg)

    case _:
        # single_tool без названного инструмента — та же логика: эскалируем вверх
        return await run_agent(msg)
```

Порог `0.6` здесь просто пример. Его нужно калибровать на своих данных. Сырое confidence модели почти всегда переоценено, а нормальная граница видна только на golden set.

Одна оговорка: `single_tool` — решение о стоимости, а не о безопасности. Один вызов инструмента всё ещё может удалить, оплатить, отправить — дёшево смаршрутизировать не значит дёшево ошибиться. Роутинг выше намеренно минимален; перед прямым `call_tool` поставь гейт по сигналу безопасности (один из сигналов классификатора из списка выше) — без присмотра только когда явно безопасно, иначе подтверждение или эскалация в агента. Пропустить агента не значит пропустить защиту.

Внутри диалога маршрут зависит от контекста. «Да» после сложного вопроса — не тривиальный запрос. Но классификатору не нужна вся история. Обычно хватает последних одной-двух реплик. Если давать ему весь диалог, он начнёт стоить как система, которую должен удешевлять.

И ещё одна важная вещь: классификатор дрейфует. По мере роста числа интентов промпт пухнет, а точность начинает плыть. Лечится это тремя практиками:

- версионировать prompt;
- держать golden set и confusion matrix;
- не раздувать таксономию.

Три-четыре маршрута обычно надёжнее, чем пятнадцать красивых интентов.

Классификатор — это код, а не просто конфиг. К нему нужно относиться как к коду: тестировать, версионировать и регулярно смотреть, где он ошибается.

## Что мерить

Без метрик непонятно, работает ли воронка вообще.

Минимальный набор:

- распределение маршрутов: `trivial / single_tool / agent`;
- стоимость запроса по каждому маршруту;
- средняя стоимость запроса;
- доля misroute-ошибок;
- отдельно — доля случаев, где сложный запрос уехал в дешёвый путь;
- доля эскалаций из-за low confidence;
- p50/p95 латентности по маршрутам;
- A/B: «всё в агент» против «воронки».

Последний пункт важнее всего. Он показывает, не испортили ли вы качество ради экономии.

## Когда это не нужно

Паттерн не бесплатный. Есть случаи, где классификатор только мешает.

Если почти все запросы действительно сложные, классификатор становится чистым overhead: вы добавили вызов перед каждым запросом, но почти ничего не сэкономили.

На сложном пути классификатор и агент идут последовательно. Значит, вы добавляете латентность именно туда, где она и так большая.

Есть несколько способов это смягчить:

- держать классификатор очень быстрым;
- блокироваться на нём только ради дешёвого пути;
- при необходимости стартовать агента спекулятивно параллельно и отменять лишнее после вердикта.

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

Способность важнее экономии.

## Вывод

Самая дешёвая оптимизация в LLM-приложении — не звать дорогую модель там, где она не нужна.

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

Тир модели — это маршрутное решение.

Ошибайтесь в сторону способности. Мерьте воронку. Версионируйте классификатор как код.