Долговременная память для локальных AI: скользящее окно и агенты | AiManual
AiManual Logo Ai / Manual.
19 Янв 2026 Гайд

Когда память кончается: как заставить локальный AI помнить больше 8К токенов

Техническое решение проблемы ограниченного контекста через агентские подпрограммы, суммаризацию и скользящее окно контекста для локальных LLM.

Проблема, которая бесит всех: почему ваш AI-агент - золотая рыбка

Вы только что обсудили с агентом детальный план проекта, расписали архитектуру, обговорили техдолг. Через 20 сообщений спрашиваете: "А про что мы говорили в начале?" И получаете классическое: "Извините, я не могу вспомнить предыдущий контекст".

Ограничение контекста - не техническая мелочь. Это фундаментальный барьер. Когда я запускаю локальные модели на трёх 3090, меня бесит не потребление памяти GPU. Меня бесит, что я плачу за железо, а агент ведёт себя как пациент с амнезией.

Типичная ошибка: пытаться запихнуть всю историю в промпт. Результат? Либо OOM, либо качество ответов падает до нуля, потому что модель теряется в тысячах токенов.

Проблема глубже, чем кажется. Это не просто "добавить больше RAM". Архитектура трансформеров экспоненциально усложняется с ростом контекста. KV-cache ломается, внимание рассеивается, и вместо умного агента получаем нейросеть, которая пережевывает собственный хвост.

Скользящее окно: не решение, а костыль, который работает

Скользящее окно контекста - это как пытаться читать "Войну и мир" через замочную скважину. Вы видите только последнюю страницу, но делаете вид, что поняли весь роман.

Технически это просто: храним N последних токенов, старые выбрасываем. Всё. Но вот загвоздка - что именно выбрасывать? Случайные токены? Ключевые детали? Инструкции системы?

# Наивная реализация - так НЕ делать
class BadSlidingWindow:
    def __init__(self, max_tokens=4000):
        self.max_tokens = max_tokens
        self.history = []
    
    def add(self, text):
        tokens = tokenize(text)
        self.history.extend(tokens)
        
        # Просто отрезаем начало - ужасная идея
        while len(self.history) > self.max_tokens:
            self.history.pop(0)  # Прощай, системный промпт!

Видите проблему? Первым делом мы теряем системные инструкции. Агент забывает, кто он и что должен делать. Типичная история: через час работы ваш кодирующий ассистент начинает писать стихи на Python.

💡
Скользящее окно должно быть умным. Системный промпт - священная корова. Его нельзя трогать никогда. Инструкции пользователя - приоритет средний. Мета-размышления агента - первыми на вылет.

1 Разделяй и властвуй: сегментируй контекст

Первое правило: не храни всё в одной куче. Разбей контекст на слои:

Слой Что содержит Приоритет удаления
Системный Роль, инструкции, запреты Никогда
Ядро диалога Последние 5-10 обменов Низкий
Исторический Старые сообщения (суммаризованные) Средний
Мета-размышления "Думаю о...", "Планирую..." Высокий
# Правильная сегментация
class SegmentedContext:
    def __init__(self):
        self.system_prompt = []  # Никогда не трогаем
        self.recent_dialog = deque(maxlen=10)  # Последние обмены
        self.summarized_history = []  # Сжатая история
        self.agent_thoughts = []  # Первые на удаление
    
    def compress_if_needed(self):
        total = self.count_tokens()
        if total > MAX_TOKENS:
            # Сначала убираем мета-размышления
            while self.agent_thoughts and total > MAX_TOKENS:
                removed = self.agent_thoughts.pop(0)
                total -= count_tokens(removed)
            
            # Потом суммаризируем старую историю
            if total > MAX_TOKENS:
                self.compress_history()

Агентские подпрограммы: когда одна нейросеть управляет другой

Вот где начинается магия. Скользящее окно - механизм. Агентские подпрограммы - интеллект. Идея проста: создаём специализированного агента, чья единственная работа - управлять памятью основного агента.

Это не просто суб-агент. Это архитектурный паттерн. Memory Agent работает постоянно в фоне, как сборщик мусора в JVM, только умный.

2 Memory Agent: что он делает и почему он нужен

Memory Agent решает три задачи:

  • Суммаризация: Сжимает старые диалоги в плотные выжимки
  • Извлечение: Находит релевантные куски истории по запросу
  • Приоритизация: Решает, что сохранить, а что выбросить

Ключевой момент: Memory Agent работает на той же модели, что и основной агент. Зачем? Консистентность. Если основной агент - CodeLlama 13B, а Memory Agent - TinyLlama 1B, они будут говорить на разных языках. Суммаризации будут бесполезны.

