Streamlit превращает Python-скрипт в веб-приложение, а chat_input, chat_message и session_state закрывают базовый интерфейс диалога. Мы подключим локальную Ollama, покажем историю после каждого rerun, выведем ответ потоком и добавим очистку. HTML и JavaScript для первой версии не понадобятся.
Streamlit удобен для быстрой проверки учебного помощника или чата по документу. При взаимодействии скрипт запускается сверху вниз заново, поэтому обычная переменная потеряет историю. Session State сохраняет данные текущей сессии между rerun. Локальная Ollama даёт ответ без платного API, весь проект помещается в app.py.
chat_message показывает роли, chat_input остаётся внизу страницы.
session_state сохраняет историю между перезапусками скрипта.
Ollama отдаёт chunks, а write_stream печатает их по мере появления.
Почему Streamlit перезапускает код и не теряет чат
При первом открытии код создаёт messages с приветствием. Цикл рисует историю. chat_input возвращает новый вопрос, который сразу добавляется в состояние. В контейнере assistant поток Ollama передаётся st.write_stream. Функция показывает части и возвращает собранный текст для messages. На следующем rerun весь диалог рисуется снова. Кнопка sidebar сбрасывает список.
| Элемент | Когда работает | Что хранит |
|---|---|---|
| st.session_state | На каждом rerun сессии | Список сообщений |
| st.chat_message | Во время отрисовки | Контейнер одной роли |
| st.chat_input | При отправке формы чата | Текущий prompt |
| st.write_stream | Пока идут chunks | Показывает и собирает ответ |
| st.sidebar.button | При клике | Сбрасывает историю |
История существует не в браузерном HTML и не внутри модели. Это Python-объект в session_state конкретного WebSocket-соединения. Перезагрузка вкладки или потеря соединения может создать новую сессию, поэтому постоянное хранение является отдельной задачей.

