Настроить Memory (in-memory + vector)

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Настроить Memory (in-memory + vector)

1. Цель задачи

Научиться проектировать и реализовывать двухуровневую память для AI-агента: краткосрочную (память|working memory в оперативной памяти) и долгосрочную (векторное хранилище на основе Qdrant). В результате агент сможет удерживать контекст в пределах одного диалога и извлекать релевантные факты из предыдущих сессий, что критично для production-систем, где требуется персонализация и сохранение знаний между взаимодействиями.

Ключевой результат Функционирующий агент, который после перезапуска сессии (новый диалог) может ответить на вопрос «Что мы обсуждали в прошлой сессии про X?», используя долговременную память.


2. Исходные данные

Перед началом необходимо иметь:

Что нужноОткуда взять
Базовый AI-агент (например, на LangGraph или CrewAI)Пет-проект из предыдущих задач или заготовка с минимальным LLM-вызовом
Код для взаимодействия с LLM (OpenAI / Anthropic / vLLM)API-ключ, скрипт-обёртка (желательно async)
Qdrant — векторное хранилищеЛокальный Docker-контейнер qdrant/qdrant:latest
Набор тестовых фактов для храненияСписок из 10-15 коротких предложений (например, «Пользователь любит научную фантастику»)
Клиент для Qdrant (Python)qdrant-client в requirements.txt
In-memory буферСтандартный collections.deque или самописный класс ConversationBuffer

Если нет реального агента — симулируем:

  1. Создаём простой скрипт agent.py, который принимает строку запроса и возвращает ответ от LLM (можно использовать gpt-3.5-turbo или локальную модель через Ollama).
  2. Оборачиваем call|вызов LLM в асинхронную функцию с обработкой ошибок и таймаутами.

3. Технологический стек

КомпонентИнструментыНазначение
Язык программированияPython 3.11+Реализация агента и памяти
LLMOpenAI API / Anthropic / OllamaГенерация ответов и извлечение фактов
Краткосрочная памятьcollections.deque или listХранение последних N раундов диалога
Долгосрочная памятьQdrant (Docker)Векторное хранение + поиск по семантике
Эмбеддингиsentence-transformers (модель all-MiniLM-L6-v2)Преобразование текста в векторы
Оркестрация агентаLangGraph (опционально)Управление состоянием и памятью
МониторингPython loggingТрассировка операций с памятью
Тестированиеpytest + pytest-asyncioПроверка сценариев памяти

4. Этапы выполнения

Этап 1: Проектирование архитектуры памяти (30-40 минут)

Действия

  1. Определить интерфейсы двух типов памяти

    • ShortTermMemory: методы add(role, content), get_context(limit=N) → возвращает последние N сообщений.
    • LongTermMemory: методы store(fact_text, metadata), query(query_text, top_k=3) → возвращает список фактов с оценками релевантности.
  2. Спроектировать, как долгосрочная память заполняется:

    • После каждого ответа агента LLM анализирует диалог и выделяет факты (например, через prompt: «Извлеки из последнего обмена факты о пользователе в виде коротких утверждений»).
    • Извлечённые факты эмбеддятся и сохраняются в Qdrant с метаданными (время, сессия_id).
  3. Определить политику очистки краткосрочной памяти

    • Максимальный размер буфера: 20 сообщений (10 раундов).
    • При превышении — вытеснение самых старых (FIFO).

Ожидаемый результат этапа Документ (markdown) с диаграммой потоков данных и описание классов/функций.


Этап 2: Реализация краткосрочной (in-memory) памяти (40-60 минут)

Действия

  1. Написать класс ShortTermMemory
from collections import deque
from dataclasses import dataclass, field
from typing import List, Tuple

@dataclass
class Message:
    role: str
    content: str

class ShortTermMemory:
    def __init__(self, max_size: int = 20):
        self.buffer: deque[Message] = deque(maxlen=max_size)

    def add(self, role: str, content: str) -> None:
        self.buffer.append(Message(role, content))

    def get_context(self, limit: int = None) -> List[Message]:
        if limit is None:
            return list(self.buffer)
        return list(self.buffer)[-limit:]

    def clear(self) -> None:
        self.buffer.clear()
  1. Интегрировать с агентом

    • При каждом вызове LLM передавать get_context() как историю сообщений.
    • После получения ответа добавить в буфер сообщение пользователя и ассистента.
  2. Написать тест

    • Добавить 25 сообщений, проверить, что размер буфера = 20 и сохранились последние 20.