# Архитектура Memory Agent
class MemoryAgent:
    def __init__(self, llm):
        self.llm = llm  # Та же модель, что у основного агента
        self.memory_store = {}  # Временное хранилище
        
    async def summarize_chunk(self, dialog_chunk):
        """Сжимает фрагмент диалога в 10% от оригинала"""
        prompt = f"""Суммаризируй этот диалог, сохранив:
        1. Ключевые решения
        2. Изменения в требованиях
        3. Технические детали
        
        Диалог:
        {dialog_chunk}
        """
        
        summary = await self.llm.generate(prompt)
        return compress_to_tokens(summary, target_ratio=0.1)
    
    def retrieve_relevant(self, query, history):
        """Находит релевантные части истории"""
        # Простейший semantic search
        query_embed = embed(query)
        scores = []
        
        for idx, memory in enumerate(history):
            mem_embed = embed(memory)
            score = cosine_similarity(query_embed, mem_embed)
            if score > 0.7:  # Порог релевантности
                scores.append((idx, memory, score))
        
        return sorted(scores, key=lambda x: x[2], reverse=True)[:3]

Memory Agent должен работать асинхронно. Пока основной агент думает над ответом, Memory Agent уже суммаризирует предыдущий обмен. Задержка должна быть меньше времени генерации основного ответа.

Гибридный подход: скользящее окно + семантический поиск

Вот где становится интересно. Мы комбинируем:

  1. Скользящее окно для немедленного контекста (последние 2K токенов)
  2. Семантический поиск по суммаризованной истории (ещё 2K токенов)
  3. Динамическую подгрузку по требованию

Когда пользователь спрашивает: "А что мы решили насчёт базы данных?", система:

  • Ищет в суммаризованной истории по "база данных"
  • Если находит - загружает этот фрагмент в контекст
  • Автоматически вытесняет менее релевантные части
class HybridMemorySystem:
    def __init__(self, main_llm, embedder):
        self.main_context = []  # Текущее скользящее окно
        self.memory_agent = MemoryAgent(main_llm)
        self.embedder = embedder
        self.memory_index = {}  # {embedding: (summary, metadata)}
        
    async def process_query(self, query):
        """Обработка запроса с гибридной памятью"""
        
        # Шаг 1: Поиск в долговременной памяти
        relevant = self.search_long_term(query)
        
        # Шаг 2: Добавление релевантного в контекст
        context = self.build_context(query, relevant)
        
        # Шаг 3: Генерация ответа
        response = await self.main_llm.generate(context)
        
        # Шаг 4: Асинхронное обновление памяти
        asyncio.create_task(
            self.update_memory(query, response)
        )
        
        return response
    
    def search_long_term(self, query):
        """Семантический поиск по embeddings"""
        query_embed = self.embedder.encode(query)
        results = []
        
        for mem_embed, (summary, meta) in self.memory_index.items():
            similarity = cosine_similarity(query_embed, mem_embed)
            if similarity > 0.65:
                results.append({
                    'summary': summary,
                    'similarity': similarity,
                    'metadata': meta
                })
        
        return sorted(results, key=lambda x: x['similarity'], reverse=True)

Ошибки, которые сломают вашу систему

Я видел десятки реализаций. 90% падают на этих граблях:

1. Циклическая суммаризация

Memory Agent суммаризирует диалог. Потом суммаризирует суммаризацию. Ещё раз. В итоге получаем: "Мы говорили о чём-то важном". Вся конкретика утеряна.

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

2. Забывание системного промпта

Самая частая ошибка. Через час работы агент забывает свою роль. Начинает отвечать как чат-бот, а не как эксперт по коду.

# КАТЕГОРИЧЕСКИ НЕЛЬЗЯ
class TerribleMemory:
    def trim_context(self):
        # Так теряется личность агента
        if len(self.context) > MAX_LEN:
            self.context = self.context[-MAX_LEN:]  # Прощай, системный промпт!

3. Слепой semantic search

Ищете по embeddings без учёта времени? Получаете релевантные, но устаревшие ответы. Пользователь спрашивает про текущую архитектуру, а система подгружает решения недельной давности.

Решение: весовой коэффициент времени. Чем свежее память, тем выше приоритет.

Практическая реализация: с нуля за 200 строк

Вот минимальная рабочая система. Используем локальную модель через llama.cpp:

import asyncio
from typing import List, Dict
from dataclasses import dataclass
from datetime import datetime
import numpy as np

@dataclass
class MemoryChunk:
    text: str
    embedding: np.ndarray
    timestamp: datetime
    chunk_type: str  # 'system', 'dialog', 'summary', 'thought'
    
