Память AI-агентов: архитектура, RAG, векторные базы

Три уровня памяти агентов: рабочая, краткосрочная, долгосрочная. Как реализовать с ChromaDB, Pinecone, Mem0. Практические примеры и сравнение подходов.

📊 Продвинутый⏱ 18 мин

# 1. АРХИТЕКТУРА ПАМЯТИ АГЕНТА: ТРИ УРОВНЯ

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

Рабочая память (Working Memory) — это то, что языковая модель «видит» в момент генерации: контекстное окно, ограниченное 4K–200K токенов в зависимости от модели. Это самый быстрый, но и самый дорогой уровень — каждый токен в контексте оплачивается и замедляет инференс. Краткосрочная память (Short-term) — история сообщений текущей сессии, обычно хранящаяся в Redis или PostgreSQL. Она переживает один вызов LLM, но теряется между сессиями. Долгосрочная память (Long-term) — это векторная база данных, где каждое значимое взаимодействие или факт сохраняется в виде эмбеддинга и может быть найдено по семантической близости через месяцы.

╔══════════════════════════════════════════════════════════════════╗
║          🧠  ТРИ УРОВНЯ ПАМЯТИ AI-АГЕНТА                        ║
╠══════════╦══════════════════╦══════════════╦═════════════════════╣
║ Уровень  ║ Горизонт         ║ Хранилище    ║ Объём / Стоимость   ║
╠══════════╬══════════════════╬══════════════╬═════════════════════╣
║ WORKING  ║ секунды-минуты   ║ RAM (inference)║ 4K–200K токенов   ║
║ SHORT    ║ часы-дни         ║ Redis / PG    ║ ~10K сообщений     ║
║ LONG     ║ недели-годы      ║ Vector DB     ║ миллионы записей   ║
╚══════════╩══════════════════╩══════════════╩═════════════════════╝

# Ключевой принцип: каждый уровень — компромисс между скоростью,
# стоимостью и глубиной хранения. Архитектура агента должна
# оркестрировать все три уровня как единый конвейер.

class MemoryTier:
    """Абстракция одного уровня памяти."""
    def __init__(self, name, ttl, capacity, cost_per_unit):
        self.name = name             # working | short | long
        self.ttl = ttl               # время жизни записи в секундах
        self.capacity = capacity       # максимальный объём
        self.cost = cost_per_unit     # стоимость хранения единицы

WORKING = MemoryTier("working", ttl=300,  capacity="128K tokens", cost="$0.01/1K tokens")
SHORT  = MemoryTier("short",   ttl=86400, capacity="10K msgs",     cost="~$0.00/msg")
LONG   = MemoryTier("long",    ttl=float('inf'), capacity="unlimited", cost="~$0.0001/record")

# 2. WORKING MEMORY: ТОНКАЯ НАСТРОЙКА КОНТЕКСТНОГО ОКНА

Рабочая память — это самое «горячее» и дорогое место. Каждый токен, попадающий в контекстное окно, увеличивает latency (квадратично для attention-механизма) и стоимость. Искусство управления рабочей памятью — в балансе: не потерять важный контекст, но и не перегрузить окно шумом. Для GPT-4o контекстное окно достигает 128K токенов, но эффективная длина ответа резко падает после ~64K — модель «забывает» середину диалога (феномен lost-in-the-middle).

Практические стратегии: (1) резервировать 30% окна под ответ модели, (2) использовать sliding window с сохранением system prompt, (3) суммировать старые сообщения вместо их удаления — компрессия вместо обрезки. Для подсчёта токенов критически использовать тот же токенизатор, что и модель (tiktoken для OpenAI, sentencepiece для opensource моделей).

pip install tiktoken

import tiktoken
from openai import OpenAI

client = OpenAI()
enc = tiktoken.encoding_for_model("gpt-4o")

