БэкендData и ИИPython

Как сделать ИИ-приложение на Python в Streamlit: поле, ответ и история

Создаём ИИ-приложение на Python в Streamlit: чат-поле, сообщения, локальная модель Ollama, потоковый ответ, session_state и кнопка очистки истории.

Кодик

Автор

7 мин чтения

Streamlit превращает Python-скрипт в веб-приложение, а chat_input, chat_message и session_state закрывают базовый интерфейс диалога. Мы подключим локальную Ollama, покажем историю после каждого rerun, выведем ответ потоком и добавим очистку. HTML и JavaScript для первой версии не понадобятся.

Streamlit удобен для быстрой проверки учебного помощника или чата по документу. При взаимодействии скрипт запускается сверху вниз заново, поэтому обычная переменная потеряет историю. Session State сохраняет данные текущей сессии между rerun. Локальная Ollama даёт ответ без платного API, весь проект помещается в app.py.

1Рисуем интерфейс

chat_message показывает роли, chat_input остаётся внизу страницы.

2Храним состояние

session_state сохраняет историю между перезапусками скрипта.

3Получаем ответ

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 Streamlit-чата
Состояние проходит через весь файл. Следующий rerun снова начинается с сохранённого state

Соберите интерфейс до подключения модели. Сначала замените answer на фиксированную строку и проверьте rerun, роли и очистку. Если история исчезает, Ollama не поможет. Подключайте медленный внешний компонент только после устойчивого состояния UI.

Пишем 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
Разница видна после первого клика. Постоянная база данных всё равно является отдельным слоем

Как связаны session_state, роли и streaming

Streamlit повторно выполняет файл и связывает виджеты с сессией. Условная инициализация не даёт перезаписать messages. Цикл восстанавливает историю, новый вопрос рисуется отдельно в текущем проходе. st.write_stream показывает строки генератора и возвращает итоговый текст. При исключении ветка else не выполняется, поэтому незавершённый ответ не сохраняется.

СобытиеЧто делает скриптЧто видит пользователь
Первое открытиеСоздаёт messagesПриветствие и пустое поле
Отправка promptRerun и append userСвой вопрос сразу
Поток моделиyield chunksПостепенный ответ
Успешный конецappend assistantИстория готова к следующему вопросу
Ошибкаst.error без appendПонятная причина
ОчисткаЗаменяет списокТолько приветствие

Session State разделён по сессиям, но связан с WebSocket и не является базой. Не храните бесконечную историю: каждый запрос снова передаёт messages модели. Ограничьте последние реплики или делайте сводку. Не переносите messages в глобальную переменную. cache_resource подходит общему тяжёлому клиенту, но не личной истории.

Rerun является нормальной моделью Streamlit. Не пытайтесь удерживать бесконечный while вокруг интерфейса. Скрипт описывает текущее состояние страницы сверху вниз, а session_state переносит нужные данные в следующий проход.

Проверяем несколько сессий и ошибки модели

Сначала временно замените ollama_tokens генератором из трёх строк и убедитесь, что write_stream возвращает склеенный текст. Затем подключите Ollama. Откройте приложение в двух разных браузерах или обычном и приватном окне: истории не должны смешиваться. Остановите сервис и проверьте st.error. После восстановления отправьте второй вопрос без перезагрузки. Добавьте вывод len(st.session_state.messages) в sidebar и проследите рост списка. В качестве улучшения ограничьте историю приветствием и последними десятью сообщениями.

Проверка своими руками
  1. Запустите приложение с фиктивным генератором из трёх chunks.
  2. Подключите Ollama и задайте два связанных вопроса.
  3. Откройте вторую браузерную сессию и сравните истории.
  4. Остановите Ollama, получите st.error и снова запустите сервис.
  5. Очистите историю и проверьте, что осталось одно приветствие.
  6. Добавьте ограничение до десяти последних сообщений без удаления приветствия.
Готово, если выполняются все пункты
  • История переживает rerun, но не смешивается между сессиями.
  • Streaming выводит текст и возвращает полный answer.
  • Сбой модели не записывается как успешный ответ.
  • Очистка и ограничение истории работают предсказуемо.

Четыре проверки чата
До добавления файлов и настроек. Интерфейс устойчив без дополнительных функций

Доводим прототип до удобного учебного приложения

Дальше добавьте выбор модели, режим ответа или один текстовый файл. Для каждого элемента решите: session_state, общий кэш или база. Локальная Ollama на ноутбуке не станет доступна серверу Streamlit после публикации. Хостингу нужен свой endpoint, ресурсы и доступ. Если приложение локальное, напишите это в интерфейсе.

Как изучить тему в Кодике

В Кодике эту тему удобно разделить на два слоя. Сначала изучите Python-списки, словари, функции и генераторы. Затем соберите Streamlit-интерфейс с фиктивным ответом. Ollama подключается последней. Так вы сможете определить, ошибка находится в состоянии страницы, потоке или модели.

ШагЧто изучитьКонтрольная задача
1Списки и словариХранить роли и content
2Функции и yieldОтдать три chunks
3Условия и исключенияНе записать ошибочный answer
4Streamlit Session StateПережить rerun
5Ollama chatПодключить локальную модель

В тренажёре Кодика реализуйте список messages и функцию генератора без UI. Затем восстановите пять ключевых строк Streamlit: инициализацию, цикл истории, chat_input, chat_message и write_stream. Когда вы понимаете эти точки, оформление и новые режимы добавляются без переписывания приложения.

messages создаётся обычной переменной

На следующем rerun история начинается заново. Используйте session_state.

Интерфейс обёрнут в while True

Streamlit сам перезапускает файл при событиях. Цикл блокирует страницу.

Глобальная история общая для всех

Пользователи видят чужие сообщения. Храните чат в сессии.

Ошибка модели добавляется как ответ

Записывайте assistant message только в ветке успешного завершения.

Сверьтесь с первичным источником. Команды, API и ограничения примера проверяйте по официальной документации Streamlit по chat elements. Если интерфейс или версия изменились, первичная документация важнее скриншота из старой инструкции.

Что получится в итоге

Готовый результат: Как сделать ИИ-приложение на Python в Streamlit: поле, ответ и история
Веб-приложение показывает отдельные роли, печатает локальный ответ потоком, хранит историю текущей сессии и корректно переживает ошибку модели.

Короткие ответы
Почему обычный список не подходит для истории?

Streamlit выполняет файл заново. Обычная переменная создаётся повторно, а session_state сохраняется в сессии.

Что возвращает write_stream для текстового потока?

Он показывает chunks и возвращает собранную строку, которую можно сохранить как ответ.

Является ли session_state постоянной базой?

Нет. Он связан с пользовательской сессией и WebSocket, поэтому постоянное хранение нужно проектировать отдельно.

Как понять, что проект действительно работает?

Проверьте основной сценарий, ошибочный ввод, повторный запуск и один граничный случай. Затем объясните вслух, где появляются данные, кто принимает решение и где хранится состояние.

Можно ли начать с готового кода из статьи?

Да. После первого запуска измените один параметр, добавьте одну проверку и повторите проект без подсказки. Так пример превращается в собственный навык.

Соберите интерфейс из знакомых Python-структур

Списки, функции и исключения закрепите в курсе Python. Локальную модель подключайте после статьи про Ollama и Python.

Для обычного веб-бэкенда сравните подход со статьёй про Flask и HTML, а работу со внешними запросами повторите в разборе API на Python. В Кодике сначала восстановите UI с фиктивным ответом, затем включите модель.