Память для LLM-чата на Python. Часть 3: добавляем историю сообщений и контекст

Память для LLM-чата на Python. Часть 3: добавляем историю сообщений и контекст

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

Но у этого чата есть важное ограничение: каждый новый запрос для модели почти независим.

Если сначала спросить:

Составь простой план изучения Python на 2 недели.

а потом написать:

Сделай его короче и оставь только самое важное.

модель может ответить нормально. А может и не понять, к чему относится слово «его». Потому что для неё второй запрос — это просто новый отдельный вызов.

В этой части мы исправим это. Добавим историю сообщений, чтобы чат начал видеть предыдущие реплики и воспринимать разговор как единый диалог.

Что сделаем в этой части

  • разберём, почему текущий чат не помнит контекст;
  • добавим список сообщений conversation_history;
  • научим программу передавать модели не только новый вопрос, но и предыдущие реплики;
  • начнём сохранять в память пары user + assistant;
  • добавим простое ограничение на размер истории;
  • посмотрим, как меняется поведение чата после этого.

Почему чат без истории ещё не настоящий чат

Во второй части мы собирали запрос так: системная инструкция + текущий вопрос. Это значит, что при каждом новом вызове модель видит только две вещи: системную инструкцию и текущий вопрос пользователя. Предыдущие ответы и вопросы в запрос не попадают.

Для модели это выглядит так, будто каждый раз с ней разговаривают с нуля.

Именно поэтому без истории консольный чат остаётся скорее серией одиночных запросов, чем полноценным диалогом.

Как на самом деле работает память у LLM

У модели нет «памяти» между вызовами в человеческом смысле. Она не хранит ваш прошлый диалог между запросами.

Память в таких приложениях реализуется проще: сама программа хранит прошлые сообщения и передаёт их модели при каждом новом запросе.

То есть память нашего чата — это не магия и не особый режим Ollama. Это обычный Python-список.

Когда пользователь задаёт новый вопрос, мы отправляем не только его. Мы отправляем system + историю прошлых сообщений + новый user. Тогда модель видит контекст разговора.

Какие роли сообщений нам нужны

В запросе три роли: system, user, assistant. Для истории нужны только user и assistant — они и есть переписка. Роль system в историю не включается: это постоянная инструкция, которую добавляем в каждый запрос отдельно.

Порядок на каждом шаге цикла:

  1. пользователь вводит вопрос;
  2. программа собирает messages: system + история + новый user;
  3. модель отвечает;
  4. программа сохраняет в историю вопрос и ответ.

Пишем новый main.py

Откройте main.py из второй части и замените содержимое целиком.

Запустите программу.

Проверьте на вопросах, где нужен контекст.

Теперь модель держит нить разговора.

Разберём код

Константы вверху файла

MAX_HISTORY_MESSAGES — сколько сообщений максимум хранить в истории. Почему это нужно — объясним ниже при разборе trim_history.

conversation_history = []

В начале main() создаём пустой список. В нём живёт память текущего диалога. Пока программа работает — список растёт. Закрыли скрипт — память пропала. Для этой статьи этого достаточно.

send_request_to_llm(user_message, history) — теперь принимает историю

Раньше функция принимала только строку. Теперь она принимает и список прошлых сообщений.

Список messages собирается из трёх частей:

Порядок принципиален: system → история → новый вопрос. Оператор *history разворачивает список сообщений внутрь нового списка. Если история пустая — всё работает нормально.

Сохраняем обе реплики

После успешного ответа записываем в историю сразу две записи: вопрос пользователя и ответ модели.

Это важно. Если сохранить только вопрос, а ответ модели не сохранить, на следующем шаге контекст сломается: модель увидит, что пользователь что-то спрашивал, но не увидит, что сама уже отвечала.

trim_history — ограничиваем размер истории

Без ограничения история растёт бесконечно: запросы становятся тяжелее, модель получает слишком много старых сообщений. MAX_HISTORY_MESSAGES = 6 означает, что в памяти хранятся последние 6 сообщений — три последних обмена репликами. Для стартовой версии достаточно.

Главная идея всей структуры: send_request_to_llm по-прежнему отвечает только за запрос к модели. Управление историей — снаружи, в main(). Логика не смешана.

Что изменилось в поведении чата

После запуска попробуйте такую цепочку:

Без истории на второй вопрос модель не знала бы, что такое «второй». Теперь знает — потому что видит предыдущую реплику.

Ограничение этой версии

История живёт только в памяти текущего запуска. Закрыли скрипт — пропала. Для учебного примера это нормально, для реального приложения нужно сохранение на диск или в базу.

Что у нас получилось за три части

За серию мы собрали работающую основу LLM-приложения:

  • подняли локальную модель через Ollama без API-ключей и интернета;
  • подключили её к Python через LiteLLM;
  • сделали консольный чат с system prompt и обработкой ошибок;
  • добавили историю сообщений и ограничение её размера.

Этот main.py можно взять как основу и встроить в Telegram-бота, веб-сервис или CLI-инструмент — логика останется той же.

Если смотреть на код трезво, мы сделали немного: добавили список сообщений и стали передавать его в запрос. Но именно это превращает скрипт в чат — модель начинает видеть разговор, а не набор одиночных запросов.

Читать оригинал