LLM nie czyta tekstu: tokenizacja, kontekst i pierwszy model
Najpierw przygotujesz repozytorium i środowisko, które będzie rozwijane przez cały kurs. Zanim poprosimy model o używanie narzędzi albo zbudujemy RAG, musimy zobaczyć, co naprawdę trafia do modelu. W tej części przejdziemy drogę od tekstu do identyfikatorów tokenów, policzymy rozmiar wejścia i uruchomimy ten sam kontrakt w trybie API oraz lokalnym.
Najpierw zbudujesz intuicję, potem uruchomisz eksperyment, a na końcu zapiszesz krótki rezultat.
Czego nauczysz się w tej części?
- utworzyć jedno repozytorium i środowisko używane przez cały kurs
- zainstalować zależności oraz sprawdzić kompletność środowiska
- wyjaśnić, że model przyczynowy przewiduje rozkład następnego tokenu
- odróżnić tekst, token i identyfikator tokenu
- sprawdzić tokenizację tego samego tekstu w dwóch językach
- wyjaśnić, co zajmuje okno kontekstu
- uruchomić minimalne wywołanie API albo model lokalny
- nie umieszczać klucza API w kodzie ani repozytorium
- zapisać czas i zużycie tokenów bez logowania sekretów
- traktować nazwę modelu jako konfigurację
Nie musisz jeszcze: znać macierzy uwagi (ang. attention matrix), trenować modelu ani rozumieć wszystkich parametrów biblioteki
transformers. Dzisiaj interesuje nas granica między tekstem użytkownika a wejściem numerycznym modelu.
Jak będziemy pracować?
- Krok 1repozytorium, środowisko i zależności całego projektu
- Krok 2model LLM jako predyktor kolejnego tokenu
- Krok 3eksperyment z tokenizatorem
- Krok 4okno kontekstu, limity i koszt
- Krok 5pierwsze wywołanie API lub modelu lokalnego
- Krok 6wspólny format wyniku i pomiar
- Krok 7rezultaty pracy i zapis wyniku
Zanim zaczniesz
W tym laboratorium zakładasz jeden projekt, który będzie rozwijany do końca kursu. Nie twórz osobnego środowiska dla każdego kolejnego ćwiczenia. W następnych tygodniach do tego samego repozytorium dodasz narzędzia, RAG, testy, API oraz raport końcowy.
Potrzebujesz Pythona 3.11 lub nowszego. Wybierz jeden dostępny tryb pracy: API z kluczem zapisanym w pliku .env albo mały model lokalny. Jeśli komputer nie ma dość pamięci, pierwsze kroki możesz wykonać ze stubem (uproszczoną, tymczasową wersją kodu, udającą prawdziwe działanie) – nie zaczynaj zajęć od wielogodzinnego pobierania wag.
Zanim uruchomisz tokenizer, zapisz własną odpowiedź na pytanie: „Ile tokenów ma słowo nieprzewidywalność?”. Dopiero potem sprawdź wynik. Bez wskazania konkretnego tokenizera to pytanie nie ma jednej poprawnej odpowiedzi.
Jedno repozytorium i jedno środowisko na cały kurs
Projekt kursowy budujemy przyrostowo. Dzisiejszy tokenizer i backend staną się później częścią aplikacji z narzędziami, wyszukiwaniem dokumentów, testami oraz interfejsem API. Wspólne repozytorium pozwoli rozwijać ten sam kod, a plik z wersjami zależności ułatwi odtworzenie środowiska przed oddaniem projektu.
Utwórz katalog projektu i środowisko wirtualne
W terminalu wykonaj poniższe polecenia. Nazwę katalogu możesz zmienić, ale używaj jej konsekwentnie przez cały kurs.
mkdir llm-course-project cd llm-course-project python -m venv .venv # Windows PowerShell: .\.venv\Scripts\Activate.ps1 # Linux lub macOS: source .venv/bin/activate python -m pip install --upgrade pip
Po aktywacji środowiska polecenia
pythonipippowinny wskazywać katalog.venv. Jeżeli system używa nazwypython3, zastosuj ją także przy tworzeniu środowiska.
Zależności całego projektu
Utwórz plik requirements.txt. Zawiera pakiety potrzebne nie tylko dzisiaj, lecz także w dalszych laboratoriach: do modeli lokalnych, wyszukiwania, testów i wystawienia API.
openai python-dotenv pydantic>=2,<3 numpy pandas torch transformers sentence-transformers faiss-cpu rank-bm25 fastapi uvicorn httpx pytest google-genai
python -m pip install -r requirements.txt python -m pip freeze > requirements-lock.txt
Plik requirements.txt opisuje bezpośrednie potrzeby projektu, a requirements-lock.txt zapisuje dokładny stan działającego środowiska. Jeżeli na kursie otrzymasz przetestowany plik z wersjami, potraktuj go jako źródło nadrzędne i nie aktualizuj pakietów w połowie ćwiczeń bez powodu.
Instalacja PyTorch zależy od systemu i sposobu użycia GPU. Jeśli chcesz korzystać z CUDA, wybierz polecenie z oficjalnego konfiguratora PyTorch. Nie instaluj losowych plików wheel znalezionych w sieci. Gdy
faiss-cpunie jest dostępny na Twojej platformie, skorzystaj ze środowiska kursowego albo z wyszukiwania dokładnego w NumPy do czasu laboratorium o RAG.
Szkielet, który będzie rósł razem z projektem
Utwórz poniższą strukturę. Na razie część katalogów będzie pusta; w kolejnych laboratoriach dodasz do pakietu moduły narzędzi, retrievalu, ewaluacji i API.
llm-course-project/ ├── src/ │ └── llm_course/ │ ├── __init__.py │ ├── config.py │ └── backends.py ├── data/ │ └── .gitkeep ├── tests/ │ └── .gitkeep ├── reports/ │ └── .gitkeep ├── logs/ │ └── .gitkeep ├── .env ├── .env.template ├── .gitignore ├── check_environment.py ├── pyproject.toml ├── requirements.txt ├── requirements-lock.txt └── README.md
Przy układzie z katalogiem src dodaj minimalny pyproject.toml. Dzięki temu pakiet llm_course będzie można importować z testów i skryptów bez ręcznego zmieniania PYTHONPATH.
[build-system] requires = ["setuptools>=68"] build-backend = "setuptools.build_meta" [project] name = "llm-course-project" version = "0.1.0" requires-python = ">=3.11" [tool.setuptools.packages.find] where = ["src"] [tool.pytest.ini_options] pythonpath = ["src"] testpaths = ["tests"]
python -m pip install -e .
W pliku README.md zapisz na początek: cel projektu, wymaganą wersję Pythona, sposób instalacji, wybór trybu local/api, polecenie uruchomienia testów oraz listę znanych ograniczeń. Aktualizuj tę instrukcję po każdym laboratorium – pod koniec kursu nie będziesz odtwarzać jej z pamięci.
Konfiguracja i sekrety
Do repozytorium dodaj .env.template z nazwami zmiennych, ale bez prawdziwych sekretów. Następnie skopiuj go lokalnie jako .env i uzupełnij wyłącznie na swoim komputerze. Tryb local pozwala rozpocząć bez płatnego API.
# .env.template - ten plik trafia do repozytorium LLM_MODE=local GEMINI_API_KEY= GEMINI_MODEL= OPENAI_API_KEY= OPENAI_MODEL= LOCAL_MODEL=Qwen/Qwen2.5-0.5B-Instruct DATA_DIR=data LOG_DIR=logs
# Windows PowerShell: Copy-Item .env.template .env # Linux lub macOS: cp .env.template .env
Dodaj również .gitignore:
.venv/ .env __pycache__/ *.py[cod] .pytest_cache/ logs/* !logs/.gitkeep data/private/
Po przypadkowym opublikowaniu klucza samo usunięcie go z ostatniego commita nie wystarcza. Klucz należy unieważnić u dostawcy, wygenerować nowy i sprawdzić historię repozytorium.
Sprawdź środowisko, zanim zaczniesz pisać aplikację
Zapisz poniższy skrypt jako check_environment.py. Dzięki niemu szybko odróżnisz brak pakietu od błędu w kodzie laboratorium.
from importlib.util import find_spec
import platform
import sys
MODULES = [
"dotenv",
"openai",
"pydantic",
"numpy",
"pandas",
"torch",
"transformers",
"sentence_transformers",
"faiss",
"rank_bm25",
"fastapi",
"uvicorn",
"httpx",
"pytest",
"google-genai"
]
missing = [name for name in MODULES if find_spec(name) is None]
print("Python:", sys.version.split()[0])
print("System:", platform.platform())
if find_spec("torch"):
import torch
print("PyTorch:", torch.__version__)
print("CUDA dostępna:", torch.cuda.is_available())
if missing:
print("Brakujące moduły:", ", ".join(missing))
raise SystemExit(1)
print("Środowisko jest gotowe.")
python check_environment.py python check_environment.py > environment.txt git init git status --short
Plik environment.txt dołącz do pierwszego checkpointu. Przed pierwszym commitem sprawdź wynik git status: plik .env nie może się na nim pojawić.
Co jest gotowe po tym kroku?
Masz bazę projektu odpowiadającą wymaganiom końcowym: miejsce na kod aplikacji, dane, testy, logi i raporty; dwa tryby modelu; jawny zestaw zależności; oraz konfigurację bez sekretów w repozytorium. Kolejne laboratoria będą rozszerzać tę strukturę, a nie zastępować ją nowymi projektami.
Co właściwie robi model językowy?
W uproszczeniu przyczynowy model językowy otrzymuje dotychczasową sekwencję tokenów i przypisuje prawdopodobieństwo każdemu tokenowi, który może pojawić się jako następny. Po wybraniu jednego tokenu dopisuje go do sekwencji i powtarza operację. Długa odpowiedź powstaje więc krok po kroku, a nie przez jednorazowe wyszukanie gotowego akapitu w bazie.
Co znaczy wzór?
W tym zapisie t1, …, tn oznaczają kolejne tokeny, a n – długość sekwencji. Wyrażenie P(ti | t1, …, ti−1) oznacza prawdopodobieństwo przypisane tokenowi na pozycji i, gdy znane są wcześniejsze tokeny. Symbol ∏ mówi, że mnożymy te prawdopodobieństwa dla wszystkich pozycji. Wynikiem jest prawdopodobieństwo całej konkretnej sekwencji według modelu.
Iloczyn można liczyć krok po kroku. Jeśli pierwszy token ma prawdopodobieństwo 0,5, a drugi – pod warunkiem wystąpienia pierwszego – 0,2, to prawdopodobieństwo sekwencji tych dwóch tokenów wynosi 0,5 × 0,2 = 0,1. Prawdopodobieństwo trzeciego wybranego tokenu mnożymy przez ten dotychczasowy wynik.
Gdyby na drugiej pozycji wybrać inny token, o prawdopodobieństwie 0,3, powstałaby inna sekwencja o prawdopodobieństwie 0,5 × 0,3 = 0,15. Model przewidywałby następny token na podstawie tej nowej sekwencji. Wcześniejsze tokeny wpływają więc na prawdopodobieństwa kolejnych tokenów, a iloczyn pozwala obliczyć prawdopodobieństwo całej wybranej sekwencji.
To wyjaśnia kilka zachowań modeli: odpowiedź może być płynna, lecz fałszywa; mała zmiana początku może zmienić dalszą kontynuację; model nie zna automatycznie bieżących danych; a wygenerowany tekst nie jest dowodem, że model wykonał obliczenie albo sprawdził źródło.
Model nie pracuje na słowach i nie przechowuje zdań jak wyszukiwarka. Jego bezpośrednim zadaniem jest przewidywanie tokenów. Wiedza, styl i wiele umiejętności ujawniają się pośrednio dzięki wzorcom wyuczonym w parametrach.
Tekst staje się identyfikatorami tokenów
Tokenizer ma słownik oraz reguły podziału tekstu. Tokenem może być całe krótkie słowo, fragment słowa, znak interpunkcyjny, spacja zakodowana razem z fragmentem albo część zapisu emoji. Ten sam tekst może zostać podzielony inaczej przez dwa modele.
| Warstwa | Przykład | Co widzi kolejny etap |
|---|---|---|
| tekst | Modele uczą się. | ciąg znaków Unicode |
| tokeny | zależne od tokenizera | fragmenty należące do słownika |
| token IDs | np. [42, 817, 19] | liczby całkowite |
| embeddingi | wektory liczb rzeczywistych | reprezentacje wejściowe sieci |
Obejrzyj tokeny zamiast o nich zgadywać
Uruchom poniższy eksperyment. Model jest celowo mały; korzystamy przede wszystkim z jego tokenizera. Zmień listę przykładów tak, aby zawierała Twoje nazwisko, fragment kodu, liczbę, emoji oraz zdanie z polskimi znakami.
from transformers import AutoTokenizer
MODEL_NAME_V1 = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer_v1 = AutoTokenizer.from_pretrained(MODEL_NAME_V1)
MODEL_NAME_V2 = "bert-base-cased"
tokenizer_v2 = AutoTokenizer.from_pretrained(MODEL_NAME_V2)
samples = [
"kot",
"nieprzewidywalność",
"Large language model",
"Duży model językowy",
"print('Hello!')",
"LLM-y 🤖 kosztują 0.01 USD",
]
for name, tokenizer in [(MODEL_NAME_V1, tokenizer_v1), (MODEL_NAME_V2, tokenizer_v2)]:
print("\n", name)
for text in samples:
ids = tokenizer.encode(text, add_special_tokens=False)
pieces = [tokenizer.decode([token_id]) for token_id in ids]
print(f"\nTEXT: {text!r}")
print(f"IDS: {ids}")
print(f"PIECES: {pieces}")
print(f"COUNT: {len(ids)}")
- Czy liczba tokenów jest równa liczbie słów?
- Jak tokenizer traktuje spacje i znaki interpunkcyjne?
- Czy polska i angielska wersja zdania mają tyle samo tokenów?
- Czy
decode(encode(text))odtwarza tekst? Sprawdź to jawnie.
Jak interpretować wynik?
Nie szukaj jednej liczby poprawnej dla wszystkich modeli. Wynik zależy od tokenizera i jego wersji. Najważniejszy wniosek brzmi: token nie jest synonimem słowa, a koszty i limity trzeba liczyć tokenizatorem właściwym dla modelu albo na podstawie pola usage zwróconego przez dostawcę.
Okno kontekstu nie jest pamięcią bez dna
Do limitu kontekstu wchodzą wszystkie elementy przekazane modelowi: instrukcje systemowe lub deweloperskie, historia rozmowy, pytanie użytkownika, fragmenty RAG, definicje narzędzi, wyniki narzędzi i zwykle także generowana odpowiedź. Dokładny sposób rozliczania zależy od API i modelu.
Więcej kontekstu nie zawsze oznacza lepszą odpowiedź. Może zwiększyć koszt i czas jej przygotowania, wprowadzić sprzeczne informacje oraz utrudnić odnalezienie tego, co istotne. Dlatego w dalszej części będziemy mierzyć jakość wyszukiwania fragmentów (retrieval) i kontrolować liczbę tokenów przekazywanych modelowi.
Policz budżet, nie cenę z pamięci
Napisz funkcję obliczającą koszt na podstawie liczby tokenów oraz cen podanych w konfiguracji. Ceny podawaj jako koszt miliona tokenów. Nie wpisuj do materiałów stawek zapamiętanych z Internetu – zmieniają się.
def estimate_cost_usd(
input_tokens: int,
output_tokens: int,
input_usd_per_million: float,
output_usd_per_million: float,
) -> float:
return (
input_tokens / 1_000_000 * input_usd_per_million
+ output_tokens / 1_000_000 * output_usd_per_million
)
print(estimate_cost_usd(12_000, 800, 0.0, 0.0))
Najbliższym prawdy źródłem liczby tokenów w trybie API jest pole
usageodpowiedzi dostawcy. Lokalny tokenizer daje pomiar dokładny dla lokalnego modelu, ale nie musi odpowiadać tokenizacji innego modelu.
Jeden kontrakt, dwa tryby pracy
Kod aplikacji nie powinien wiedzieć, czy tekst generuje usługa zewnętrzna, czy model na komputerze. Na razie wystarczy wspólny obiekt wyniku. Umieść kontrakt i implementacje w src/llm_course/backends.py. Dzięki temu kolejne laboratoria nie będą uzależnione od jednego dostawcy.
from dataclasses import dataclass
from typing import Protocol
@dataclass
class Generation:
text: str
input_tokens: int | None
output_tokens: int | None
latency_s: float
model: str
mode: str
class LLMBackend(Protocol):
def generate(self, system: str, user: str, max_tokens: int = 160) -> Generation:
...
Minimalny backend API
import os
import time
class ApiBackend:
def __init__(self) -> None:
from google import genai
# OpenAI: from openai import OpenAI
self.model = os.getenv("GEMINI_MODEL", "gemini-2.5-flash-lite")
self.client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
# OpenAI: self.model = os.environ["OPENAI_MODEL"]
# OpenAI: self.client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def generate(self, system: str, user: str, max_tokens: int = 160) -> Generation:
from google.genai import types
started = time.perf_counter()
response = self.client.models.generate_content(
model=self.model,
contents=user,
config=types.GenerateContentConfig(
system_instruction=system,
max_output_tokens=max_tokens,
),
)
# OpenAI:
# response = self.client.responses.create(
# model=self.model,
# instructions=system,
# input=user,
# max_output_tokens=max_tokens,
# )
latency_s = time.perf_counter() - started
generated_text = response.text
# OpenAI: generated_text = response.output_text
if not generated_text:
raise RuntimeError("API nie zwróciło tekstu odpowiedzi")
# OpenAI:
# if response.status != "completed":
# raise RuntimeError(
# f"Generowanie nie zostało zakończone: "
# f"{response.status}, {response.incomplete_details}"
# )
usage = response.usage_metadata
# OpenAI: usage = response.usage
return Generation(
text=generated_text,
input_tokens=getattr(usage, "prompt_token_count", None),
output_tokens=getattr(usage, "candidates_token_count", None),
# OpenAI: input_tokens=getattr(usage, "input_tokens", None),
# OpenAI: output_tokens=getattr(usage, "output_tokens", None),
latency_s=latency_s,
model=self.model,
mode="api",
)
Minimalny backend lokalny
import os
import time
class LocalBackend:
def __init__(self) -> None:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
self.torch = torch
self.model_name = os.getenv("LOCAL_MODEL", "Qwen/Qwen2.5-0.5B-Instruct")
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
self.model = AutoModelForCausalLM.from_pretrained(
self.model_name,
dtype="auto",
)
self.model.to(self.device).eval()
def generate(self, system: str, user: str, max_tokens: int = 160) -> Generation:
started = time.perf_counter()
messages = [
{"role": "system", "content": system},
{"role": "user", "content": user},
]
inputs = self.tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_dict=True,
return_tensors="pt",
).to(self.device)
with self.torch.inference_mode():
output = self.model.generate(
**inputs,
max_new_tokens=max_tokens,
do_sample=False,
pad_token_id=self.tokenizer.eos_token_id,
)
input_tokens = int(inputs["input_ids"].shape[1])
new_ids = output[0, input_tokens:]
generated_text = self.tokenizer.decode(new_ids, skip_special_tokens=True)
latency_s = time.perf_counter() - started
return Generation(
text=generated_text,
input_tokens=input_tokens,
output_tokens=int(new_ids.shape[0]),
latency_s=latency_s,
model=self.model_name,
mode="local",
)
Jeżeli dany tokenizer nie ma szablonu rozmowy, aplikacja powinna mieć kontrolowany fallback. Nie sklejaj przypadkowo napisów typu [SYSTEM], jeśli dokumentacja modelu wymaga innego formatu – format rozmowy jest częścią kontraktu modelu.
To samo pytanie, dwa wykonania
Wyślij to samo pytanie do dostępnego backendu API i lokalnego. Nie oceniaj jeszcze, który model jest „mądrzejszy”. Porównaj format odpowiedzi, liczbę tokenów, czas oraz zachowanie przy limicie 30 i 160 tokenów.
import os
def make_backend() -> LLMBackend:
mode = os.getenv("LLM_MODE", "local")
if mode == "local":
return LocalBackend()
if mode == "api":
return ApiBackend()
raise ValueError("LLM_MODE musi mieć wartość 'local' albo 'api'")
if __name__ == "__main__":
import dotenv
dotenv.load_dotenv()
backend = make_backend()
result = backend.generate(
system="Odpowiadaj krótko i jasno po polsku.",
user="Wyjaśnij, czym jest token w modelu językowym.",
max_tokens=160,
)
print(result.text)
print(
f"\nModel: {result.model} | tryb: {result.mode} | "
f"wejście: {result.input_tokens} tokenów | "
f"wyjście: {result.output_tokens} tokenów | "
f"czas: {result.latency_s:.2f} s"
)
Jeśli nie masz klucza API, wykonaj ćwiczenia na modelu lokalnym lub stubie. Tryb API możesz obejrzeć na wspólnej demonstracji i dodać później – opanowanie materiału nie powinno zależeć od dostępu do płatnej usługi.
Loguj zdarzenie, nie całą prywatną rozmowę
Na tym etapie wystarczy pojedynczy rekord JSONL zawierający czas, model, tryb, liczbę tokenów i status. Surowy prompt może zawierać dane osobowe albo sekret, dlatego nie powinien domyślnie trafiać do logów.
import json
from dataclasses import asdict
from datetime import datetime, timezone
def append_event(path: str, result: Generation, status: str = "ok") -> None:
event = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": status,
**{k: v for k, v in asdict(result).items() if k != "text"},
}
with open(path, "a", encoding="utf-8") as file:
file.write(json.dumps(event, ensure_ascii=False) + "\n")
Praca z AI: możesz poprosić narzędzie o wyjaśnienie nieznanego parametru albo wygenerowanie klasy pomocniczej. Potem wskaż w kodzie: skąd biorą się tokeny wejścia, gdzie kończy się prompt, jak odcinane są tokeny odpowiedzi i dlaczego tekst nie jest logowany.
Rezultat po tej części
Oddaj repozytorium projektu kursowego. Powinno zawierać:
- strukturę
src/llm_course,data,tests,reportsilogs, - pliki
requirements.txtorazrequirements-lock.txtalbo przetestowany plik wersji udostępniony na kursie, pyproject.tomli krótkieREADME.mdz instrukcją odtworzenia środowiska,- pliki
.env.templatei.gitignore; lokalny.envnie może być śledzony przez Git, - wynik skryptu kontrolnego zapisany jako
environment.txt, - plik
lab01a.jsonz nazwą tokenizera i wynikami dla co najmniej pięciu różnych tekstów, - jedno wywołanie modelu z trybem, czasem i liczbą tokenów,
- krótką odpowiedź: dlaczego liczby tokenów nie można wiarygodnie oszacować liczbą słów,
- krótkie potwierdzenie, że to repozytorium będzie rozwijane w kolejnych laboratoriach.
Podsumowanie
Co naprawdę warto zapamiętać?
- jedno repozytorium i jedno środowisko rozwijasz przez cały kurs
- requirements.txt opisuje potrzeby projektu, a plik lock dokładny działający stan
- LLM przyczynowy przewiduje następny token warunkowo względem poprzednich
- token nie jest tym samym co słowo
- tokenizacja zależy od modelu i jego tokenizera
- instrukcje, historia, RAG i narzędzia zajmują wspólne okno kontekstu
- tryb API oraz lokalny powinny realizować wspólny kontrakt
- plik .env.template może trafić do repozytorium, lecz .env z sekretami nie
Najpierw ustal, co rzeczywiście trafia do modelu i co z niego wraca. Dopiero potem oceniaj jakość odpowiedzi.
Co możesz sprawdzić w dokumentacji lub z pomocą AI?
- dokładną nazwę i dostępność modelu na danym koncie
- aktualną cenę miliona tokenów
- maksymalny kontekst i format rozmowy konkretnego modelu
- szczegóły konfiguracji urządzenia w PyTorch
- zgodność pakietu faiss-cpu z używanym systemem i wersją Pythona
Pytania kontrolne
1. Dlaczego przez cały kurs rozwijamy jedno repozytorium i jedno środowisko?
2. Czym różni się requirements.txt od pliku z dokładnie zablokowanymi wersjami?
3. Dlaczego .env.template śledzimy w Git, a plik .env ignorujemy?
4. Dlaczego dwa tokenizatory mogą podzielić ten sam tekst inaczej?
5. Które elementy aplikacji zużywają okno kontekstu?
6. Dlaczego pole usage z API jest lepsze niż heurystyka liczby znaków?
7. Po co aplikacji wspólny interfejs dla modelu lokalnego i API?
8. Jakie informacje warto logować, a jakich domyślnie nie zapisywać?