Парадокс reasoning, 300+ промптов на o4-mini, иерархический retrieval и уроки LongMemEval на 100+ миллионах токенов

Это технический отчёт о том, как мы строили Superagent Memory OS — систему агентной памяти, которая на сегодня удерживает граф из десятков тысяч концептов и почти 2.4 миллиона рёбер поверх примерно 106.7 миллиона обработанных токенов. Здесь будут конкретные цифры, провалы, развороты и архитектурные решения, к которым мы пришли через боль, а не через слайды.

Оглавление

  1. Парадокс reasoning: как умная модель ломает extraction

  2. Почему flat RAG ломается на длинной истории

  3. Базовая онтология как старт любой системы памяти

  4. Data contract и staging database: где начинается доказуемость

  5. Concept Memory Loop: create / attach / refine / reject

  6. Топическая кластеризация: UMAP, HDBSCAN, Optuna и weighted links

  7. LongMemEval: четыре фазы отладки агента и петля Exploration vs Execution

  8. Stateful multi-agent inference: Planner, Scout, Synthesizer

  9. Agentic Gardener: обслуживание графа как отдельная инженерная задача

  10. Invalidation, repair и bi-temporal facts

  11. MCP как суверенный интерфейс памяти

  12. Reflective layer: эмоции, скрытые паттерны и психологический слой

  13. Масштаб: 106.7 млн токенов, 632 868 семантических единиц, 2.4 млн рёбер

  14. Куда движется передний край и главный вывод


1. Парадокс reasoning: как умная модель ломает extraction

Самый неприятный вывод во всей этой работе появился не на графе и не на бенчмарке. Он появился на extraction — на этапе, где LLM достаёт из сырого текста структурированные объекты по заранее заданной схеме.

Мы прогнали через o4-mini более 300 вариантов промптов с разными режимами reasoning_effort (параметр модели, который регулирует, сколько внутренних шагов рассуждения она тратит перед выдачей ответа: low / medium / high). Интуиция была банальной: чем больше reasoning, тем лучше модель поймёт сложный материал и аккуратнее разложит его по структуре. На практике получилось наоборот.

При reasoning_effort=high модель начинала не извлекать структуру, а «улучшать» её по собственному усмотрению. Она объединяла объекты, которые по контракту должны оставаться раздельными. Она меняла имена полей в JSON, считая, что так «логичнее». Она сглаживала различия между типами концептов. Местами она просто ломала детерминизм пайплайна, потому что в одном и том же промпте на разных запусках возвращала разные структурные решения.

Здесь надо ввести ещё один термин. SGR (Schema-Guided Reasoning) — техника, при которой Pydantic-модель (а не свободный JSON-пример в промпте) передаётся напрямую в LLM как response_format, и модель обязана генерировать ответ строго под эту схему. Это не «парсинг JSON задним числом», а constrained decoding на уровне самого вызова. Так вот, в режиме высокого reasoning модель начинала бороться с собственной SGR-схемой: пыталась её «допроектировать» вместо того, чтобы исполнить.

Решение оформилось в отдельный слой, который внутри проекта получил имя Semantic Mapper. Его смысл был не в том, чтобы сделать модель умнее, а в том, чтобы снизить когнитивный налог на extraction-путь и компенсировать это жёстким data contract, валидацией и постобработкой. Мы сознательно использовали более низкий reasoning_effort и переложили дисциплину на схему.

Результат:

  • Стоимость токенов снизилась примерно на 48%.

  • Качество extraction по нашей внутренней шкале выросло с 5.2 до 7.11.

  • Полная traceability по идентификаторам сообщений сохранилась.

Параллельно мы отдельно зафиксировали, как именно делается SGR в Azure Strict Mode: все Pydantic-классы наследуются от строгого StrictBase с extra="forbid", ни одного Optional, ни одного default, все поля обязаны попасть в массив required JSON-схемы. Если значение может быть пустым, модель обязана вернуть "" или [], а не пропускать поле. Мы добавили отдельный валидатор, который рекурсивно проверяет, что для каждого object-узла в сгенерированной JSON-схеме additionalProperties: false и properties совпадает с required. Без этого батчи Azure начинают сыпаться на ровном месте.

Эта история важна не как локальный optimization trick. Она важна как общий сигнал для всей индустрии agent memory: на extraction и normalization задачах raw reasoning power не является универсальным благом. Модель должна подчиняться схеме, а не заменять её собственными умозаключениями. На этом этапе интеллект LLM — это не творчество, а исполнение.


2. Почему flat RAG ломается на длинной истории

Почти весь рынок агентной памяти стартовал с одной и той же простой идеи: если модель не может удержать длинную историю в контексте, нужно вынести историю наружу, нарезать на куски, проиндексировать и возвращать top-k фрагментов по запросу. На уровне демо это работает. На уровне больших корпусов — научных архивов, многолетних переписок, смешанных историй проектов с конфликтами и ревизиями — этого недостаточно.

Проблема не только в качестве retrieval. Проблема в том, что retrieval сам по себе не определяет, что именно система хранит. Он не задаёт границу между фактом и гипотезой. Он не даёт объектам времени. Он не гарантирует lineage. Он не решает update semantics — что делать, когда новое утверждение противоречит старому. И он не защищает от тихой деградации, при которой на поверхности всё работает, а внутри память уже несколько недель отвечает по устаревшим источникам.