class WorkingMemoryManager:
    """Управление контекстным окном с компрессией и приоритизацией."""

    def __init__(self, model="gpt-4o", max_tokens=96000, reserve_ratio=0.25):
        self.enc = tiktoken.encoding_for_model(model)
        self.model = model
        self.limit = max_tokens
        self.reserve = int(max_tokens * reserve_ratio)  # ~24K под ответ

    def count_tokens(self, text):
        """Точный подсчёт токенов."""
        return len(self.enc.encode(text))

    def prepare_context(self, system_prompt, history, new_query,
                       injected_memories=None):
        """Формирует оптимальное контекстное окно с компрессией."""
        available = self.limit - self.reserve

        # System prompt — нерушимый
        sys_tokens = self.count_tokens(system_prompt)
        available -= sys_tokens

        # Инжектим долгосрочные воспоминания (RAG)
        if injected_memories:
            mem_text = "\n".join(f"• {m}" for m in injected_memories[:5])
            available -= self.count_tokens(mem_text)

        # Новый запрос — обязательно
        available -= self.count_tokens(new_query)

        # История: старые сообщения суммируем, новые — as-is
        context_msgs = []
        spent = 0
        for msg in reversed(history):
            t = self.count_tokens(msg["content"])
            if spent + t > available * 0.7:
                # Старые сообщения — компрессия вместо обрезки
                summary = self._summarize_messages(history[:history.index(msg)])
                context_msgs.insert(0, {"role": "system", "content": f"[Резюме диалога] {summary}"})
                break
            context_msgs.insert(0, msg)
            spent += t

        return context_msgs

    def _summarize_messages(self, messages):
        """Компрессия: cheap модель суммирует старый контекст."""
        dialogue = "\n".join(f"{m['role']}: {m['content'][:200]}" for m in messages[-20:])
        resp = client.chat.completions.create(
            model="gpt-4o-mini",  # дешёвая модель для суммаризации
            messages=[{"role":"user", "content":f"Summarise this dialogue in 3 bullet points in Russian:\n{dialogue}"}],
            max_tokens=200
        )
        return resp.choices[0].message.content

# 3. SHORT-TERM MEMORY: REDIS КАК СЕССИОННЫЙ СЛОЙ

Краткосрочная память — это связующее звено между вызовами LLM в рамках одной сессии. Когда агент делает 5 последовательных tool-call'ов, каждый вызов LLM должен «помнить» результаты предыдущих. Redis идеален для этой роли: атомарные операции, TTL на ключи, встроенные структуры (list для истории, hash для метаданных сессии). Важно: short-term память должна быть сериализуемой — при перезапуске сервера сессия восстанавливается из Redis.

Паттерн: append-only log сообщений в Redis List + периодическая компрессия в сводку. Для production-агентов с сотнями одновременных сессий необходимо шардирование по session_id и LRU-эвикция старых сессий. Альтернативы: PostgreSQL (если нужна SQL-аналитика по истории), Dragonfly (совместим с Redis, но в 25 раз быстрее на больших нагрузках).

pip install redis[hiredis] orjson

import redis, orjson, time
from datetime import datetime, timedelta

r = redis.Redis(host='localhost', port=6379, decode_responses=True,
                      socket_keepalive=True, health_check_interval=30)

class SessionMemory:
    """Production-ready сессионная память с компрессией и лимитами."""

    def __init__(self, session_id, ttl_hours=24, max_messages=500):
        self.sid = session_id
        self.key_msg = f"sess:{session_id}:msgs"
        self.key_meta = f"sess:{session_id}:meta"
        self.key_summary = f"sess:{session_id}:summary"
        self.ttl = ttl_hours * 3600
        self.max_msgs = max_messages

    def add_message(self, role, content, metadata=None):
        """Добавить сообщение с атомарным ограничением длины."""
        msg = {
            "role": role,
            "content": content[:8000],  # защита от раздувания
            "ts": datetime.now().isoformat(),
            "meta": metadata or {}
        }
        pipe = r.pipeline()
        pipe.rpush(self.key_msg, orjson.dumps(msg))
        pipe.ltrim(self.key_msg, -self.max_msgs, -1)  # держим только последние N
        pipe.expire(self.key_msg, self.ttl)
        pipe.hset(self.key_meta, mapping={"last_activity": str(time.time())})
        pipe.execute()

    def get_history(self, limit=50, since=None):
        """Получить историю с фильтрацией по времени."""
        raw = r.lrange(self.key_msg, -limit, -1)
        msgs = [orjson.loads(m) for m in raw]
        if since:
            msgs = [m for m in msgs if m["ts"] >= since]
        return [{"role": m["role"], "content": m["content"]} for m in msgs]

    def store_summary(self, summary_text):
        """Сохранить сжатую сводку диалога."""
        r.setex(self.key_summary, self.ttl, summary_text)

    def get_summary(self):
        """Получить сводку предыдущего контекста."""
        return r.get(self.key_summary)

    def is_active(self, timeout_sec=1800):
        """Проверить, активна ли сессия."""
        last = r.hget(self.key_meta, "last_activity")
        if not last: return False
        return (time.time() - float(last)) < timeout_sec

# 4. LONG-TERM MEMORY: CHROMADB ДЛЯ ЛОКАЛЬНОГО ДЕПЛОЯ

