Наивная арифметика, которая всех обманывает
Классический RAG-ассистент работает так: пользователь пишет вопрос → эмбеддинг → поиск в векторе → один вызов LLM для синтеза. Одна итерация. Чисто, быстро, дёшево. Если ответ правильный.
Проблема в том, что с первой попытки он правильный не всегда. Пользователь переспрашивает: «нет, я имел в виду другое». Ассистент делает второй запрос. Потом третий. К четвёртому переспросу пользователь машет рукой и зовёт живого оператора. Оператор тратит 10–15 минут, находит нужный документ, копирует абзац в чат. Вопрос закрыт. Стоимость вопроса — не 1 вызов LLM, а:
-
4 вызова LLM на переспросы
-
10–15 минут времени оператора
-
испорченное настроение пользователя
А теперь представим агента, который сам перебирает гипотезы внутри ReAct-цикла. Он не ждёт переспроса пользователя — он сам понимает, что первый результат неполный, и копает дальше. 4 итерации — и ответ готов. Вызовов LLM может быть больше, чем у ассистента. Но пользователь получил ответ с первой попытки, а оператор вообще не понадобился.
Главный тезис этой статьи: считать надо не стоимость одного ответа, а стоимость закрытого вопроса. И в этой метрике агент часто выигрывает.
Как устроен ReAct-цикл в ISTOK
Ядро агента — класс AgentEngine с методом execute. На входе — вопрос пользователя и системный промпт, на выходе — финальный ответ и лог всех действий.

