Aby uczciwie porównać model lokalny z chmurowym, uruchom ten sam analizator na tych samych dokumentach przez jedną bramkę i zmieniaj tylko alias modelu, mierząc czas każdego wywołania oraz oceniając streszczenia według tej samej listy kontrolnej. Wyniki zależą od Twojego sprzętu, więc porównanie wykonaj na własnym komputerze. Model lokalny wygrywa prywatnością, brakiem kosztu za token i pracą offline, a chmurowy zwykle jakością i szybkością. O wyborze decyduje to, które z tych kryteriów jest dla Twojego zadania najważniejsze.
To ostatnia część cyklu. Mamy model lokalny w Ollamie i bramkę LiteLLM z konfiguracją w YAML. Teraz sprawdzamy, co z tego wynika w praktyce, na projekcie, który już znasz: analizatorze dokumentów.
Jak przepiąć analizator na bramkę LiteLLM?
Zmiany w stosunku do wersji z OpenAI są niewielkie. Klient wskazuje na bramkę, nazwa modelu pochodzi z konfiguracji, a raport zbiera dodatkowo czas wywołań:
import time
from dataclasses import dataclass
from openai import APIConnectionError, OpenAI
from settings import Settings
@dataclass
class Report:
model: str
calls: int = 0
prompt_tokens: int = 0
completion_tokens: int = 0
seconds: float = 0.0
def add(self, usage, seconds: float) -> None:
self.calls += 1
self.prompt_tokens += usage.prompt_tokens
self.completion_tokens += usage.completion_tokens
self.seconds += seconds
def format(self) -> str:
return (
f"Model (alias): {self.model}\n"
f"Wywołania: {self.calls}\n"
f"Tokeny wejściowe: {self.prompt_tokens}\n"
f"Tokeny wyjściowe: {self.completion_tokens}\n"
f"Czas: {self.seconds:.1f} s"
)
def make_client(settings: Settings) -> OpenAI:
return OpenAI(
base_url=settings.gateway_url,
api_key=settings.api_key,
timeout=settings.timeout_seconds,
max_retries=0,
)
def ask(client: OpenAI, settings: Settings, prompt: str, report: Report) -> str:
started = time.perf_counter()
try:
response = client.chat.completions.create(
model=settings.model,
messages=[
{"role": "system", "content": "Streszczasz dokumenty po polsku, zwięźle i rzeczowo."},
{"role": "user", "content": prompt},
],
)
except APIConnectionError as error:
raise SystemExit(f"Brak połączenia z bramką {settings.gateway_url}: {error}")
report.add(response.usage, time.perf_counter() - started)
return response.choices[0].message.content
Zwróć uwagę na dwie decyzje. max_retries=0 wyłącza ponawianie wewnątrz klienta, żeby czas jednego wywołania nie zawierał ukrytych powtórzeń. Zwiększony timeout_seconds jest potrzebny, bo model lokalny na słabszym sprzęcie odpowiada dużo dłużej niż API w chmurze. Reszta (dzielenie na fragmenty, streszczanie dwuetapowe) zostaje taka sama jak w oryginalnym analizatorze.
Do dzielenia tekstu nadal używamy tiktoken, ale pamiętaj, że to tylko przybliżenie dla modelu lokalnego, który ma własny tokenizer. Dlatego w raporcie bierzemy liczby z response.usage, a nie z własnego liczenia.
Jak uczciwie porównać dwa modele?
Najczęstszy błąd to porównanie jednego zapytania „na oko”. Żeby wynik coś znaczył:
| Zasada | Dlaczego |
|---|---|
| Te same dokumenty dla obu modeli | Inaczej porównujesz dokumenty, nie modele |
| Kilka dokumentów o różnej długości (krótki, średni, długi) | Czas rośnie z długością, ale nie tak samo u obu |
| Pierwsze wywołanie lokalnego modelu odrzuć lub oznacz | Zawiera ładowanie modelu do pamięci |
| Kilka powtórzeń, mediana zamiast jednego pomiaru | Pojedynczy czas zależy od chwilowego obciążenia komputera |
| Jedna lista kontrolna jakości dla obu streszczeń | Ocena „podoba mi się” nie jest powtarzalna |
| Nic innego nie obciąża komputera podczas pomiaru | Przeglądarka z wieloma kartami zafałszuje czas |
Prosty skrypt benchmark.py, który przechodzi po dokumentach i aliasach:
import statistics
import sys
from pathlib import Path
from analyzer import make_client, summarize
from settings import Settings
REPEATS = 3
def run(alias: str, path: Path) -> tuple[float, str]:
settings = Settings(model=alias)
client = make_client(settings)
times = []
summary = ""
for _ in range(REPEATS):
summary, report = summarize(client, settings, path.read_text(encoding="utf-8"))
times.append(report.seconds)
return statistics.median(times), summary
if __name__ == "__main__":
for path in map(Path, sys.argv[1:]):
for alias in ("local", "cloud"):
seconds, summary = run(alias, path)
print(f"{path.name} | {alias} | mediana {seconds:.1f} s")
Path(f"wynik-{alias}-{path.stem}.txt").write_text(summary, encoding="utf-8")
Uruchom: python benchmark.py krotki.txt sredni.txt dlugi.txt. Skrypt zapisuje streszczenia do plików, żebyś mógł je ocenić według tej samej listy.
Jak ocenić jakość streszczenia?
Zamiast ogólnego wrażenia sprawdź każde streszczenie według tych samych pytań:
- Czy zawiera najważniejszy wniosek dokumentu?
- Czy każda podana informacja da się znaleźć w oryginale (brak zmyśleń)?
- Czy jest po polsku i nie wtrąca obcych słów?
- Czy mieści się w poleconych pięciu zdaniach?
- Czy nie pomija całej części dokumentu (objaw ucięcia kontekstu, opisany w artykule o ucinaniu kontekstu w Ollamie)?
Oceniaj każdy punkt jako tak/nie. Dwie osoby ze wspólną listą dojdą do podobnego wyniku, a z „wrażeniem” już niekoniecznie.
Dlaczego Twoje wyniki będą inne niż moje?
W tym artykule nie podaję tabeli czasów, bo liczby z jednego komputera niewiele mówią o Twoim. Ten sam model może na różnych komputerach działać wielokrotnie szybciej lub wolniej. Na wynik wpływają:
| Czynnik | Dlaczego zmienia wynik |
|---|---|
| Sprzęt (procesor, karta graficzna, pamięć) | Czy model mieści się w pamięci w całości, decyduje o szybkości odpowiedzi |
| Wielkość modelu i kwantyzacja | Ten sam model w innej kwantyzacji ma inną jakość, rozmiar i szybkość |
| Okno kontekstu | Większe okno zużywa więcej pamięci i spowalnia odpowiedzi |
| Obciążenie komputera | Inne programy zakłócają pomiar |
| Zimny start | Pierwsze wywołanie ładuje model do pamięci |
| Sieć i dostawca (chmura) | Czas zależy od połączenia, pory dnia i obciążenia po stronie dostawcy |
Dlatego porównanie wykonaj na swoim komputerze i z modelami, których naprawdę użyjesz. Interesuje Cię przecież to, co zadziała u Ciebie, a nie to, co wyszło komuś innemu.
Jak odczytać wyniki na swoim komputerze?
Uruchom benchmark.py na kilku dokumentach i wpisz wyniki w tabelę:
| Dokument | Długość (tokeny) | Alias | Mediana czasu | Punkty z listy jakości |
|---|---|---|---|---|
| krótki | local | |||
| krótki | cloud | |||
| średni | local | |||
| średni | cloud | |||
| długi | local | |||
| długi | cloud |
Potem odpowiedz na cztery pytania:
- O ile lokalny model jest wolniejszy lub szybszy? Porównuj mediany dla tego samego dokumentu, nie średnią ze wszystkich.
- Jak czas rośnie z długością dokumentu? Jeśli u modelu lokalnego rośnie wyraźnie szybciej, długie dokumenty są jego słabą stroną.
- Czy różnica w punktach jakości jest istotna? Różnica jednego punktu na pięć przy jednym dokumencie to jeszcze nie wniosek. Powtórz na kolejnym dokumencie.
- Czy lokalny model jest wystarczająco dobry dla tego zadania? To jest właściwe pytanie, a nie „który wygrał”.
Kiedy model lokalny się opłaca, a kiedy nie?
Niezależnie od wyników konkretnego pomiaru, kryteria wyboru są stałe:
| Sytuacja | Lepszy wybór |
|---|---|
| Dokumenty zawierają dane osobowe lub poufne | Lokalny |
| Dużo eksperymentów w trakcie nauki | Lokalny (brak kosztu za token) |
| Praca bez internetu | Lokalny |
| Wymagana wysoka jakość lub trudny język specjalistyczny | Zwykle chmura |
| Ważny krótki czas odpowiedzi, a sprzęt jest słaby | Zwykle chmura |
| Duży ruch od wielu użytkowników naraz | Zwykle chmura (jeden komputer się nie skaluje) |
Dla analizatora dokumentów firmowych, który nie może opuszczać komputera, odpowiedź jest prosta: lokalny, nawet kosztem czasu. Dla publicznych tekstów, gdy liczy się jakość, wybierzesz chmurę. Dzięki bramce nie musisz decydować raz na zawsze: zmieniasz jedną wartość w config.yaml.
Dlaczego ten projekt działa bez Docker Compose?
Całość to trzy zwykłe procesy: Ollama, bramka LiteLLM i Twój skrypt Pythona, każdy w osobnym terminalu. Dla projektu uczącego to wystarczy i ma przewagę, że widzisz logi każdego elementu. Compose ma sens, gdy aplikacja trafia na serwer albo gdy współpracujesz z zespołem i potrzebujesz powtarzalnego środowiska. Warto go dodać na końcu, jako osobny krok. Pamiętaj wtedy o pułapce z localhost w kontenerze Dockera: w kontenerze localhost nie oznacza Twojego komputera.
Jak rozbudować projekt dalej?
- Dodaj trzeci alias z innym modelem lokalnym i porównaj rozmiary (mały vs duży).
- Zapisuj wyniki pomiarów do pliku CSV i rysuj, jak czas rośnie z długością dokumentu.
- Dodaj testy z atrapami, tak jak w artykule o testowaniu kodu z API LLM. Bramka to po prostu kolejny adres do podmienienia.
- Jeśli dokumentów jest dużo, nie streszczaj wszystkiego, tylko wyszukuj trafne fragmenty (bazy wektorowe i RAG w Pythonie).
- Poproś narzędzie AI o pomoc przy refaktoryzacji, ale zweryfikuj wynik własnymi testami (vibe coding).
Taki projekt dobrze wygląda w portfolio: pokazuje pracę z bramką, konfiguracją, pomiarami i świadomy dobór narzędzia. Jeśli chcesz, żeby ktoś doświadczony ocenił go przed rozmową rekrutacyjną, zobacz mentoring w ścieżce Python.