Внутри проекта Jocker — корпус 3 253 беседы, 24 639 сообщений из экспорта ChatGPT — этот предел был достигнут быстро. На ранней ветке архитектуры было сделано типичное компромиссное решение: ради скорости ingestion система местами обрабатывала беседы как почти атомарные блоки, а message-level structure и строгая привязка к UUID сохранялись не на всех этапах. Это породило не просто снижение качества ответа, а более тяжёлый класс сбоев. Агент видел тематически похожие куски, но не мог уверенно доказать, какой именно источник является основанием вывода и в каком месте длинной истории вообще находится нужный факт.

Внутри команды этот режим деградации мы называем Graph Sludge — графовый шлам. Это состояние, когда у вас есть тысячи узлов (в Jocker до пивота их было больше 26 000), но они лишены гранулярности и связности: анонимные сегменты без привязки к message_id, потерянные UUID, отсутствие таймстемпов, плоские схемы без вложенности. Граф на картинке выглядит впечатляюще, но навигироваться по нему нельзя, и каждый второй ответ агента — на грани галлюцинации.

Именно здесь становится видно, что retrieval layer и memory layer — не одно и то же. Retrieval отвечает на вопрос «какие фрагменты похожи на запрос». Память должна отвечать на более жёсткие вопросы:

  • какие объекты знания вообще существуют в системе;

  • как они связаны со своими источниками;

  • как они меняются во времени;

  • какие правила ревизии знания допустимы;

  • как обнаруживать собственную деградацию.

Если не разделить эти два слоя, любая система рано или поздно скатывается в красивый чанк-индекс, который умеет отвечать на простые вопросы и тонет на всём, что требует сборки знания.


3. Базовая онтология как старт любой системы памяти

Серьёзная система памяти начинается не с выбора векторной базы и не с графовой СУБД. Она начинается с базовой онтологии — явного списка того, какие объекты существуют в памяти и какие отношения между ними допустимы.

Это звучит сухо, но именно здесь проходит граница между инженерной системой и красивой свалкой данных. Пока вы не определили, какие типы объектов существуют, вы не можете строить корректный retrieval, консолидацию или reasoning. Даже если часть вашей онтологии будет достраиваться динамически через gardener loops (про них дальше), стартовый каркас должен быть жёстким. Иначе система скатывается либо в over-compression (несколько размытых сущностей якобы покрывают весь корпус), либо в concept explosion (память зарастает тысячами почти одинаковых объектов).

Именно поэтому мы ушли от loose entities и summary chunks к более строгому объекту — concept hypothesis (концепт-гипотеза). Это не keyword, не entity label и не generic summary. Это ревизуемый эпистемический объект: у него есть стабильный идентификатор, тип, описание, evidence spans (привязки к источникам), aliases, relations (типизированные связи с другими концептами), first_seen / last_seen, confidence и revision_count. Такой объект неудобен для прототипа, но именно он позволяет перейти от keyword indexing к управляемому семантическому сжатию.

Упрощённая Pydantic-схема, в продакшене мы используем расширенную её версию:

from pydantic import BaseModel, ConfigDict, Field
from typing import List, Literal, Optional
from datetime import datetime

class StrictBase(BaseModel):
    model_config = ConfigDict(extra="forbid")

class EvidenceSpan(StrictBase):
    source_id: str
    message_id: str
    text: str
    confidence: float

class ConceptRelation(StrictBase):
    target_id: str
    relation: Literal["causes", "conflicts_with", "supports", "instance_of", "enables"]
    confidence: float

class ConceptHypothesis(StrictBase):
    concept_id: str
    title: str
    type: Literal["behavioral_pattern", "project", "event", "tension", "goal", "belief", "entity"]
    description: str
    state: Literal["active", "weak", "merged", "split", "archived"]
    confidence: float
    aliases: List[str]
    evidence_spans: List[EvidenceSpan]
    relations: List[ConceptRelation]
    revision_count: int
    first_seen: Optional[datetime]
    last_seen: Optional[datetime]

Из такой постановки следует один важный практический вывод. Если вы хотите, чтобы агент умел работать с временными изменениями, конфликтами версий, повторяющимися паттернами, опровержениями и накоплением evidence — это не появится из лучшего retriever’a. Temporal reasoning требует отдельного онтологического слоя. Event, state, interval, revision, contradiction, evidence, hypothesis — это должны быть first-class concepts, а не побочные свойства текстовых чанков.


4. Data contract и staging database: где начинается доказуемость

Когда говорят о памяти, обсуждать любят reasoning model. На практике всё начинается с data contract — формального описания того, как именно структурированы данные на каждой границе пайплайна. Если на этом уровне система неточна, дальше она уже не становится честнее.

Для Jocker критическим разворотом стал переход к SQLite как source of truth (единственному источнику истины). Не JSONL, не сериализованные summary, не промежуточные артефакты модели. Таблица messages хранит UUID сообщения, оригинальный текст, роль (user / assistant / system), parent_id, временную метку и conversation_id. Это позволяет восстанавливать message-level хронологию и делать точное цитирование вплоть до конкретной реплики.

Поверх этого строится граф в Neo4j с чётко определёнными узлами Conversation, Message, Segment, Concept, Topic и отношениями HAS_MESSAGE, NEXT, CONTAINS_MESSAGE, MENTIONS, EVIDENCE_FOR, BELONGS_TO. Главный structural pivot здесь — связь EVIDENCE_FOR. Пока концепт не привязан напрямую к конкретному message-level evidence, он остаётся красивой абстракцией без epistemic grounding.

