ГлавнаяБлогСемантическое кэширование в AI-чат-ботах: кейс банка
AI / Нейросети

Семантическое кэширование в AI-чат-ботах: кейс банка

Как мы строили production banking AI chatbot и почему семантическое кэширование с Redis спасло проект. Практический опыт Node.js-разработчика.

Al
Редакция Algolitalgolit.ru
12 мин чтения16 июля 2026 г.

Когда наша команда запустила проект банковского AI-чат-бота, я искренне считал свою задачу решённой. Я был Node.js-разработчиком. Моя часть звучала почти унизительно просто: открыть WebSocket при подключении пользователя, отправить его сообщение в AI-сервис, вернуть ответ потоком. И всё. WebSockets — моя вторая натура, я собирал real-time системы годами. Я вышел с kickoff-встречи, думая: «Это будет легко». Самая большая ложь в разработке.

Архитектура v1: четыре блока, которые продержались неделю

Пользователь
  │
  ▼
WebSocket
  │
  ▼
LLM
  │
  ▼
Пользователь

Так выглядела первая версия архитектуры. Четыре блока. Я гордился ей. Она продержалась меньше недели.

Глава 1 — Вопрос про IPL

Через несколько дней на синхронизации с AI-командой я задал вопрос, оказавшийся пророческим: «Что будет, если кто-то спросит бота, кто выиграл вчерашний матч IPL?» Никто не ответил сразу. Кто-то засмеялся, решив, что я шучу. Лид AI задумался: «Подожди… а что на самом деле произойдёт?» Мы открыли приложение и попробовали прямо во время созвона. Бот взял вопрос, поискал в базе знаний, полной ставок FD и условий кредитов, ничего похожего не нашёл, но уверенно ответил — что-то расплывчатое вроде «пожалуйста, обратитесь в отделение по вопросам, связанным с матчами», склеенное из ближайших фрагментов векторного поиска. Сначала было смешно, потом — нет. У нашего чат-бота не было понятия «это не моё дело». А я уже знал из многолетнего опыта: дай пользователю половину шанса — и он задаст странный вопрос. Не со зла, просто чат-бокс провоцирует. Нам нужен был привратник — что-то, что сядет перед всем остальным и решит: это вообще наша проблема? Так появилось определение намерений (intent detection).

Пользователь
  │
  ▼
Intent Detection
  │
  ▼
Банковское?
  │
  ├── Да → RAG → LLM → Пользователь
  │
  └── Нет → Отклонение / перенаправление

AI-команда дала мне один endpoint: отправляешь запрос, получаешь намерение и метаданные. С моей стороны — десять строк. Я был счастлив. Я ещё не знал, что подписался на долгую историю.

Глава 2 — «Можно это закэшировать?»

Слой определения намерений работал. Демо прошло отлично. Клиент был доволен. Все улыбались. А потом кто-то реально измерил нагрузку. Каждый запрос — даже скучный «какая сегодня ставка FD» — теперь проходил полный LLM-вызов только для того, чтобы понять, куда направить вопрос, прежде чем попасть к LLM, который даст ответ. Этот крюк стоил 200–300 мс каждый раз. Мы подняли вопрос на следующем ревью. Лид AI спросил: «Сурадж, можем это закэшировать?» Я уверенно ответил: «Да, конечно. Redis». Мгновенный ответ, который даёшь, когда сказал «Redis» так много раз в карьере, что он вылетает раньше, чем ты обдумал вопрос. Через пять секунд до меня дошло: «…А что именно кэшировать?»

Глава 3 — К доске. Дважды

Моим первым инстинктом было сохранить запрос и ответ как ключ-значение. «Какая сегодня ставка FD?» → кэш → HIT ✅. Здорово, пока следующий тест: «Не подскажете сегодняшнюю процентную ставку по фиксированному депозиту?» → кэш → MISS ❌. Тот же вопрос, то же намерение, другая строка. Пользователи не повторяют одну и ту же фразу дважды, а точное совпадение знает только символы, не смысл.