class PracticalMemorySystem:
    """Полная система за 200 строк"""
    
    def __init__(self, llm, embed_model, max_tokens=8000):
        self.llm = llm
        self.embed_model = embed_model
        self.max_tokens = max_tokens
        
        # Иерархическая память
        self.system_chunks: List[MemoryChunk] = []  # Не удаляем никогда
        self.recent_chunks: List[MemoryChunk] = []  # Скользящее окно
        self.long_term_chunks: List[MemoryChunk] = []  # Суммаризованное
        
        # Текущий счётчик токенов
        self.current_tokens = 0
    
    async def add_interaction(self, user_msg: str, agent_msg: str):
        """Добавляем новый обмен в память"""
        
        # 1. Сохраняем как есть в recent
        dialog = f"User: {user_msg}\nAgent: {agent_msg}"
        embed = self.embed_model.encode(dialog)
        chunk = MemoryChunk(
            text=dialog,
            embedding=embed,
            timestamp=datetime.now(),
            chunk_type='dialog'
        )
        
        self.recent_chunks.append(chunk)
        self.current_tokens += self.count_tokens(dialog)
        
        # 2. Проверяем лимит
        await self.enforce_limit()
        
        # 3. Асинхронно суммаризируем старые диалоги
        if len(self.recent_chunks) > 10:
            asyncio.create_task(self.summarize_old_dialogs())
    
    async def enforce_limit(self):
        """Умное управление лимитом токенов"""
        while self.current_tokens > self.max_tokens:
            # Удаляем в порядке приоритета
            if self.recent_chunks:
                # Ищем мета-размышления
                thoughts = [c for c in self.recent_chunks 
                           if c.chunk_type == 'thought']
                if thoughts:
                    chunk = thoughts[0]
                    self.recent_chunks.remove(chunk)
                    self.current_tokens -= self.count_tokens(chunk.text)
                    continue
                    
                # Удаляем самый старый диалог
                chunk = self.recent_chunks.pop(0)
                self.current_tokens -= self.count_tokens(chunk.text)
                
                # Перемещаем в долговременную (уже суммаризованную)
                if chunk.chunk_type == 'dialog':
                    await self.move_to_long_term(chunk)
    
    async def move_to_long_term(self, chunk: MemoryChunk):
        """Суммаризация и перемещение в долговременную память"""
        summary = await self.summarize_chunk(chunk.text)
        summary_embed = self.embed_model.encode(summary)
        
        summary_chunk = MemoryChunk(
            text=summary,
            embedding=summary_embed,
            timestamp=chunk.timestamp,
            chunk_type='summary'
        )
        
        self.long_term_chunks.append(summary_chunk)
        
        # Ограничиваем долговременную память
        if len(self.long_term_chunks) > 100:
            # Удаляем наименее релевантные (по времени и использованию)
            self.long_term_chunks.sort(key=lambda x: x.timestamp)
            self.long_term_chunks = self.long_term_chunks[-50:]
    
    async def query(self, question: str) -> str:
        """Поиск в памяти с гибридным подходом"""
        question_embed = self.embed_model.encode(question)
        
        # Ищем в долговременной памяти
        relevant = []
        for chunk in self.long_term_chunks:
            similarity = self.cosine_sim(question_embed, chunk.embedding)
            if similarity > 0.7:
                relevant.append((chunk, similarity))
        
        # Сортируем по релевантности
        relevant.sort(key=lambda x: x[1], reverse=True)
        
        # Собираем контекст
        context_parts = []
        
        # 1. Системный промпт (всегда первый)
        for chunk in self.system_chunks:
            context_parts.append(chunk.text)
        
        # 2. Релевантная долговременная память
        for chunk, _ in relevant[:3]:  # Топ-3
            context_parts.append(f"[Из памяти] {chunk.text}")
        
        # 3. Недавний диалог
        for chunk in self.recent_chunks[-5:]:
            context_parts.append(chunk.text)
        
        # 4. Текущий вопрос
        context = "\n\n".join(context_parts)
        context += f"\n\nUser: {question}\nAgent:"
        
        # Генерация ответа
        response = await self.llm.generate(context)
        
        # Сохраняем взаимодействие
        await self.add_interaction(question, response)
        
        return response
    
    @staticmethod
    def cosine_sim(a, b):
        return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
    
    @staticmethod
    def count_tokens(text: str) -> int:
        # Упрощённо - 1 токен ≈ 4 символа
        return len(text) // 4

Зачем всё это, если есть RAG?

Справедливый вопрос. RAG (Retrieval-Augmented Generation) - модное слово. Но у него другая задача. RAG ищет в документах. Наша система работает с диалогом, с течением времени, с изменяющимся контекстом.

RAG не понимает, что решение от вчера уже устарело. RAG не отличает системные инструкции от пользовательских запросов. RAG не управляет скользящим окном в реальном времени.

Это как сравнивать библиотекаря (RAG) и личного секретаря (наша система). Библиотекарь находит книги. Секретарь помнит, что вы говорили на прошлой встрече, что изменилось, и что важно прямо сейчас.

💡
Идеальный стек: наша система для диалоговой памяти + RAG для документации + Agent Skills для выполнения задач. Три слоя, три специализации.

Что будет, когда контекст станет бесконечным?

Производители обещают модели с 1M токенов. Заманчиво? Опасно. Потому что проблема не в хранении, а в поиске. Хранить миллион токенов - легко. Найти в них нужную информацию за разумное время - нет.

Мой прогноз: будущее за гибридными системами. Сверхдлинный контекст для фонового хранения. Умные агенты для извлечения. И иерархическая структура, где каждый слой оптимизирован под свою задачу.

Пока индустрия гонится за длиной контекста, умные разработчики строят архитектуру. Потому что железо упирается в физические limits (апгрейд памяти GPU становится экстремальным спортом), а архитектура - нет.

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