Как ускорить ИИ-агента с RAG в проде и не слить бюджет: кэшируем ответы на примере ИИ-консультанта
Заметил, и вы наверное тоже, что сейчас почти каждый домен и почти каждый продукт развивает своего ИИ-агента. Яндекс Маркет запустил Маркет AI, который подбирает товары, у Авто.ру есть Авто.ру AI, в Лавке появился ассистент, вот у одного только Яндекса их сколько. А кто-то уже начал перестраивать CJM вокруг агента: пользователь не ходит по фильтрам и каталогу, а просто пишет, что ему нужно. Назвать это полным переходом на агентские интерфейсы или на агентский поиск, конечно, пока нельзя. Но первые шаги уже есть.
Я и сам, собственно, «владелец» нескольких таких агентов в е-коме.
Статья будет полезна инженерам, продактам и всем неравнодушным, кто хочет разбираться в теме чуть глубже, чем просто обычный пользователь.
А поговорим мы сегодня о том, с какими проблемами такие агенты живут в продакшене, на что жалуются пользователи и как бороться с главной болячкой – скоростью.
С какими проблемами такие агенты сталкиваются в проде
Про каждую можно писать отдельную статью, поэтому коротко:
Скорость (латенси). Каждый поход за данными добавляет время: поиск по базе, обход графа, вызов инструмента. А после каждого шага модель заново читает всю переписку и конечно заявленные 20-30 секунды редко получается держать.
Пустые и неверные ответы. Модель может передать в поиск мусор и ничего не найти или найти не то.
Устаревшие данные. Цены, наличие и статусы заказов меняются постоянно, а агент отвечает по тому, что нашёл десять минут назад. В е-коме это самая дорогая ошибка: человек выбрал товар по одной цене, а в корзине видит другую и уходит
Надёжность. Провайдер тормозит, инструмент отвалился, и без запасного сценария агент просто молчит. Авто.ру AI, например, при снижении производительности переключается на другую модель.
По моему опыту, самая больная проблема из этого списка это латенси.
Пользователь привык к ChatGPT: спросил и через пару секунд ответ уже печатается на экране. С агентом, который ходит за данными, так не выходит: ИИ-консультант из прошлой статьи собирал подборку полторы-две минуты, и всё это время человек смотрит на крутящийся индикатор. Большинство столько не ждёт закрывает чат и уходит в привычный каталог с фильтрами.
Хочу поделиться своим экспериментом, как с этим бороться. Главный вопрос звучит почти как парадокс: как кэшировать ответы агента, если запросы у людей каждый раз разные? Сначала выжмем скорость из самого агента, потом соберём кэш, который отдаёт готовый ответ быстрее секунды и не врёт про цены и товары. На истину не претендую, расскажу как я решал эту задачу
Эксперимент
Давайте представим, что у нас есть ИИ-консультант магазина электроники: покупатель пишет, что ему нужно, консультант ищет товары в каталоге, смотрит отзывы и объясняет, какие подходят и почему.
Первый шаг, конечно, замер точки отсчёта. Как замеряю я, если коротко: снимаете показатели скорости ответа p50 и p95 времени на своём трафике. Если цифры вас не устраивают,
то три лайфхака, которыми я ускоряю ответ агента,
Пакетные передача данных. Отзывы сразу по всем кандидатам одним вызовом, а не по товару за раз. Меньше вызовов модели и меньше перечитываний переписки.
Быстрый путь вместо агентного цикла. Понятные запросы вроде «наушники с шумоподавлением до 5000» разбираю правилами: код сам собирает фильтры и ищет товары, модель вызывается один раз объяснить выбор. Агентный цикл остаётся для длинных личных запросов.
Короткие объяснения. Прошу описывать товар не длиннее 12-16 слов. Звучит мелко, а по времени даёт больше, чем два предыдущих приёма вместе. И 16 слов более чем достаточно для описания коротких характеристик
Вывод простой: время ответа определяют выходные токены. Вдвое ускорить получилось, в десять раз вряд ли: упрётесь в скорость генерации самой модели.
Дальше остаётся менять модель, и смотреть при выборе надо на две метрики. Скорость генерации, токенов в секунду на вашем железе или по API, она решает, сколько человек ждёт готовый ответ. И TTFT, время до первого токена, она решает, как быстро в чате вообще начнёт что-то печататься.
Первая важнее: стриминг маскирует ожидание, только если есть что показывать сразу, а у агента с RAG показывать нечего, пока идёт поиск.
Есть еще второй путь: на частых запросах не звать модель вообще. Один раз собрали ответ, сохранили, следующему с тем же вопросом отдали готовое скажем своего рода кэш.
Можно ли кэшировать ответ агента, если это товары? Короткий ответ: можно, но.....
Смысл вот в чём: представьте консультанта в обычном магазине техники. Какие наушники хорошие и почему, он помнит наизусть, а цену каждый раз смотрит на ценнике, потому что она могла поменяться утром, с ИИ-консультантом ровно так же. Его ответ состоит из нескольких частей, и живут они по-разному:
Ядро: какие товары подходят и почему. Меняется медленно, вместе с ассортиментом и отзывами. Может жить сутки.
Цена, наличие, доставка. Меняются постоянно. Например если взять те же маркетплейсах, продавцы с репрайсерами меняют цены десятки раз в день.
Персонализация: порядок товаров и акценты под конкретного человека, под запрос и т.д
Отсюда главное правило всей архитектуры: ядро кэшируем, цены и наличие не кэшируем никогда, персонализацию накладываем сверху. В промпте агента у меня есть строчка типа «не пиши цены в тексте». Цены подставляет система. Если цена попадёт в текст, такой ответ уже нельзя переиспользовать.
Из каких слоев состоит кэш моего ИИ-консультанта магазина электроники?
Точный кэш. Для того чтобы искать по словам.
Семантический кэш. Для того чтобы искать по смыслу, а не по словам например: «шумодав» вместо «шумоподавления». Такие перефразы ловим векторами, но ищем только среди запросов с теми же слотами.
У каждого запроса есть ключ то, по чему мы ищем в кэше. В ключ идёт не весь текст, а поля, которые меняют ответ: категория, бюджет, модель и другие признаки. Я называю их слотами. Два запроса с разными слотами не получат один ответ, даже если написаны почти одинаково.
Если в двух слоях кэши ничего не нашли только тогда идем к агенту, а ядро его ответа кладём в кэш.
Давайте разберем сначала два слоя кэша: точный по ключевому слову и семантический, а слоты разберем в конце, так будет проще и понятнее

