Недавно мы рассказали, как держим долговременную память агента на обычных markdown-файлах, без вектор-базы и эмбеддингов. За кадром той статьи осталось главное: показали, к чему пришли, а вот от чего ушли и почему — нет. И ещё одно, о чём стоит сказать прямо: у Claude Code есть своя встроенная память почти того же вида. Мы про неё знаем — и сознательно держим свою, в проекте. Дальше объясним, почему.
Если совсем коротко, вот к чему сводится разница:
- у Claude Code уже есть родная память того же устройства — индекс плюс файлы-заметки;
- живёт она в системной папке пользователя, вне репозитория, и по умолчанию перехватывает «запомни»;
- мы держим память в проекте под git — ради сохранности, переносимости и контроля над тем, как она устроена;
- за это отвечает одна строчка в настройках плюс пара правил в конфиге.
Дальше — по порядку. Если нужен только рецепт «как поднять у себя», прыгайте сразу к последнему разделу.
Что было в прошлой статье (перечитывать не обязательно)
Если в двух словах: память агента у нас — это папка с md-файлами, где каждый факт лежит отдельным файлом. Плюс один всегда загружаемый индекс-оглавление (MEMORY.md), плюс пара правил в конфиге проекта, которые велят модели читать индекс на старте и дописывать новые факты отдельными файлами. Никакой вектор-базы и никакого сервиса поверх модели — только файлы и договорённость, как с ними обращаться. Подробности — в прошлой статье.
Прошлая статья описала пункт назначения, но не дорогу к нему. Разберём две развилки: почему мы ушли от вектор-поиска, и чем наш подход отличается от памяти, которую Claude Code даёт из коробки.
От чего ушли: вектор-база и RAG
Классический способ дать модели «память» — сложить тексты в векторную базу и на каждый запрос доставать похожие куски по смыслу. Для памяти агента и для кода мы от этого отказались, и вот почему.
Вектор ищет по «смыслу», а «смысл» он считает по кускам-чанкам. Для сплошного текста это работает. Но код — жёстко детерминированный синтаксис: там не нужен приблизительный смысл, там нужно точное совпадение, и обычный grep с регулярками находит его точнее, чем вектор по нарезанным фрагментам. Смысл куска кода, вырванного из функции, — вообще странная величина. То же и с нашей памятью: фактов немного, у каждого есть короткий заголовок в индексе, и модель открывает нужный файл по оглавлению — примерно как человек открывает документацию по содержанию, а не листает всю базу разом.
Здесь честно: формального замера «RAG против грепа» мы не гоняли. Выбор идёт не из бенчмарка, а из практики — на наших объёмах оглавление плюс grep стабильно находят нужное, без отдельной инфраструктуры и без риска, что нарезка порвёт смысл. Вектор мы держим в уме для другого класса задач — большие слабоструктурированные тексты, где нет ни оглавления, ни жёсткого синтаксиса. Это просто разные инструменты, и для памяти и кода мы по умолчанию к вектору не идём.
Чем наша память отличается от встроенной
А теперь то, чего в прошлой статье не было вовсе. У Claude Code есть своя встроенная память, и включена она по умолчанию. Устроена почти так же, как наша: индекс MEMORY.md плюс файлы-заметки с теми же заголовками. Мы про неё знаем — и всё равно держим свою. Причина сводится к одному слову: место.
Встроенная память живёт в системном каталоге пользователя — ~/.claude/..., вне репозитория, привязанная к конкретной машине. Наша лежит в проекте под git. И разница тут не косметическая, а по двум осям.
Сохранность и история. Память в репозитории видно в pull-request, она едет вместе с кодом, переживает смену машины, и каждое её изменение хранится в коммитах. Системная память ничего этого не даёт — она в домашней папке одного пользователя, невидима в проекте и не путешествует с ним.
Контроль над устройством. У встроенной памяти всегда-загружаемый индекс — это один файл MEMORY.md с фиксированным потолком: первые ~200 строк или 25 КБ, что раньше наступит. Поднять этот порог настройкой нельзя — в документации такого ключа нет; когда индекс перерастает лимит, лишнее не грузится, а Claude Code просит переписать оглавление короче. У нас индекс под нашим контролем: держим его тонким и, когда фактов становится много, делим на под-индексы — по отдельным проектам, по крупным темам. Так структура оглавления растёт вместе с базой, а не упирается в один фиксированный файл.
Здесь же снимается частое опасение про командную работу: раз индекс общий и лежит в git, не будет ли он вечно конфликтовать при параллельной правке? На практике — не чаще любого другого общего файла. MEMORY.md это обычный текст, он мёржится построчно, а дробление на под-индексы разводит правки по разным файлам, так что пересечений становится ещё меньше.
Именно поэтому мы сознательно держим память в проекте, а встроенную — выключаем. Отключается она одной строкой: autoMemoryEnabled: false в настройках проекта. Это не мелочь: по умолчанию встроенная память перехватывает «запомни» первой и уносит запись в свою системную папку, мимо вашего проекта. С выключенной встроенной памятью «запомни» проваливается в обычную инструкцию, и уже наш протокол кладёт факт туда, где ему место — в репозиторий.
Резонный вопрос: почему выключать, а не просто перенаправить встроенную память в папку проекта? Потому что её путь задаётся настройкой autoMemoryDirectory, которая принимает только абсолютный путь или путь от домашней папки, но не относительный. Указать на ./memory внутри проекта переносимо не выйдет — получится путь, привязанный к конкретной машине, и вся идея git-tracked памяти рассыпается. Поэтому чище выключить встроенную и вести свою.
Для личного проекта на одной машине встроенной памяти вполне достаточно, и это честно стоит признать. Но как только память должна быть общей, переносимой и настраиваемой под рост — ей место в репозитории.
Что вообще стоит помнить
Ещё одно, что стоит прояснить сразу: память не сохраняет всё подряд, что вы пишете. Иначе индекс распухнет и перестанет быть полезным.
Запись случается по двум поводам: по явному «запомни» или когда агент сам узнаёт долгоживущий факт — ваше предпочтение, ограничение проекта, исправление. Обычный ход разговора, разовые указания под конкретную задачу и всё, что и так лежит в коде или в истории git, в память не идёт. Ценность памяти именно в том, что она не журнал, а короткий выверенный список того, к чему стоит возвращаться.
И раз память лежит в git, важно, чего туда не писать: секреты и персональные данные. Однажды закоммиченное остаётся в истории, поэтому паролям, ключам и чужим данным в памяти не место — она для фактов и договорённостей, а чувствительное держим вне репозитория.
Как поднять у себя
Самый быстрый путь — отдать агенту готовый промпт из репозитория, он развернёт всё сам: выключит встроенную память, заведёт папку memory/ со стартовыми фактами по вашему проекту и подключит индекс. Репозиторий с примером, протоколом и промптом — github.com/mngerasimenko/keepware-llm-lab. Там же — подробный разбор и FAQ.
Отдельно стоит разобрать коллизии: что делать, когда на одну тему претендуют сразу два-три файла — общее правило и что-то специфичное под проект. Автоматического разрешения тут нет и не задумано. Индекс — это оглавление, а не уникальный ключ; какой файл открыть, решает сама модель по заголовкам, и на стыке общего и частного она обычно открывает оба и сводит их: частное уточняет общее, «победителя» механизм не выбирает. Чтобы такие пересечения не плодились, при записи держим простое правило — одно понятие, один файл, и перед созданием нового проверяем, нет ли уже подходящего. Это ручная аккуратность, а не автоматика, и это, пожалуй, главная цена подхода против векторной базы, которая ранжирует за вас.
За контекст можно не переживать: на старте грузится только индекс, а сами файлы подтягиваются по запросу. Открыть два-три соседних файла — это маленькое точечное чтение, а не заливка всего в контекст. Когда индекс разрастается, его делят на несколько частей по темам.
Итог
Прошлая статья показала пункт назначения — память на файлах вместо вектор-базы. Эта показала развилки и цену: почему не вектор, чем встроенная память отличается от проектной и что вообще стоит в память писать. Родная память Claude Code проще — ноль настройки. Наша требует одной строки в конфиге, но взамен живёт под git, переносима, общая для команды и настраивается под рост так, как нам нужно. Выбор зависит от того, нужна ли памяти жизнь за пределами одной машины.
И, пожалуй, главное: память — это не только «где искать факт», но и «что вообще стоит запомнить и как это хранить». Первое неплохо решает и встроенный механизм. Второе мы предпочли держать в своих руках.
Статья опубликована в блоге keepware как canonical. Кросспост — vc.ru / Habr — с указанием канонической ссылки.
Первая статья серии — память агента без вектор-БД: как устроено оглавление плюс тела по требованию, где это упрётся и когда возьмём вектор.
