Во второй части у нас получился уже не одноразовый скрипт, а маленький консольный чат: программа принимает вопрос, отправляет его модели, печатает ответ и ждёт следующего ввода.
Но пока у этого чата есть важное ограничение: каждый новый запрос для модели почти независим.
Если сначала спросить:
Составь простой план изучения 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-инструмент — логика останется той же.
Вывод
Если смотреть на код трезво, мы сделали немного: добавили список сообщений и стали передавать его в запрос. Но именно это превращает скрипт в чат — модель начинает видеть разговор, а не набор одиночных запросов.
Если хотите пойти дальше
Если интересен более последовательный путь — с практикой, заданиями и развитием этого же проекта в сторону агентов и работы с документами, — подробности у меня в профиле.
Автор: kuz1