Дополнительно мы ввели хронологический скелет: каждое сообщение связано с предыдущим и следующим через NEXT / PREVIOUS. Это позволяет восстанавливать ход беседы прямо в графе, полностью минуя сортировку в исходной базе, что критично для скорости инференса при иерархическом обходе.

Следующий важный слой — staging database, отдельный SQLite, в котором живёт промежуточное состояние пайплайна между стадиями. Отказ от file-based pipeline и переход к stage.db сделал linking детерминированным. В staging-слое лежат:

  • segments — структурные единицы, привязанные к conversation_id;

  • mentions — каждый сырой концепт, извлечённый из текста, со ссылкой на исходное сообщение и nullable consolidated_id;

  • canonical_concepts — «золотой стандарт» базы знаний с алиасами и confidence;

  • topics — тематические кластеры.

Логика стала такой: extractor работает инкрементально, спрашивая stage.db, какие conversation_id уже обработаны, и не делает повторных вызовов LLM. Consolidator читает mentions, выравнивает их семантически против canonical_concepts и пишет hard-link через прямой UPDATE. Graph builder выполняет SQL JOIN между staging-таблицами и грузит в Neo4j уже прелинкованные данные — name-based fallback’и и duplication logic исчезают как класс.

В цифрах для Jocker это выглядело так: исходная масса в 34 805 сырых mention’ов сведена к 7 339 canonical concepts. Это не просто дедупликация — это превращение «графового шлама» в осмысленный реестр.

Главный вывод этой главы простой. Доказуемость начинается не в final answer prompt и не в RAG-оркестрации. Она начинается в том месте, где система хранит lineage цепочкой:

source text → message UUID → segment → mention → canonical concept → answer

Если это звено рвётся хоть в одной точке, дальше agent memory превращается в narrative generator с неплохой стилистикой.


5. Concept Memory Loop: create / attach / refine / reject

Если память строится не на flat chunks, а на concept hypotheses, то нужен и другой operational loop. Его нельзя свести к «извлекли сущности, склеили похожие, положили в граф».

Один chunk, как правило, содержит сразу несколько тем, слабые мотивы, эмоциональный фон и ссылки на прошлые эпизоды. Summary-first подход кажется практичным, но быстро убивает ambiguity и слабые повторяющиеся линии — а именно они часто и есть самое ценное в долгой памяти.

Поэтому новые данные должны интерпретироваться относительно уже существующего состояния памяти. Базовой рабочей единицей становится цикл create / attach / refine / reject. Более опасные операции — merge, split, archive — мы держим оффлайн, потому что именно они чаще всего портят память при ранней автоматизации.

Минимальный online-цикл выглядит так:

  1. Teacher model извлекает candidate concepts, evidence spans, provisional types и relations.

  2. Retriever поднимает top-k ближайших объектов из текущей памяти по семантическому сходству, лексическому совпадению, окрестности в графе и временной близости.

  3. Router принимает по каждому кандидату одно из четырёх решений:

    • create — новый concept hypothesis;

    • attach — добавить evidence к существующему;

    • refine — переписать описание / алиасы / confidence слабого концепта;

    • reject — отбросить в residuals (шум, не вошедший в структуру).

  4. Каждое действие пишется в ActionLog — append-only журнал когнитивных операций агента.

  5. Residuals при этом не выбрасываются полностью, а уходят в отдельный пул для последующего offline-анализа: именно там Gardener и (в перспективе) Dreamer ищут скрытые паттерны.

Это уже не retrieval в привычном смысле. Это progressive alignment against evolving consensus structure — постепенное согласование новых данных с эволюционирующей структурой консенсуса в памяти. Смысл такой архитектуры в том, чтобы каждый раз не искать заново по всему корпусу, а поддерживать устойчивые, но ревизуемые semantic objects, поверх которых уже можно строить explanation, longitudinal reasoning и planning.

Отсюда же рождаются и правильные метрики качества. Recall на retrieval tasks недостаточно. Нужно смотреть на:

  • coverage — какая доля корпуса покрыта концептами;

  • high-confidence coverage — то же, но только для уверенных концептов;

  • concept purity — гомогенность evidence внутри одного концепта;

  • concept churn — насколько часто концепты переписываются / объединяются / расщепляются;

  • residual novelty — насколько остатки реально приносят новую информацию;

  • evidence grounding — доля утверждений с верифицируемой ссылкой на источник;

  • downstream utility — реальная польза в задачах конечного агента.

Память должна доказывать не только то, что она что-то хранит, но и то, что она не разрушает смысл при сжатии.


6. Топическая кластеризация: UMAP, HDBSCAN, Optuna и weighted links

Следующий важный кусок проекта — это не просто кластеризация, а попытка превратить граф из плоской коллекции concepts и segments в иерархически navigable structure.

Ранний пайплайн использовал PCA (Principal Component Analysis, метод линейного снижения размерности) и HDBSCAN (Hierarchical Density-Based Spatial Clustering of Applications with Noise — иерархический плотностный кластеризатор) с фиксированными параметрами. Это давало низкую плотность кластеров, silhouette score (метрика качества кластеризации, оценивает насколько объект ближе к своему кластеру, чем к соседним; диапазон от –1 до 1) около 0.25, и практически не давало хорошего macro-level entry point в память. Topic layer формально существовал, но был слишком слаб, чтобы реально улучшать retrieval.