Ожидаемый результат этапа Рабочий класс ShortTermMemory, протестированный в isolation.


Этап 3: Развёртывание и настройка Qdrant (30-40 минут)

Действия

  1. Запустить Qdrant в Docker
docker run -d --name qdrant -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest
  1. Создать коллекцию через Python-клиент
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams

client = QdrantClient(host="localhost", port=6333)
COLLECTION_NAME = "long_term_memory"

if not client.collection_exists(COLLECTION_NAME):
    client.create_collection(
        collection_name=COLLECTION_NAME,
        vectors_config=VectorParams(size=384, distance=Distance.COSINE),  # размер для all-MiniLM-L6-v2
    )
  1. Настроить эмбеддер
from sentence_transformers import SentenceTransformer

embedder = SentenceTransformer('all-MiniLM-L6-v2')

def embed(texts: List[str]) -> List[List[float]]:
    return embedder.encode(texts, show_progress_bar=False).tolist()
  1. Проверить
    • Вставить один тестовый вектор и выполнить поиск.

Ожидаемый результат этапа Запущенный Qdrant, созданная коллекция, функция эмбеддинга, тестовый поиск возвращает результат.


Этап 4: Реализация долгосрочной памяти и интеграция (1-1.5 часа)

Действия

  1. Реализовать класс LongTermMemory с методами store и query:
import uuid
from typing import List, Dict, Optional
from datetime import datetime

class LongTermMemory:
    def __init__(self, client: QdrantClient, collection: str, embedder):
        self.client = client
        self.collection = collection
        self.embedder = embedder

    def store(self, fact_text: str, metadata: Dict = None) -> None:
        vector = self.embedder([fact_text])[0]
        point_id = str(uuid.uuid4())
        payload = {
            "text": fact_text,
            "timestamp": datetime.utcnow().isoformat(),
            **(metadata or {})
        }
        self.client.upsert(
            collection_name=self.collection,
            points=[{"id": point_id, "vector": vector, "payload": payload}]
        )
        return point_id

    def query(self, query_text: str, top_k: int = 3) -> List[Dict]:
        query_vector = self.embedder([query_text])[0]
        results = self.client.search(
            collection_name=self.collection,
            query_vector=query_vector,
            limit=top_k,
            with_payload=True
        )
        return [{"text": hit.payload["text"], "score": hit.score} for hit in results]
  1. Добавить механизм извлечения фактов после ответа агента:

    • Написать prompt для LLM:
      Извлеки из диалога ниже все факты о пользователе в виде коротких предложений (по одному на строку). 
      Если фактов нет, верни пустую строку.  
      Диалог:  
      {context}  
      Факты:
      
    • Распарсить ответ, для каждого факта вызвать long_term_memory.store().
  2. Связать обе памяти в агенте

    • При старте новой сессии (новый session_id):
    • Формировать полный контекст: «Известные факты: {результаты поиска} + История диалога: {буфер}».

Ожидаемый результат этапа Агент умеет сохранять факты в Qdrant и извлекать их в новой сессии.


Этап 5: Сквозное тестирование и демонстрация (30-60 минут)

Действия

  1. Написать интеграционный тест (или скрипт demo.py):

    • Сессия 1: пользователь говорит «Я люблю научную фантастику и киберпанк». Агент отвечает, факты сохраняются.
    • Сессия 1 завершена. Краткосрочная память сброшена.
    • Сессия 2 (новый вызов программы): пользователь говорит «Что я люблю читать?». Агент должен найти факты из Qdrant и ответить «Вы любите научную фантастику и киберпанк».
  2. Проверить граничные случаи

    • Пустой запрос → не должно быть ошибок.
    • Факт, который не относится к пользователю (например, «Погода сегодня хорошая») — он не должен сохраняться; для этого можно фильтровать через LLM: «Сохраняй только факты о пользователе».
    • Повторяющиеся факты — избежать дублирования (через проверку эмбеддингов: если похожесть > 0.95, не сохранять).
  3. Задокументировать архитектуру и результаты тестов

Ожидаемый результат этапа Демонстрационный скрипт + отчёт о тестах, подтверждающий, что агент помнит факты через сессии.


