Память

Bi-temporal память: два времени у одного факта

«Когда факт был верен» и «когда мы о нём узнали» — разные оси. Смешение даёт ассистента, который уверенно оперирует вашей позапрошлогодней жизнью. Разбираем состояния факта, откат и цену реализации.

В марте вы сказали ассистенту, что работаете в компании А. В августе упомянули, что теперь в компании Б. В сентябре спрашиваете: «напомни, что я говорил про корпоратив». Наивная память отдаст оба факта, потому что оба записаны и оба про работу. Модель выберет тот, который ближе к формулировке вопроса, — то есть случайный.

Проблема не в поиске. Проблема в том, что у факта нет отметки о том, до какого момента он был верен.

#Две оси, а не одна

У любой записи в памяти есть два независимых времени.

Время события (когда факт был верен). Вы переехали в июне. Это свойство мира, и оно не зависит от того, узнал ли о нём кто-нибудь.

Время записи (когда система об этом узнала). Вы сказали об этом в августе. Это свойство базы.

Обычно их путают, потому что в простых случаях они совпадают. Расходятся они в трёх ситуациях, и каждая встречается в реальной переписке:

  • Задним числом. «Кстати, я ушёл из того проекта ещё весной» — время события в прошлом, время записи сейчас.
  • Наперёд. «С понедельника выхожу на новую работу» — время события в будущем.
  • Исправление. «Я ошибся, встреча была не во вторник, а в среду» — новая запись отменяет старую, но старая версия существовала и влияла на ответы.

Система, у которой одна ось, не может отличить «факт неверен» от «факт был верен раньше». А это принципиально разные вещи: первое надо забыть, второе — помнить как историю. Как эта разница выглядит со стороны пользователя, показано в обзорном материале про чат с памятью и bi-temporal факты; здесь разбирается механика.

#Состояния факта

Практическая реализация обычно упрощает картину до трёх состояний с двумя полями: отметка «действителен до» и ссылка «заменён записью».

Состояние «Действителен до» «Заменён» Что делает
Актуальный пусто пусто попадает в ответы и в промпт
Заменённый дата ссылка на новый факт в ответы не идёт, история видна
Отозванный дата пусто признан неверным, тоже остаётся в истории

Ключевое свойство схемы: удаления нет. Строка не исчезает — она перестаёт быть актуальной. В Persona это два столбца на таблице личной памяти (valid_until, superseded_by, миграция 187_user_memory_bitemporal.sql), и функция invalidate_memory в app/chat/user_memory.py только проставляет отметку времени. Выборка актуального — это WHERE valid_until IS NULL.

Обратная операция симметрична: restore_memory обнуляет оба поля, и факт возвращается в актуальные. Именно поэтому ошибочную правку можно откатить — прежнее значение никуда не девалось.

#Что происходит при появлении противоречия

Сложность не в схеме, а в решении: пришёл новый факт — что делать со старым?

Возможных действий четыре: добавить как независимый, обновить существующий, признать существующий опровергнутым, ничего не делать. В Persona выбор делает языковая модель по строгой JSON-схеме: ей показывают новый факт и до дюжины похожих существующих, она возвращает действие и, если нужно, номер целевой записи (reconcile_and_add в app/chat/user_memory.py).

Две детали реализации важнее самого решения.

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

Закреплённый факт не трогается. Если пользователь явно закрепил запись, автоматика её не отменяет: проверка на закрепление стоит прямо в запросе выбора цели. Явное решение человека сильнее вывода модели.

#Где автоматическое разрешение противоречий отказывает

Здесь честная часть, которую в текстах про темпоральную память обычно опускают. Схема хранения решена; распознавание противоречия — нет.

Модель не знает, противоречие это или уточнение. «Работаю в компании Б» после «работаю в компании А» — это смена работы или вторая занятость? Человек по контексту понимает, модель на маленьком локальном контексте — как повезёт.