После этого мы провели отдельный исследовательский цикл:

  • PCA → UMAP (Uniform Manifold Approximation and Projection — нелинейное снижение размерности, лучше сохраняющее локальную и глобальную структуру).

  • HDBSCAN с фиксированными параметрами → HDBSCAN с автоподбором через Optuna (фреймворк для байесовской оптимизации гиперпараметров).

  • Целевая функция оптимизации стала учитывать сразу три метрики: silhouette score, noise ratio (доля объектов, не попавших ни в один кластер) и outlier score.

Результат: silhouette score вырос до 0.41, в Jocker удалось выделить 722 плотных кластера.

Но главное произошло на следующем шаге. Мы построили weighted semantic links между Topics и Concepts:

  1. Для каждого кластера вычислили topic centroid — средний вектор сегментов внутри кластера.

  2. Для всех 7 339 canonical concepts подняли эмбеддинги из кэша по точному ключу f"{name}: {description}".

  3. Посчитали cosine distance между каждым topic centroid и каждым concept embedding.

  4. Применили min-max нормализацию и привели всё к шкале 0–100% relevance.

  5. Для каждого топика оставили top-10 концептов с весом > 20.

  6. Записали в Neo4j 7 220 weighted RELATED_TO рёбер между Topics и Concepts.

Это не просто улучшение навигации. Это смена retrieval regime. Вместо того чтобы бросать модель в семантический хаос на уровне отдельных сегментов, система получает возможность входить в память сверху:

topic → concept → graph synthesis → evidence assembly

Сверху над топиками строится ещё один слой — макро-таксономия. Топики кластеризуются в Domains (через UMAP+HDBSCAN уже по их LLM-сгенерированным профилям, а не по исходным эмбеддингам), и внутри каждого домена через Azure Batch выделяются Anchor Entities — стабильные проекты, системы или сущности, вокруг которых группируются темы. В графе это записывается как (Topic)-[:BELONGS_TO]->(AnchorEntity)-[:BELONGS_TO]->(Domain), и Neo4j остаётся единственным источником истины — никаких параллельных JSON-файлов с таксономией. Полная иерархия памяти теперь выглядит так: Domain → AnchorEntity → Topic → Concept → Segment → Message.

Валидация на LongMemEval: distance от topic embedding до centroid его сегментов — около 0.35, то есть LLM-сгенерированные имена топиков действительно сидят в центре своих кластеров, а не дрейфуют в сторону.

Этот переход стал одной из главных опор для последующего иерархического инференса. Topical layer перестал быть декоративным уровнем «для красивых картинок» и стал полноценным operational primitive.


7. LongMemEval: четыре фазы отладки агента и петля Exploration vs Execution

LongMemEval — открытый бенчмарк, который оценивает способности систем долговременной памяти по пяти осям: information extraction, multi-session reasoning, temporal reasoning, knowledge updates и abstention (умение не выдумывать, когда данных нет). Его удобно использовать как бинарный экзамен — прошли или нет. Для реальной инженерной работы это плохая рамка.

Наш проект агентного инференса на LongMemEval с моделью gemini-3.1-flash-lite-preview прошёл четыре фазы, и каждая — отдельный урок. (мы сознательно работаем на легкой моделе не используя возможности PRO)

Фаза 1. The Dumb Agent (Bottom-Up). Агент использовал базовый keyword search и read_source_message. Flash-Lite страдала от Keyword Bias: она искала точные термины вроде «maintenance» и пропускала релевантные сегменты типа «washed car» или «repairs». У агента не было чувства структуры графа.

Фаза 2. Top-Down Graph RAG. Ввели инструменты explore_node и lens, чтобы агент мог идти по иерархии Domain → Topic → Concept → Segment. Появились две новые проблемы. Первая — Tool Blindness: lens был захардкожен только под Concept-узлы и возвращал пустоту для Topic и Domain. Вторая — Context Drowning: lens возвращал 50+ идентификаторов, контекстное окно агента забивалось нерелевантными ID, и агент входил в панику, повторяя одни и те же действия.

Фаза 3. Multi-Agent Reflective Loop. Ввели схему Planner → Scout → Synthesizer. Это сразу дало три новых сбоя:

  • Amnesia. Scout пересоздавался каждую итерацию с чистым контекстом и не помнил, какие поиски уже провалились. Получались бесконечные петли одних и тех же запросов.

  • Paranoid Analyst. Synthesizer был перенастроен на «DO NOT GUESS» и отвергал валидное evidence типа «mid-February», потому что это не точная дата.

  • Haystack Leakage. Реализация allowed_sessions (фильтрации по разрешённым сессиям) была вмешана в базовый utils.py, что приводило к ошибкам «Access Denied» и ещё больше пугало агента.

В этой же фазе проявилась ключевая патология, которую называют в литературе петлёй Exploitation vs Exploration. Агент идентифицирует, что ему нужны данные, но из-за отсутствия точек опоры в плоском массиве не может локализовать доказательство. Не имея возможности сослаться на конкретный UUID, он теряет уверенность и инициирует новый цикл поиска. Контекст забивается, токены кончаются, ответа нет. До перестройки агент нередко делал 22 и более шагов, блуждая между тематическими кластерами когда ответ УЖЕ был в котнексте.

Фаза 4. Stateful Middleware Architecture. Это и есть текущий рабочий вариант. Чистое разделение ответственности:

  • Base tools — универсальные graph.py, utils.py, без всякой логики фильтрации сессий.

  • Haystack Middleware — отдельный proxy-слой, который редактирует ID вне текущей задачи.

  • Sensemaking upgrade — Synthesizer теперь использует Chain-of-Thought через явное поле reasoning, что даёт ему право на логическую дедукцию (например, сравнивать «mid-February» с «Feb 27»).

  • Stateful Orchestration — Planner держит chat session между итерациями и помнит, какие стратегии уже провалились.

