Маленький LLM-чат на Python с Ollama и LiteLLM. Часть 2: делаем консольный чат

Маленький LLM-чат на Python с Ollama и LiteLLM. Часть 2: делаем консольный чат

В первой части мы сделали самый важный стартовый шаг: подняли локальную модель черезOllama, подключили её к Python черезLiteLLMи получили первый осмысленный ответ из кода.

Но пока это ещё не чат. Нашmain.pyумел только одно: отправить один заранее заданный вопрос, вывести ответ и завершиться.

Для учебного эксперимента этого достаточно. Для приложения — уже нет.

Во второй части превратим этот одноразовый скрипт вмаленький консольный чат: программа будет ждать ввод, отправлять сообщение модели, печатать ответ и снова ждать следующий вопрос. Плюс сразу добавим несколько полезных вещей, которые делают код заметно взрослее:system prompt,разделение логики на функции,замер времени ответаибазовую обработку ошибок.

Серия статей

  • Часть 1. Ставим окружение и пишем первый запрос
  • Часть 2.Делаем маленький консольный чат ← вы здесь
  • Часть 3. Добавляем историю сообщений и контекст
  • Часть 4. Разбираем структуру ответа и метаданные
  • Часть 5. Ошибки и таймауты: делаем код устойчивее
  • Часть 6. Что дальше: локальные и облачные модели, развитие проекта

Что сделаем в этой части

  • вынесем запрос к модели в отдельную функцию;
  • добавимsystem prompt, который задаёт поведение ассистента;
  • организуем цикл общения: программа будет принимать вопросы, пока пользователь сам не выйдет;
  • добавим замер времени ответа;
  • сделаем так, чтобы одна ошибка не роняла весь чат.

Зачем это нужно

В конце первой части у нас был такойmain.py:

Это не чат. Это один захардкоженный вопрос и завершение программы. Чтобы задать следующий вопрос, нужно перезапускать скрипт.

Реальный чат работает иначе: программа ждёт ввода, отправляет его модели, печатает ответ — и снова ждёт. И так по кругу, пока пользователь не решит выйти.

Именно это мы и будем строить. Плюс добавим две полезные вещи, которые сразу поднимают качество кода:system promptиразделение логики на функции.

Коротко о том, как это работает

Перед тем как писать код, стоит понять одну вещь про формат сообщений.

LLM API работает не с одной строкой текста, а сосписком сообщений. У каждого сообщения есть роль:

  • system— инструкция для модели: кто она, как должна отвечать, что важно учитывать. Это сообщение пользователь не видит, оно задаётся в коде.
  • user— вопрос или сообщение от пользователя.
  • assistant— ответ модели. В следующей части именно сюда будем добавлять историю диалога.

Сейчас наша схема выглядит так:

Каждый запрос пока независимый — модель не помнит предыдущих сообщений. Это изменится в третьей части, когда добавим историю.

Пишем новый main.py

Откройтеmain.pyиз первой части и замените его содержимое целиком:

Запустите:

Попробуйте задать несколько вопросов подряд. Когда захотите выйти — напишитевыход.

Разберём код

Константы вверху файла

Всё, что может меняться между запусками или экспериментами, вынесено наверх. Хотите поменять модель или поведение ассистента — меняете одну строку, не ищете её в глубине кода.

SYSTEM_PROMPT— это и есть системный промпт. Он передаётся в каждом запросе первым сообщением с рольюsystem. Модель читает его как инструкцию: кто она и как должна отвечать.

Функция send_request_to_llm

Эта функция делает ровно одно: отправляет запрос модели и возвращает текст ответа. Всё остальное — ввод, вывод, цикл — снаружи.

Внутри функцииmessagesсобирается из двух частей: сначала системный промпт, потом вопрос пользователя. Именно в таком порядке модель их и читает.

Замер времени — две строки вокруг вызова. После ответа видно, сколько секунд ушло на генерацию. Это полезно: сразу замечаешь, как длина вопроса или сложность задачи влияют на скорость.

При любой ошибке функция возвращаетNoneвместо того чтобы упасть. Это сделано специально:main()проверяет результат и предупреждает пользователя, не завершая программу. Ollama упала — перезапустили, вернулись к чату.

Функция main и цикл

Бесконечный цикл — это и есть чат. Программа живёт, пока пользователь сам не выйдет.

.strip()убирает пробелы по краям. Нужно, чтобы случайный пробел перед словом не передавался в модель как часть вопроса.

Команды выхода проверяются до отправки запроса..lower()нужен, чтобыВЫХОД,Exitивыход— всё работало одинаково.

Если пользователь нажал Enter без текста, цикл продолжается без запроса к модели.

Главная идея всей структуры:send_request_to_llmотвечает за работу с моделью,mainотвечает за общение с пользователем.Эти две вещи не смешаны в одну кучу — и это важно, потому что дальше функцию запроса мы будем расширять, не трогая логику цикла.

Поэкспериментируйте с system prompt

Самое интересное в этом коде —SYSTEM_PROMPT. Попробуйте поменять его и посмотрите, как меняется поведение модели при тех же вопросах.

Один и тот же вопрос — три разных ответа. Именно здесь впервые становится видно, что поведением модели можно управлять из кода.

Что у нас получилось

На этом этапе у нас уже есть:

  • функция, которая отправляет запрос и возвращает ответ илиNoneпри ошибке;
  • system prompt, который задаёт поведение ассистента одной строкой;
  • интерактивный цикл: вопрос → ответ → следующий вопрос;
  • замер времени генерации после каждого ответа;
  • базовая устойчивость: ошибка не роняет всю программу.

То есть теперь у наснастоящий консольный чат, а не скрипт с одним захардкоженным вопросом.

Частые проблемы на этом этапе

Программа зависает после ввода вопроса

Почему возникает: Ollama не запущена или модель ещё не загрузилась в память.

Что сделать: убедитесь, что Ollama работает (ollama listв отдельном терминале отвечает). Первый запрос после запуска всегда медленнее — просто подождите.

Ответы очень короткие

Почему возникает: модель по умолчанию отвечает коротко, особенно если в system prompt написано «отвечай кратко».

Что сделать: поменяйтеSYSTEM_PROMPT— уберите слово «кратко» или явно попросите «давай развёрнутый ответ с примерами».

KeyboardInterruptпри нажатии Ctrl+C

Почему возникает: это стандартное поведение Python. Программа прерывается без сообщения «До свидания».

Что сделать: это нормально. Если хотите перехватить — обернитеmain()вtry/except KeyboardInterrupt. Но для учебного проекта в этом нет необходимости.

Кракозябры в ответе (Windows)

Почему возникает: кодировка терминала.

Что сделать: выполните перед запуском:

В этой части мы сделалиглавный шаг от скрипта к приложению: добавили структуру, цикл и разделили ответственность между функциями.

Пока у чата нет памяти — каждый запрос независимый, модель не помнит, что вы говорили две реплики назад. Это ощущается сразу, как только пробуешь сослаться на предыдущий ответ.

Что дальше

Сейчас чат работает, но не помнит контекст. Если написать «а можешь сделать то же самое, но короче?» — модель не поймёт, о чём речь.

В следующей части добавимисторию сообщений: будем накапливать диалог и передавать его в каждый запрос. Это и есть то, что делает чат настоящим чатом, а не просто серией одиночных вопросов.

<- НазадВперед ->

Читать оригинал