Кандидаты подбираются грубо. Похожие факты для сравнения отбираются по пересечению ключевых слов длиной от четырёх букв. Если старый факт сформулирован другими словами — «моё место работы» против «компания, где я числюсь», — он в кандидаты не попадёт, и модель не увидит противоречия вовсе. Останутся два активных факта.

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

Время события всё равно не хранится отдельно. Реализация помечает, когда факт перестал считаться актуальным, но не хранит, с какой даты он был верен. Фраза «я ушёл оттуда ещё весной» запишется с временем записи, а не с временем события. Это упрощение, и оно означает, что на вопрос «где я работал в мае» точного ответа система не даст.

Последний пункт — граница между «bi-temporal схемой» в академическом смысле и тем, что реально реализовано. Называть вещи своими именами важнее, чем красиво.

#Что это даёт на практике

Три вещи, которых без версионирования не бывает.

Ответ по актуальному. В промпт уходят только факты без отметки об устаревании. Ассистент не путается между старым и новым, потому что старое до него не доходит.

Видимая история. Инспектор памяти умеет показать и недействительные записи — выборка с флагом «включая отменённые». Пользователь видит не только что система о нём думает, но и что думала раньше и когда передумала.

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

Третий пункт связывает bi-temporal схему с автоматической записью вообще. Любое автоматическое изменение памяти допустимо ровно настолько, насколько его легко отменить, — и наоборот, система, которая переписывает свою память необратимо, не имеет права делать это без человека. Вторая половина того же требования — чтобы у каждой записи была ссылка на источник, из которого она выведена; об этом — в материале про провенанс памяти.

#Как это связано с удалением и забыванием

Три соседних механизма, которые легко перепутать.

Отметка недействительности — факт остаётся в базе и в выгрузке, но не влияет на ответы. Это то, о чём весь этот текст.

Настоящее удаление — строка исчезает физически, вместе с зеркалами в поисковых индексах. Это отдельная процедура, и без неё «удалённый» факт возвращается из индекса; подробности — в гайде о том, как удалить факт из памяти ИИ.

Забывание — политика, по которой факты со временем теряют вес или уходят из выдачи без явного противоречия. Это не то же самое, что устаревание по факту замены; стратегии разобраны в материале про то, как ИИ забывает.

Отдельно стоит сказать про граф связей: рёбра тоже помечаются недействительными, а не удаляются, и поддержка ребра снимается, когда факт, из которого оно выведено, заменён более новым. Как устроен сам граф — в статье про граф памяти.

#Частые вопросы

#Чем это отличается от обычного лога изменений?

Лог отвечает на вопрос «что происходило с записью». Bi-temporal схема отвечает на вопрос «что считается верным сейчас» — и делает это на уровне выборки, а не восстановлением состояния по журналу. Практически это значит, что ответ ассистента не требует проигрывания истории.

#Растёт ли база, если ничего не удалять?

Растёт, но медленно: речь о текстовых записях в сотни символов, а не о медиафайлах. Верхняя граница длины факта в Persona — 600 символов, а автоматическое пополнение памяти вообще ограничено бюджетом активных записей, после которого новые факты просто не сохраняются.

#Что произойдёт, если ассистент ошибочно отменит верный факт?

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

#Можно ли задать факту дату, с которой он верен?

В описанной реализации — нет, хранится только момент утраты актуальности. Если вам нужно отвечать на вопросы вида «что было верно в мае», ось времени события придётся вводить отдельным полем; это заметно усложняет и запись, и выборку.

#Кто решает, что факты противоречат?

Языковая модель, работающая локально, по строгой схеме ответа с четырьмя вариантами действия. Её решение затрагивает только незакреплённые факты и всегда обратимо. При облачном провайдере этот шаг не выполняется, и новые факты просто добавляются рядом со старыми.

#Как это встроено в общую память?

Версионирование относится к слою отобранных фактов о вас, а не к истории сообщений — та хранится как есть. Общее устройство слоёв разобрано в статье о том, как работает память у ИИ.

#Вывод

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

Хотите ИИ, который помнит вас?

Persona держит контекст между чатами и работает на вашем железе — если развернуть её у себя и подключить локальную модель.

Создать аккаунт