Budowa chatbota CLI

2. Przygotowanie środowiska

2.1. Ustawienia i połączenie z Ollama

Lokalne chatboty, agenci i systemy RAG z własną bazą wiedzy

Instrukcja krok po kroku · materiał uzupełniający

Budujemy chatbota w konsoli

config.json i lokalny.py

Odniesienie: M4 R2   |   Czas: 20 min

Oddziel ustawienia i komunikację z modelem od reszty programu. Dzięki temu kolejne pliki będą krótkimi etapami tego samego przepływu.

Ustawienia wspólne

Otwórz config.json. Na początku pozostaw te wartości. JSON używa null, a Python None; nie zamieniaj tych zapisów między plikami.

{
  "chat_model": "qwen3:4b-instruct-2507-q4_K_M",
  "embedding_model": "bge-m3:latest",
  "top_k": 3,
  "similarity_threshold": null,
  "context_chars": 4500,
  "question_chars": 1000,
  "temperature": 0.0,
  "seed": 42,
  "num_ctx": 4096,
  "num_predict": 384,
  "timeout": 180
}
PoleZnaczenie
chat_model / embedding_modelDwa różne zadania: generowanie tekstu oraz wektory do wyszukiwania.
top_kMaksymalna liczba fragmentów z rankingu.
similarity_thresholdnull oznacza brak filtrowania po wyniku podobieństwa.
context_charsBudżet znaków na pełne fragmenty. To przybliżenie, a nie liczba tokenów.
num_ctx / num_predictOkno modelu i limit generowanej odpowiedzi; wspólne dla porównywanych prób.
timeoutCzas oczekiwania klienta na odpowiedź Ollama, w sekundach. Nie ma związku z parametrem fikcyjnej aplikacji DocAssist.

Utwórz plik lokalny.py

Skopiuj cały kod do pliku lokalny.py i zapisz jako UTF-8. ROOT wskazuje folder skryptu. api() wysyła JSON do Ollama, chat() obsługuje generowanie, a embed() zwraca wektor o długości jeden.

"""Wspólne ustawienia i klient lokalnej Ollama. Python 3.10+."""
import json
import math
from pathlib import Path
from urllib.request import Request, build_opener, ProxyHandler
from urllib.error import HTTPError, URLError

ROOT = Path(__file__).resolve().parent
CONFIG = json.loads((ROOT / "config.json").read_text(encoding="utf-8-sig"))
BASE_URL = "http://127.0.0.1:11434"


def api(endpoint, payload=None):
    body = None if payload is None else json.dumps(payload).encode("utf-8")
    request = Request(BASE_URL + endpoint, data=body,
                      headers={"Content-Type": "application/json"})
    try:
        # Połączenie z localhost nie przechodzi przez firmowy serwer proxy.
        with build_opener(ProxyHandler({})).open(
                request, timeout=CONFIG["timeout"]) as response:
            return json.load(response)
    except HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="replace")[:500]
        raise RuntimeError(f"Ollama HTTP {exc.code}: {detail}") from exc
    except (URLError, TimeoutError, OSError) as exc:
        raise RuntimeError("Brak odpowiedzi lokalnej Ollama. Sprawdź usługę i timeout.") from exc


def models():
    return {m["name"]: m["digest"] for m in api("/api/tags")["models"]}


def require_models(names):
    installed = models()
    for name in names:
        if name not in installed:
            raise ValueError(f"Brak modelu {name}. Sprawdź ollama list i config.json.")
    return installed


def chat(messages):
    result = api("/api/chat", {
        "model": CONFIG["chat_model"], "messages": messages, "stream": False,
        "options": {k: CONFIG[k] for k in
                    ("temperature", "seed", "num_ctx", "num_predict")},
    })
    if not result.get("message", {}).get("content", "").strip():
        raise RuntimeError("Model zwrócił pustą odpowiedź.")
    return result


def unit(vector):
    if not vector or any(type(x) not in (int, float) or not math.isfinite(x) for x in vector):
        raise ValueError("Niepoprawny wektor.")
    norm = math.sqrt(sum(x * x for x in vector))
    if norm == 0:
        raise ValueError("Wektor zerowy.")
    return [x / norm for x in vector]


def embed(text):
    result = api("/api/embed", {
        "model": CONFIG["embedding_model"], "input": text, "truncate": False,
    })
    return unit(result["embeddings"][0])


if __name__ == "__main__":
    installed = require_models([CONFIG["chat_model"], CONFIG["embedding_model"]])
    print("Ollama:", api("/api/version")["version"])
    for name in (CONFIG["chat_model"], CONFIG["embedding_model"]):
        print(name, installed[name])
    print("Połączenie i obecność modeli: OK. Generowanie sprawdzisz w kolejnym kroku.")

Wysyłamy stream=False, aby otrzymać jedną odpowiedź JSON. Przy embeddingach truncate=False powoduje zgłoszenie błędu dla zbyt długiego wejścia. Nie chcemy po cichu indeksować uciętego tekstu. Szczegóły pól opisują API chat i API embed.

Uruchom kontrolę

.\.venv\Scripts\python.exe lokalny.py

Oczekiwany wynik: wersja Ollama, nazwy obu modeli, ich identyfikatory digest i komunikat „Połączenie i obecność modeli: OK”. To potwierdza dostępność usługi i modeli. Pierwsze generowanie sprawdzisz dopiero w następnym rozdziale.

Punkt kontrolny

  • Polecenie kończy się komunikatem OK.
  • Wiesz, dlaczego adres to 127.0.0.1 i dlaczego używamy dwóch modeli.

Instrukcja uczestnika · Python i Ollama · wydanie 29.09.2026