Состояние проходит через весь файл. Следующий rerun снова начинается с сохранённого state
Пишем app.py с полем, ответом и историей
Создайте проект командами uv init streamlit-chat и uv add streamlit ollama. Убедитесь, что локальная модель gemma3:1b отвечает через Ollama. Сохраните код в app.py и запустите uv run streamlit run app.py. Генератор ollama_tokens получает копию messages и отдаёт только непустой текст каждого chunk. В случае ошибки приложение показывает st.error и не добавляет сломанный ответ в историю.
import streamlit as st
from ollama import ResponseError, chat
MODEL = "gemma3:1b"
WELCOME = "Привет! Спросите меня о Python."
st.title("Учебный помощник")
if "messages" not in st.session_state:
st.session_state.messages = [{"role": "assistant", "content": WELCOME}]
if st.sidebar.button("Очистить историю"):
st.session_state.messages = [{"role": "assistant", "content": WELCOME}]
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
def tokens(messages):
for chunk in chat(model=MODEL, messages=messages, stream=True):
if chunk.message.content:
yield chunk.message.content
if prompt := st.chat_input("Задайте вопрос о Python"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
try:
with st.chat_message("assistant"):
answer = st.write_stream(tokens(st.session_state.messages))
except (ConnectionError, ResponseError) as error:
st.error(f"Модель не отвечает: {error}")
else:
st.session_state.messages.append(
{"role": "assistant", "content": answer})- Страница показывает приветствие, историю ролей и поле внизу.
- После отправки вопрос появляется сразу, а ответ печатается потоком.
- Следующий вопрос видит предыдущий диалог из session_state.
- Кнопка очистки оставляет одно приветствие.
- При недоступной Ollama на странице появляется ошибка без белого экрана.

Разница видна после первого клика. Постоянная база данных всё равно является отдельным слоем
Как связаны session_state, роли и streaming
Streamlit повторно выполняет файл и связывает виджеты с сессией. Условная инициализация не даёт перезаписать messages. Цикл восстанавливает историю, новый вопрос рисуется отдельно в текущем проходе. st.write_stream показывает строки генератора и возвращает итоговый текст. При исключении ветка else не выполняется, поэтому незавершённый ответ не сохраняется.
| Событие | Что делает скрипт | Что видит пользователь |
|---|---|---|
| Первое открытие | Создаёт messages | Приветствие и пустое поле |
| Отправка prompt | Rerun и append user | Свой вопрос сразу |
| Поток модели | yield chunks | Постепенный ответ |
| Успешный конец | append assistant | История готова к следующему вопросу |
| Ошибка | st.error без append | Понятная причина |
| Очистка | Заменяет список | Только приветствие |
Session State разделён по сессиям, но связан с WebSocket и не является базой. Не храните бесконечную историю: каждый запрос снова передаёт messages модели. Ограничьте последние реплики или делайте сводку. Не переносите messages в глобальную переменную. cache_resource подходит общему тяжёлому клиенту, но не личной истории.
Проверяем несколько сессий и ошибки модели
Сначала временно замените ollama_tokens генератором из трёх строк и убедитесь, что write_stream возвращает склеенный текст. Затем подключите Ollama. Откройте приложение в двух разных браузерах или обычном и приватном окне: истории не должны смешиваться. Остановите сервис и проверьте st.error. После восстановления отправьте второй вопрос без перезагрузки. Добавьте вывод len(st.session_state.messages) в sidebar и проследите рост списка. В качестве улучшения ограничьте историю приветствием и последними десятью сообщениями.
- Запустите приложение с фиктивным генератором из трёх chunks.
- Подключите Ollama и задайте два связанных вопроса.
- Откройте вторую браузерную сессию и сравните истории.
- Остановите Ollama, получите st.error и снова запустите сервис.
- Очистите историю и проверьте, что осталось одно приветствие.
- Добавьте ограничение до десяти последних сообщений без удаления приветствия.
- История переживает rerun, но не смешивается между сессиями.
- Streaming выводит текст и возвращает полный answer.
- Сбой модели не записывается как успешный ответ.
- Очистка и ограничение истории работают предсказуемо.

До добавления файлов и настроек. Интерфейс устойчив без дополнительных функций
Доводим прототип до удобного учебного приложения
Дальше добавьте выбор модели, режим ответа или один текстовый файл. Для каждого элемента решите: session_state, общий кэш или база. Локальная Ollama на ноутбуке не станет доступна серверу Streamlit после публикации. Хостингу нужен свой endpoint, ресурсы и доступ. Если приложение локальное, напишите это в интерфейсе.
Как изучить тему в Кодике
В Кодике эту тему удобно разделить на два слоя. Сначала изучите Python-списки, словари, функции и генераторы. Затем соберите Streamlit-интерфейс с фиктивным ответом. Ollama подключается последней. Так вы сможете определить, ошибка находится в состоянии страницы, потоке или модели.
| Шаг | Что изучить | Контрольная задача |
|---|---|---|
| 1 | Списки и словари | Хранить роли и content |
| 2 | Функции и yield | Отдать три chunks |
| 3 | Условия и исключения | Не записать ошибочный answer |
| 4 | Streamlit Session State | Пережить rerun |
| 5 | Ollama chat | Подключить локальную модель |
В тренажёре Кодика реализуйте список messages и функцию генератора без UI. Затем восстановите пять ключевых строк Streamlit: инициализацию, цикл истории, chat_input, chat_message и write_stream. Когда вы понимаете эти точки, оформление и новые режимы добавляются без переписывания приложения.
На следующем rerun история начинается заново. Используйте session_state.
Streamlit сам перезапускает файл при событиях. Цикл блокирует страницу.
Пользователи видят чужие сообщения. Храните чат в сессии.
Записывайте assistant message только в ветке успешного завершения.
Что получится в итоге

Веб-приложение показывает отдельные роли, печатает локальный ответ потоком, хранит историю текущей сессии и корректно переживает ошибку модели.
Почему обычный список не подходит для истории?
Streamlit выполняет файл заново. Обычная переменная создаётся повторно, а session_state сохраняется в сессии.
Что возвращает write_stream для текстового потока?
Он показывает chunks и возвращает собранную строку, которую можно сохранить как ответ.
Является ли session_state постоянной базой?
Нет. Он связан с пользовательской сессией и WebSocket, поэтому постоянное хранение нужно проектировать отдельно.
Как понять, что проект действительно работает?
Проверьте основной сценарий, ошибочный ввод, повторный запуск и один граничный случай. Затем объясните вслух, где появляются данные, кто принимает решение и где хранится состояние.
Можно ли начать с готового кода из статьи?
Да. После первого запуска измените один параметр, добавьте одну проверку и повторите проект без подсказки. Так пример превращается в собственный навык.
Списки, функции и исключения закрепите в курсе Python. Локальную модель подключайте после статьи про Ollama и Python.
Для обычного веб-бэкенда сравните подход со статьёй про Flask и HTML, а работу со внешними запросами повторите в разборе API на Python. В Кодике сначала восстановите UI с фиктивным ответом, затем включите модель.