Как сохранить KV-cache для долговременной памяти LLM: полное руководство | AiManual
AiManual Logo Ai / Manual.
08 Янв 2026 Гайд

KV-cache в долговременной памяти: почему всё ломается и как это починить

Разбираем проблему сохранения KV-cache при работе с долговременной памятью агентов. Практические решения, оптимизация памяти, ускорение инференса.

Тот самый момент, когда ваш агент начинает тормозить

Вы запускаете локального агента с долговременной памятью. Первые несколько диалогов — летает. Потом начинает задумываться. Ещё через час вы уже успеваете сходить за кофе, пока он генерирует ответ. Знакомо? Проблема не в модели и не в железе. Проблема в том, что каждый новый запрос пересчитывает KV-cache с нуля.

KV-cache — это кэш ключей и значений внимания (Key-Value cache). Модель вычисляет его один раз для каждого токена контекста, и если контекст повторяется — можно не пересчитывать. В теории. На практике с долговременной памятью всё сложнее.

Что на самом деле происходит с памятью агента

Допустим, вы используете векторное хранилище для долговременной памяти. Агент получает запрос, ищет релевантные фрагменты из прошлого, добавляет их в промпт. Казалось бы — идеально. Но каждый раз, когда вы добавляете в промпт историю, модель заново вычисляет KV-cache для всей этой истории. Даже если 90% контекста не изменилось.

Представьте: у вас 16К токенов контекста. Из них 14К — системный промпт и история диалогов. 2К — новый запрос пользователя. Модель каждый раз пересчитывает те самые 14К. А это — время. И память. И терпение.

💡
Проблема в том, что большинство реализаций RAG и долговременной памяти работают по принципу "собрали промпт — отдали модели". Никто не думает о том, что модель внутри делает лишнюю работу.

Почему стандартные подходы не работают

1 Наивное кэширование

Первая мысль — закешировать KV-cache на диск. Сохранить тензоры, загрузить при следующем запуске. Звучит логично, пока не сталкиваесь с реальностью:

  • Размер. KV-cache для Llama 3.1 70B с контекстом 32К — это десятки гигабайт. На диске.
  • Структура. Контекст не статичен. Вы добавляете новые диалоги, удаляете старые. Индексы смещаются.
  • Совместимость. Перезагрузили модель с другими параметрами квантования? Прощай, кэш.

2 Разделение контекста

Другая идея — разделить контекст на статичную и динамичную части. Системный промпт и базовая история — в статичную. Новые диалоги — в динамичную. Вычисляем KV-cache для статичной части один раз, потом только добавляем к ней динамику.

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

Особенно критично для RAG-систем, где именно связи между историей и текущим запросом определяют качество ответа.

Практическое решение: гибридный подход

1 Слоистая архитектура KV-cache

Вместо того чтобы кэшировать весь контекст целиком, работаем с уровнями:

Уровень Что хранит Частота обновления Примерный размер
Базовый Системный промпт, инструкции агента Никогда (или при изменении промпта) 2-4К токенов
Сессионный Текущая сессия диалога (последние N сообщений) Каждое новое сообщение 4-8К токенов
Долговременный Релевантные фрагменты из векторного хранилища При изменении запроса 2-6К токенов

Базовый уровень вычисляем один раз и храним в памяти. Сессионный — обновляем инкрементально. Долговременный — пересчитываем только когда действительно меняется суть запроса.

2 Инкрементальное обновление с sliding window

Вот где начинается магия. Вместо полного пересчёта сессионного KV-cache используем sliding window:

  1. Храним KV-cache для последних N токенов диалога
  2. При новом сообщении пользователя добавляем его токены в конец
  3. Если окно переполняется — удаляем самые старые токены из начала
  4. Обновляем только attention для новых токенов

Это работает, потому что внимание в трансформерах — авторегрессионное. Новые токены зависят от старых, но старые не зависят от новых. Можно безопасно добавлять в конец, не пересчитывая всё.

💡
Тот же принцип, что в continuous batching, только для одного сеанса. Если умеете работать с батчами — эта техника вам знакома.

3 Умный выбор из долговременной памяти

Самая ресурсоёмкая часть — поиск в векторизованной памяти. Вы же не хотите каждый раз пересчитывать KV-cache для всей вашей истории жизни?

Решение: кэшировать не сами фрагменты, а их эмбеддинги и метаданные. При новом запросе:

  • Быстрый поиск по эмбеддингам (это дешевле, чем LLM-инференс)
  • Проверка, не использовали ли мы уже эти фрагменты в предыдущих запросах
  • Если фрагмент уже в кэше — берём его KV-cache из памяти
  • Если нет — вычисляем и кэшируем

Получается двухуровневое кэширование: эмбеддинги на диске, KV-cache в оперативке.

Техническая реализация: что нужно знать

Выбор библиотеки и фреймворка

