Tworzenie LetsGit.IT z LLM: uczciwy quiz
Jak naprawiliśmy popularny błąd w quizach: użytkownicy mogli zgadywać poprawną odpowiedź, wybierając po prostu najdłuższą opcję.
Podsumowanie
Historia poprawki: usunęliśmy efekt „najdłuższa odpowiedź = poprawna” w quizie LetsGit.IT, zachowując pełne wyjaśnienia poza quizem.
Najwazniejsze wnioski
- Najpierw mierz stronniczość, potem poprawiaj UX.
- W quizie używaj osobnych, krótszych wariantów odpowiedzi zamiast przycinania.
- Dystraktory muszą być wiarygodne — usuń absoluty typu „zawsze/nigdy”.
- Tanie generowanie (odwrócenie trade-offu) potrafi dać subtelne błędne opcje.
- Domykaj zmianę czystymi commitami z pomocą commit-work.
Tworzenie LetsGit.IT z LLM: uczciwy quiz
To jest pierwszy artykuł z serii o budowaniu LetsGit.IT z pomocą LLM. Nie chodzi o „AI czary”, tylko o praktyczną inżynierię produktu: zmierzyć problem, poprawić model danych (a nie tylko UI), i dowieźć zmianę w sposób, który da się utrzymać.
Ten tekst bazuje na notatkach z sesji z 2025-12-29 (sessions/articles/2025-12-29.md). Wersja angielska: /en/blog/building-letsgit-it-with-llm-quiz-fairness.
Problem: quiz był rozwiązywalny „po długości”
W LetsGit.IT jest tryb quizu wielokrotnego wyboru: jedna odpowiedź poprawna + (zwykle) dwa dystraktory. Teoretycznie wygrywasz dzięki wiedzy. W praktyce dane tworzyły bardzo mocny skrót myślowy:
- poprawna odpowiedź była prawie zawsze najdłuższa
- błędne odpowiedzi były krótkie, ogólnikowe albo „oczywiście złe” (często z absolutami)
To już nie jest subtelny problem UX. To problem sensowności quizu: przestaje mierzyć wiedzę, a zaczyna mierzyć umiejętność rozpoznawania wzorca.
Krok 1: zmierzyć (zamiast dyskutować)
Zanim ruszyliśmy UI czy copy, zrobiliśmy szybki audyt seedów (recruitment-assistant/prisma/data/migrations/*.json), sprawdzając per pytanie, czy poprawna odpowiedź jest ściśle najdłuższa w zestawie opcji quizu.
Wynik był prosty i bezlitosny: 100% pytań we wszystkich kategoriach (EN/PL) miało poprawną odpowiedź jako najdłuższą.
Najważniejsza lekcja nie jest w samej liczbie. Jest w podejściu:
- wybierz metrykę, która opisuje realne zachowanie użytkownika („czy długość przewiduje poprawność?”)
- zrób ją tanią do przeliczenia
- używaj jej jako guardraila na przyszłość
Krok 2: kuszący „quick fix” — i dlaczego go cofnięliśmy
Pierwsza próba naprawy była typowa dla sytuacji „dowozimy teraz”: normalizacja wysokości opcji i clamp/trim tekstu odpowiedzi w UI quizu. Pomysł: jeśli użytkownik nie widzi różnicy długości, znika podpowiedź.
To rzeczywiście zmniejsza wizualny sygnał, ale wprowadza nowe problemy:
- przycinanie potrafi usunąć fragment, który czyni odpowiedź poprawną
- zaczynasz „ukrywać sens”, zamiast poprawiać jakość treści
- to kłóci się z wymaganiem produktu: poza quizem pełne odpowiedzi muszą zostać (lista pytań, widok szczegółów)
Dlatego cofnięliśmy tę ścieżkę i przeszliśmy na czystszy model.
Krok 3: osobne warianty odpowiedzi do quizu (krótkie odpowiedzi)
Lepsza naprawa to przyznać wprost, czego potrzebuje quiz: krótszych, quizowych wariantów odpowiedzi, przy zachowaniu pełnych odpowiedzi wszędzie poza quizem.
Zrobiliśmy to przez małą, jawną warstwę:
recruitment-assistant/src/data/quiz-answers.tstrzyma per-pytanie override’y (EN + PL)recruitment-assistant/src/lib/questions.tsrozwiązuje wariant odpowiedzi quizowej w runtime- UI quizu bierze „quiz answer”, a listy/szczegóły nadal pokazują pełną odpowiedź
Konceptualnie:
type QuizAnswerVariant = {
questionSlug: string;
en: { quizAnswer: string };
pl: { quizAnswer: string };
};
To jest celowo „nudne” rozwiązanie:
- jest jawne (żadnego heurystycznego przycinania w renderze)
- jest testowalne (pytanie ma override albo nie)
- spełnia obietnicę produktu: „w quizie krótko, w szczegółach pełny kontekst”
I to jest też miejsce, gdzie LLM realnie pomaga: generuje pierwsze wersje skrótów, które potem można dopracować. Kluczowe ograniczenie: skrót musi zachować poprawność merytoryczną.
Krok 4: wiarygodne dystraktory (bez ręcznego przepisywania wszystkiego)
Gdy „długość poprawnej odpowiedzi” przestała być sygnałem, wyszła kolejna słabość: dystraktory były za słabe. Dwa powtarzające się wzorce:
- absoluty („zawsze”, „nigdy”, „niemożliwe”) — w inżynierii to rzadko prawda
- odpowiedzi błędne były zbyt krótkie albo semantycznie odjechane
Zrobiliśmy automatyczny pass po wrong answers i przerobiliśmy ok. 2.4k wpisów, usuwając najbardziej oczywiste „podpowiedzi”, a jednocześnie zachowując to, że odpowiedź ma być błędna.
To nie jest „utrudnianie quizu na siłę”. To uczynienie go użytecznym. Dobry dystraktor ma być „blisko prawdy”, żeby użytkownik musiał pamiętać kluczowy szczegół.
Lista wykluczeń powstała… ale ją wstrzymaliśmy
Wygenerowaliśmy też listę kandydatów do wykluczenia pytań, które są zbyt szerokie do formatu multiple choice (głównie na podstawie długości i zakresu odpowiedzi). Decyzja na ten moment: nie wykluczamy jeszcze pytań (lista była zbyt agresywna).
Automatyzacja może zasugerować listę, ale decyzja produktowa nadal wymaga ręcznego przeglądu.
Odwracanie trade-offów: tanie, wiarygodne błędne opcje
Dla części odpowiedzi da się wygenerować wiarygodną błędną opcję przez odwrócenie trade-offu. Przykłady:
- „większa spójność, większa latencja” → „większa spójność, mniejsza latencja”
- „mniej hopów sieciowych, mniejsza elastyczność” → „mniej hopów, większa elastyczność”
Dodaliśmy lekki krok runtime w generatorze opcji quizu: jeśli poprawna odpowiedź zawiera jawny trade-off, generujemy „odwrócony” wariant jako dodatkowego kandydata na dystraktor.
Uproszczony schemat:
To nie jest „LLM w produkcji”. To deterministyczne, lekkie wzbogacenie, które poprawia wiarygodność tam, gdzie dane już zawierają trade-off.
Krok 5: domknąć zmianę (tu brakowało info o skillu commit-work)
Notatki z sesji często kończą się na „wdrożyliśmy”. Ale ostatnie 10% ma znaczenie:
- zmiana ma być czytelna za miesiąc
- ma dać się zdebugować i zrevertować
- historia w git ma pomagać, a nie przeszkadzać
W Codex CLI wchodzi tu commit-work (skill „commit”). To w praktyce ustrukturyzowana checklista + pomocnik:
- przegląd diffu (co się zmieniło i czy na pewno to miało się zmienić)
- propozycja sensownego podziału na commity (kod vs docs, refactor vs dane)
- przygotowanie komunikatu w stylu Conventional Commits
W historii repo widać właśnie taki podział:
feat(quiz): improve distractors and quiz answersdocs: update session log
Praktyczna pętla „LLM pomaga, ale odpowiedzialność zostaje po naszej stronie”:
cd recruitment-assistant
bun lint
bun run test
Następnie:
- staging tylko plików, które mają wejść do commit
- użycie skilla do wygenerowania komunikatu i sanity-checku diffu
LLM jest przydatny wtedy, gdy workflow nadal ma twarde granice.
Co to mówi o budowaniu LetsGit.IT z LLM
- Dane są częścią produktu. Jeśli dane są stronnicze, UX będzie stronniczy.
- UI to nie substytut jakości treści. Przycinanie „leczy objaw”.
- Preferuj jawne kontrakty.
quizAnswerto kontrakt; „przytnij w UI” to hack. - Automatyzuj passy, potem dopracuj przypadki brzegowe. Masowa poprawka robi 80%, człowiek domyka 20%.
- Dowiezienie jest etapem pierwszej klasy. Czyste commity obniżają koszt kolejnych iteracji (łatwiej dać LLM właściwy kontekst).
Co dalej w serii
W kolejnych tekstach wejdziemy głębiej w:
- ręczne dopracowanie per kategoria (start od kategorii typu /pl/category/data-structures)
- pomiar trudności quizu i jakości dystraktorów w czasie
- utrzymanie spójności EN/PL bez zamieniania edycji treści w udrękę
Jeśli budujesz własne narzędzie do przygotowań rekrutacyjnych, ten wzorzec możesz skopiować niezależnie od stacku: pomiar biasu → kontrakt treści → deterministyczne wzbogacenie dystraktorów → dowiezienie w czytelnych commitach.