Параллельно были закрыты технические долги: lens починили под произвольные типы узлов и множественные пути, smart_search получил component-based (OR) fallback, для Scout появился exit-tool submit_evidence, и весь процесс стал писаться в agent_trace.log для полной observability.

После этой перестройки траектория рассуждений сократилась с 22+ шагов до 4–6, time to first token упал примерно с 8+ секунд до менее 2 секунд. Это не «оптимизация», это смена эксплуатационного класса системы.

И главный вывод тут не про бенчмарк и низкий скор, а про его правильную интерпретацию. LongMemEval не показал «провал» нашей системы. Он показал ровно то, что и должен показывать: где базовая онтология уже достаточна, а где начинает трещать. Особенно жёстко он подсвечивает temporal reasoning. Event, interval, state transition, time of utterance, time of observation, version conflict, reactivation of stale knowledge ничего из этого не появляется само собой из vector retrieval и graph traversal. Для этого нужен отдельный temporal layer, и мы знаем, что это следующий слой архитектуры.


8. Stateful multi-agent inference: Planner, Scout, Synthesizer

История с LongMemEval — частный случай более общего урока: когда память становится многослойной, монолитный агент перестаёт справляться. Он не может эффективно сузить пространство поиска, теряет traceability и входит в неуправляемые reasoning loops.

Поэтому стандартный inference-цикл в Memory OS у нас сейчас выглядит как stateful multi-agent loop с тремя ролями.

  • Planner — стратег. Раскладывает пользовательский запрос в roadmap и сопоставляет намерение с тематическими кластерами графа. Решает, какие «районы» памяти вообще нужно обходить. Держит state между итерациями, помнит провалы предыдущих стратегий.

  • Scout (Researcher) — навигатор по графу Neo4j. Идёт по NEXT (хронологические связи) и EVIDENCE_FOR (концептуальные). Возвращает структурированный JSON с конкретными message UUID, на которых построены кандидаты в evidence.

  • Synthesizer — финальный аудитор. Не ищет, а собирает ответ только по уже подтверждённому evidence. Каждое его утверждение обязано иметь цитату формата [Concept:ID] или [Source:UUID].

Над всем этим лежит Haystack Middleware с graceful fallback — многоуровневой деградацией поиска:

  1. Первый путь — Neo4j associative traversal по EVIDENCE_FOR.

  2. Второй — FAISS-векторный поиск по семантическому сходству.

  3. Третий — raw retrieval из SQLite по timestamp / автору.

Это значит, что inference перестаёт зависеть от одного-единственного retrieval-механизма. Если граф ещё не достроил концептуальные связи в нужной области, система всё равно вернёт ответ, привязанный к исходному материалу. Это особенно важно на ранних стадиях наполнения базы, когда граф местами разреженный.

Именно эта связка — иерархический retrieval поверх weighted links + stateful multi-agent loop + graceful fallback — и дала те 4–6 шагов вместо 22+ из предыдущей главы.


9. Agentic Gardener: обслуживание графа как отдельная инженерная задача

Хороший ingestion pipeline не гарантирует хорошую память в долгую. Любая большая memory system начинает деградировать, если её не поддерживать отдельно.

У нас это оформилось как Agentic Memory Gardener. Здесь важен сам сдвиг оптики: обслуживание графа — не побочная housekeeping task, которую можно сделать ночью одним cron-скриптом, а отдельный инженерный контур со своими рисками, метриками и модельными требованиями.

Сначала про то, как это не работает. Первая попытка рефакторинга 26 000+ концептов Moshael шла по принципу top-down taxonomy: «давайте попросим LLM построить иерархию Entity → Domain → Insight». Это закончилось тем, что мы внутри команды называем Consultant-Slop эффектом: модель переключается в режим «MBA-консультанта», перестаёт опираться на сырое evidence и галлюцинирует красивую «карту мира». Конкретный пример: сложный стартап-проект «HeartScan» (с несколькими продуктами, внутренними противоречиями и эволюционирующей идентичностью) был сведён к плоскому PRODUCT в домене Outreach Strategy. Вся фактура исходного текста была убита.

Вывод: top-down таксономия — это faux-ontology, она уничтожает нюансы и фиделити. Поэтому Gardener был переписан под другой принцип — Concept Lifecycle, где каждый узел рассматривается как ревизуемая гипотеза, а не как клетка в чужой таблице.

Архитектурно это разнесли на две фазы.

Phase 0 — Global Lifecycle Triage. Глобальная инвентаризация через Azure Batch API (в текущей конфигурации — GPT-O4-mini). На вход — экспорт concepts + topics + сэмплированное evidence + статистика частот. На выход — JSON-манифест LifecycleProposals с четырьмя категориями кандидатов:

  • merge_candidates — вероятные семантические дубли;

  • split_candidates — слишком широкие концепты, склеивающие разные идеи;

  • macro_concept_candidates — высокоуровневые «хабы» (типа HeartScan, Moshael);

  • noise_candidates — эфемерный технический шум, кандидат на архив.

Ключевой safety invariant: Phase 0 производит Proposals, а не Truth. Они пишутся в pending_review-стейджинг и никогда не мутируют граф автоматически.