Не все фреймворки поддерживают манипуляции с KV-cache. vLLM — поддерживает. Text Generation Inference — поддерживает с оговорками. Ollama — вроде бы нет (по крайней мере, в публичном API).

Если вы используете трансформеры напрямую — у вас полный контроль. И полная головная боль. Потому что нужно:

  • Следить за форматами тензоров (они различаются между моделями)
  • Управлять памятью GPU (особенно актуально для карт с ограниченной VRAM)
  • Обрабатывать edge cases (например, когда модель переключается между режимами)

Сериализация и десериализация

Хранить KV-cache на диске — не просто сохранить numpy array. Нужно:

  1. Сжимать (квантовать в меньший тип данных)
  2. Добавлять метаданные (размеры, версия модели, хеш промпта)
  3. Обеспечивать миграцию при обновлении модели

Квантование KV-cache — отдельная тема. В статье про Q8 KV-cache для vision-моделей есть хорошие примеры, но для языковых моделей пороги другие.

Не пытайтесь сохранять KV-cache в pickle. Серьёзно. Формат тензоров меняется между версиями PyTorch, да и безопасность оставляет желать лучшего. Используйте специализированные форматы вроде safetensors.

Типичные ошибки и как их избежать

Ошибка 1: Кэширование без валидации

Сохранили KV-cache, перезагрузили систему, загрузили кэш — а модель выдаёт бред. Почему? Потому что изменился системный промпт, или вы обновили лора-адаптеры, или сменили температуру генерации.

Решение: добавлять цифровой отпечаток (fingerprint) в ключ кэша. Хеш от:

  • Модели (название + версия + параметры квантования)
  • Системного промпта
  • Параметров генерации (temperature, top_p, etc)
  • Лора-адаптеров (если есть)

Ошибка 2: Игнорирование памяти

KV-cache растёт. И растёт. И вот уже 48 ГБ RAM из статьи про выживание на 48 ГБ кажутся смешной цифрой.

Решение: политика вытеснения (eviction policy). Самые простые варианты:

Политика Как работает Когда использовать
LRU (Least Recently Used) Удаляем то, что дольше всего не использовалось Для сессионного кэша
LFU (Least Frequently Used) Удаляем то, что реже всего использовалось Для долговременного кэша
По размеру Удаляем самые большие фрагменты Когда важна скорость, а не релевантность

Ошибка 3: Слепое доверие кэшу

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

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

Решение: механизм инвалидации. Помечать фрагменты как "устаревшие" при явном противоречии. Или просто не кэшировать спорные фрагменты.

Интеграция с существующими системами

Если вы уже используете локальную LLM с долговременной памятью, добавление KV-cache кэширования потребует:

  1. Модификации пайплайна генерации (вставить хук между сборкой промпта и вызовом модели)
  2. Системы хранения (память + диск для холодного хранения)
  3. Механизма инвалидации (когда кэш устаревает)

Самый простой способ начать — обернуть вызов модели в декоратор, который проверяет кэш. Грубо, но работает для прототипа.

Когда это НЕ нужно делать

Да, бывают случаи, когда оптимизация KV-cache только вредит:

  • Короткие диалоги (менее 10 сообщений) — оверхед от кэширования перевесит выгоду
  • Часто меняющийся системный промпт — если инструкции агента меняются каждый запрос, кэш бесполезен
  • Очень маленькие модели (до 7B параметров) — они и так быстрые, оптимизация не даст заметного прироста
  • Когда память дороже времени — если у вас ограниченная RAM/VRAM, лучше потратить время на пересчёт, чем место на хранение

И главное — не начинайте с оптимизации. Сначала убедитесь, что у вас действительно есть проблема. Замерьте время генерации до и после добавления долговременной памяти. Если разница меньше 20% — возможно, проблема не в KV-cache.

Что будет дальше

Сохранять KV-cache вручную — это костыль. Пусть и эффективный. Настоящее решение придёт со стороны фреймворков и самих моделей.

Уже сейчас появляются модели с архитектурными улучшениями для долговременной памяти. Mamba, RWKV — у них вообще нет квадратичной сложности внимания. А значит, и проблемы KV-cache в классическом понимании.

Другой путь — аппаратный. Специализированные AI-ускорители с огромной быстрой памятью. Когда можно хранить весь KV-cache в SRAM, а не выгружать на диск.

А пока что — да, приходится изобретать велосипеды. Но зато какие велосипеды получаются.

💡
Если после прочтения этой статьи у вас появилось желание переписать всю систему долговременной памяти — остановитесь. Сначала попробуйте самый простой вариант: кэшировать только системный промпт. Часто этого достаточно для 2-3 кратного ускорения.

И последний совет — не зацикливайтесь на оптимизации. Основные ошибки при запуске локальных LLM обычно лежат в более простых вещах: неправильная квантовка, неоптимальные параметры генерации, кривой системный промпт. Сначала исправьте их, потом беритесь за KV-cache.