Эта статья — про долговременную память AI-агентов и про то, как её измерить. Я сделал MarathonMemBench — платформу, где LLM-персона часами беседует с тестируемым агентом: рассказывает факты о своей жизни, меняет решения, путается и поправляется, а под конец — когда начало разговора давно вытеснено из контекста — устраивает экзамен на всё сказанное. Подключить можно любого агента, у которого есть чат, — больше от него ничего не требуется. Дальше будут: обзор существующих бенчмарков памяти и почему каждый из них требует дорабатывать агента под себя, устройство платформы, результаты open-source агентов — неожиданно сильные — и вскрытие их памяти после прогонов. Но начать придётся с корня проблемы — с конечности контекста языковых моделей.

У этой статьи есть предыстория. В октябре я писал о гипотезе: достаточно умная языковая модель сможет решить проблему долговременной памяти сама — обычными вызовами инструментов. Тогда это было не более чем предположение. Но ровно через месяц вышла Opus 4.5, а ещё через месяц Andrej Karpathy написал, что агентные способности моделей перешли порог в агентском кодинге и он, программист с двадцатилетним стажем, «никогда не чувствовал себя настолько отставшим». Эта статья — про другую способность, о которой говорят намного реже: самостоятельно вести долговременную память. И, как будет видно по цифрам, здесь всё оказалось неожиданно хорошо ровно в той же степени.
Конечность контекста — фундаментальное ограничение языковых моделей
Если задуматься, вся история триумфа языковых моделей — это эксплуатация одного неожиданного открытия: архитектура трансформера для предсказания следующего токена рождает интеллект, выраженный в словах. Всё, что мы видим последние годы, весь этот триллион инвестиций, — по сути развитие одной этой идеи. Модели становятся больше, обучающих данных — больше; придумали SFT — дообучение на примерах образцовых диалогов, потом RLHF — обучение с подкреплением на человеческих оценках ответов, совсем недавно — reasoning, когда модель учат сначала «подумать» и только потом отвечать. Но ничего из этого не выходит за рамки обучения предсказанию следующего токена на трансформере.
И у этого подхода есть одно фундаментальное ограничение — конечность контекста. Да, за последние годы контекст вырос очень сильно, но пусть это сто тысяч токенов, пусть миллион — нам-то нужно бесконечное число: конечность зашита в саму архитектуру. Была бы архитектура с бесконечным контекстом — ничего из дальнейшего не потребовалось бы. Но такой архитектуры нет, и, судя по тому, как идёт эволюция, не будет — по крайней мере, рассчитывать на это мы не можем.
«А почему конечность контекста — это проблема?» — скажет кто-то. Я ведь наоборот: переключаюсь на новую сессию, начинаю новую задачу и спокойно её делаю. И мне даже хорошо, что новая сессия ничего не помнит про старую.
И действительно, этот подход несомненно удобен для целого класса задач. Взять хоть кодинг: начал задачу, агент изучил код под текущую задачу, сделал, коммит — забыл, поехал дальше с чистой сессии.
Но есть и другие задачи, где хотелось бы, чтобы модель ничего не забывала. Представим себе персонального помощника - работать с ним отдельными сессиями уже неудобно. Хотелось бы, чтобы он помнил всё, о чём шла речь: чтобы можно было сказать «а помнишь, три недели назад мы… — так вот, давай…». То есть ждать от модели поведения человека с блокнотом в руках, который по ходу записывает и в любой момент может к записанному вернуться.
LLM + харнес = LLM с бесконечным контекстом
Если архитектура сегодня не способна решить эту проблему — как же к ней подступиться? А решить её на самом деле можно. В последнее время огромную популярность получили агенты на базе LLM. Упрощённо говоря, агент — это LLM, которая генерирует не просто текст, а инструкции, которые должны быть выполнены «снаружи».
Исполняет эти инструкции харнес. Мне недавно попалось удачное определение: харнес — это управляющая инженерная среда вокруг модели, которая превращает вероятностный искусственный интеллект в повторяемого программного исполнителя. Практически это набор инструментов плюс операции вокруг цикла модели, которые принято называть хуками. Отсюда возникает идея: почему бы харнесу не взять на себя и проблему конечности контекста? Тем или иным способом обрезать контекст (компактизация, суммаризация), параллельно экстрактить из него информацию в долгосрочную память — и уметь эту информацию из памяти доставать.
И это уже сейчас работает. Известные open-source-агенты — например, OpenClaw или Hermes — прямо из коробки умеют складывать информацию из общения с пользователем в папку с Markdown-файлами. И пользоваться этой информацией при необходимости. Также эти агенты умеют компактизировать длинную беседу, так что теоретически она может длиться бесконечно (правда, у OpenClaw на момент написания статьи компактизация работала с ошибками).
Но остаётся вопрос — насколько хорошо это работает на самом деле. Насколько надёжно агент достанет факт, который вы сообщили ему два миллиона токенов назад? И хорошо ли он вспомнит его, если за это время вы этот факт пару раз меняли? Попытке оценить подобные способности агентов посвящена эта статья.
Существующие бенчмарки памяти — и почему ни один мне не подошёл
Когда я решил измерить, насколько хорошо работает память у моего собственного агента, я пошёл искать подходящий бенчмарк. Нашёл много, но использовать не смог ни один — все они требовали, чтобы мой агент соблюдал определённые соглашения, специфические для каждого бенчмарка. И соглашения эти были далеко за гранью поддержки режима чата.
Off-policy и on-policy бенчмарки
По способу «загрузки» исходной информации — фактов, которые потом будут проверяться, — бенчмарки для тестирования памяти грубо делятся на две большие группы.
Off-policy — бенчмарк состоит из заранее записанных бесед между юзером и ассистентом. В этой переписке юзер сообщает какие-то факты, а ассистент что-то отвечает. В каждой беседе также есть заготовленные вопросы и референсные ответы. Отсюда сразу две проблемы:
Подобную переписку нельзя скормить агенту через обычный чат-интерфейс — необходим специальный способ загрузки готовой беседы в агента.
Ответы ассистента в этих беседах не являются ответами нашего агента и будут для него, скажем так, чужеродными.
У подхода есть один жирный плюс: можно быстро загрузить в агента исходные данные и сразу начать проверку — это радикально ускоряет и удешевляет тестирование.
On-policy — вместо заранее предопределённых бесед бенчмарк с LLM под капотом ведёт реальную беседу с тестируемым агентом. Соответственно, не нужно специального способа загрузки беседы, и все ответы ассистента — настоящие ответы агента. Теоретически этот подход позволяет тестировать агента исключительно через чат-интерфейс; на практике ни один существующий бенчмарк этого не делает (см. ниже).
Из минусов — заметно большая длительность и стоимость прогонов.
Группа первая: off-policy
Всем нужен способ вложить в агента чужую историю. Различаются они тем, куда именно и как её вкладывают.
Старейший и самый цитируемый — LoCoMo: заранее сгенерированные очень длинные беседы (в среднем ~300 реплик, до 35 сессий) плюс заготовленные вопросы с референсными ответами. Чтобы его запустить, агент должен уметь проглотить чужую стенограмму целиком и предоставить отдельный вход, через который потом задаются вопросы. Записанные беседы там вообще не user-assistant, а диалоги двух синтетических персонажей между собой — то есть агенту предлагается «вспоминать» разговор, в котором он не участвовал даже формально.
LongMemEval — нынешний канон жанра: 500 вопросов, встроенных в длинные user-assistant истории (варианты примерно на 115K и 1.5M токенов). Схема та же: стенограмма грузится одним куском, вопросы задаются через отдельный интерфейс. Ни того, ни другого у продукта с обычным чатом нет.
Остальные — вариации на ту же тему: MemoryAgentBench подаёт чужую историю инкрементально, чанками по 512–4096 токенов («inject once, query multiple times»), но специальный вход для загрузки всё равно нужен; BEAM — то же, только стенограммы до 10 миллионов токенов; SubtleMemory «проигрывает» готовые истории через механизм формирования памяти агента, нарезая их под гранулярность конкретной системы; а ClawArena и вовсе пишет историю прямо в session store продукта — .jsonl-файлы сессий, метаданные, агентские файлы в workspace.
Для обычного агента, у которого есть только чат, эта группа отпадает целиком и сразу.
Группа вторая: on-policy
Здесь чужую переписку не грузят — с агентом разговаривают. Казалось бы, то, что нужно. Но каждый бенчмарк требует чего-то своего на этапе тестирования.
Ближе всех подходит AMemGym — он единственный прямо декларирует on-policy-оценку для чат-ассистентов: их LLM-симулятор пользователя ведёт с агентом настоящую беседу. Но проблема — в проверке. Проверочные вопросы (в базовой конфигурации — 200 вопросов на 11 контрольных точках) задаются не в чате, а в обход него: состояние агента нужно уметь «заморозить» и отвечать по замороженной копии — так, чтобы ни вопросы, ни ответы не попали в память и не испортили дальнейшую беседу. Я пробовал его запускать: без снапшота состояния агента и встраивания в их Python-обвязку он не работает.
MEMPROBE идёт с другой стороны: беседа честная, зато проверяется не поведение агента, а содержимое его хранилища. Для этого агент обязан предоставить программный доступ к памяти: get_all_memories() для полного дампа и search(query, k) для чтения top-k записей. По сути, это аудит внутренностей, а не тест агента.
MemoryArena даёт агенту прожить свою историю самому — но исключительно внутри их окружений (веб-навигация, планирование, поиск, формальные задачи). Своё окружение принести нельзя, а значит, и своего агента «как есть» — тоже.
Остальные — в том же духе: STATE-Bench устроен по принципу «bring your own memory» — агент, инструменты и симулятор пользователя их, от вас только memory-модуль, подключаемый через их retrieval hook; MemGym требует физически отделить память от reasoning-модели и подключить обе через их общий интерфейс; а в ClawMark агент работает внутри их пяти stateful-сервисов (файлы, почта, календарь, база знаний, таблицы), и результат проверяют 1537 детерминированных чекеров по итоговому состоянию этих сервисов.
Что с ними со всеми не так
Каждый бенчмарк требует переделывать агента под себя. Специальный вход для загрузки чужой истории, снапшот состояния, программный доступ к памяти, работа только в их окружении — всякий раз это свои соглашения, которым готовый агент не соответствует. Протестировать агента «как есть» нельзя: сначала его нужно дописывать под конкретный бенчмарк. Это само по себе плохая идея: под каждый бенчмарк приходится встраивать в агента функциональность, которая нужна только этому бенчмарку, — а потом её ещё и поддерживать.
А у проверки памяти через внутренности есть и методические проблемы:
Память должна быть организована определённым образом. Чтобы проверить качество созданной памяти, бенчмарк должен поддерживать тот формат, в котором её хранит агент.
Наличие факта в памяти не означает, что агент его найдёт. И наоборот: если факта в памяти нет, агент всё ещё может достать его поиском по истории переписки — а в отдельных случаях, когда факт не стоил сохранения, это, на мой взгляд, вполне корректное поведение.
Отсюда единственный адекватный протокол: факты скармливать через чат и проверять через чат же — за границей компактизации или в новой сессии. Только такой бенчмарк можно натравить на неподготовленного агента и полноценно протестировать. Такого не было. Поэтому я его сделал.
Мой MarathonMemBench
Сразу нужно сказать: MarathonMemBench — это не просто бенчмарк, а платформа для создания и проигрывания бенчмарк-сценариев. Движок и тестовые прогоны конфигурируются файлом config.jsonc; прогоны могут быть длинными, поэтому их можно запускать несколько параллельно. А бенчмарк — это папка с файлом benchmark.json и несколькими markdown файлами.
Новый сценарий легко создать при помощи кодинг-агента: правила написания бенчмарков изложены в CLAUDE.md проекта в разделе Benchmarks — достаточно описать, как вы хотите протестировать агента, и кодинг-агент соберёт бенчмарк под вашу задачу.
Агент персоны
В основе платформы — отдельный LLM-агент, симулирующий пользователя-персону: живого человека со своим бэкграундом и текущими делами. Персона ведёт разговор с агентом: рассказывает, обсуждает, переспрашивает, меняет решения, оценивает ответы. Личность и сюжет целиком заданы данными бенчмарка. Проще всего понять, как работает агент персоны, — прочитать его системный промпт src/system.md и набор инструментов в src/tools.
Сам системный промпт универсален — персоны в нём нет. Подогнать его под конкретный сценарий, не меняя сам промпт, позволяет файл бенчмарка instructions.md: его содержимое инжектируется в конец системного промпта, в раздел Your scenario, и содержит общую информацию о персоне. Например, что весь разговор укладывается в один свободный день, а пишет персона по-деловому, короткими сообщениями и любит точные цифры.
Типовой сценарий состоит из двух частей. Сначала персона выгружает на агента множество фактов, меняет темы, меняет значение этих фактов, а потом через некоторое время начинает задавать вопросы по тому, что говорила ранее.
Сценарий = последовательность фаз
Сценарий, которому следует агент персоны, состоит из последовательности фаз. Фаза — это просто текстовое описание того, о чём персона должна рассказать агенту. Например:
на работе тебя повысили до senior-дизайнера, зарплата выросла до 4000 евро.сегодня отдых от бега. К тебе приехала погостить на неделю младшая сестра Аня.
Фазы объединяются в главы. Глава — это один markdown-файл, где каждый ##-раздел — одна фаза. Главы перечисляются в поле chapters в benchmark.json:
{ "chapters": [ "chapter1.md", "chapter2.md", ... ] ... }
Первую фазу агент персоны получает в начале прогона бенчмарка, а в дальнейшем сам управляет сменой фаз. Решив, что фаза закончена, он вызывает инструмент next_phase — и получает сценарий следующей фазы. Такой подход позволяет писать сценарии неограниченной длины: агент персоны всегда сконцентрирован только на выполнении текущей фазы.
Файлы с фактами
Но просто перечислять факты в фазах оказалось неэффективно — слишком много текста. А мы хотим вывалить на агента огромное число фактов на разные темы. Для этого я придумал специальный сценарий для агента персоны — рассказать всё из отдельного файла с фактами: «Скажи, что теперь расскажешь про семью — расскажи факты из family», где family.md:
# Семья ## 1. Жена Лена 1.1 Лене 31 год, UX-дизайнер, работает удалённо 1.2 день рождения Лены — 14 марта ...
Такой формат записи фактов намного компактнее, чем писать в фазе «а теперь расскажи о …». Агент персоны загружает файл с фактами инструментом facts_read и начинает в живом диалоге рассказывать факты из списка, выбирая адекватную моменту последовательность изложения и отвечая на встречные вопросы агента.
Чтобы агент персоны гарантированно не забыл рассказать какой-то факт из списка, у него есть ещё два инструмента: facts_mark_told — вызывается с id факта сразу после того, как факт рассказан, — и facts_get_untold — возвращает список ещё не рассказанных фактов.
JS-блок в фазе
В начале фазы также можно добавить специальный блок JavaScript-кода, который выполняет некоторые действия:
vars.require_all_facts = "family";
Например, конструкция выше заставит next_phase возвращать ошибку, пока агент персоны не расскажет все факты из файла family.md.
Профиль персоны
В каждом бенчмарке предусмотрен специальный файл с фактами profile.md — главная информация о персоне. Пример содержимого:
# Сергей ## 1. О себе 1.1 Сергей Волков, 34 года 1.2 Бэкенд-разработчик, работаю удалённо на европейскую компанию 1.3 Год назад переехал в Батуми из Питера 1.4 Живём с женой Леной, есть кот Босс ## 2. Квартира и ремонт 2.1 Купил двухкомнатную квартиру без отделки, 65 м², 9-й этаж, район Новый бульвар ...
Особенность этого файла заключается в том, что он всё время присутствует в контексте агента персоны (как CLAUDE.md в Claude Code). Это позволяет персоне не терять свою идентичность и в моменты, когда она бурно рассказывает про ремонт квартиры, не забывать о том, кто она такая.
Компактизация контекста агента персоны
Агент персоны спроектирован так, чтобы диалог мог идти бесконечно. Компактизация контекста конфигурируется в файле config.jsonc:
"compaction": { "max_tokens": 30000, "trim_to_tokens": 10000 },
После того как размер контекста отрастает до max_tokens, история просто обрезается — сохраняются последние сообщения общим объёмом trim_to_tokens токенов. Если быть точнее, обрезание всегда происходит по границе фазы. Также в контекст добавляется сообщение с содержимым profile.md и списком всех файлов с фактами, которые загружал агент персоны.
Актуализация фактов
Факты могут меняться — например, персона поменяла работу или получила повышение. Чтобы после компактизации профиль персоны и другие файлы с фактами оставались актуальными, агент персоны должен их обновлять — для этого у него есть два инструмента: facts_edit и facts_add. Удалить факт тоже можно, записав в него —, — а его id в дальнейшем можно переиспользовать. Пример сценария обновления факта: 10 км за 52:30, новый личный рекорд! Обнови факт в профиле.
Тестирование агента
Чтобы добавить в сценарий проверку, нужно добавить в фазу строчку вида: ❓ 57.1 Сколько всего километров я пробежала с 1 июня? Правильный ответ: 279 км. Встретив такую строку, агент персоны задаст вопрос агенту и оценит его ответ при помощи специального инструмента log_evaluation. Результат оценивается по шкале 0–10. При этом если ожидаемый ответ — одно число, возможны только две оценки: 0 и 10. А если персона просит агента сообщить несколько фактов, оценка будет пропорциональна числу верных ответов.
Агенту иногда свойственно отвечать, что он чего-то не знает, хотя эта информация есть у него в постоянной памяти. Чтобы адекватно оценить такой кейс, персона следует простой процедуре: если агент отвечает что-то вроде «ты мне не говорил», она просит его поискать — и если со второй попытки ответ правильный, результат засчитывается с коэффициентом 0.5.
Компактизация/переключение сессии на стороне тестируемого агента
Перед тестированием нужно, чтобы факты, сообщённые агенту, пропали из его контекста. Для этого существует два основных способа:
Настроить компактизацию сессии у агента так, чтобы она происходила незадолго до начала тестирования. Этот подход требует нескольких итераций: параметры компактизации нужно подобрать так, чтобы разрез примерно попадал на нужный момент сценария.
Также большинство агентов поддерживают переключение на новую сессию — контекст очищается, но у агента по-прежнему есть доступ к памяти, созданной в предыдущей сессии. Получается, что в одной сессии мы загружаем факты, а во второй тестируем.
Чтобы переключить сессию на тестируемом агенте, нужно добавить в фазу вот такой блок кода:
await bench.newSession();
Состояние и результаты прогона
Каждый прогон получает свою папку results/<id>/, где id — имя прогона из config.jsonc. Главное в ней — файл state.json: полное текущее состояние прогона — на каком месте сценария находится персона, история её сообщений, все выставленные оценки и живые копии всех файлов с фактами (правки меняют именно их — исходные markdown-файлы бенчмарка остаются нетронутыми). Состояние сохраняется при каждом изменении, поэтому прогон можно в любой момент остановить по Ctrl-C и запустить снова — он продолжится с того же места. А чтобы начать прогон с нуля, достаточно удалить его папку.
Итоги собираются в общий журнал results/report.md: по завершении прогона — даже упавшего — туда дописывается запись: средний балл, распределение оценок, аудит покрытия (все ли запланированные проверки получили оценку), время прогона, наблюдения о компактизациях агента. А самое полезное — каждый неидеальный ответ приводится целиком: вопрос, ожидаемый ответ, ответ агента и комментарий оценщика. Вот реальная запись одного из прогонов OpenClaw:
## 2026-07-26 16:10 — openclaw-1: max @ openclaw
