В первой части мы сделали самый важный стартовый шаг: подняли локальную модель через Ollama, подключили её к Python через LiteLLM и получили первый осмысленный ответ из кода.
Но пока это ещё не чат. Наш main.py умел только одно: отправить один заранее заданный вопрос, вывести ответ и завершиться.
Для учебного эксперимента этого достаточно. Для приложения — уже нет.
Во второй части превратим этот одноразовый скрипт в маленький консольный чат: программа будет ждать ввод, отправлять сообщение модели, печатать ответ и снова ждать следующий вопрос. Плюс сразу добавим несколько полезных вещей, которые делают код заметно взрослее: system prompt, разделение логики на функции, замер времени ответа и базовую обработку ошибок.
Серия статей
-
Часть 2. Делаем маленький консольный чат ← вы здесь
-
Часть 3. Добавляем историю сообщений и контекст
-
Часть 4. Разбираем структуру ответа и метаданные
-
Часть 5. Ошибки и таймауты: делаем код устойчивее
-
Часть 6. Что дальше: локальные и облачные модели, развитие проекта
Что сделаем в этой части
-
вынесем запрос к модели в отдельную функцию;
-
добавим
system prompt, который задаёт поведение ассистента; -
организуем цикл общения: программа будет принимать вопросы, пока пользователь сам не выйдет;
-
добавим замер времени ответа;
-
сделаем так, чтобы одна ошибка не роняла весь чат.
Зачем это нужно
В конце первой части у нас был такой main.py:
answer = ask("Привет! Напиши одно короткое предложение о Python.")
print(answer)
Это не чат. Это один захардкоженный вопрос и завершение программы. Чтобы задать следующий вопрос, нужно перезапускать скрипт.
Реальный чат работает иначе: программа ждёт ввода, отправляет его модели, печатает ответ — и снова ждёт. И так по кругу, пока пользователь не решит выйти.
Именно это мы и будем строить. Плюс добавим две полезные вещи, которые сразу поднимают качество кода: system prompt и разделение логики на функции.
Коротко о том, как это работает
Перед тем как писать код, стоит понять одну вещь про формат сообщений.
LLM API работает не с одной строкой текста, а со списком сообщений. У каждого сообщения есть роль:
-
system— инструкция для модели: кто она, как должна отвечать, что важно учитывать. Это сообщение пользователь не видит, оно задаётся в коде. -
user— вопрос или сообщение от пользователя. -
assistant— ответ модели. В следующей части именно сюда будем добавлять историю диалога.
Сейчас наша схема выглядит так:
пользователь вводит текст
→ system prompt + вопрос → LiteLLM → Ollama → модель
→ ответ → печатаем в консоль
→ ждём следующего ввода
Каждый запрос пока независимый — модель не помнит предыдущих сообщений. Это изменится в третьей части, когда добавим историю.
Пишем новый 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 = "Ты полезный ассистент. Отвечай кратко и по делу."
def send_request_to_llm(user_message: str) -> Optional[str]:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"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")
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)
if answer is not None:
print(f"n🦙 ИИ: {answer}n")
else:
print("n⚠️ Не удалось получить ответ. Проверьте, запущена ли Ollama.n")
if __name__ == "__main__":
main()
Запустите:
python main.py
Попробуйте задать несколько вопросов подряд. Когда захотите выйти — напишите выход.
Разберём код
Константы вверху файла
MODEL = "ollama_chat/qwen2.5:3b"
API_BASE = "http://localhost:11434"
SYSTEM_PROMPT = "Ты полезный ассистент. Отвечай кратко и по делу."
Всё, что может меняться между запусками или экспериментами, вынесено наверх. Хотите поменять модель или поведение ассистента — меняете одну строку, не ищете её в глубине кода.
SYSTEM_PROMPT — это и есть системный промпт. Он передаётся в каждом запросе первым сообщением с ролью system. Модель читает его как инструкцию: кто она и как должна отвечать.
Функция send_request_to_llm
def send_request_to_llm(user_message: str) -> Optional[str]:
Эта функция делает ровно одно: отправляет запрос модели и возвращает текст ответа. Всё остальное — ввод, вывод, цикл — снаружи.
Внутри функции messages собирается из двух частей: сначала системный промпт, потом вопрос пользователя. Именно в таком порядке модель их и читает.
start_time = time.time()
response = completion(...)
duration = time.time() - start_time
print(f"n⏱ Время генерации: {duration:.2f} сек")
Замер времени — две строки вокруг вызова. После ответа видно, сколько секунд ушло на генерацию. Это полезно: сразу замечаешь, как длина вопроса или сложность задачи влияют на скорость.
except Exception as e:
print(f"n❌ Ошибка при запросе: {e}")
return None
При любой ошибке функция возвращает None вместо того чтобы упасть. Это сделано специально: main() проверяет результат и предупреждает пользователя, не завершая программу. Ollama упала — перезапустили, вернулись к чату.
Функция main и цикл
while True:
user_input = input("👤 Вы: ").strip()
Бесконечный цикл — это и есть чат. Программа живёт, пока пользователь сам не выйдет.
.strip() убирает пробелы по краям. Нужно, чтобы случайный пробел перед словом не передавался в модель как часть вопроса.
if user_input.lower() in ("выход", "exit", "quit"):
break
Команды выхода проверяются до отправки запроса. .lower() нужен, чтобы ВЫХОД, Exit и выход — всё работало одинаково.
if not user_input:
print("⚠️ Введите вопрос.")
continue
Если пользователь нажал Enter без текста, цикл продолжается без запроса к модели.
Главная идея всей структуры: send_request_to_llm отвечает за работу с моделью, main отвечает за общение с пользователем. Эти две вещи не смешаны в одну кучу — и это важно, потому что дальше функцию запроса мы будем расширять, не трогая логику цикла.
Поэкспериментируйте с system prompt
Самое интересное в этом коде — SYSTEM_PROMPT. Попробуйте поменять его и посмотрите, как меняется поведение модели при тех же вопросах.
Например:
SYSTEM_PROMPT = "Ты саркастичный помощник. Отвечай с иронией."
SYSTEM_PROMPT = "Ты строгий преподаватель. Исправляй ошибки в вопросах."
SYSTEM_PROMPT = "Отвечай только на русском языке. Всегда давай пример кода."
Один и тот же вопрос — три разных ответа. Именно здесь впервые становится видно, что поведением модели можно управлять из кода.
Что у нас получилось
На этом этапе у нас уже есть:
-
функция, которая отправляет запрос и возвращает ответ или
Noneпри ошибке; -
system prompt, который задаёт поведение ассистента одной строкой;
-
интерактивный цикл: вопрос → ответ → следующий вопрос;
-
замер времени генерации после каждого ответа;
-
базовая устойчивость: ошибка не роняет всю программу.
То есть теперь у нас настоящий консольный чат, а не скрипт с одним захардкоженным вопросом.
Частые проблемы на этом этапе
Программа зависает после ввода вопроса
Почему возникает: Ollama не запущена или модель ещё не загрузилась в память.
Что сделать: убедитесь, что Ollama работает (ollama list в отдельном терминале отвечает). Первый запрос после запуска всегда медленнее — просто подождите.
Ответы очень короткие
Почему возникает: модель по умолчанию отвечает коротко, особенно если в system prompt написано «отвечай кратко».
Что сделать: поменяйте SYSTEM_PROMPT — уберите слово «кратко» или явно попросите «давай развёрнутый ответ с примерами».
KeyboardInterrupt при нажатии Ctrl+C
Почему возникает: это стандартное поведение Python. Программа прерывается без сообщения «До свидания».
Что сделать: это нормально. Если хотите перехватить — оберните main() в try/except KeyboardInterrupt. Но для учебного проекта в этом нет необходимости.
Кракозябры в ответе (Windows)
Почему возникает: кодировка терминала.
Что сделать: выполните перед запуском:
$OutputEncoding = [System.Text.Encoding]::UTF8
Вывод
В этой части мы сделали главный шаг от скрипта к приложению: добавили структуру, цикл и разделили ответственность между функциями.
Пока у чата нет памяти — каждый запрос независимый, модель не помнит, что вы говорили две реплики назад. Это ощущается сразу, как только пробуешь сослаться на предыдущий ответ.
Что дальше
Сейчас чат работает, но не помнит контекст. Если написать «а можешь сделать то же самое, но короче?» — модель не поймёт, о чём речь.
В следующей части добавим историю сообщений: будем накапливать диалог и передавать его в каждый запрос. Это и есть то, что делает чат настоящим чатом, а не просто серией одиночных вопросов.
<- Назад
Вперед ->
Автор: kuz1