5. Критерии приемки (Definition of Done)

  • Класс ShortTermMemory реализован, протестирован, корректно ограничивает размер буфера.
  • Qdrant запущен локально в Docker, коллекция создана с корректной размерностью векторов.
  • Функция эмбеддинга через sentence-transformers работает и возвращает векторы нужного размера.
  • LongTermMemory реализует store и query, возвращает релевантные факты с оценками.
  • Агент после каждого ответа извлекает факты из диалога (через промпт LLM) и сохраняет их в долгосрочную память.
  • В новой сессии агент использует как краткосрочную историю (пустую), так и долгосрочные факты, полученные через поиск.
  • Интеграционный тест проходит: факт, сохранённый в сессии 1, доступен в сессии 2.
  • Код покрыт логами для отладки (логгируются операции add, store, query).
  • README содержит инструкцию по запуску и пример использования.

6. Ожидаемый результат

Основной артефакт
Репозиторий с кодом, содержащий:

  • memory/short_term.py — класс краткосрочной памяти.
  • memory/long_term.py — класс долгосрочной памяти.
  • memory/embedder.py — загрузка модели и функция эмбеддинга.
  • agent.py — интегрированный агент с обеими памятью.
  • demo.py — скрипт, демонстрирующий сценарий «запоминание через сессии».
  • tests/test_short_term.py, tests/test_long_term.py, tests/test_integration.py — тесты.
  • docker-compose.yml (или команда запуска Qdrant).
  • requirements.txt со всеми зависимостями.
  • README.md с описанием архитектуры и инструкцией.

Дополнительные результаты (опционально):

  • График времени ответа агента до и после внедрения памяти.
  • Лог-файл с примерами извлечения фактов из LLM.
  • Набор тестовых данных для Qdrant (seed-факты).

7. Возможные сложности и их решение

СложностьРешение
LLM неточно извлекает факты (лишние или не все)Улучшить промпт: дать примеры, ограничить формат вывода (только строки, без пояснений). Добавить пост-фильтр: проверять, содержит ли факт слова «пользователь», «люблю» и т.п.
Qdrant не отвечает или глючный контейнерПроверить порт curl http://localhost:6333/health. Использовать docker logs qdrant. В крайнем случае — заменить на in-memory FAISS для тестов.
Эмбеддинги слишком большие (384 → можно 128)Выбрать меньшую модель (all-MiniLM-L6-v2 уже компактна). Для productionquantized версии.
Дублирование фактовПри store сначала искать похожие векторы (top_k=1 с порогом >0.95). Если найден — не добавлять, а обновить метаданные (например, увеличить счётчик).
Агент не использует память из-за переполнения контекстного окна LLMОграничить количество извлекаемых фактов (top_k=3) и сумму их длин. Краткосрочную историю триммировать по абсолютной длине токенов.

8. Бюджет времени (оценка)

ЭтапВремя
Этап 1: Проектирование архитектуры30-40 мин
Этап 2: Краткосрочная память40-60 мин
Этап 3: Развёртывание Qdrant30-40 мин
Этап 4: Долгосрочная память + интеграция1-1.5 ч
Этап 5: Сквозное тестирование30-60 мин
Итого (чистое время)3.5-5 ч

Примечание для первого раза Если вы не работали с Qdrant ранее, добавьте +1 час на изучение API. Если LLM-вызовы платные, используйте локальный Ollama с моделью mistral для отладки.


9. Связанные вопросы из базы знаний

ВопросТема
#17Как построить простое RAG-приложение
#41Что такое векторное хранилище и зачем оно нужно
#52Отличие краткосрочной и долгосрочной памяти в агентах
#78Как выбрать модель эмбеддингов
#114Настройка Qdrant: конфигурация коллекции
#203Паттерн Memory в LangGraph
#309Обработка дубликатов в векторной БД
#415Промпт-инжиниринг для извлечения фактов
#572Мониторинг состояния памяти агента (логи)
#698Тестирование интеграций с Qdrant (mock/fixtures)

10. Чек-лист самопроверки

  • Я запустил Qdrant и убедился, что коллекция создана и отвечает на запросы.
  • Я написал юнит-тесты для ShortTermMemory (тест на overflow).
  • Я проверил, что store в Qdrant сохраняет payload с текстом и временем.
  • Я протестировал query с запросом, семантически близким к сохранённому факту, и получил score > 0.7.
  • Я запустил сквозной сценарий «сессия 1 → сессия 2» и убедился, что факт извлекается.
  • Я добавил логирование всех операций с памятью и убедился, что в логах виден поток данных.
  • Я проверил, что при дублирующемся факте в Qdrant не создаётся новая точка (или обновляется счётчик).
  • Я написал README с примерами запуска и пояснил архитектуру памяти.