Во второй части у нас получился уже не одноразовый скрипт, а маленький консольный чат: программа принимает вопрос, отправляет его модели, печатает ответ и ждёт следующего ввода.
Но пока у этого чата есть важное ограничение: каждый новый запрос для модели почти независим.
Если сначала спросить:
Составь простой план изучения Python на 2 недели.
а потом написать:
Сделай его короче и оставь только самое важное.
модель может ответить нормально. А может и не понять, к чему относится слово «его». Потому что для неё второй запрос — это просто новый отдельный вызов.
В этой части исправим именно это. Добавим историю сообщений, чтобы чат начал видеть предыдущие реплики и воспринимать разговор как единый диалог.
Серия статей
Часть 3. Добавляем историю сообщений и контекст ← вы здесь
Что сделаем в этой части
разберём, почему текущий чат не помнит контекст;
добавим список сообщений
conversation_history;научим программу передавать модели не только новый вопрос, но и предыдущие реплики;
начнём сохранять в память пары
user + assistant;добавим простое ограничение на размер истории;
посмотрим, как меняется поведение чата после этого.
Почему чат без истории ещё не настоящий чат
Во второй части мы собирали запрос так:
messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_message}, ]
Это значит, что при каждом новом вызове модель видит только две вещи: системную инструкцию и текущий вопрос пользователя. Предыдущие ответы и предыдущие вопросы в запрос не попадают.
Для модели это выглядит так, будто каждый раз с ней разговаривают с нуля.
Именно поэтому без истории консольный чат остаётся скорее серией одиночных запросов, чем полноценным диалогом.
Как на самом деле работает память у LLM
У модели нет «памяти» между вызовами в человеческом смысле. Она не хранит ваш прошлый диалог где-то внутри себя между запросами.
Память в таких приложениях делается проще: сама программа хранит прошлые сообщения и снова передаёт их модели при следующем запросе.
То есть память нашего чата — это не магия и не особый режим Ollama. Это обычный Python-список:
conversation_history = [ {"role": "user", "content": "Составь план изучения Python"}, {"role": "assistant", "content": "Вот простой план на 2 недели..."}, ]
Когда пользователь задаёт новый вопрос, мы не отправляем только его одного. Мы отправляем system + историю прошлых сообщений + новый user. И тогда модель уже видит контекст разговора.
Какие роли сообщений нам нужны
В запросе три роли: system, user, assistant. Для истории нужны только user и assistant — они и есть переписка. Роль system в историю не идёт: это постоянная инструкция, которую добавляем в каждый запрос отдельно.
Порядок на каждом шаге цикла:
пользователь вводит вопрос;
программа собирает
messages:system+ история + новыйuser;модель отвечает;
программа сохраняет в историю вопрос и ответ.
Пишем новый main.py
Откройте main.py из второй части и замените содержимое целиком:
# -*- coding: utf-8 -*- import time from typing import Optional from litellm import completion MODEL = "ollama_chat/qwen2.5:3b" API_BASE = "http://localhost:11434" SYSTEM_PROMPT = "Ты полезный ассистент. Отвечай кратко и по делу." MAX_HISTORY_MESSAGES = 6 def trim_history(history: list, limit: int) -> list: if len(history) <= limit: return history return history[-limit:] def send_request_to_llm(user_message: str, history: list) -> Optional[str]: messages = [ {"role": "system", "content": SYSTEM_PROMPT}, *history, {"role": "user", "content": user_message}, ] try: start_time = time.time() response = completion( model=MODEL, messages=messages, api_base=API_BASE, request_timeout=120, ) duration = time.time() - start_time print(f"\nВремя генерации: {duration:.2f} сек") return response.choices[0].message.content except Exception as e: print(f"\nОшибка при запросе: {e}") return None def main() -> None: print("Локальный ИИ-ассистент с памятью запущен.") print("Введите вопрос или 'выход' для завершения.\n") conversation_history = [] while True: user_input = input("Вы: ").strip() if user_input.lower() in ("выход", "exit", "quit"): print("До свидания!") break if not user_input: print("Введите вопрос.") continue print("\nМодель думает...") answer = send_request_to_llm(user_input, conversation_history) if answer is not None: print(f"\nИИ: {answer}\n") conversation_history.append({"role": "user", "content": user_input}) conversation_history.append({"role": "assistant", "content": answer}) conversation_history = trim_history(conversation_history, MAX_HISTORY_MESSAGES) else: print("\nНе удалось получить ответ. Проверьте, запущена ли Ollama.\n") if __name__ == "__main__": main()
Запустите:
python main.py
Проверьте на вопросах, где нужен контекст:
Вы: Составь план изучения Python на 2 недели. ИИ: ... Вы: Сделай его короче. ИИ: ... Вы: Добавь в него практику по 20 минут в день. ИИ: ...
Теперь модель держит нить разговора.
Разберём код
Константы вверху файла
MAX_HISTORY_MESSAGES = 6
Сколько сообщений максимум хранить в истории. Почему это нужно — объясним ниже при разборе trim_history.
conversation_history = []
В начале main() создаём пустой список. В нём живёт память текущего диалога. Пока программа работает — список растёт. Закрыли скрипт — память пропала. Для этой статьи этого достаточно.
send_request_to_llm(user_message, history) — теперь принимает историю
Раньше функция принимала только строку. Теперь принимает ещё и список прошлых сообщений.
messages собирается из трёх частей:
messages = [ {"role": "system", "content": SYSTEM_PROMPT}, *history, {"role": "user", "content": user_message}, ]
Порядок принципиален: system → история → новый вопрос. Оператор *history разворачивает список сообщений внутрь нового списка. Если история пустая — всё работает нормально.
Сохраняем обе реплики
После успешного ответа записываем в историю сразу две записи:
conversation_history.append({"role": "user", "content": user_input}) conversation_history.append({"role": "assistant", "content": answer})
Это важно. Если сохранить только вопрос пользователя и не сохранить ответ модели, на следующем шаге контекст сломается: модель увидит что пользователь что-то спрашивал, но не увидит что сама отвечала.
trim_history — ограничиваем размер истории
def trim_history(history: list, limit: int) -> list: if len(history) <= limit: return history return history[-limit:]
Без ограничения история растёт бесконечно: запросы становятся тяжелее, модель получает слишком много старых сообщений. MAX_HISTORY_MESSAGES = 6 означает, что в памяти хранятся последние 6 сообщений — три последних обмена репликами. Для стартовой версии достаточно.
Главная идея всей структуры: send_request_to_llm по-прежнему отвечает только за запрос к модели. Управление историей — снаружи, в main(). Логика не смешана.
Что изменилось в поведении чата
После запуска попробуйте такую цепочку:
Вы: Назови три фреймворка для Python. ИИ: Django, Flask, FastAPI. Вы: Расскажи подробнее про второй. ИИ: Flask — минималистичный веб-фреймворк...
Без истории на второй вопрос модель не знала бы, что такое «второй». Теперь знает — потому что видит предыдущую реплику.
Ограничение этой версии
История живёт только в памяти текущего запуска. Закрыли скрипт — пропала. Для учебного примера это нормально, для реального приложения нужно сохранение на диск или в базу.
Что у нас получилось за три части
За эту серию мы собрали работающую основу LLM-приложения:
подняли локальную модель через Ollama без API-ключей и без интернета;
подключили её к Python через LiteLLM;
сделали консольный чат с system prompt и обработкой ошибок;
добавили историю сообщений и ограничение её размера.
Этот main.py можно взять как основу и встроить в Telegram-бота, веб-сервис или CLI-инструмент — логика останется той же.
Вывод
Если смотреть на код трезво, мы сделали немного: добавили список сообщений и стали передавать его в запрос. Но именно это превращает скрипт в чат — модель начинает видеть разговор, а не набор одиночных запросов.
Если хотите пойти дальше
Если интересен более последовательный путь — с практикой, заданиями и развитием этого же проекта в сторону агентов и работы с документами, — подробности у меня в профиле.
