Проблема: LLM с амнезией
Ты пишешь чат-бота. Подключаешь API к GPT-5.5 (или к Claude 4, или к какой у тебя там локальной модели через Ollama). Отправляешь приветствие. 'Привет, как дела?' — 'Отлично, чем могу помочь?' — 'Меня зовут Вася'. — 'Приятно познакомиться, Вася!'. Все работает.
А потом спрашиваешь: 'Как меня зовут?'. И получаешь в ответ: 'Извините, я не знаю, как вас звать'.
Вот она, амнезия. Модель не помнит. Не потому что глупая. А потому что stateless. Каждый твой запрос — это чистый лист. Ты не передал предыдущие сообщения — модель их не видит. Все.
Это базовая проблема любого чата. И решается она одной фразой: история сообщений. Но если просто складывать все сообщения в массив, через 50 обменов твой контекст упрется в лимит токенов (даже у моделей 2026 года с контекстом в 128K или 1M). Запрос начнет стоить как обед в ресторане, а ответы станут странными — модель 'забудет' самое начало диалога.
Значит, нужна не просто память, а умная память с ограничением. Сегодня разберем, как это сделать на Python без лишних фреймворков.
Решение: не память, а история
Запомни раз и навсегда: у LLM нет памяти. У LLM есть контекстное окно. Ты передаешь список сообщений — модель их 'видит' и отвечает, учитывая. Ты не передал — модель не знает.
Твоя задача как разработчика — поддерживать этот список сообщений (историю диалога) и вовремя его обрезать, чтобы не вылезти за лимиты.
1 Голый массив — лучше, чем ничего
Начнем с самого простого. Просто список словарей. Каждое сообщение — роль и контент.
conversation_history = []
# Функция для добавления сообщения пользователя
history.append({"role": "user", "content": "Привет, меня зовут Вася"})
# После получения ответа от LLM добавляем его
history.append({"role": "assistant", "content": "Привет, Вася! Рад познакомиться."})
# Теперь, делая новый запрос, мы шлем всю историю
response = client.chat.completions.create(
model="gpt-5.5-turbo", # Актуальная модель на 02.04.2026
messages=conversation_history,
max_tokens=500
)
Это работает. Модель теперь помнит, что тебя зовут Вася. Но история растет. Бесконечно.
2 System prompt — твой лучший друг
Роль 'system' — это инструкция для модели. Ее обычно ставят в самое начало истории, и она задает тон всему диалогу. Ее нужно сохранять при любом обрезании контекста.
SYSTEM_PROMPT = "Ты — полезный ассистент. Отвечай вежливо и кратко."
# Инициализируем историю с system prompt
conversation_history = [
{"role": "system", "content": SYSTEM_PROMPT}
]
Теперь наш чат имеет личность. Но история все еще растет.
Частая ошибка: добавлять system prompt перед каждым запросом. Не делай так. Добавь его один раз в начало истории и больше не трогай. Иначе потратишь токены впустую и можешь запутать модель.
3 Считаем токены, а не символы
Лимиты у API — всегда в токенах. 1 токен ≠ 1 символ. Для английского ~1 токен = 4 символа, для русского ~1 токен = 2-3 символа. Нужно считать точно.
Используй библиотеку tiktoken от OpenAI. Она умеет работать с токенизаторами GPT, Claude и других популярных моделей (обновлена для актуальных моделей 2026 года).
import tiktoken
def count_tokens_in_messages(messages, model="gpt-5.5-turbo"):
"""Возвращает общее количество токенов в списке сообщений."""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base") # Часто используется
tokens_per_message = 3 # Базовая накладка для каждого сообщения
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(encoding.encode(value))
num_tokens += 3 # Завершающая накладка
return num_tokens
Теперь мы можем точно измерить, сколько места занимает наша история.
4 Обрезаем историю по уму
Когда токенов становится больше лимита (например, 8000 из 8192), нужно удалять старые сообщения. Но не все подряд. System prompt оставляем. Удаляем самые старые пары 'user'/'assistant'.
def trim_conversation_history(history, model, max_tokens=8000):
"""Обрезает историю, оставляя system prompt и последние сообщения."""
current_tokens = count_tokens_in_messages(history, model)
# Если все в порядке, возвращаем как есть
if current_tokens <= max_tokens:
return history
# Находим индекс system prompt (обычно первый)
system_message = history[0] if history[0]["role"] == "system" else None
# Оставляем system prompt и все, что после него (диалог)
dialog_messages = history[1:] if system_message else history
# Удаляем самые старые пары (user + assistant), пока не уложимся в лимит
while current_tokens > max_tokens and len(dialog_messages) > 1:
# Удаляем самую старую пару (первые два сообщения диалога)
removed = dialog_messages.pop(0)
# Если следующее сообщение той же роли? Обычно чередуется, но на всякий случай
if dialog_messages and dialog_messages[0]["role"] == removed["role"]:
dialog_messages.pop(0)
# Пересчитываем токены (можно оптимизировать, но для ясности оставим так)
new_history = [system_message] + dialog_messages if system_message else dialog_messages
current_tokens = count_tokens_in_messages(new_history, model)
# Собираем финальную историю
final_history = [system_message] + dialog_messages if system_message else dialog_messages
return final_history
Перед каждым запросом к API вызывай эту функцию. История будет автоматически подрезаться.
5 Класс-обертка для чистоты кода
Таскать глобальные переменные и функции — не комильфо. Создадим простой класс Conversation.
class Conversation:
def __init__(self, system_prompt="", model="gpt-5.5-turbo", max_context_tokens=8000):
self.model = model
self.max_context_tokens = max_context_tokens
self.messages = []
if system_prompt:
self.messages.append({"role": "system", "content": system_prompt})
def add_user_message(self, content):
self.messages.append({"role": "user", "content": content})
def add_assistant_message(self, content):
self.messages.append({"role": "assistant", "content": content})
def get_messages_for_api(self):
"""Возвращает обрезанную историю для отправки в API."""
return trim_conversation_history(self.messages, self.model, self.max_context_tokens)
def clear(self):
"""Очищает историю, но оставляет system prompt."""
system_msg = self.messages[0] if self.messages and self.messages[0]["role"] == "system" else None
self.messages = [system_msg] if system_msg else []
Теперь использование становится тривиальным:
chat = Conversation(system_prompt="Ты — саркастичный бот.")
chat.add_user_message("Привет")
# ... получаешь ответ от API ...
chat.add_assistant_message("О, привет. Чего надо?")
# Когда нужно отправить следующий запрос:
messages_to_send = chat.get_messages_for_api()
response = client.chat.completions.create(model=chat.model, messages=messages_to_send)
chat.add_assistant_message(response.choices[0].message.content)
Ошибки, которые сломают ваш чат
Собрал историю? Подсчитал токены? Все равно что-то идет не так. Вот частые косяки.
| Ошибка | Что происходит | Как исправить |
|---|---|---|
| Счетчик токенов не соответствует API | Ты считаешь 7500 токенов, а API выдает ошибку 'context length exceeded'. Потому что у модели лимит 8192, но API резервирует токены под внутренние нужды. | Всегда оставляй запас. Если лимит 8192, обрезай до 7500-7800. Проверь документацию конкретного провайдера. |
| Обрезается только по одному сообщению | Удаляешь одно старое сообщение, токенов все равно много. Удаляешь еще одно. Запрос становится медленным. | Удаляй пары (user+assistant) сразу. Или реализуй бинарный поиск по количеству удаляемых сообщений. |
| System prompt тоже обрезается | В пылу обрезки забыл проверить роль и удалил системное сообщение. Модель теряет инструкции. | Всегда проверяй role == 'system' и исключай его из процесса удаления. |
| История не очищается между сессиями | Пользователь №1 закончил диалог. Пользователь №2 получает в контексте обрывки чужого разговора. | Привязывай объект Conversation к сессии (например, к ID пользователя в Telegram). Или явно вызывай chat.clear() при старте нового диалога. |
Самое противное — это когда обрезание контекста ломает логику диалога. Удалили ключевое сообщение, где пользователь указал свои требования, и модель начинает нести чушь. Это называется контекстный рот, и скользящее окно здесь не всегда спасает.
А что если нужна настоящая долговременная память?
Описанный подход — это short-term memory. Он работает для одного диалога. Но что если твой бот должен помнить, что пользователь любит кофе, а не чай, в течение недель?
Тогда нужно подключать долговременную память. Базу данных. Векторные поиски. Или специальные системы вроде тех, что мы разбирали в статье про системы долговременной памяти для LLM.
Алгоритм такой:
- В конце диалога (или периодически) выделяй из истории ключевые факты о пользователе.
- Сохраняй их в базу (просто JSON или PostgreSQL).
- В начале нового диалога загружай эти факты и добавляй в system prompt или первые сообщения контекста.
Это уже следующий уровень. Но начинать всегда нужно с простой истории сообщений.
Итоговый код: все вместе
Вот полный пример класса Conversation с подсчетом токенов и обрезкой. Готов к использованию.
import tiktoken
class Conversation:
"""Управление историей диалога с LLM с ограничением контекста."""
def __init__(self, system_prompt="", model="gpt-5.5-turbo", max_context_tokens=8000):
"""
Args:
system_prompt: Системный промпт, который всегда остается в контексте.
model: Название модели (для выбора токенизатора).
max_context_tokens: Максимальное количество токенов для отправки в API.
"""
self.model = model
self.max_context_tokens = max_context_tokens
self.messages = []
if system_prompt:
self.messages.append({"role": "system", "content": system_prompt})
def _count_tokens(self, messages):
"""Подсчет токенов в списке сообщений."""
try:
encoding = tiktoken.encoding_for_model(self.model)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base")
tokens_per_message = 3
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(encoding.encode(value))
num_tokens += 3
return num_tokens
def _trim_messages(self):
"""Обрезает историю сообщений, чтобы уложиться в лимит токенов."""
current_tokens = self._count_tokens(self.messages)
if current_tokens <= self.max_context_tokens:
return self.messages
# Сохраняем system prompt
system_message = None
if self.messages and self.messages[0]["role"] == "system":
system_message = self.messages[0]
dialog = self.messages[1:]
else:
dialog = self.messages[:]
# Удаляем пары с начала, пока не уложимся в лимит
while current_tokens > self.max_context_tokens and len(dialog) > 1:
# Удаляем два самых старых сообщения диалога (user + assistant)
dialog.pop(0)
if dialog and dialog[0]["role"] != "user": # Если следующее не user, удаляем еще одно
dialog.pop(0)
# Собираем временную историю для пересчета
temp_messages = [system_message] + dialog if system_message else dialog
current_tokens = self._count_tokens(temp_messages)
# Финальная сборка
self.messages = [system_message] + dialog if system_message else dialog
return self.messages
def add_user_message(self, content):
self.messages.append({"role": "user", "content": content})
def add_assistant_message(self, content):
self.messages.append({"role": "assistant", "content": content})
def get_messages(self):
"""Возвращает обрезанную историю для отправки в API."""
return self._trim_messages()
def clear(self, keep_system=True):
"""Очищает историю."""
if keep_system and self.messages and self.messages[0]["role"] == "system":
self.messages = [self.messages[0]]
else:
self.messages = []
def get_token_count(self):
"""Возвращает текущее количество токенов в истории."""
return self._count_tokens(self.messages)
# Пример использования
if __name__ == "__main__":
chat = Conversation("Ты — помощник по Python.", "gpt-5.5-turbo", max_context_tokens=4000)
# Имитация диалога
for i in range(20):
chat.add_user_message(f"Вопрос номер {i}")
chat.add_assistant_message(f"Ответ номер {i}")
print(f"Токенов до обрезки: {chat.get_token_count()}")
messages_to_send = chat.get_messages()
print(f"Токенов после обрезки: {chat.get_token_count()}")
print(f"Сообщений осталось: {len(chat.messages)}")
Теперь у тебя есть основа. Добавляй сюда логирование, персистентность (сохранение истории в файл), обработку разных моделей — и получишь production-ready решение для памяти чат-бота.
А самое главное — ты понимаешь, как это работает изнутри. Не просто копируешь код из интернета, а знаешь, зачем каждый if и while.
Дальше можно экспериментировать: сжимать старые сообщения в суммаризацию, выделять ключевые сущности, прикручивать векторную память. Но это уже темы для отдельного разговора.