Общая концепция persistent memory для AI‑агентов
AI‑агенты, использующие большие языковые модели, часто работают в режиме «одноразовой сессии»: после завершения диалога все контекстные данные теряются, и при следующем запуске агент начинает с нуля. Это приводит к повторному анализу уже решённых задач, потере исторических знаний о багах и принятых архитектурных решениях. Для устранения этой проблемы был разработан Memory MCP (Memory Communication Protocol) Server — сервис, предоставляющий агентам длительное хранилище контекста с поддержкой семантического поиска.
Сервер построен на языке Go, что обеспечивает низкую латентность и простую интеграцию в существующие pipelines. Основная цель — предоставить единый слой памяти, доступный различным агентам (Cursor, Claude Code, Codex) без необходимости дублирования данных или отдельного конфигурирования в каждом инструменте.
Хранилище данных на SQLite
В качестве долговременного хранилища выбран SQLite. Этот выбор обусловлен несколькими факторами:
- Лёгкость развертывания – отсутствие отдельного сервера и возможность встраивания в любой процесс.
- Транзакционная надёжность – ACID‑свойства гарантируют целостность записей даже при аварийных завершениях.
- Поддержка JSON‑колонок – позволяет хранить произвольные метаданные (источники, временные метки, типы запросов) без схемных ограничений.
База данных содержит две ключевые таблицы:
| Таблица | Поля | Описание |
|---|---|---|
documents | id (INTEGER PK), content (TEXT), embedding (BLOB), metadata (JSON), created_at (TIMESTAMP) | Хранит исходный текст, предвычисленное векторное представление и сопутствующие данные. |
updates | doc_id (INTEGER FK), operation (TEXT), timestamp (TIMESTAMP) | Журнал изменений, используемый для инкрементального обновления индексов. |
Для ускорения чтения встраиваются индексы на created_at и на хэш‑значения, получаемые из embedding‑векторов (см. ниже).
Эмбеддинг‑поток и отказ от fallback
Изначально планировалось реализовать fallback‑механизм между несколькими провайдерами эмбеддингов (OpenAI, Cohere, локальные модели). Идея заключалась в том, что при недоступности одного сервиса запрос автоматически перенаправлялся бы к альтернативному. На практике такой подход привёл к скрытым ошибкам:
- Несоответствие размерности: разные модели генерируют векторы разной длины, что ломало расчёт косинусного сходства без дополнительной нормализации.
- Разный семантический смысл: одинаковые запросы могли давать сильно различающиеся вектора, из‑за чего поиск возвращал нерелевантные результаты.
- Трудности отладки: ошибки падали в тихий режим, а разработчики получали лишь «плохой результат», а не сообщение о смене провайдера.
В результате fallback был полностью удалён из архитектуры. Вместо этого выбран единый провайдер – модель text-embedding-ada-002 от OpenAI, обеспечивающая стабильные 1536‑мерные векторы. При необходимости переключения на локальную модель процесс требует полной миграции данных и пересчёта всех embedding‑ов, что делается в отдельном скрипте, а не «на лету».
Поиск по семантике в памяти
Для реализации семантического поиска был отказ от традиционных векторных баз данных (FAISS, Milvus) в пользу in‑memory cosine similarity. Основные шаги:
- Загрузка embedding‑ов из SQLite в массив
[][]float32. - Нормализация каждого вектора до единичной длины (L2‑норма).
- Косинусное сходство вычисляется как скалярное произведение нормализованных векторов, что эквивалентно косинусу угла между ними.
- Отбор top‑k результатов с помощью кучи (min‑heap) для поддержания O(N log k) сложности.
Поскольку все вектора находятся в оперативной памяти, запросы отрабатывают за миллисекунды даже при десятках тысяч записей. При росте объёма данных (>100 k записей) планируется переход к сегментированным кешам, где часть embedding‑ов хранится в LRU‑буфере, а остальные остаются в SQLite.
RAG‑индексирование и инкрементальное обновление
Retrieval‑Augmented Generation (RAG) требует построения индекса, связывающего запросы с релевантными документами. В Memory MCP Server реализован инкрементальный RAG‑индекс:
- Инициализация: при старте сервера построение индекса происходит на основе всех записей в
documents. - Обновление: при добавлении нового документа в базу генерируется его embedding, сразу нормализуется и вставляется в in‑memory массив. Параллельно в таблице
updatesфиксируется операцияINSERT. - Удаление/модификация: операции
DELETEиUPDATEтакже регистрируются, а соответствующие векторы помечаются как «мёртвые». Реальная перестройка происходит в фоновом потоке, который периодически (по configurable‑интервалу) очищает «мёртвые» элементы и перестраивает кэш‑индекс.
Такой подход гарантирует, что поиск всегда использует актуальные данные, а затраты на полное переиндексирование минимальны.
Типичные проблемы и их решение
1. Тихие баги при смешивании embedding‑ов
Как уже упоминалось, использование нескольких моделей приводило к несогласованности векторных пространств. Решение – единственная модель и полный пересчёт при смене провайдера.
2. Погрешности из‑за плавающей точки
Косинусное сходство чувствительно к небольшим ошибкам округления. Для стабилизации используется двойная точность (float64) при расчёте скалярного произведения, а только финальный результат приводится к float32.
3. Перегрузка памяти при больших объёмах
Иногда количество embedding‑ов превышало доступную ОЗУ. Был внедрён lazy‑loading: только последние N записей (по времени) находятся в памяти, остальные запрашиваются из SQLite по мере необходимости. Кроме того, реализован механизм compression для embedding‑ов (Quantization до 8‑bit), что снижает потребление памяти без заметного ухудшения качества поиска.
4. Конкурентный доступ к базе
Несмотря на ACID‑свойства SQLite, одновременные записи из нескольких Go‑горутин вызывали «database is locked». Проблема решена через pool‑ing соединений с ограничением количества одновременно открытых транзакций и использованием PRAGMA journal_mode=WAL.
5. Синхронизация индекса с базой
Индексация в памяти могла отставать от записей в базе, если процесс обновления прерывался. Для надёжности введён журнал updates, который хранит все операции. При рестарте сервера система читает журнал и восстанавливает состояние индекса до последней зафиксированной операции.
Эти практики позволяют поддерживать стабильную работу Memory MCP Server в продакшене, обеспечивая AI‑агентам доступ к историческому контексту, быстрый семантический поиск и гибкую интеграцию с инструментами генерации кода.