На той неделе, когда мы дали агенту настоящую долгосрочную память, поиск стал хуже.

Не сразу. Первый день был волшебным: агент в среду вспомнил особенность развёртывания из сессии в понедельник — без повторных объяснений. К концу второй недели он возвращал пять почти одинаковых заметок на запрос «как мы обрабатываем ограничение частоты запросов», каждая чуть-чуть неверная, и ни одна не была той, что мне на самом деле нужна. Агент усердно вызывал memorize на каждое наблюдение, каждый тупик, каждую полусформированную догадку. Он построил себе ящик хлама и теперь рылся в нём при каждом запросе.

Это та часть фразы «дайте агенту память», о которой никто не предупреждает. Память, которая только растёт, — это не память, это журнал. А журнал, по которому ищут по смыслу, становится более шумным по мере роста, потому что каждый сохранённый почти-дубликат борется за один и тот же кусок ранжирования. Больше воспоминаний — хуже поиск. Ровно противоположно тому, на что вы рассчитывали.

Octobrain — наше решение этой проблемы с открытым исходным кодом: MCP-сервер памяти на Rust, который даёт любому агенту постоянную, семантически-искомую долгосрочную память. Мы представили его ещё в апреле, а в релизе 0.7.0 память научилась забывать. Эта статья — то руководство, которого мне самому не хватало: как настроить постоянную память LLM, остающуюся точной, с помощью двух идей — разграничьте её по проектам и дайте ей консолидироваться — плюс практический вопрос о том, что хранить, а что оставить в покое.


Настоящая проблема — это поиск, а не хранение

Хранить вещи легко. Это умеет любое key-value хранилище. Сложная часть долгосрочной памяти агента — та же, что и у человеческой памяти: вернуть нужное в нужный момент из кучи, которая никогда не перестаёт расти.

Octobrain ищет по смыслу, а не по ключевым словам. Под капотом он встраивает каждое воспоминание в вектор с помощью локальной модели (по умолчанию fastembed:nomic-ai/nomic-embed-text-v1.5, 768 измерений, работает на CPU, без API-ключа) и хранит их в векторной базе данных LanceDB на диске. Запрос remember встраивается так же, и возвращаются ближайшие векторы. Чтобы чисто семантический поиск не упускал точные строки, поиск гибридный: полнотекстовый BM25 и векторное сходство объединяются через Reciprocal Rank Fusion. Поверх этого — опциональное переранжирование cross-encoder'ом.

Эта механика хороша. На бенчмарке SciFact из BEIR гибридный поиск Octobrain даёт 0,742 nDCG@10 против 0,665 у классического BM25 и 0,713 у чистого эмбеддинга — слияние добавляет около +2 nDCG@10 поверх самого эмбеддера. Качество поиска никогда не было узким местом.

Узким местом было то, чем мы его кормили. Пять перекрывающихся заметок об одной и той же ошибке не поднимают ни одну из них выше в ранжировании — они делят сигнал. Хороший поиск по шумному корпусу всё равно даёт шумный ответ. Так что решение — не лучший эмбеддер. Это курирование корпуса, причём автоматическое, потому что ни один человек не будет вручную подрезать память агента.


Идея первая: разграничьте память по проектам

Первый источник шума — перекрёстное загрязнение. Без изоляции воспоминания всех проектов лежат в одном пуле, и запрос про «поток аутентификации» в вашем веб-приложении вытягивает заметки об аутентификации из трёх несвязанных сервисов.

Octobrain изолирует воспоминания по области видимости (scope). Область — это строка, и по умолчанию она выводится из вашего git-remote. Откройте репозиторий этого самого блога — и областью будет github.com/muvon/muvon-web. Вывод именно такой, как вы и предположили бы:

git remote get-url origin  →  host/org/repo

Так что [email protected]:muvon/octobrain.git превращается в github.com/muvon/octobrain. Нет git-remote? Тогда применяется область на основе пути — local/<parent>/<dir>, чтобы даже черновой каталог получил изоляцию. И есть одна зарезервированная область — пустая строка "" — для глобальных воспоминаний, которые должны быть видны везде.

Это разделение глобальное/проектное — вся ментальная модель, и оно соответствует реальному различию:

Область Хранить здесь Примеры
Проект (github.com/org/repo) Факты, истинные внутри этого репозитория «Тесты запускаются через pnpm -r test:unit», «staging-API требует заголовок X-Acme-Token», «здесь мы выбрали token-bucket для ограничения частоты»
Глобальная ("") Факты, истинные о вас, везде «Предпочитает табы», «всегда хочет conventional-commit сообщения», «использует fish, а не bash»

Выигрыш: когда ваш агент работает в репозитории блога, вызов remember ранжирует только по воспоминаниям этого репозитория плюс ваши глобальные предпочтения. Заметки об аутентификации из того другого сервиса просто отсутствуют в наборе кандидатов. Чище кандидаты — точнее поиск, ещё до того, как запустится хоть какая-то консолидация.

Как задаётся область

Если вы управляете Octobrain через MCP-клиент, который выполняет рукопожатие сессии, область автоматически блокируется по рабочему каталогу — агенту вообще не нужно о ней думать, а параметр scope полностью убирается из схемы инструмента, чтобы он не мог ошибиться. Сервер даже сообщает доступные области в своих MCP-инструкциях:

Available project scopes (pass as the 'scope' parameter):
  github.com/muvon/muvon-web

Когда сессия не заблокирована, инструменты выставляют scope напрямую, а memorize принимает флаг global: true, чтобы намеренно положить факт в общую область независимо от того, где вы находитесь. Этот флаг появляется только тогда, когда есть заблокированная проектная область, которую можно переопределить; иначе вы просто сами передаёте scope: "". Указание, зашитое в описание инструмента, прямолинейно: используйте global для межпроектных фактов вроде предпочтений и соглашений; не используйте его для проектно-специфичных знаний. Утечка области — это тот самый шум, которого вы пытаетесь избежать.


Поверхность инструментов, которую вы реально вызываете

Octobrain намеренно выставляет небольшую MCP-поверхность. Каждый выставленный инструмент стоит агенту контекста на каждом ходу — он читает схему, прежде чем что-либо сделать, — поэтому релиз 0.7.0 сузил поверхность, а 0.8.0 сохранил её лёгкой. Четыре инструмента, четыре задачи:

memorize    сохранить воспоминание (с опциональными встроенными связями)
remember    семантический поиск (возвращает соседей графа на 1 шаг)
forget      удалить (требует confirm=true)
knowledge   индексировать/искать/читать/сопоставлять внешние документы и URL

memorize

Глагол сохранения. Обязательные: title (5–200 символов) и content (10–10000 символов). Всё остальное опционально, но стоит понимать:

  • memory_type — категория вроде architecture, bug_fix, decision, user_preference, goal. Используется для фильтрации при поиске.
  • importance — 0.0–1.0. В описании инструмента даны реальные ориентиры: факты, заявленные пользователем, 0.8–1.0; решения 0.7–0.9; исправления ошибок 0.6–0.8; выводы ИИ 0.3–0.6.
  • source — уровень доверия: либо user_confirmed (пользователь сказал это явно), либо agent_inferred (агент сделал вывод). Это не косметика: подтверждённые факты ранжируются выше при поиске, а результаты возвращаются с метками [CONFIRMED] / [INFERRED], чтобы агент знал, насколько доверять каждому.
  • tags, related_files — для фильтрации и для привязки воспоминания к коду, который оно описывает.
  • related_to — самый интересный. Массив типизированных связей с существующими воспоминаниями, создаваемых в том же вызове. Так вы строите граф без второго обращения.

Конкретное сохранение в том виде, как его выдал бы агент:

{
	"title": "Rate limiting uses a token bucket",
	"content": "We rate-limit the public API with a token bucket (100 req/min/key), returning 429 + Retry-After on exhaustion. Chosen over fixed-window to avoid burst-at-boundary.",
	"memory_type": "decision",
	"importance": 0.85,
	"source": "user_confirmed",
	"tags": ["api", "rate-limiting"],
	"related_files": ["src/middleware/ratelimit.rs"]
}

remember

Глагол поиска, и его стоит вызывать первым — перед запоминанием (чтобы не сохранить дубликат) и в начале задачи (чтобы загрузить контекст). Он принимает query, который представляет собой либо одну строку, либо, предпочтительно, массив из 2–5 связанных терминов для более широкого семантического охвата. Он возвращает лучшие совпадения плюс их непосредственных соседей по графу автоматически, так что попадание в одно воспоминание вытаскивает связанные с ним.