Phase 1 — Surgical Agentic Refactoring. Tool-augmented агент (в продакшене — Gemini Pro), который работает не над всем графом, а над «соседствами» (neighborhoods) — небольшими подграфами + связанными triage-предложениями + макро-контекстом. Constraint: агент обязан вызывать read_source_segments при любой неоднозначности. Action space:

  • refine_concept — обновить тип, описание, алиасы;

  • merge_concepts — переподключить evidence к каноническому узлу;

  • split_concept — раскидать evidence по новым узлам;

  • create_relation — построить типизированную связь (HAS_PRODUCT, PART_OF, ENABLES).

Каждое действие пишется как GardenerAction узел в Neo4j с полным agent_rationale — для аудита и rollback.

EXP-GARDENER-001: эксперимент про то, что Flash не годится

Отдельный исследовательский интерес здесь вызвал эксперимент EXP-GARDENER-001. Мы прогнали в sandbox dry-run (мутации перехватывались и писались в JSON, а не выполнялись на графе) две модели на 50 unresolved seed concepts из живой Neo4j-базы:

Метрика

gemini-3.1-pro-preview

gemini-3-flash-preview

Дельта

Total Actions

521

1014

+94.6% (over-activity)

Total Evidence Links

661

1410

+113% (redundancy)

Execution Errors

9

22

+144% (failure rate)

Avg. Rationale Length

90 chars

146 chars

+62% (verbosity)

На бумаге Flash «активнее»: больше действий, больше evidence links. Но это была ложная активность.

  • Pro группировал несколько сырых концептов в один канонический PERSON или ORG-узел, создавал связь только при явном evidence, выполнял сложные split_concept без нарушения схемы, давал плотные технические rationale про «epistemic consolidation».

  • Flash пытался создать связь для каждого упоминания концепта вне зависимости от salience, регулярно вызывал split_concept с пропущенными полями, ронял внутренние reasoning loops на NoneType, плодил дубли-evidence-links, указывающие на нерелевантные сегменты, и писал «fluff»-обоснования без структурного смысла.

Вывод однозначный: для maintenance loops цена токена не главное. Главное — способность модели действовать эпистемически сдержанно, не плодить ложные связи и не подменять structural work verbosity-эффектом. Flash может быть допустим только в Phase 0 Triage с жёстко ограниченными промптами; на Phase 1 мы оставили Pro.

Метрики успеха для Gardener у нас сейчас зафиксированы такие: Anchored Rate >60% (доля сегментов, связанных с refined concepts), Concept Purity >0.70, и 100% semantic traceability — каждое merge/split обязано цитировать source IDs.


10. Invalidation, repair и bi-temporal facts

Как только память становится ревизуемой, ей нужен отдельный контур invalidation and repair — иначе система начинает жить в режиме ложной стабильности. Старые claims остаются как будто валидными после изменения source, confidence не падает, stale hypotheses продолжают участвовать в synthesis, и со временем ответы агента всё больше расходятся с реальностью корпуса.

Это решается через explicit invalidation semantics. У claim есть state из набора active / stale / disputed / rejected / archived. У concept hypothesis — отдельный operational trust state: stable / weakened / disputed / broken / stale / archived. Правила распространения такие:

  1. Source → claim. Если изменился исходный chunk или span, зависимые claims помечаются stale (если grounding неуверенный), disputed (если есть противоречие) или rejected.

  2. Claim → hypothesis. Гипотеза обновляет счётчики поддержки и свежесть evidence.

  3. Confidence capping. Гипотеза не может удерживать confidence выше, чем это поддерживается выжившим активным evidence.

  4. Repair queue. Любая гипотеза, попавшая в weakened/disputed/broken/stale, добавляется в RepairQueue.

Дальше отдельный Reflective Maintenance Agent работает с этой очередью. Это не open-ended dreamer, это bounded worker с конечным action space: reattach, refine, split, merge, reject/archive, rebuild. Он работает только над регионами из очереди, обязан цитировать supporting claims и source spans, и каждое его действие пишется в ActionLog. Глобальный re-synthesis не разрешён без явной эскалации.

Параллельно с этим у нас в работе bi-temporal модель фактов (ADR-019). Идея простая: для каждого ревизуемого факта храним две оси времени:

  • Valid time — когда факт был истинным в реальном мире;

  • System time — когда система этот факт записала или обновила.

В графе это реализовано через паттерн с FactState-узлами:

(c:Concept)-[:HAS_STATE {valid_from: ..., valid_until: ...}]->(s:FactState {value: "..."})

Текущий вид графа выбирается простым WHERE r.valid_until IS NULL. Историческая «time travel» проекция — WHERE r.valid_from <= $target_date < r.valid_until. Это сразу даёт две вещи: полную аудируемость того, как «истина» менялась во времени, и возможность спросить систему «что я думал о своих карьерных целях полгода назад» — без overwrite старых claims при поступлении новых. Цена — заметный overhead в storage и более сложные Cypher-запросы для базовых property-обращений.

Это и есть то, без чего любая «memory for agents» рано или поздно превращается в недостоверный summary-генератор.


11. MCP как суверенный интерфейс памяти

Когда memory system становится многослойной, interface layer перестаёт быть технической деталью — он становится частью архитектуры знания. У нас эту роль играет MCP server (Model Context Protocol — открытый протокол для интеграции инструментов и контекста с LLM).

Изначально MCP-сервер был тонкой обёрткой над KnowledgeAgent из agent_core и тащил за собой всю heavy-машинерию синтеза, роутинга и контекст-билдеров. Это создавало две проблемы: сервер был тяжелее, чем нужно, и любые изменения в когнитивной логике агента ломали data access.

