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 = "nnПамять: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 в диалоге зависит не от самой модели, а от слоя, который управляет памятью, контекстом и личностью.
Автор: florid696