Он также принимает фильтры, которые стали действующими в недавних релизах: created_after / created_before (ISO-8601) для временных вопросов вроде «что мы решили на прошлой неделе», min_relevance для порога качества и memory_types / tags для сужения. Расширение запроса, которое спасает расплывчатые формулировки, — псевдорелевантная обратная связь HyDE-lite от Octobrain — работает автоматически и добавляет заявленные +10–30% к полноте поиска на запросах длинного хвоста без LLM и без настройки.

forget

Постоянное удаление. Требует confirm: true — случайного забывания не бывает. Удаляйте по memory_id для одного или по query + фильтры для массовой зачистки. Описание инструмента несёт правило, которое стоит усвоить: не забывайте воспоминания только потому, что они старые — этим занимается затухание (об этом ниже). Используйте forget только тогда, когда что-то по-настоящему неверно или было заменено.

knowledge

Другая половина памяти — внешние документы. memorize — для дистиллированных инсайтов, которые производит агент; knowledge — для исходного материала, который агент читает. Пять команд, диспетчеризуемых через поле command:

# Индексирует и ищет в одном источнике (индексируется на лету)
knowledge command=search query="how to handle async tasks" source=https://docs.rs/tokio/

# Читает полный текст файла или URL (крайнее средство, когда поиск скуден)
knowledge command=read source=/path/to/design-doc.md

# Делает grep по индексированному содержимому регуляркой
knowledge command=match pattern="spawn_blocking|block_in_place"

# Сохраняет и удаляет сырой текст под ключом (в рамках сессии)
knowledge command=store key=meeting-notes content="..."
knowledge command=delete key=meeting-notes

Одно жёсткое правило, которое схема предписывает, а сервер повторяет в своих инструкциях: source — это всегда один файл или URL, никогда не каталог. Передадите каталог — он будет отклонён. Поддерживаемые типы: .html, .txt, .md, .pdf, .docx, а также URL http/https.


Идея вторая: дайте памяти консолидироваться самой

Разграничение области режет межпроектный шум. Оно ничего не делает с проблемой пяти-почти-дубликатов-в-одном-проекте. Для этого есть консолидация, и ключевым проектным решением в 0.7.0 было сделать её автономной — потому что единственное, на что нельзя полагаться, это что агент (или человек) вспомнит навести порядок.

Консолидация во сне

При запуске, если со времени последнего прохода прошёл день, Octobrain выполняет проход «сна»: находит кластеры схожих, недавних, всё ещё в состоянии Working воспоминаний и сворачивает каждый кластер в один консолидированный инсайт. Оригиналы не удаляются — они переходят в состояние Consolidated с приглушённой важностью, связанные под новым родителем как происхождение. Пять размытых заметок об ограничении частоты становятся одним чистым воспоминанием с более высокой уверенностью, а след назад к сырым заметкам сохраняется для аудита.

Он управляется файлом-маркером, поэтому он ленивый — не cron и не фоновый планировщик, — и медленный или неудачный проход никогда ничего не блокирует. Значения по умолчанию консервативны:

[memory]
sleep_consolidation_enabled          = true   # opt-out, not opt-in
sleep_consolidation_interval_hours   = 24     # at most once a day
sleep_consolidation_threshold        = 0.85   # cosine similarity to cluster
sleep_consolidation_min_cluster_size = 3      # need 3+ to bother
sleep_consolidation_max_age_days     = 7      # only recent Working memories

Можно принудительно запустить проход из CLI для тестирования — octobrain memory sleep-consolidate --threshold 0.85 --min-size 3 — но смысл в том, что вам никогда не придётся.

Консолидация, привязанная к цели

Консолидация во сне группирует по сходству. Более острая версия группирует по намерению. В Octobrain есть тип воспоминания goal и настоящий жизненный цикл: Working → Consolidated → Archived. Рабочий процесс, целиком выраженный через связи related_to инструмента memorize, таков:

  1. memorize воспоминание типа goal, фиксирующее то, что вы пытаетесь сделать.
  2. Для каждой находки по пути — memorize с related_to: [{ target_id: <goal_id>, relationship_type: "achieves" }].
  3. Когда задача закрывается — memorize заметку с извлечённым уроком и relationship_type: "closes".