В ADR-017 мы вынесли отдельный лёгкий mcp_core — слой Data Access Layer, не зависящий от agent_core:

  • mcp_core/embeddings.py — стрипованный клиент только для /embeddings, без synthesis-логики;

  • mcp_core/search.py — загрузка и поиск по FAISS-индексам (topics, concepts, segments), отвязанный от абстракций KnowledgeBase;

  • mcp_core/graph.py — Cypher-запросы для иерархического retrieval;

  • mcp_core/fast_memory.py — минимальная реализация Fast Memory.

Параллельно внутри MCP-сервера живёт Embedded Cognitive Executor — внутренний reasoning loop, который обрабатывает сложные многошаговые операции вроде кросс-референса топиков, валидации evidence и генерации гипотез. Главная точка входа — инструмент query_memory с иерархической логикой:

  1. Topical Entry — поиск по topics_index.faiss.

  2. Concept Retrieval — поиск по concepts_index.faiss.

  3. Graph Synthesis — внутренний loop обходит weighted RELATED_TO рёбра в Neo4j.

  4. Evidence Assembly — сборка контекстных блоков в строгом XML-формате с character-level provenance ( прослеживаемость: возможность для каждого утверждения системы показать, откуда оно взялось, вплоть до конкретной строки в источнике.).

Контракт промпта тоже жёсткий. Контекст, который сервер отдаёт наружу, размечен XML-тэгами по типу evidence:

  • <direct_evidence> — атомарные факты и явные предпочтения из Fast Memory (FAISS);

  • <associative_evidence> — связанные концепты и тематические линии из Knowledge Graph;

  • <reflective_hypotheses> — лонгитюдинальные паттерны из Reflective Memory Layer;

  • <source_provenance> — метаданные с привязкой к исходным эпизодам / чанкам.

И жёсткое требование цитирования: каждое утверждение синтезирующего агента обязано содержать цитату формата [Concept:ID] или [Source:UUID]. Без цитаты — невалидный ответ.

Именно этот переход превращает память из «библиотеки внутри приложения» в отдельную вычислительную систему со своим протоколом, routing logic и diagnostics.


12. Reflective layer: эмоции, скрытые паттерны и психологический слой

Одна из ошибок ранней версии текста состояла в том, что система сводилась почти исключительно к factual extraction и retrieval discipline. Это неверно по отношению к реальной идее проекта.

С самого начала Memory OS мыслилась не только как factual memory, но и как система, способная выделять эмоционально и мотивационно насыщенные паттерны в длинной истории. В раннем vision-слое это было зафиксировано прямо: поверх memory service должен существовать когнитивный слой агентов, включающий reflection, critic, control и отдельный psychologist-oriented subsystem, который классифицирует факты и связи по тону, эмоциональному весу и мотивационному содержанию.

Технически это формализовано в ADR-016 (User State and Reaction Extraction). Из текста LLM извлекает объекты UserReaction по строгой SGR-схеме:

  • state — controlled vocabulary из 12 значений: Frustration, Anxiety, Exhaustion, Clarity, Inspiration, Joy, Confusion, Neutral, Determination, Overwhelmed, Insight, Other. Значение Other — escape hatch для эволюции таксономии.

  • state_raw — заполняется только если state == "Other", чтобы накапливать данные для будущего расширения словаря.

  • intensity — float [0.0, 1.0].

  • valencepositive / negative / neutral / ambiguous.

  • epistemic_statusexplicit (пользователь сам сказал) или inferred (выведено из контекста). Это критическая граница: модель обязана различать наблюдаемое и интерпретируемое.

  • evidence_message_id — прямая привязка к сообщению-источнику.

В графе эти узлы получают отдельный label :UserReaction и связываются с темами через ребро (UserReaction)-[:DURING]->(Topic). Это позволяет отвечать на запросы вроде «какие эмоциональные состояния преобладают, когда я обсуждаю карьерный рост». Важная деталь приватности: emotional data изолирована от основного Concept-графа, хранится с собственными retention/purge политиками, и MCP-серверы обязаны валидировать флаг psychological_access=true в context metadata, прежде чем отдавать :UserReaction узлы наружу.

Метрики качества для этого слоя:

  • Schema validity ≥ 99%;

  • Controlled vocabulary coverage ≥ 95% (доля сегментов, где state != "Other");

  • NULL timestamp rate = 0% для всех сохранённых reactions;

  • 100% UserReactions имеют как минимум одну связь :DURING к топику.

На уровне семантики это позволяет в длинной истории переписок видеть recurring tensions, motivational conflicts, identity statements, event sequences и слабые повторяющиеся паттерны, которые не редуцируются к именованным сущностям. Concept hypothesis может описывать не только «PostgreSQL» или «Neo4j», но и конструкции уровня «automation as avoidance of direct sales exposure», «нарастающее профессиональное истощение перед кризисом», «переход от исследовательского интереса к defensive system-building».

И сразу важная честная граница. Reflective concept можно создать только если выполнены три условия:

  1. система видит несколько эпизодов, поддерживающих один и тот же глубинный паттерн;

  2. эпизоды связаны через cross-episode evidence, а не через одну удачную формулировку модели;

  3. это не противоречит наблюдаемой temporal dynamics темы.

То есть reflective layer — это не «LLM as psychologist», а эпистемически дисциплинированный поиск скрытых паттернов на длинной истории, с отдельной eval discipline на evidence grounding, false pattern induction, over-interpretation и устойчивость к seductive narratives. Этот слой реален и важен, но именно поэтому он требует больше, а не меньше инженерной строгости.


