Свой чат с памятью диалогов на базе Ollama
Ставите Ollama, спрашиваете модель «как тебя зовут», она отвечает, а следующим сообщением пишете «а что я спросил только что» — и получаете растерянное «не знаю, о чём вы». Это не баг и не глупая модель: Ollama API по умолчанию не хранит контекст между запросами. Разберёмся, почему так устроено, и как получить нормальный чат с памятью — без сторонних облачных сервисов, на своём сервере.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Почему Ollama API не помнит предыдущие сообщения
Ollama — это HTTP-сервер поверх движка инференса, и его API спроектирован как stateless: каждый запрос к /api/generate или /api/chat обрабатывается независимо, без знания о том, что было раньше. Сервер не хранит сессии, не привязывает запросы к пользователю и не ведёт журнал переписки — он просто получает текст (или список сообщений), прогоняет через модель и отдаёт ответ.
Проверить это легко:
curl http://localhost:11434/api/chat -d '{
"model": "llama3.1",
"messages": [{"role": "user", "content": "Меня зовут Игорь"}],
"stream": false
}'
А следующим отдельным запросом:
curl http://localhost:11434/api/chat -d '{
"model": "llama3.1",
"messages": [{"role": "user", "content": "Как меня зовут?"}],
"stream": false
}'
Модель честно ответит, что не знает — потому что второй запрос физически не содержит информации о первом. Это архитектурное решение, а не недоработка: у самой большой языковой модели нет памяти между вызовами, есть только то, что лежит в поле messages конкретного запроса. Вся «память» диалога — это ответственность клиента, который вызывает API, а не сервера Ollama.
Здесь и разветвляются два практических пути: либо вы сами собираете историю и передаёте её целиком при каждом обращении, либо ставите перед Ollama обвязку, которая делает это за вас. Дальше — оба варианта с конкретными командами.
Вариант 1: передавать всю историю сообщений вручную
Самый прямой способ дать модели «память» — накапливать список сообщений на своей стороне и при каждом новом запросе отправлять весь диалог целиком, включая предыдущие реплики пользователя и ответы ассистента. Формат /api/chat для этого и создан: поле messages принимает массив с ролями system, user, assistant.
curl http://localhost:11434/api/chat -d '{
"model": "llama3.1",
"messages": [
{"role": "user", "content": "Меня зовут Игорь"},
{"role": "assistant", "content": "Приятно познакомиться, Игорь!"},
{"role": "user", "content": "Как меня зовут?"}
],
"stream": false
}'
Теперь модель увидит весь контекст в одном запросе и ответит правильно — потому что имя буквально присутствует в тексте, который она обрабатывает. Никакой магии: это просто конкатенация истории.
Минимальный рабочий пример на Python, который поддерживает список сообщений в памяти процесса:
import requests
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "llama3.1"
history = [{"role": "system", "content": "Ты — вежливый ассистент, отвечай кратко."}]
def ask(user_text: str) -> str:
history.append({"role": "user", "content": user_text})
resp = requests.post(OLLAMA_URL, json={
"model": MODEL,
"messages": history,
"stream": False
})
reply = resp.json()["message"]["content"]
history.append({"role": "assistant", "content": reply})
return reply
print(ask("Меня зовут Игорь"))
print(ask("Как меня зовут?"))
Работает надёжно, но у подхода есть очевидный минус: с каждым сообщением объём передаваемых токенов растёт. Модель на каждом запросе заново «читает» весь диалог с начала — это не бесплатно ни по времени обработки, ни по месту в контекстном окне (о нём — ниже). Для короткого чата на 10-20 реплик это несущественно, для многочасового диалога в саппорт-боте или ассистенте — уже проблема.
Чтобы история пережила перезапуск процесса, её стоит хранить не в оперативной памяти, а в файле или базе — например, SQLite с таблицей session_id, role, content, created_at, откуда список сообщений собирается перед каждым запросом к API.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть OllamaВариант 2: Open WebUI — память из коробки
Если не хочется писать и поддерживать свою обвязку, разумный выбор — Open WebUI: open-source веб-интерфейс, который изначально проектировался как полноценный чат с сессиями, а не голый API-клиент. Он хранит историю бесед в собственной базе (SQLite по умолчанию, можно вынести на PostgreSQL), показывает список чатов в боковой панели и подставляет предыдущие сообщения в каждый новый запрос к Ollama автоматически — вам не нужно писать код.
Быстрый запуск через Docker рядом с уже установленной Ollama:
docker run -d \
--name open-webui \
-p 3000:8080 \
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
-v open-webui:/app/backend/data \
--restart unless-stopped \
ghcr.io/open-webui/open-webui:main
На Linux вместо host.docker.internal может понадобиться указать реальный IP хоста или флаг --network host — поведение зависит от версии Docker и дистрибутива, проверяйте на своём сервере. Подробности пробрасывания сети и частые проблемы разобраны в статье про то, когда Open WebUI не видит Ollama.
После запуска на http://ваш-сервер:3000 вы получаете интерфейс, где каждый новый чат — это отдельная сессия с собственной историей: закрыли вкладку, вернулись через день — контекст диалога никуда не делся, потому что он лежит в базе, а не в оперативной памяти процесса. Именно так выглядит «память» в привычных чат-ботах вроде ChatGPT — только здесь всё хранится на вашем сервере.
Из практических плюсов: multi-user режим с логинами, поддержка нескольких моделей и переключение между ними прямо в диалоге, RAG по загруженным документам, экспорт истории. Из минусов — это уже не «чистый API», а полноценное веб-приложение: лишний слой, который тоже требует ресурсов и обновлений. Если задача — встроить память в свой продукт (телеграм-бота, внутренний сервис), путь через собственный код с хранением истории обычно проще, чем городить Open WebUI как прослойку.
Ограничение контекстного окна: где предел памяти
У обоих вариантов есть общий физический предел — размер контекстного окна модели. Это не про «сколько сообщений помнит чат-бот», а про то, сколько токенов модель вообще способна обработать за один вызов: system-промпт, вся история, новый вопрос и место под ответ должны уместиться в это окно целиком.
По умолчанию Ollama использует относительно скромное окно контекста (у многих моделей это 2048-4096 токенов, если не переопределено), что для длинного диалога может оказаться мало. Увеличить окно можно параметром num_ctx:
curl http://localhost:11434/api/chat -d '{
"model": "llama3.1",
"messages": [...],
"options": {"num_ctx": 8192},
"stream": false
}'
Либо зафиксировать значение постоянно через Modelfile:
FROM llama3.1
PARAMETER num_ctx 8192
ollama create llama3.1-longctx -f Modelfile
Важный нюанс: увеличение num_ctx напрямую увеличивает потребление видеопамяти или оперативной памяти — модель держит KV-кэш пропорционально размеру окна. На слабом сервере поднять окно до 32K может означать, что модель перестанет помещаться в доступную память вообще. Ориентируйтесь на конкретные цифры VRAM/RAM своего тарифа — точные значения зависят от модели и квантизации, универсального числа тут нет. Как оценить память под конкретную модель, разобрано в статье про расчёт RAM для Ollama.
Даже с увеличенным окном предел никуда не девается — он просто отодвигается. Рано или поздно диалог, который вы копите вручную или который копит Open WebUI, упрётся в потолок токенов. Дальше — что делать в этой ситуации.
Что делать, когда диалог становится слишком длинным
Когда история перестаёт помещаться в контекстное окно, есть три рабочих стратегии, и на практике их обычно комбинируют.
Обрезка по хвосту (sliding window). Простейший вариант — хранить только последние N сообщений, отбрасывая самые старые. Дёшево в реализации, но модель буквально забывает начало разговора — если там было что-то важное («у меня аллергия на X», «мой проект называется Y»), это потеряется без предупреждения.
Суммаризация старой части. Более аккуратный подход: когда история приближается к лимиту, отправить старые сообщения самой модели с просьбой сжать их в короткое резюме, заменить этим резюме исходные реплики, и продолжать диалог уже с ним вместо полного текста.
def summarize_and_compact(history, keep_last=6):
old_part = history[1:-keep_last] # без system-промпта и последних сообщений
if not old_part:
return history
summary_prompt = [
{"role": "system", "content": "Сожми диалог ниже в краткое резюме "
"(5-8 предложений), сохранив ключевые факты и договорённости."},
{"role": "user", "content": str(old_part)}
]
resp = requests.post(OLLAMA_URL, json={
"model": MODEL,
"messages": summary_prompt,
"stream": False
})
summary = resp.json()["message"]["content"]
return (
[history[0]] +
[{"role": "system", "content": f"Краткое содержание предыдущей части диалога: {summary}"}] +
history[-keep_last:]
)
Такой подход требует дополнительного вызова модели (значит, лишнего времени и небольшой нагрузки), а качество резюме зависит от модели — слабая модель может упустить детали при сжатии. Тем не менее это заметно лучше грубой обрезки: важные факты сохраняются в сжатом виде, а не исчезают целиком.
Векторная память (semantic memory). Продвинутый вариант — хранить всю историю в векторной базе и на каждый новый вопрос подтягивать не последние N сообщений, а наиболее релевантные по смыслу, независимо от того, когда они были сказаны. Это уже приближается к RAG-архитектуре и требует эмбеддингов и отдельного хранилища — оправдано для ассистентов с очень долгой историей общения, избыточно для простого чат-бота. Open WebUI умеет часть этого из коробки через встроенный RAG для документов, но полноценная семантическая память диалога — это уже отдельная надстройка вроде AnythingLLM поверх Ollama.
Для большинства практических сценариев (личный ассистент, внутренний чат-бот компании, саппорт с ограниченным контекстом задачи) достаточно комбинации: последние 10-15 сообщений хранить дословно, всё, что старше — сжимать в резюме раз в несколько ходов диалога.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть OllamaОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Ollama сама не может научиться помнить разговоры без обвязки?
Нет, это ограничение архитектуры API, а не временная недоработка. Сервер Ollama намеренно stateless — вся логика хранения истории должна жить на стороне клиента: вашего скрипта, бота или Open WebUI.
Что проще — своя обвязка на Python или Open WebUI?
Если нужен готовый чат-интерфейс для людей — Open WebUI быстрее по времени внедрения. Если память нужна внутри своего продукта (бот, API-сервис) — свой код с хранением истории в базе обычно даёт больше контроля и меньше лишних зависимостей.
Можно ли просто сделать контекстное окно очень большим и не думать о суммаризации?
Технически да через num_ctx, но это резко увеличивает потребление памяти и время генерации ответа — на практике для большинства серверных тарифов разумнее держать окно умеренным и сжимать старую историю, чем бесконечно расширять окно.
История в Open WebUI хранится локально или уходит куда-то наружу?
По умолчанию всё в локальной базе (SQLite/PostgreSQL) на вашем сервере, никуда не уходит — это одно из ключевых отличий self-hosted решения от облачных чат-ботов.
Что будет, если история сообщений превысит контекстное окно и её не сжать?
Ollama либо обрежет самые старые сообщения автоматически (поведение зависит от версии и настроек), либо вернёт ошибку — рассчитывать на предсказуемое поведение без явного контроля длины истории не стоит.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.