Вторая попытка: кэшировать по намерению, а не по тексту. Если классификатор уже сказал, что это FD_RATE, почему бы не использовать это как ключ? Выглядело гениально день. Потом кто-то задал два вопроса подряд: «Что такое ставка FD?» → FD_RATE, «Какие преимущества FD?» → FD_RATE. Одно намерение, совершенно разные вопросы и ответы. Если бы я отдал кэшированный ответ о ставке тому, кто спрашивает о преимуществах, я бы вручил неверный ответ с полной уверенностью — что, пожалуй, хуже, чем отсутствие кэша.

Глава 4 — Салфетка

Мы были на кофе-брейке, когда наш архитектор взял салфетку со стола. Ничего не сказал. Просто написал два слова и подвинул: «Семантический кэш». — «Почитай об этом сегодня вечером», — сказал он и вернулся к кофе. Я кивнул, делая вид, что понял. Я не понял. В тот вечер я открыл документацию Redis. Прочитал страницу про векторный поиск раз. Закрыл вкладку. Открыл снова. Redis поддерживает векторный поиск? С каких пор? Я проверил changelog, наполовину уверенный, что неправильно прочитал. Оказалось, Redis поддерживает это уже давно. Просто я раньше не нуждался в этом, поэтому не искал. Около полуночи до меня дошло: вместо сопоставления точного текста запроса мы преобразуем его в эмбеддинг — вектор, который отражает смысл, — и сравниваем с ранее сохранёнными эмбеддингами. Если похоже, отдаём кэшированный ответ, не тратя ресурсы на поиск и генерацию.

Запрос пользователя
    │
    ▼
Эмбеддинг
    │
    ▼
Векторный поиск Redis
    │
    ▼
Сходство ≥ 0.90 ?
    │
    ├── ДА → Возвращаем кэшированный ответ (быстрый путь)
    │
    └── НЕТ → Идём в RAG (полный путь)

Теперь «какая сегодня ставка FD» и «не подскажете сегодняшнюю процентную ставку по депозиту» оказываются достаточно близко в векторном пространстве, чтобы попасть в ту же запись, а «преимущества FD» остаётся достаточно далеко, чтобы промахнуться. Смысл, а не символы. Мы собрали это за неделю. В пятницу я шёл домой, впервые за проект думая: «Я становлюсь хорошим backend-инженером».

Глава 5 — Утро понедельника

Утро понедельника. Кофе в одной руке, ноутбук в другой. Открываю Grafana, ожидая победного круга. Cache Hit Ratio: 8%. Я смотрел на это десять секунд. Обновил — то же число. Как это вообще возможно? Оказалось, выбрать 0.90 как порог сходства только потому, что статья на Medium использовала 0.90 — не то же самое, что выбрать 0.90 на основе тестов с собственным трафиком. Некоторые действительно похожие вопросы оказывались чуть ниже линии и промахивались. Несколько не связанных проскальзывали чуть выше. Мы потратили почти два дня, вытаскивая реальные логи запросов и вручную определяя, где на самом деле должна проходить линия. Когда это сделали, hit ratio вырос. Затраты упали. Уверенность ненадолго вернулась. Потом мы измерили end-to-end задержку — она почти не изменилась. Мы попробовали стриминг — отправлять токены по мере генерации, не дожидаясь полного ответа. Пользователям стало казаться быстрее. Клиент обожал демо. Но фактическое время выполнения не уменьшилось ни на миллисекунду. Стриминг исправил ощущение ожидания, но не саму работу.

Глава 6 — Рубить овощи мечом

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

Глава 7 — Нож

Он выдержал паузу, затем указал на блок с надписью Intent Detection — тот самый LLM-вызов, всё ещё стоивший нам 200–300 мс на каждый запрос. «Меч выглядит внушительно, — сказал он. — Нож — лучший инструмент для этой работы. Это не работа для полной модели. Это работа для классификатора». Маленький классификатор — это не LLM. Он ничего не генерирует. Его задача — прочитать запрос и сказать, к какому ведру он относится. Думайте о нём как о секретаре: он не решает вашу проблему, а просто указывает на нужную комнату.

