LLM-модели хорошо решают задачи диалога, но имеют одно ключевое ограничение: отсутствие встроенной долговременной памяти. Модель опирается только на текущий контекст сообщений, и при его обрезании:
забывает факты
путает детали
теряет согласованность личности
повышается стоимость из-за длины контекста
В этой статье я хочу разобрать архитектуру, которую использовал для реализации выборочной памяти в Telegram-боте на Python. Эта система позволяет сохранять важные сведения о пользователе и автоматически внедрять их в системный промпт при каждом запросе, обеспечивая стабильное и естественное поведение модели.
Статья не про бота или продукт, а только про техническую реализацию.
Проблема: LLM не имеют устойчивой памяти
Если использовать GPT или любую другую LLM «как есть», возникают типичные эффекты:
модель забывает имя пользователя через десяток сообщений
не различает важные и неважные факты
начинает выдумывать несвязанные данные
качество падает при увеличении истории диалога
стоимость растёт пропорционально длине контекста
Хранить всю переписку невозможно, это дорого и нарушает поведение модели.
Необходим был механизм извлечения значимой информации, её хранения и последующей интеграции.
Архитектура решения
Механизм памяти состоит из трёх слоёв:
Извлечение фактов из сообщений пользователя
Нормализация (дедупликация, фильтрация)
Интеграция фактов в системный промпт
Сессии хранятся локально, в структуре вида:
{
"history": [...],
"user_name": "Иван",
"facts": ["Он любит кофе", "У него есть собака"],
"last_message_time": 17328131.3
}
История ограничена 20 сообщениями для экономии токенов (мой проект не коммерческий, полностью личная разработка и поддержание).
1. Извлечение значимой информации
Простейший пример: извлечение имени из текста.
def extract_and_store_name(user_message: str, session: dict):
if session.get("user_name"):
return # имя уже сохранено
patterns = [
r"меня зовут\s+([а-яёА-ЯЁ]+)",
r"зовут\s+([а-яёА-ЯЁ]+)",
r"мое имя\s+([а-яёА-ЯЁ]+)",
]
for p in patterns:
match = re.search(p, user_message.lower())
if match:
session["user_name"] = match.group(1).capitalize()
return
Для эмоций, предпочтений или обстоятельств правила аналогичны.
2. Извлечение других фактов
Более общий обработчик:
def extract_facts(content: str):
content = content.lower().strip()
memory = []
if "кофе" in content:
memory.append("Он любит кофе")
if "собака" in content:
memory.append("У него есть собака")
if any(w in content for w in ["устал", "грустно", "плохо"]):
memory.append("Он устал или грустит")
return memory
В рабочей версии таких правил несколько десятков.
Важно: факты извлекаются только из сообщений пользователя, никогда из ответов модели.
Это предотвращает галлюцинации в памяти.
3. Дедупликация и фильтрация
Память может быстро засориться, поэтому перед записью выполняется очистка:
def normalize_memory(memory: list[str]):
memory = [m.strip() for m in memory if m.strip()]
memory = list(dict.fromkeys(memory)) # удаление повторов
return memory
Если этого не делать, модель начинает путаться и переоценивать отдельные слова («кофе», значит пользователь - бариста и т.п.).
4. Интеграция памяти в системный промпт
Самая важная часть архитектуры.
Перед отправкой запроса LLM системный prompt динамически обновляется:
def update_system_prompt(base_prompt: dict, memory: list[str]):
memory_text = "\n\nПамять:\n" + "\n".join(f"- {m}" for m in memory)
return {
"role": "system",
"content": base_prompt["content"] + memory_text
}
Далее промпт заменяет нулевой элемент истории:
session["history"][0] = update_system_prompt(base_prompt, session["facts"])
Это даёт два эффекта:
Модель видит память как часть собственной личности, а не как сторонние подсказки.
Важные сведения доступны в каждом запросе, но контекст остаётся компактным.
Пример: как это работает в диалоге
Пользователь:
«Сегодня устал на работе.»
Извлечённые факты:
Он работает
Он устал или грустит
Через сутки:
«Не могу уснуть.»
Модель отвечает приблизительно так:
«После напряжённого дня иногда сложно переключиться…»
Это достигается простым присутствием фактов в системном промпте.
Хранение данных
Сессии сохраняются в pickle:
user_sessions.pkl
Структура:
{
"history": [...],
"facts": [...],
"user_name": "...",
"last_message_time": 17328131.3
}
Сессия загружается при каждом сообщении и обновляется после обработки.
Основные проблемы и решения
1. Модель придумывает факты
Решение:
Хранить только факты, явно полученные из сообщений пользователя.
2. Забывание имени
Решение:
Имя включено прямо в системный промпт, а значит сохраняется стабильно.
3. Слишком быстрый переход на фамильярность
Решение:
Правила в системном промпте + ограничение эмоциональной близости.
4. Путаница времен года
Решение:
Фильтрация календарных выводов модели + использование timestamp последнего сообщения.
Использованный стек
Python
python-telegram-bot
OpenAI GPT-3.5-turbo (позднее обновлено до 4o-mini)
Fly.io как хостинг
локальное хранение (pickle + json)
простой словарь для представления состояния пользователя
Итоги
Выборочная долговременная память позволяет:
сохранять важные сведения о пользователе,
поддерживать устойчивую личность модели,
уменьшать стоимость контекста,
улучшать согласованность диалога,
контролировать поведение LLM на уровне промптов.
Главный вывод:
Эффективная работа LLM в диалоге зависит не от самой модели, а от слоя, который управляет памятью, контекстом и личностью.
Комментарии (3)

mikeinside
14.12.2025 08:37Тоже решаю задачу сохранения памяти у ИИ персонажа и что-то как-то все не нравится.
Может есть у кого хорошие мысли или ссылки, чтобы реализовать хорошую взрослую архитекутуру памяти?
NeriaLab
14.12.2025 08:37У LLM не может быть LTM, т.к. это фундаментальное ограничение, которое изначально не заложено в архитектуру. Все пляски с LTM - это всего лишь попытки улучшить STM. Большинство даже не понимают как должна работать LTM, не говоря уж про релизацию
Даже в когнитивно-символьных системах, где LTM изначально встроена в архитектуру, полностью взаимодействует с STM и локальной БЗ, то описание занимает несколько листов текста, а не "пару строк кода" как в статье
NeriaLab
То, что Вы описали - это даже не пародия на LTM, а только STM.