Память для LLM-чата на Python: история сообщений и ограничение контекста | AiManual
AiManual Logo Ai / Manual.
02 Апр 2026 Гайд

Реализация памяти для LLM-чата на Python: от простого history до ограничения контекста

Пошаговый гайд по реализации памяти для диалога с LLM на Python. Код для истории сообщений, подсчета токенов и ограничения длины контекста.

Проблема: LLM с амнезией

Ты пишешь чат-бота. Подключаешь API к GPT-5.5 (или к Claude 4, или к какой у тебя там локальной модели через Ollama). Отправляешь приветствие. 'Привет, как дела?' — 'Отлично, чем могу помочь?' — 'Меня зовут Вася'. — 'Приятно познакомиться, Вася!'. Все работает.

А потом спрашиваешь: 'Как меня зовут?'. И получаешь в ответ: 'Извините, я не знаю, как вас звать'.

Вот она, амнезия. Модель не помнит. Не потому что глупая. А потому что stateless. Каждый твой запрос — это чистый лист. Ты не передал предыдущие сообщения — модель их не видит. Все.

Это базовая проблема любого чата. И решается она одной фразой: история сообщений. Но если просто складывать все сообщения в массив, через 50 обменов твой контекст упрется в лимит токенов (даже у моделей 2026 года с контекстом в 128K или 1M). Запрос начнет стоить как обед в ресторане, а ответы станут странными — модель 'забудет' самое начало диалога.

Значит, нужна не просто память, а умная память с ограничением. Сегодня разберем, как это сделать на Python без лишних фреймворков.

Решение: не память, а история

Запомни раз и навсегда: у LLM нет памяти. У LLM есть контекстное окно. Ты передаешь список сообщений — модель их 'видит' и отвечает, учитывая. Ты не передал — модель не знает.

Твоя задача как разработчика — поддерживать этот список сообщений (историю диалога) и вовремя его обрезать, чтобы не вылезти за лимиты.

💡
Новые модели, такие как Gemini 2.5 Pro (релиз начала 2026) или обновленный Claude 3.7 Opus, имеют улучшенные алгоритмы работы с длинным контекстом, но физические лимиты токенов и стоимость запроса никуда не делись. Слать 100К токенов, когда нужно только 10К — расточительно.

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.

Алгоритм такой:

  1. В конце диалога (или периодически) выделяй из истории ключевые факты о пользователе.
  2. Сохраняй их в базу (просто JSON или PostgreSQL).
  3. В начале нового диалога загружай эти факты и добавляй в system prompt или первые сообщения контекста.

Это уже следующий уровень. Но начинать всегда нужно с простой истории сообщений.

💡
Если ты организуешь несколько чатов для разных задач (как советуем в статье про процесс разработки с LLM), то для каждого чата создавай отдельный экземпляр Conversation. Так контекст не будет перемешиваться, и модель будет 'специализироваться' на своей задаче.

Итоговый код: все вместе

Вот полный пример класса 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.

Дальше можно экспериментировать: сжимать старые сообщения в суммаризацию, выделять ключевые сущности, прикручивать векторную память. Но это уже темы для отдельного разговора.

Подписаться на канал