Эта связь closes запускает консолидацию автоматически, без отдельного вызова инструмента. Закрывающая заметка становится консолидированным родителем (её важность поднимается до max(sources) * 1.1), а каждый источник achieves переходит в состояние Consolidated с приглушённой важностью, оставаясь доступным для запроса в целях аудита. Пять разрозненных находок схлопываются в одно воспоминание, которое что-то значит, привязанное к намерению, которое его породило.

Это также объясняет, почему отдельного инструмента consolidate больше нет в MCP-поверхности: закрытие цели теперь — это просто memorize со связью closes, а консолидация во сне работает сама. Меньше инструментов — та же мощь. (CLI-команда octobrain memory consolidate <goal-id> всё ещё на месте для ручного и административного использования.)

Затухание по полупериоду: забывание как сигнал ранжирования

Третий механизм работает непрерывно. Важность каждого воспоминания затухает по графику полупериода Эббингауза — и каждый раз, когда вы обращаетесь к воспоминанию, оно подкрепляется. Часто вспоминаемые воспоминания остаются сильными; те, к которым никто не прикасается, мягко угасают в ранжировании.

[memory]
decay_enabled            = true
decay_half_life_days     = 90    # higher = memories stay relevant longer
access_boost_factor      = 1.2   # reinforcement multiplier per access
min_importance_threshold = 0.05  # floor — decay never deletes

Две защитные меры делают это безопасным. Есть пол важности: устаревшее воспоминание опускается назад, но никогда не исчезает, потому что затухание — это сигнал переранжирования, а не удаление. И счётчики обращений сохраняются между перезапусками, так что ощущение памяти о том, «что горячо», не сбрасывается каждую сессию. Чистый эффект: воспоминания, которыми вы реально пользуетесь, всплывают, а те, что вы сохранили однажды и больше не понадобились, тонут — без всякого курирования с вашей стороны. Именно поэтому инструмент forget говорит агенту не удалять вещи за их возраст: затухание уже справляется с этим, изящно, не теряя историю.


Что хранить, а что оставить в покое

Механизм доводит вас только до определённой точки. Дисциплина, которая починила наш поиск, заключалась в решении о том, что заслуживает воспоминания. Инстинкт агента — хранить всё; правильная политика ближе к обратному.

Храните:

  • Решения и их обоснование. «Мы выбрали token-bucket вместо фиксированного окна из-за burst-at-boundary». Почему — это та часть, которую дорого реконструировать.
  • Трудно добытые факты об этом репозитории. Неочевидная команда тестов, заголовок staging, миграция, которая должна выполниться первой. То, что агент иначе переоткрывает (или переугадывает) каждую сессию.
  • Предпочтения пользователя — в глобальной области. Сказано один раз, истинно везде.
  • Исправления ошибок с корневой причиной. Не «починил webhook», а «повторам webhook нужны были ключи идемпотентности; без них повторы списывали оплату дважды».

Не храните:

  • Преходящее состояние. Текущую ветку, что в рабочем дереве, какой тест падает сегодня. Это устаревает до следующей сессии и навсегда после неё становится чистым шумом.
  • Всё, что тривиально перевыводится. Если однострочная команда shell это воссоздаёт, то именно эта команда — воспоминание, а не её вывод.
  • Непроверенные догадки как факты. Если агент обязан записать предположение, для этого есть source: agent_inferred и низкая важность. Помечайте неопределённость, чтобы поиск ставил её ниже подтверждённых знаний.
  • Один и тот же инсайт пять раз. Вызывайте remember перед memorize. Если возвращается почти-совпадение, обновите или замените его вместо того, чтобы наслаивать ещё один почти-дубликат.

У последнего пункта есть поддержка инструмента. Когда новый факт заменяет старый — изменилось значение, отменено решение — memorize новый факт с related_to: [{ target_id: <old_id>, relationship_type: "supersedes" }]. Тогда поиск ставит текущий факт выше устаревшего, который остаётся доступным для истории. Вы исправляете запись, не теряя её.


Настройка

Установите и направьте на него MCP-клиент.

# crates.io
cargo install octobrain