Пять инструментов агента
Агент вооружён пятью инструментами, каждый под свою задачу:
-
VectorSearch — семантический поиск. Гибрид Dense (эмбеддинг) + BM25 через Milvus. Для концептуальных запросов: «как настроить», «проблемы с производительностью».
-
Search — полнотекстовый поиск. ILIKE по PostgreSQL, поддержка регулярных выражений (
~*). Для точных терминов: коды ошибок, идентификаторы, названия. -
OpenChunk — открыть полный текст найденного чанка. После VectorSearch или Search агент видит только summary; чтобы прочитать содержимое, нужен OpenChunk.
-
GetDocumentChunks — оглавление документа. Позволяет агенту понять структуру, не открывая всё подряд.
-
CallOperator — вызов живого оператора. Разрешён только после 2–3 собственных попыток поиска.
Агент сам решает, какой инструмент вызвать следующим. Жёсткого сценария нет: в одном вопросе он начнёт с VectorSearch, в другом — с Search по точному коду ошибки.
Честная история: как мы контекст держали (и теряли)
У агента есть проблема: каждый вызов LLM в цикле добавляет сообщения в историю. 15 итераций с результатами поиска — и контекстное окно забито. Что делать?
Версия 1: LLM-суммаризация
Первым решением была суммаризация: когда контекст подходил к лимиту, мы просили модель сжать историю в короткий итог. Логика понятная: модель лучше всех знает, что в её же контексте важно, а что нет.
На практике — дорого и ненадёжно. Суммаризация сама по себе занимает дорогой вызов LLM. И модель иногда теряла критические детали: то код ошибки пропустит, то название документа. Мы получали «сжатый» контекст, в котором не было самого нужного.
Версия 2: скользящее окно
Переписали на тупое, зато надёжное решение — скользящее окно:
DEFAULT_MAX_TOKENS = 120_000 # ~120K токенов — порог срабатывания
MIN_KEEP_MESSAGES = 6 # Минимум сообщений всегда сохраняется
При достижении порога просто удаляем старейшие tool-сообщения. Системный промпт и последние 6 сообщений неприкосновенны. Никаких дополнительных вызовов LLM. Грубо, но дёшево и без потери данных.
Суммаризация не выброшена полностью — она осталась как fallback, но с жёстким лимитом:
-
До 2 суммаризаций за сессию (после — переход на скользящее окно)
-
Каждая успешная суммаризация добавляет +5 к лимиту итераций
-
Промпт суммаризации требует сохранить: исходный вопрос дословно, ключевые находки, текущий лучший частичный ответ и ровно 2–3 шага плана
Это и есть подход, который мы теперь применяем везде: сначала пробуем умное, и если умное оказывается дорогим и ненадёжным — заменяем на тупое, которое работает.
Не все вызовы LLM стоят одинаково
Ещё одно упрощение из наивной арифметики: считать, что каждый вызов LLM стоит одну и ту же цену. В ISTOK запросы к LLM-шлюзу разделены по типам:
class RequestType(str, Enum):
EMBEDDING = "EMBEDDING" # Эмбеддинги — дёшево
GENERATION_LIGHT = "GENERATION_LIGHT" # Простые шаги цикла — дёшево
GENERATION_MEDIUM = "GENERATION_MEDIUM" # Основной режим (по умолчанию)
GENERATION_HEAVY = "GENERATION_HEAVY" # Сложный синтез — дорого
RERANK = "RERANK" # Реранжирование — дёшево
Платим за тяжёлую модель только там, где она действительно нужна: финальный синтез ответа или суммаризация. Эмбеддинги, реранжирование и простые вызовы идут через более лёгкие и дешёвые модели. В классическом RAG-пайплайне (поиск → синтез) такого разделения обычно нет — каждый запрос идёт одной и той же моделью.
Правильная арифметика: стоимость закрытого вопроса
Давайте соберём всё вместе. Считаем не стоимость ответа, а стоимость вопроса, после которого пользователю больше не нужна помощь.
Вопрос пользователя — типичный для линии поддержки 1С-продуктов:
«После обновления на релиз 2.4 перестали создаваться обращения из писем. В журнале регистрации ошибка “Поле объекта не обнаружено (КаналСвязи)”. Настройки обмена не трогали. Что случилось?»
Вопрос коварный: ответ не лежит в одном документе. Причина спрятана в заметках релиза, а решение — в инструкции по миграции. Смотрим, как справляются оба.
Сценарий A: ассистент (один запрос → синтез)
Попытка 1: ассистент находит общую инструкцию «Настройка загрузки
писем» и отвечает: проверьте учётную запись, пароль
и расписание регламентного задания — 1 × LLM
Пользователь: «настройки не менялись, ошибка про поле КаналСвязи,
появилась сразу после обновления»
Попытка 2: ассистент находит описание справочника «Каналы связи»
и советует проверить его заполнение — 1 × LLM
Пользователь: «справочник заполнен, ошибка именно при загрузке писем»
Попытка 3: ассистент добирается до заметок релиза 2.4 и наконец
отвечает про переименование поля — 1 × LLM
──────────────────────────────────────────────────────────────
Итого: 3 вызова LLM + 2 переспроса пользователя
Время пользователя до получения ответа: ~5–8 минут
Сценарий Б: агент (ReAct-цикл) — тот же вопрос, та же база знаний:
Итерация 1: VectorSearch по полному тексту вопроса → 5 чанков: общая
инструкция по загрузке писем, справочник каналов и заметки
релиза 2.4. Агент видит по summary: в заметках релиза,
скорее всего, причина — 1 × LLM (GENERATION_MEDIUM)
Итерация 2: OpenChunk «Изменения в релизе 2.4» → находит: «поле
КаналСвязи переименовано в КаналСвязиИсточника, требуется
обновление соответствий в настройках обмена»
— 1 × LLM (GENERATION_MEDIUM)
Итерация 3: Search по точному термину «КаналСвязиИсточника» →
инструкция «Миграция настроек обмена после обновления 2.4»
— 1 × LLM (GENERATION_MEDIUM)
Итерация 4: OpenChunk инструкции по миграции → три конкретных шага
для обмена с почтой — 1 × LLM (GENERATION_MEDIUM)
Итерация 5: синтез финального ответа со ссылками — 1 × LLM (GENERATION_HEAVY)
──────────────────────────────────────────────────────────────
Итого: 5 вызовов LLM (4 средних + 1 тяжёлый)
Ответ, который пользователь получил через ~40 секунд:
Причина найдена: в релизе 2.4 поле «КаналСвязи» переименовано в «КаналСвязиИсточника», поэтому настройки обмена, созданные до обновления, больше не совпадают со структурой данных [1]. Чтобы восстановить загрузку писем:
откройте настройки обмена,
запустите обработку «Обновление соответствий каналов связи»,
перезапустите регламентное задание загрузки [2].
Источники: [1] «Изменения в релизе 2.4», [2] «Миграция настроек обмена после обновления 2.4».
Да, у агента на два вызова LLM больше. Но:
-
4 из 5 вызовов идут как GENERATION_MEDIUM (дешевле тяжёлого синтеза)
-
Пользователь получил ответ с первой попытки — и не абстрактный «проверьте настройки», а причину, три шага решения и ссылки на источники
-
Ни один оператор не отвлёкся от своей работы
А теперь добавим сценарий, где ассистент не справляется вообще:
-
4 переспроса × LLM → ответа нет
-
Эскалация на оператора → 15 минут рабочего времени → стоимость ~150₽ (при средней ставке 600₽/час)
-
Агент справляется за 6 итераций → меньше минуты → стоимость только LLM-токенов
В этом и есть суть: агент сам перебирает гипотезы вместо того, чтобы заставлять пользователя делать это вручную через переспросы.