ChromaDB — open-source векторная база данных, оптимизированная для AI-приложений. В отличие от Pinecone (managed cloud), ChromaDB можно развернуть локально за 5 минут. Она поддерживает два режима: in-memory (для тестов) и persistent (на диске через DuckDB). Для агента с долгосрочной памятью ChromaDB хранит каждое значимое взаимодействие как (id, embedding, document, metadata)-кортеж. При новом запросе агент семантически ищет top-K релевантных воспоминаний и инжектит их в system prompt.

Ключевые метрики: размерность эмбеддинга (OpenAI ada-002 = 1536, text-embedding-3-large = 3072), расстояние (косинусное, L2, IP), стратегия индексации (HNSW для скорости, flat для точности). Важно: ChromaDB не масштабируется горизонтально из коробки — для production с >1M записей рассмотрите Qdrant (Rust, gRPC-native) или Pinecone.

pip install chromadb openai

import chromadb, hashlib, json
from datetime import datetime
from openai import OpenAI

client = OpenAI()

class ChromaLongTermMemory:
    """Долгосрочная память на ChromaDB с автоматической дедупликацией."""

    def __init__(self, persist_dir="./agent_ltm", collection_name="memories"):
        self.chroma = chromadb.PersistentClient(path=persist_dir)
        self.coll = self.chroma.get_or_create_collection(
            name=collection_name,
            metadata={"hnsw:space": "cosine"}  # косинусное расстояние
        )
        self.emb_model = "text-embedding-3-small"  # дешевле ada-002

    def _embed(self, text):
        """OpenAI text-embedding-3-small: 1536-dim, $0.02/1M tokens."""
        r = client.embeddings.create(model=self.emb_model, input=text)
        return r.data[0].embedding

    def remember(self, fact, category="general", importance=1.0):
        """Сохранить факт с дедупликацией по хешу."""
        fact_id = f"mem_{hashlib.md5(fact.encode()).hexdigest()[:12]}"

        # Проверяем, не существует ли уже такой факт
        existing = self.coll.get(ids=[fact_id])
        if existing['ids']:
            self.coll.update(ids=[fact_id], metadatas=[{"last_seen": datetime.now().isoformat()}])
            return fact_id  # обновили timestamp, не дублируем

        self.coll.add(
            ids=[fact_id],
            documents=[fact],
            embeddings=[self._embed(fact)],
            metadatas=[{
                "category": category,
                "importance": importance,
                "created": datetime.now().isoformat(),
                "access_count": 0
            }]
        )
        return fact_id

    def recall(self, query, n=5, category_filter=None, min_importance=0.0):
        """Семантический поиск релевантных воспоминаний."""
        where_clause = {}
        if category_filter:
            where_clause["category"] = category_filter
        if min_importance > 0:
            where_clause["importance"] = {"$gte": min_importance}

        results = self.coll.query(
            query_embeddings=[self._embed(query)],
            n_results=n,
            where=where_clause if where_clause else None,
            include=["documents", "metadatas", "distances"]
        )
        return list(zip(results['documents'][0], results['distances'][0]))

    def forget_old(self, days=90, min_access=1):
        """Удаление устаревших/неиспользуемых воспоминаний."""
        cutoff = datetime.now() - timedelta(days=days)
        all_data = self.coll.get(include=["metadatas"])
        to_delete = []
        for i, (mid, meta) in enumerate(zip(all_data['ids'], all_data['metadatas'])):
            created = datetime.fromisoformat(meta['created']) if meta else cutoff
            if created < cutoff and meta.get('access_count', 0) < min_access:
                to_delete.append(mid)
        if to_delete:
            self.coll.delete(ids=to_delete)
            print(f"🧹 Forgot {len(to_delete)} stale memories")

# 5. PINECONE: MANAGED ВЕКТОРНАЯ ПАМЯТЬ ДЛЯ PRODUCTION

Pinecone — полностью управляемая векторная база данных с serverless-архитектурой. В отличие от ChromaDB, которую вы администрируете сами, Pinecone берёт на себя репликацию, шардирование, бэкапы и мониторинг. Вы платите за поды (pod-based) или за потребление (serverless). Для production-агента с >100K пользователей Pinecone часто оказывается дешевле, чем содержание DevOps-инженера для self-hosted решения.