# или на macOS через Homebrew
brew install muvon/tap/octobrain

Сначала попробуйте полный цикл из CLI:

octobrain memory memorize --title "API Design Pattern" \
  --content "Use REST for CRUD, GraphQL for complex queries" \
  --memory-type architecture --tags "api,design"

octobrain memory remember "how should I design APIs"

Затем подключите к MCP-клиенту. Для Claude Desktop добавьте в claude_desktop_config.json:

{
	"mcpServers": {
		"octobrain": {
			"command": "/path/to/octobrain",
			"args": ["mcp"]
		}
	}
}

Запустите octobrain mcp для stdio или octobrain mcp --bind 0.0.0.0:12345 для HTTP. Хранилище находится в ~/.local/share/octobrain/ (macOS/Linux) или %APPDATA%\octobrain\ (Windows), а проектные воспоминания изолированы по области, как описано выше. У всего есть разумные значения по умолчанию: консолидация во сне, затухание, гибридный поиск и HyDE включены из коробки. Полный набор настроек задокументирован в config-templates/default.toml в репозитории.

Единственное решение по конфигурации, которое стоит принять осознанно, — это decay_half_life_days. Значение по умолчанию в 90 дней подходит для равномерно развивающейся кодовой базы. Поднимите его для стабильного проекта, где решения остаются в силе долго; понизьте для быстро меняющегося прототипа, где истина прошлого месяца — ложь этого.


Часто задаваемые вопросы

Теряет ли забывание информацию?

Нет. Затухание — это сигнал ранжирования с полом: малоиспользуемые воспоминания тонут, но никогда не удаляются. Консолидация переводит источники в состояние Consolidated, приглушённые, но всё ещё доступные для запроса, связанные под их родителем как происхождение. Единственное, что по-настоящему удаляет, — это явный forget с confirm: true. Вы всегда можете проследить назад.

Чем memorize отличается от knowledge?

memorize хранит инсайты, которые агент производит — решения, исправления, выводы. knowledge индексирует документы, которые агент читает — доки, файлы дизайна, URL — чтобы он мог их искать и цитировать. Дистиллированная мудрость против исходного материала. Разные жизненные циклы, разные инструменты.

Нужно ли управлять областями вручную?

Обычно нет. С MCP-клиентом, выполняющим рукопожатие сессии, область блокируется по git-remote вашего рабочего каталога, а параметр scope убирается из схемы. Вы трогаете область явно только для того, чтобы сохранить глобальное предпочтение (global: true или scope: "").

Почему всего четыре MCP-инструмента? Другие серверы памяти выставляют дюжину.

Потому что каждый выставленный инструмент — это схема, которую модель перечитывает на каждом ходу, а точность выбора инструмента падает по мере роста каталога. Консолидация стала автономной, а relate/consolidate свернулись в связи отношений у memorize, так что задачи, которым раньше нужен был собственный инструмент, теперь обходятся без него. В CLI по-прежнему есть полный набор для административной работы.

Нужен ли API-ключ или GPU?

Нет. Модель эмбеддинга по умолчанию работает локально на CPU через fastembed — без ключа, без сети. Облачные эмбеддеры (Voyage, OpenAI, Google, Jina) поддерживаются, если вам нужно более высокое качество, но они опциональны.


То, что починило наш поиск, было не лучшим алгоритмом поиска — поиск Octobrain уже обыгрывал BM25 на бенчмарках, пока мы тонули. Это было признание того, что память должна забывать, чтобы оставаться полезной. Разграничьте её так, чтобы нужные вещи вообще были кандидатами; дайте ей консолидироваться, чтобы дубликаты схлопывались; дайте ей затухать, чтобы мёртвый груз тонул. Тогда агент перестаёт рыться в ящике хлама и начинает отвечать из памяти.

Храните решения и трудно добытые факты. Пропускайте преходящий шум. Вызывайте remember перед memorize. Остальное пусть работает само.

— Don


Octobrain — открытый исходный код под Apache-2.0. Механика консолидации и затухания, описанная здесь, появилась в 0.7.0; система областей видимости и более лёгкая поверхность инструментов перешли в 0.8.0. Если какая-то деталь настройки вас споткнула или вы хотите изменить значение по умолчанию для области или затухания, откройте issue — мы их читаем.