Во второй части у нас получился уже не одноразовый скрипт, а маленький консольный чат: программа принимает вопрос, отправляет его модели, печатает ответ и ждёт следующего ввода.
Но у этого чата есть важное ограничение: каждый новый запрос для модели почти независим.
Если сначала спросить:
Составь простой план изучения Python на 2 недели.
а потом написать:
Сделай его короче и оставь только самое важное.
модель может ответить нормально. А может и не понять, к чему относится слово «его». Потому что для неё второй запрос — это просто новый отдельный вызов.
В этой части мы исправим это. Добавим историю сообщений, чтобы чат начал видеть предыдущие реплики и воспринимать разговор как единый диалог.
Что сделаем в этой части
- разберём, почему текущий чат не помнит контекст;
- добавим список сообщений conversation_history;
- научим программу передавать модели не только новый вопрос, но и предыдущие реплики;
- начнём сохранять в память пары user + assistant;
- добавим простое ограничение на размер истории;
- посмотрим, как меняется поведение чата после этого.
Почему чат без истории ещё не настоящий чат
Во второй части мы собирали запрос так: системная инструкция + текущий вопрос. Это значит, что при каждом новом вызове модель видит только две вещи: системную инструкцию и текущий вопрос пользователя. Предыдущие ответы и вопросы в запрос не попадают.
Для модели это выглядит так, будто каждый раз с ней разговаривают с нуля.
Именно поэтому без истории консольный чат остаётся скорее серией одиночных запросов, чем полноценным диалогом.
Как на самом деле работает память у LLM
У модели нет «памяти» между вызовами в человеческом смысле. Она не хранит ваш прошлый диалог между запросами.
Память в таких приложениях реализуется проще: сама программа хранит прошлые сообщения и передаёт их модели при каждом новом запросе.
То есть память нашего чата — это не магия и не особый режим Ollama. Это обычный Python-список.
Когда пользователь задаёт новый вопрос, мы отправляем не только его. Мы отправляем system + историю прошлых сообщений + новый user. Тогда модель видит контекст разговора.
Какие роли сообщений нам нужны
В запросе три роли: system, user, assistant. Для истории нужны только user и assistant — они и есть переписка. Роль system в историю не включается: это постоянная инструкция, которую добавляем в каждый запрос отдельно.
Порядок на каждом шаге цикла:
- пользователь вводит вопрос;
- программа собирает messages: system + история + новый user;
- модель отвечает;
- программа сохраняет в историю вопрос и ответ.
Пишем новый 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-инструмент — логика останется той же.
Если смотреть на код трезво, мы сделали немного: добавили список сообщений и стали передавать его в запрос. Но именно это превращает скрипт в чат — модель начинает видеть разговор, а не набор одиночных запросов.