Ключевые особенности: metadata filtering (фильтрация по категориям, датам, важности без ущерба скорости), namespaces (изоляция данных разных пользователей в одном индексе), real-time upserts (изменения видны в следующем запросе). Стоимость: $0.096/час за pod на 1M 1536-dim векторов (~$70/мес). Serverless: $0.33/1M запросов на чтение + $2.00/1M на запись.

pip install pinecone-client openai

import os, time
from pinecone import Pinecone, ServerlessSpec
from openai import OpenAI

client = OpenAI()
pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY"))

class PineconeMemory:
    """Production long-term memory на Pinecone Serverless."""

    def __init__(self, index_name="agent-memory", dim=1536):
        # Создать serverless индекс, если не существует
        if index_name not in pc.list_indexes().names():
            pc.create_index(
                name=index_name,
                dimension=dim,
                metric="cosine",
                spec=ServerlessSpec(cloud="aws", region="us-east-1")
            )
            time.sleep(5)  # ждём инициализацию

        self.index = pc.Index(index_name)
        self.emb_model = "text-embedding-3-small"

    def _embed(self, texts):
        """Batched эмбеддинг (Pinecone позволяет upsert пачками)."""
        if isinstance(texts, str): texts = [texts]
        r = client.embeddings.create(model=self.emb_model, input=texts)
        return [d.embedding for d in r.data]

    def remember(self, user_id, fact, category="general"):
        """Сохранить факт в namespace пользователя."""
        vec_id = f"{user_id}_{hash(fact) % 10**10}"
        self.index.upsert(
            vectors=[{
                "id": vec_id,
                "values": self._embed(fact)[0],
                "metadata": {
                    "fact": fact[:500],
                    "category": category,
                    "user_id": user_id,
                    "ts": int(time.time())
                }
            }],
            namespace=f"user_{user_id}"  # изоляция данных пользователей
        )

    def recall(self, user_id, query, top_k=5, category=None):
        """Семантический поиск с metadata-фильтром."""
        filters = {"user_id": {"$eq": user_id}}
        if category:
            filters["category"] = {"$eq": category}

        results = self.index.query(
            vector=self._embed(query)[0],
            top_k=top_k,
            filter=filters,
            namespace=f"user_{user_id}",
            include_metadata=True
        )
        return [(m['metadata']['fact'], m['score']) for m in results['matches']]

    def get_stats(self):
        """Статистика индекса."""
        return self.index.describe_index_stats()

# 6. MEM0: ИНТЕЛЛЕКТУАЛЬНЫЙ СЛОЙ ПАМЯТИ НАД LLM

Mem0 (Memory Zero) — это не просто векторная база данных, а интеллектуальный слой памяти, который автоматически извлекает, структурирует и обновляет факты из диалогов. В отличие от ChromaDB/Pinecone, куда вы сами решаете что сохранять, Mem0 анализирует каждое сообщение с помощью LLM и решает: является ли это новым фактом? Обновляет ли он существующий? Стоит ли его забыть? Это память «человеческого» уровня — не дословное запоминание, а извлечение сути.

Mem0 поддерживает несколько бэкендов: локальный (Qdrant), managed cloud, и кастомные. Метод add() принимает сырое сообщение, внутренний LLM извлекает structured memory, а search() находит релевантные факты по семантическому запросу. Идеально для персонализированных агентов, которые должны помнить предпочтения пользователя, контекст проекта, прошлые решения.

pip install mem0ai

from mem0 import Memory

class Mem0AgentMemory:
    """Интеллектуальная память на Mem0 с автоматическим извлечением фактов."""

    def __init__(self, user_id, llm_provider="openai"):
        self.user_id = user_id

        # Конфигурация: бэкенд Qdrant (бесплатный локальный)
        config = {
            "vector_store": {
                "provider": "qdrant",
                "config": {
                    "host": "localhost",
                    "port": 6333,
                }
            },
            "llm": {
                "provider": llm_provider,
                "config": {
                    "model": "gpt-4o-mini",  # дешёвая модель для извлечения фактов
                }
            },
            "embedder": {
                "provider": "openai",
                "config": {
                    "model": "text-embedding-3-small"
                }
            }
        }
        self.memory = Memory.from_config(config)

    def add_from_message(self, message, role="user"):
        """Mem0 сам решит, является ли сообщение фактом."""
        result = self.memory.add(
            messages=[{"role": role, "content": message}],
            user_id=self.user_id
        )
        return result  # список извлечённых/обновлённых фактов

    def search(self, query, limit=5):
        """Семантический поиск релевантных воспоминаний."""
        results = self.memory.search(
            query=query,
            user_id=self.user_id,
            limit=limit
        )
        return [(r['memory'], r.get('score', 0)) for r in results]

    def get_all(self):
        """Получить все факты о пользователе."""
        return self.memory.get_all(user_id=self.user_id)

    def delete(self, memory_id):
        """Удалить конкретный факт."""
        self.memory.delete(memory_id=memory_id)