Пользователь спрашивает: «Какая сегодня ставка FD?»
Классификатор возвращает:
{
  "intent": "FD_RATE",
  "confidence": 0.99
}

Мы заменили LLM-шаг определения намерений на маленькую transformer-модель, обученную на наших категориях. Та же точность для того, что нам реально нужно. Задержка упала с ~300 мс до 10–15 мс. На стороне Node.js почти ничего не изменилось — мой слой просто вызывает другой, гораздо меньший сервис:

const intent = await intentService.predict(query);
if (intent === "FD_RATE") {
  return getFDRate();
}
if (intent === "RESET_PIN") {
  return resetPin();
}

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

Запрос пользователя
    │
    ▼
Движок правил
    │
    ▼
Маленький классификатор
    │
    ▼
Низкая уверенность?
    │
   Да ──────────► LLM-роутер (редко, дорого, но иногда нужно)
    │
    Нет
    ▼
Маршрутизация запроса

Быстро и дёшево для 95% очевидных запросов. Дорогая модель — только для 5%, которым это действительно нужно.

Глава 8 — То, что никто не рисует на схеме

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

  • Переподключения WebSocket. Мобильные сети падают. Пользователи переключаются с Wi-Fi на мобильные данные в середине разговора. Каждое переподключение должно было восстановить ту же сессию без того, чтобы пользователь заметил или повторял себя.
  • Отказ Redis. Однажды, недолго, на стейджинге. Этого хватило, чтобы усвоить: семантический кэш без fallback — это уже не слой производительности, а единая точка отказа.
  • Дублирующиеся сообщения. Нестабильное соединение плюс retry на клиенте — и тот же запрос иногда попадал на наш backend дважды с разницей в миллисекунды, и наш бот отвечал на свой собственный вопрос второй раз.
  • Незапланированная очередь. Когда трафик перестал быть идеально гладким, понадобился способ поглощать всплески без потери запросов.
  • Баги стриминга. Токены приходили в неправильном порядке под нагрузкой, или поток тихо умирал в середине ответа, а UI показывал мигающий курсор вечность.
  • Состояния гонки между записью и чтением кэша, когда два пользователя задавали вопросы, достаточно близкие, чтобы столкнуться на одном ключе почти одновременно.
  • Логи и мониторинг, которые говорили нам, что что-то медленно, задолго до того, как сказать, что именно. Grafana стала не столько дашбордом, сколько дневником всего, что мы сделали не так за неделю.

Ни одна из этих проблем не попала в питч-дек. Все они попали в продакшен.

Что никто не планировал

Однажды вечером, уже далеко в проекте, я посмотрел на диаграмму архитектуры и пересчитал блоки. WebSocket, аутентификация, маленький классификатор, семантический кэш, ретривер, RAG, LLM, стриминг, очередь, мониторинг, аналитика, лимитер запросов.

Пользователь
                      │
                      ▼
             Маленький классификатор
                      │
       ┌──────────────┼──────────────┐
       │              │              │
       ▼              ▼              ▼
 Семантический кэш   Банковские     Вне домена
       │              │
 Cache Hit?           ▼
       │            RAG поиск
       ▼              │
Возврат ответа        ▼
                  LLM генерация

То, что начиналось как четыре блока и «это будет легко», превратилось в систему, где каждый блок решает настоящую проблему, обнаруженную в бою. Ни один из них не появился из-за того, что «это звучит круто». Каждый появился, потому что что-то сломалось, и пришлось это чинить.

Практический вывод

Если вы строите AI-чат-бота для продакшена, начните с малого: используйте маленький классификатор для определения намерений вместо полного LLM — это сэкономит 200+ мс на каждом запросе. Внедрите семантическое кэширование на Redis с векторами, но обязательно тестируйте порог сходства на своих данных, а не берите с потолка. И помните: 90% проблем вскроются только под нагрузкой, поэтому закладывайте мониторинг, очереди и fallback-механизмы с первого дня. Прямо сейчас откройте свою IDE и добавьте в проект intent-классификатор на базе простой модели — это окупится сторицей.

#семантическое кэширование#AI чат-бот#Redis#intent detection#Node.js
Al
Редакция Algolit

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

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →