Настроить 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 |
Если нет реального агента — симулируем:
- Создаём простой скрипт agent.py, который принимает строку запроса и возвращает ответ от LLM (можно использовать gpt-3.5-turbo или локальную модель через Ollama).
- Оборачиваем call|вызов LLM в асинхронную функцию с обработкой ошибок и таймаутами.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык программирования | Python 3.11+ | Реализация агента и памяти |
| LLM | OpenAI 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 минут)
Действия
-
Определить интерфейсы двух типов памяти
-
Спроектировать, как долгосрочная память заполняется:
-
Определить политику очистки краткосрочной памяти
- Максимальный размер буфера: 20 сообщений (10 раундов).
- При превышении — вытеснение самых старых (FIFO).
Ожидаемый результат этапа Документ (markdown) с диаграммой потоков данных и описание классов/функций.
Этап 2: Реализация краткосрочной (in-memory) памяти (40-60 минут)
Действия
- Написать класс 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()
-
Интегрировать с агентом
- При каждом вызове LLM передавать
get_context()как историю сообщений. - После получения ответа добавить в буфер сообщение пользователя и ассистента.
- При каждом вызове LLM передавать
-
Написать тест
- Добавить 25 сообщений, проверить, что размер буфера = 20 и сохранились последние 20.
Ожидаемый результат этапа Рабочий класс ShortTermMemory, протестированный в isolation.
Этап 3: Развёртывание и настройка Qdrant (30-40 минут)
Действия
docker run -d --name qdrant -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest
- Создать коллекцию через 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
)
- Настроить эмбеддер
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()
- Проверить
- Вставить один тестовый вектор и выполнить поиск.
Ожидаемый результат этапа Запущенный Qdrant, созданная коллекция, функция эмбеддинга, тестовый поиск возвращает результат.
Этап 4: Реализация долгосрочной памяти и интеграция (1-1.5 часа)
Действия
- Реализовать класс 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]
-
Добавить механизм извлечения фактов после ответа агента:
- Написать prompt для LLM:
Извлеки из диалога ниже все факты о пользователе в виде коротких предложений (по одному на строку). Если фактов нет, верни пустую строку. Диалог: {context} Факты: - Распарсить ответ, для каждого факта вызвать
long_term_memory.store().
- Написать prompt для LLM:
-
Связать обе памяти в агенте
- При старте новой сессии (новый session_id):
- Краткосрочная память пуста.
- Долгосрочная ищет релевантные факты по первому запросу пользователя.
- Формировать полный контекст: «Известные факты: {результаты поиска} + История диалога: {буфер}».
- При старте новой сессии (новый session_id):
Ожидаемый результат этапа Агент умеет сохранять факты в Qdrant и извлекать их в новой сессии.
Этап 5: Сквозное тестирование и демонстрация (30-60 минут)
Действия
-
Написать интеграционный тест (или скрипт demo.py):
- Сессия 1: пользователь говорит «Я люблю научную фантастику и киберпанк». Агент отвечает, факты сохраняются.
- Сессия 1 завершена. Краткосрочная память сброшена.
- Сессия 2 (новый вызов программы): пользователь говорит «Что я люблю читать?». Агент должен найти факты из Qdrant и ответить «Вы любите научную фантастику и киберпанк».
-
Проверить граничные случаи
- Пустой запрос → не должно быть ошибок.
- Факт, который не относится к пользователю (например, «Погода сегодня хорошая») — он не должен сохраняться; для этого можно фильтровать через LLM: «Сохраняй только факты о пользователе».
- Повторяющиеся факты — избежать дублирования (через проверку эмбеддингов: если похожесть > 0.95, не сохранять).
-
Задокументировать архитектуру и результаты тестов
Ожидаемый результат этапа Демонстрационный скрипт + отчёт о тестах, подтверждающий, что агент помнит факты через сессии.
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 уже компактна). Для production — quantized версии. |
| Дублирование фактов | При store сначала искать похожие векторы (top_k=1 с порогом >0.95). Если найден — не добавлять, а обновить метаданные (например, увеличить счётчик). |
| Агент не использует память из-за переполнения контекстного окна LLM | Ограничить количество извлекаемых фактов (top_k=3) и сумму их длин. Краткосрочную историю триммировать по абсолютной длине токенов. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Проектирование архитектуры | 30-40 мин |
| Этап 2: Краткосрочная память | 40-60 мин |
| Этап 3: Развёртывание Qdrant | 30-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 с примерами запуска и пояснил архитектуру памяти.