Контроль: что мы считаем и как
Выводить экономику «на глаз» мы не стали — агент сам ведёт бухгалтерию по каждой сессии. Что попадает в лог:
-
Каждый вызов LLM фиксируется вместе с
usageиз ответа провайдера: сколько токенов ушло в промпт, сколько вернулось в ответе. Ни один вызов не теряется — ни поисковый, ни синтез, ни суммаризация. -
Итоги по сессии. По завершении ReAct-цикла агент складывает всё в четыре числа: prompt-токены, completion-токены, общая сумма и количество вызовов LLM. Умножаем на тариф модели — получаем себестоимость сессии с точностью до копейки.
-
Лог инструментов. Полная история расследования: какой инструмент вызван, с какими аргументами, что вернулось и с каким статусом — успех, пустой результат или ошибка. По этому логу видно, где агент шёл к ответу прямо, а где ходил кругами.
Сверху всё это накрыто трейсингом: каждый ReAct-цикл уходит в Arize Phoenix, где можно разложить сессию по итерациям — каждый вызов инструмента, каждый запрос к LLM, итоговые токены. Именно оттуда взяты цифры из следующего раздела.
Результаты: цифры одного небольшого проекта
Хватит теории — покажу данные. Один из наших пилотных проектов: техподдержка небольшой продуктовой компании, база знаний около 1200 документов, порядка 1800 обращений в месяц. Проект удобен для сравнения тем, что первые две недели система проработала в режиме простого ассистента (один запрос → синтез), а следующие две — в режиме агента. Объёмы и профиль обращений в оба периода сопоставимы.
|
Метрика |
Ассистент |
Агент |
|---|---|---|
|
Вызовов LLM на сессию |
1 |
6,4 |
|
Переспросов пользователя до закрытия вопроса |
2,3 |
0,3 |
|
Сессий с эскалацией на оператора |
34% |
11% |
|
Среднее время до закрытия вопроса |
27 минут |
4 минуты |
|
Токенов на закрытый вопрос |
~18 000 |
~15 500 |
Наивная арифметика из начала статьи подтвердилась: агент действительно делает в 6 раз больше вызовов LLM. Но дальше цифры расходятся с интуицией. Переспросы — главный скрытый множитель стоимости ассистента — упали в 7 раз: агент перебирает гипотезы сам, не дожидаясь уточнений пользователя. Эскалации упали втрое. А токены оказались почти одинаковыми: у ассистента каждый переспрос — это повторный вызов с раздутым контекстом, а у агента бо́льшая часть вызовов дешёвые.
Теперь переведём в деньги. Сами токены стоят копейки: закрытый вопрос ассистента обходится в ~0,9₽ токенов, агента — в ~0,5₽. Другое дело — оператор. Пятнадцать минут работы специалиста при средней ставке ~600₽/час — это 150₽ на каждую эскалацию. Раскладываем на все вопросы:
-
Ассистент: 0,34 × 150₽ + 0,9₽ ≈ 52₽ за закрытый вопрос
-
Агент: 0,11 × 150₽ + 0,5₽ ≈ 17₽ за закрытый вопрос
Разница — в три раза. И создана она не экономией токенов, а тем, что операторы перестали быть поисковиками и занялись кейсами, где человек действительно нужен.

Ограничения
Агент — не серебряная пуля, и у нашей арифметики есть границы. Честно про все три.
Простые фактологические вопросы. «Какой порт у сервиса X?», «Какая версия PostgreSQL поддерживается?» — на таких вопросах агент объективно проигрывает. Ассистент отвечает за один запрос и пару секунд. Агент сделает минимум две итерации — поиск и синтез — и потратит вдвое больше токенов за ровно тот же результат. Если вся ваша входящая линия состоит из справочных вопросов, ReAct-цикл вам не нужен: хватит простого ассистента. Экономия агента рождается из сложности — там, где ответ не лежит в одном чанке, а собирается из трёх документов и кода ошибки.
Пустая или нерелевантная база знаний. Никакой цикл рассуждений не компенсирует отсутствие данных. Если искать нечего, ассистент честно скажет «не знаю» за один вызов, а агент перед тем же ответом пройдёт 2–3 итерации вхолостую: переберёт VectorSearch, Search, попробует синонимы — и только потом признает поражение. Дороже и медленнее при том же исходе. Поэтому агент включается там, где база знаний уже наполнена — в нашем случае импортом истории обращений, но это тема отдельной статьи.
Неопределённые вопросы. «Расскажите что-нибудь про интеграции» — худший сценарий для агента. У него нет точки опоры: любой поиск что-то вернёт, любой результат можно «уточнить». Агент добросовестно исследует все направления, пока не упрётся в лимит итераций, и выдаст обзор, который пользователю, скорее всего, не нужен. Хорошая новость: на реальной линии поддержки таких вопросов немного — люди приходят с конкретной болью, а не за лекцией.
Заключение
Всё, что вы видели в разделе «Результаты», — один небольшой проект, один месяц, одна база знаний. Мы сознательно не экстраполируем эти данные на «среднюю инсталляцию»: профиль обращений, качество базы знаний и доля сложных вопросов у каждого заказчика свои. Разрыв в три раза — наш опыт. По мере накопления статистики с других проектов картина может сместиться — тогда обновим статью.
Автор: Semen-Chernov