13. Масштаб: 106.7 млн токенов, 632 868 семантических единиц, 2.4 млн рёбер

Чтобы разговор о Memory OS не выглядел как философия поверх маленького демо, нужен реальный масштаб. По состоянию на апрель 2026 года пред-пивотный анализ охватывает четыре крупных проекта из разных предметных областей.

Суммарно система обработала примерно:

  • 106.7 миллиона токенов (около 426 МБ чистого текста);

  • 632 868 семантических единиц;

  • 160 911 концептов;

  • 10 998 топиков;

  • 2 395 959 рёбер в графовых структурах.

Разбивка по корпусам показывает не один удачный домен, а несколько разных режимов данных.

LongMemEval — крупнейший эпизодический граф: ~66.5 млн токенов, 399 999 семантических единиц, 112 456 concept nodes, 8 456 topics, 1 568 925 рёбер. Соотношение concepts к topics ~13:1 показательно: диалоговые данные о жизни пользователя порождают огромное количество уникальных сущностей, имён, локаций и мелких фактов, которые плохо укладываются в жёсткую иерархическую таксономию без отдельного temporal layer.

Moshael — другой тип корпуса: ~18.4 млн токенов, 58 023 семантические единицы, 15 432 concepts, 845 topics, 245 678 рёбер. Это сбалансированная личная база знаний, где плотность интересов одного пользователя позволяет держать хороший баланс между topic compression и entity diversity.

OpenSCG_v2 — поведение научной литературы: ~10.6 млн токенов, 130 245 семантических единиц, 24 567 concepts, 1 245 topics, 456 789 рёбер. Для 859 научных работ это очень плотный граф, где методы, понятия, авторы и результаты многократно переплетены перекрёстными связями.

Jocker на этом фоне — самый чистый полигон для semantic search experiments: ~11.2 млн токенов, 44 601 семантическая единица, 8 456 concepts, 452 high-level topics, 124 567 рёбер. Именно здесь aggressive clustering позволил сжать массив семантических единиц в относительно компактную высокоуровневую topic structure для иерархического retrieval.

Эти числа важны не как vanity metrics, а как подтверждение того, что архитектурные решения принимались на материале, где уже начинают проявляться реальные пределы RAG-like подходов. При масштабе 100+ миллионов токенов и почти 2.4 миллиона рёбер вопрос уже не в том, можно ли что-то найти по embedding similarity. Вопрос в том, какую структуру знания система вообще способна удерживать без деградации.

Пивот в сторону более строгой ontology discipline, temporal knowledge graphs и bounded maintenance loops возник не из абстрактного желания усложнить систему, а из наблюдаемого поведения реальных больших корпусов.


14. Куда движется передний край и главный вывод

Если убрать маркетинговый шум, сильные лаборатории и серьёзные инженерные команды в области agent memory сходятся сейчас не на «ещё одном GraphRAG». Они сходятся на нескольких более неприятных, но содержательных вещах.

Memory object усложняется. Chunk и flat entity больше не воспринимаются как достаточные единицы памяти. На смену им приходят typed objects: facts, experiences, beliefs, revisable summaries, concept hypotheses.

Усиливается layering. Быстрый factual retrieval и reflective synthesis перестают жить в одном path. Это разные пути, разные модели, разные SLA, разные правила доступа.

Усиливается importance of revision semantics. Система должна не только накапливать новое знание, но и уметь invalidation, downgrade confidence, queue repair, perform bounded maintenance. Адепты Supermemory, Hindsight, Zep сходятся в одном: update и invalidation должны быть explicit graph primitives, а не emergent behavior из top-k retrieval.

Evaluation становится harder discipline. Граф, красивый на картинке, никого больше не спасает. Нужны coverage, churn, residuals, evidence grounding, downstream utility и domain-specific acceptance tests. Без отдельной eval-инфраструктуры reflective memory быстро превращается в недостоверный summarization.

Время превращается из metadata в ontological dimension. Это следующий большой слой сложности для Memory OS, и именно здесь LongMemEval-подобные задачи дают самый полезный сигнал.


Главный вывод из всей этой работы можно сформулировать в одну фразу: retrieval не равен памяти.

Память начинается с онтологии.
Затем ей нужен data contract.
Затем нужен concept memory loop с дисциплиной create / attach / refine / reject.
Затем нужен runtime inference, который умеет работать с evidence, а не только с similarity.
Затем нужен maintenance contour, который защищает граф от деградации.
И только после этого становится осмысленным разговор о том, насколько умна модель и как красиво она объясняет свои выводы.

Структурные тезисы, которые мы для себя считаем доказанными:

  • Базовая онтология обязательна даже для систем с динамической достройкой на лету.

  • Message-level provenance возвращают системе право на доказуемость.

  • Concept hypothesis сильнее, чем flat entity memory.

  • Избыточный reasoning может портить extraction — на этом слое модель должна подчиняться схеме.

  • Topical structure и weighted topic-concept links реально меняют retrieval regime.

  • LongMemEval полезен как технический стресс-тест границ онтологии, а не как бинарный приговор.

  • Graph maintenance требует отдельной архитектуры и не может быть cheap afterthought.

  • Reflective и emotional layers возможны, но требуют больше инженерной строгости, а не меньше.

Если свести всё к одной фразе: мы строим не хранилище контекста, а систему управляемого когнитивного сжатия и ревизии знания поверх длинной истории. И именно здесь проходит реальная граница между flat RAG и системой, которая действительно начинает помнить.