# Пример использования Mem0 в агенте
mem = Mem0AgentMemory(user_id="user_123")

# Агент общается — Mem0 сам извлекает факты
mem.add_from_message("Меня зовут Алексей, я Python-разработчик из Москвы")
# → Извлечёт: {"name": "Алексей", "role": "Python developer", "location": "Москва"}

mem.add_from_message("Я переехал в Санкт-Петербург")
# → Обновит location на "Санкт-Петербург", а не создаст дубликат

# Поиск контекста для нового запроса
context = mem.search("Где живёт Алексей и чем занимается?")
# → [("Алексей — Python разработчик из Санкт-Петербурга", 0.94)]

# 7. СРАВНЕНИЕ ПОДХОДОВ И АРХИТЕКТУРНЫЕ РЕКОМЕНДАЦИИ

Выбор стека памяти зависит от масштаба, требований к приватности и бюджета. Для прототипа или личного агента: ChromaDB (persistent) + Redis + Mem0. Вы получаете полноценную трёхуровневую память за $0 на инфраструктуру, только платите за API-вызовы. Для стартапа с 1K–10K пользователей: Pinecone (serverless) заменяет ChromaDB, давая managed масштабирование без DevOps. Для enterprise с жёсткими требованиями к данным: self-hosted Qdrant на Kubernetes + собственный embedding-сервер (например, text-embeddings-inference от HuggingFace).

Важнейший архитектурный принцип: не смешивайте уровни. Рабочая память — это всегда RAM инференс-сервера. Краткосрочная — быстрая key-value БД (Redis/Dragonfly). Долгосрочная — векторная БД. Попытка использовать ChromaDB как краткосрочную память приведёт к latency >100ms на каждый вызов, что убьёт UX. И наоборот — хранение всей истории в контекстном окне быстро приведёт к банкротству на API-токенах.

╔══════════════════════════════════════════════════════════════════════╗
║           🧠  КАКУЮ ВЕКТОРНУЮ БД ВЫБРАТЬ ДЛЯ ПАМЯТИ?               ║
╠═══════════╦═══════════════╦══════════════╦══════════════╦════════════╣
║ Решение   ║ Тип           ║ Масштаб       ║ Стоимость    ║ Когда      ║
╠═══════════╬═══════════════╬══════════════╬══════════════╬════════════╣
║ ChromaDB  ║ Self-hosted   ║ До 1M вект.  ║ $0           ║ Прототип   ║
║ Qdrant    ║ Self-hosted   ║ До 100M      ║ $0 (OSS)     ║ Production ║
║ Pinecone  ║ Managed Cloud ║ Не ограничен ║ ~$70/мес     ║ Стартап    ║
║ Weaviate  ║ Self/Managed  ║ До 1B        ║ $0 / $25/мес ║ Гибрид     ║
║ Mem0      ║ Слой над БД   ║ Зависит от БД║ $0 (OSS)     ║ Факты      ║
╚═══════════╩═══════════════╩══════════════╩══════════════╩════════════╝

# Финальная архитектура памяти агента:
#
#  User Message
#       │
#       ▼
#  ┌─────────────────────────────────────────┐
#  │ 1. Mem0 / Ручное извлечение фактов      │  ← извлекаем суть
#  └──────────────┬──────────────────────────┘
#                 ▼
#  ┌─────────────────────────────────────────┐
#  │ 2. Long-term search (Pinecone/ChromaDB) │  ← ищем похожий опыт
#  └──────────────┬──────────────────────────┘
#                 ▼
#  ┌─────────────────────────────────────────┐
#  │ 3. Short-term load (Redis session)      │  ← подгружаем историю
#  └──────────────┬──────────────────────────┘
#                 ▼
#  ┌─────────────────────────────────────────┐
#  │ 4. Working Memory (LLM context)         │  ← всё попадает в окно
#  └──────────────┬──────────────────────────┘
#                 ▼
#  ┌─────────────────────────────────────────┐
#  │ 5. LLM Response + Save to all tiers     │  ← сохраняем опыт
#  └─────────────────────────────────────────┘

🔗 Полезные ссылки

📖 ChromaDB Docs📖 Pinecone Docs📖 Mem0 Docs📖 Qdrant Docs📖 OpenAI Embeddings