Точный кэш
Самый простой слой: ключ это текст запроса, значение это готовый ответ. У каждой записи свой срок жизни. Хранилище я буду использовать знакомый всем Redis но экспериментировал на его форк Valkey.
Тексты (запросы) перед этим причёсываем: нижний регистр, «ё» меняем на «е», лишние пробелы убираем, «5 000 ₽» превращаем в «5000 руб» и т.д
Два правила, за которые я дебажил со слезами:
Числа и отрицания не выкидываем. Заманчиво убрать «до 5000» как мусор и получить один ключ на бюджет пять тысяч и десять. Так не нужно оставляйте
В ключ кладём не только текст, но и версию промпта, модель и город. Иначе выкатили новый промпт, а люди ещё сутки получают ответы от старого.
Работает это слой кэша с дословными повторами, и их оказалось больше, чем кажется, например: «телевизор 55 дюймов» все пишут одинаково. У меня этот слой покарывает порядка 20-25% всех запросов. Это прям много и хорошо
Кстати писать руками этот сервис совсем не обязательно: в RedisVL и GPTCache это уже есть из коробки.
Семантический кэш: отвечает на вопрос насколько этот запрос похож на то что у нас есть в сохраненных ответах?
Семантический слой работает так: Приходит запрос пользователя, переводим в числовое представление – в вектора, далее вызываем поиск, поиск возвращает одно число косинусное сходство, от 0 до 1: единица значит «тексты про одно и то же», ноль «про разное». И всё решение, отдать ответ из кэша или идти к агенту, сводится к одному порогу: выше отдаём, ниже считаем заново. Вопрос только в том, где этот порог поставить. У меня порог 0.75
Как этот слой собран у меня:
Модель эмбеддингов bge-m3: знает русский, работает локально, влезает в маленькую тачку.
Векторы лежат там же, в Redis: векторный поиск в нём встроенный, отдельная база не нужна.
Порядок слоёв: сначала точный кэш, на промахе семантический, и только потом агент. Дешёвое первым: эмбеддинг стоит десятки миллисекунд, вызов модели секунды.
Порога, который ловит перефразы и при этом не склеивает соседние товары, не существует. На 0,75 находится 97% перефраз и склеивается 60% разных товаров. На 0,9 склеек меньше, но и перефраз находится всего 38%. Другая модель эмбеддингов не помогает, добавление поиска по ключевым словам тоже: я перебрал гибрид BM25 с векторами во всех весах и порогах, рабочей точки нет ни одной.
Причина простая. В «до пяти тысяч» и «до десяти тысяч» различается один токен из восьми. Для любой метрики похожести это почти одинаковые строки, а для покупателя две разные подборки. Решающий признак нельзя взвешивать наравне с остальными словами, его надо вынимать из запроса и ставить в ключ жёстко.
Тогда нужен ли этот слой? Я скажу да, перефразы она находит отлично, а это и есть весь хвост промахов после точного кэша. Семантический поиск просто не должен выбирать товар.
А как это сделать? С помощью тех самых слотов.
Слоты
Слот – это поле, которое правила вынимают из запроса и которое меняет ответ. Например: Запрос «наушники с шумоподавлением до 5000 для бега» разбирается так:
Слот | Значение |
|---|---|
категория | наушники |
особенность | шумоподавление |
бюджет | 5000 |
остаток | для бега |
Всё, что попало в эту табличку, обязано совпасть у двух запросов иначе один ответ они не получат, даже если тексты почти одинаковые. Набор слотов свой для каждого каталога: у телефонов это модель и память, у телевизоров диагональ, у пылесосов влажная уборка и т.д.
Отдельно про остаток – это все слова, которых правила не знают: «для бега», «водонепроницаемые», «шумкой». Выкинуть его нельзя: выкинете и «наушники до 5000» сравняются с «наушники до 5000 для бега», а человек с бегом получит чужую подборку. С остатком же запрос с незнакомыми словами просто не найдёт себе пары в кэше: промах, идём к агенту. Лишний вызов модели дешевле неправильного ответа.
Дальше слоты и остаток склеиваются в одну строку паспорт запроса: category=headphones|price_max=5000|rest=для,бега. Все прошлые запросы с таким же паспортом лежат на одной полке, я называю её партицией. Внутри полки товары гарантированно одни и те же: тот же бюджет, та же категория, те же характеристики.
Вот теперь включаем семантику, но только внутри полки. Раньше векторы искали похожий вопрос по всему складу и путали iPhone 15 с 15 Pro, а теперь товар зафиксирован полкой, и у векторов остаётся единственная работа, с которой они справляются отлично: узнать перефразу.
Вот кусочек кода:
def lookup(query: str):
key = normalize(query)
if entry := exact.get(key): # 1. дословный повтор
return entry
part = signature(extract_slots(query)) # 2. паспорт запроса
if part is None or part not in partitions:
return None # категорию не поняли или полка пуста
vec = embed(key) # 3. эмбеддинг считаем только здесь, ~75 мс
entry, sim = partitions[part].nearest(vec)
return entry if sim >= 0.75 else None # порог низкий: товары уже развели слотыТри ступени: дословный повтор, нужная полка, самый похожий вопрос на этой полке. Эмбеддинг считается только на третьей ступени, так что платим за него не на каждом запросе.
И порог ранжирования семантического поиска упал до 0,75 вместо туториальных 0,95. Раньше высокий порог был единственной защитой от «перепутать товар» и он же мешал узнавать перефразы. Теперь товары разводят слоты, а семантике можно искать свободно.
Кэш инструментов
Отдельный слой, про который у меня нет доказательств, что стоит делать. Даже, когда ответ целиком не нашёлся, куски работы агента уже есть: поиск по каталогу с теми же фильтрами, сводка отзывов по тем же товарам, карточки и другие и вроде как кэшироват можно, а зачем?
На моих прогонах две разные формулировки одного запроса приводили модель к одинаковым аргументам поиска в 15 интентах из 30, а отзывы по одним и тем же товарам пересекались примерно на две трети.
Проверка перед показом
В кэш попадает только проверенный ответ: товары существуют в каталоге, подборка не пустая, бюджет соблюдён. Если ответ не прошёл проверку показываем, но не сохраняем. Иначе одна кривая генерация будет неделю раздаваться сотне человек.
И перед показом любой ответ, хоть из кэша, хоть свежий, получает живые цены и наличие по SKU. Это то самое правило, с которого мы начали: ядро кэшируем, цены никогда.
Архитектура такая у меня получилась

Когда этим вообще стоит заниматься?
Честно: не всем. Ориентир по трафику простой. Больше 5 000 запросов в день эффект заметен и по деньгам, и по скорости. Меньше 1000 не тратьте ни деньги, ни время команды
Второе условие важнее первого: запросы должны повторяться. Каталог, поддержка, подбор по параметрам повторяются. Если у вас каждый запрос уникальный, кэш ответов не ваш инструмент: смотрите в сторону кэша инструментов и ускорения самого агента.
Сколько это собирать и что даёт
Весь пайплайн я собрал за неделю плюс тесты. Это не квартальный проект, а спринт одного разработчика.
Цифры с прода: схема работает месяц, полёт нормальный. На A/B-тесте получили +12% к конверсии для нас это очень много, вложения окупились сразу. У вас будет иначе, но посчитать предел стоит до того, как начнёте: сколько стоит сэкономленная секунда именно в вашей воронке.
Апдейты по флоу выложу через пару месяцев теста, когда накопится статистика.
Статью я решил закончить на этой ноте ❤️ Спасибо, что потратили время и прочитали.
Полезные ссылки: Telegram-канал «AI-заметки продакта» — рассказываю про ИИ-агентов, лайфхаки и полезные инструменты: https://t.me/ai_vdel. Там же отвечаю на вопросы и консультирую.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.