Jakość lokalnych LLM: siedem modeli, dwa trudne pytania i wzorzec „brzmi jak ekspert, zmyśla konkrety”
Prędkość i jakość lokalnych modeli językowych nie korelują ze sobą w żaden użyteczny sposób. Najszybszy z siedmiu testowanych modeli — 25 tokenów na sekundę — wypadł merytorycznie najgorzej. Najlepszy jakościowo generował 7 tokenów na sekundę. A wspólny mianownik prawie wszystkich odpowiedzi to jeden powtarzalny wzorzec: poprawna struktura i trafny kierunek, przy zmyślonych konkretach — nazwach funkcji API, składni komend, definicjach statystycznych. To jest różnica między „brzmi jak ekspert” a „można wkleić i uruchomić”.
Metodologia
To druga część testów lokalnych modeli na ThinkPadzie L16 Gen 2 (sprzęt, backendy i wydajność opisałem w pierwszym wpisie o MoE vs dense). Tym razem nie chodzi o tokeny na sekundę, tylko o to, czy odpowiedź jest prawdziwa.
Każdy z siedmiu modeli dostał te same dwa pytania, celowo specjalistyczne, z domen, w których łatwo odróżnić wiedzę od jej imitacji:
- PostgreSQL. Tabela 80 mln wierszy, kolumna
status(5 wartości, 95% to'done'), zapytanieWHERE status='pending' AND created_at > now()-interval '7 days'wolne (4 s) mimo indeksu B-tree(status, created_at). Prośba o pełną diagnozę:EXPLAIN (ANALYZE, BUFFERS), rolępg_stats/n_distinct/selektywności dla nierównego rozkładu, indeks złożony kontra partial index, pomiar bloatu i prógREINDEX CONCURRENTLYkontraVACUUM FULL, strojenieautovacuum_*_scale_factor. - Bezpieczeństwo mobilne. Burp Suite + Android 14 bez roota + certificate pinning + HTTP/2. Prośba o konfigurację proxy, instalację CA bez roota, obejście pinningu (konkretne narzędzie), ustawienia TLS pod HTTP/2 i weryfikację w Wireshark, że ruch faktycznie idzie przez proxy.
Ocena była rygorystyczna: każdą nazwę funkcji, kolumnę, składnię i twierdzenie o zachowaniu systemu weryfikowałem osobno. „Brzmi sensownie” nie liczyło się jako punkt.
Ranking
| Model | Producent | Architektura | Backend | Prędkość | Ocena |
|---|---|---|---|---|---|
| Qwen3-30B-A3B-Thinking-2507 | Alibaba | MoE + reasoning | CPU | 7–8 tok/s | współlider |
| Nemotron-3.5-Lightning-30B-A3B | NVIDIA | MoE | CPU | 10,6 tok/s | współlider |
| Qwen3-Coder-30B-A3B | Alibaba | MoE | CPU | 12,5–17,4 tok/s | środek |
| gpt-oss-20b | OpenAI | MoE | GPU | 13–15 tok/s | środek |
| ERNIE-4.5-21B-A3B | Baidu | MoE | GPU | ~15 tok/s | środek |
| Dolphin3.0-Llama3.1-8B | — | dense | GPU | 5,8 tok/s | słaby |
| granite-4.0-h-tiny | IBM | MoE hybrydowy | GPU | ~25 tok/s | najsłabszy |
Kolejność jest niemal odwrotna do prędkości. Dwa najwolniejsze modele MoE 30B rozumowały najlepiej. Najszybszy, granite z ~1 mld aktywnych parametrów, wypadł poniżej ośmiomiliardowego Dolphina.
Wzorzec, który powtórzył się prawie wszędzie
Modele nie-reasoning — Dolphin, gpt-oss, Qwen-Coder, ERNIE — mają wspólny profil, potwierdzony na obu domenach:
Dobra struktura i trafny kierunek wysokopoziomowy, przy zawodnych weryfikowalnych konkretach. Nazwy funkcji API, działający kod, jednostki, definicje statystyczne, dokładne zachowanie systemu — tu się sypią.
Kilka przykładów z odpowiedzi na PostgreSQL, wszystkie z modeli, które poza tym brzmiały kompetentnie:
- Zepsuty SQL na pomiar bloatu. Qwen-Coder:
pg_size_pretty(...)::numeric(funkcja zwraca tekst — błąd typu), a wzór(index_size − table_size)/table_sizedaje wartość ujemną i nie mierzy bloatu. ERNIE: podwójnyJOIN pg_classbez aliasów (ambiguous column, nie uruchomi się). - Inwersja selektywności. ERNIE wielokrotnie nazywa
status='pending'(5% wierszy) „bardzo nieselektywnym” — jest dokładnie odwrotnie, 5% to wysoka selektywność. Nie trafił w prawdziwy mechanizm: domyślnyn_distinct=5prowadzi planner do estymaty ~20% (16 mln wierszy) i stąd Seq Scan. - Autovacuum ustawiany per-wartość. ERNIE proponuje różne
autovacuum_vacuum_scale_factor„dla pending” i „dla done” przez dwaALTER TABLE orders SET (...)— ustawienia są per-tabela, drugi ALTER nadpisuje pierwszy. Dolphin poszedł dalej i podał kierunek odwrotny (zwiększyć scale_factor zamiast zmniejszyć), co pogorszyłoby sytuację. pgstattuple— dokładne narzędzie, o które proszono — pominęło pięć z siedmiu modeli. Dwa, które je wymieniły, to najsłabszy granite (nazwał, nie użył) i, jak się okaże, jeden z liderów.
Po stronie bezpieczeństwa mobilnego wzorzec był identyczny. Modele nazywały właściwe narzędzia (Frida, objection), ale gubiły się w realiach:
- Sprzeczność w sercu scenariusza bez roota. Cztery modele twierdzą „bez roota”, a zaraz każą uruchomić
frida-serverprzezadb shell— co wymaga roota. Poprawna ścieżka bez roota to wstrzyknięcie frida-gadget do przepakowanego APK; wskazał ją dopiero tryb reasoning, i to nie w pełni. - Zmyślone ścieżki UI. ERNIE: eksport certyfikatu CA Burpa przez „Proxy → HTTP history → right-click → Save as certificate” (nie istnieje; eksport jest w Proxy → Options) i instalacja przez menu Androida „Settings → Security & Location → Encrypted backups → Trusted credentials” (też nie istnieje).
- Błędny port. ERNIE: „domyślny port nasłuchu Burpa 1080” — Burp używa 8080, a 1080 to SOCKS. Za tym poszedł błędny filtr Wireshark
tcp.port == 1080. - Halucynacje pełnych narzędzi. gpt-oss wymyślił aplikację „Burp Suite Proxy” rzekomo w Google Play. granite podał nieistniejące „SSL Pinning Evasion Tool” i błędny mechanizm („fałszywy certyfikat z tym samym podmiotem” — pinning przypina hash klucza, podróbka z innym kluczem nigdy nie pasuje).
- Wszystkie siedem pominęło, że od Androida 7 aplikacje domyślnie ignorują certyfikaty użytkownika bez
network_security_config, i wszystkie pominęłySSLKEYLOGFILEprzy dekrypcji w Wireshark.
Co realnie daje reasoning
Qwen3-30B-A3B-Thinking to jedyny w stawce model z jawnym rozumowaniem — połowę wygenerowanych tokenów zajmuje blok <think>. Był jakościowo najlepszy, ale poprawa jest selektywna i warto wiedzieć, gdzie przebiega granica.
Reasoning naprawił błędy logiczne:
- Odwrócone zależności. Qwen-Coder twierdził „mało wyników → Seq Scan szybszy” (odwrotnie); Dolphin odwrócił autovacuum. Model reasoning oba miał poprawnie.
- Pewnie podawane zmyślenia. Tam, gdzie gpt-oss halucynował aplikację, a Qwen-Coder podawał
SSL Kill Switch 2(to narzędzie iOS, nie Android), reasoning się powstrzymał. - Dołożył mechanizmy, których nikt inny nie ruszył. Jako jedyny wywołał
most_common_vals/most_common_freqs— sedno problemu przy skośnym rozkładzie danych. Poprawnie uzasadnił wybór Fridy zamiast Xposed/Magisk (te wymagają roota).
Reasoning nie naprawił braków wiedzy dziedzinowej:
pgstattuple— dokładne narzędzie z pytania — pominął, jak większość.- Zależność „Android 7+ ignoruje certyfikaty użytkownika” — pominął.
SSLKEYLOGFILEw Wireshark — pominął.- Realiów „frida-server wymaga roota” w scenariuszu bez roota — ta sama sprzeczność co u innych.
- Uruchamialnego SQL z dokładnymi nazwami:
pg_index_size()nie istnieje (jestpg_relation_size),avg_widthnie ma wpg_stat_all_tables.
Wniosek: profil przesunął się z „brzmi jak ekspert plus zmyślone konkrety” na „rozumuje poprawnie, konkrety wciąż chwiejne”. To wyraźny upgrade do doradztwa i nauki koncepcji, ale zastrzeżenie „zweryfikuj każdą konkretną komendę i nazwę API przed uruchomieniem” obowiązuje dalej. Koszt: około 2,5-krotnie dłuższy czas odpowiedzi, bo reasoning podwaja liczbę tokenów.
Dwaj liderzy: zasięg kontra czystość
Qwen-Thinking i Nemotron wyszły na równi, ale wygrywały czym innym.
Nemotron wygrywa surowym zasięgiem konkretów. Jako jedyny z siedmiu faktycznie użył pgstattuple (CREATE EXTENSION plus SELECT na tabeli i na indeksie). Jako jedyny podał SET STATISTICS i default_statistics_target dla skośnej kolumny — realne eksperckie strojenie estymacji. I jako jedyny w części o HTTP/2 trafił w ALPN i negocjację h2, czyli istotę pytania, którą pominęli wszyscy pozostali, łącznie z Qwen-Thinking.
Qwen-Thinking wygrywa czystością. Mniej fabrykacji i — co istotne przy polskich promptach — trzymał się polskiego. Nemotron przy polskich pytaniach zjeżdżał na angielski i dorabiał fałszywe uzasadnienia: zmyślił funkcję pg_analyze_relative(), nazwy pól pgstattuple, a przy scenariuszu bez roota wprost napisał „frida-server w trybie user-mode, bez roota” — co jest nieprawdą.
Praktycznie: do doradztwa z weryfikacją Nemotron ma szerszy zasięg trafnych szczegółów, ale każdy trzeba sprawdzić, bo obok trafnych leżą pewnie podane zmyślenia. Qwen-Thinking rzadziej zmyśla i lepiej pisze po polsku, kosztem węższego zasięgu.
Dopisek po dodatkowych testach: przewaga Qwen-Thinking w polszczyźnie okazała się krucha. Wystarczy zakończyć prompt zdaniem „Odpowiadaj wyłącznie po polsku”, żeby Nemotron utrzymał polski przez całą, kilkutysięczną odpowiedź. Kaleczy przy tym pojedyncze słowa — „Wycie semafora” zamiast „wyciek”, „martobliźniony”, „confuzję” — ale nie zjeżdża na angielski. Z dwóch przewag Qwen-Thinking zostaje więc realnie jedna: mniejsza skłonność do fabrykacji.
Trzecie pytanie: specjalista od kodu przegrał na kodzie
Powyższy ranking opiera się na dwóch pytaniach — z baz danych i z bezpieczeństwa. Zostawiał więc otwartą wątpliwość: Qwen3-Coder to model skierowany pod kod, a testowany był wyłącznie poza swoją specjalizacją. Czy na własnym poletku nie odwróci wyniku?
Test rozstrzygający: fragment Pythona z asyncio i aiohttp — pobieracz wielu URL-i z ograniczeniem współbieżności, retry i timeoutem — z dziesięcioma celowo zasianymi, weryfikowalnymi błędami. Polecenie: wskaż wszystkie realne błędy, uzasadnij każdy i podaj poprawioną wersję. Zasiane błędy obejmowały zarówno klasyki (time.sleep wewnątrz korutyny blokujący całą pętlę zdarzeń), jak i rzeczy, które trzeba naprawdę rozumieć: wyciek semafora na ścieżce CancelledError (return bez release), połknięcie CancelledError zamiast ponownego rzucenia, await session.get(...) bez async with (odpowiedź nigdy nie zwolniona), brak raise_for_status(), gather bez return_exceptions=True.
| Qwen3-Coder-30B | Nemotron-3.5 | |
|---|---|---|
| Czas do odpowiedzi | 1 min 22 s | ~13 min |
| Trafione błędy (z 10) | 4 | 7 |
| Zmyślone „błędy” | 3 | 0 |
| Poprawiona wersja kodu | zachowała 3 realne błędy | działa poprawnie |
Coder wymyślił trzy nieistniejące usterki, w tym „brak return w przypadku błędu” (return None jest w kodzie dwa razy) oraz „brak asyncio.shield()” jako lekarstwo na wyciek zasobów — shield chroni przed anulowaniem, niczego nie zwalnia. Gorzej: w sekcji „poprawiona wersja” przepisał połknięcie CancelledError jeden do jednego i nie ruszył ani wycieku odpowiedzi HTTP, ani braku sprawdzenia statusu. Semafor naprawił poprawnie (async with sem), ale nigdzie wcześniej nie zdiagnozował wycieku — trafił przypadkiem, nie ze zrozumienia.
Nemotron jako jedyny nazwał wyciek semafora wraz z konsekwencją: „po przerwaniu dziesięciu zadań wszystkie sloty pozostaną trwale zajęte, program zamarza w kwestii nowych połączeń”. Zamiast asyncio.wait_for użył aiohttp.ClientTimeout(total=5), a w poprawce dał raise zamiast return None. Nie jest bezbłędny: pominął return_exceptions=True (to akurat trafił Coder), a jego wersja trzyma semafor przez całą pętlę retry razem z odczekiwaniem backoffu, co niepotrzebnie ogranicza współbieżność.
Wniosek jest jednak jednoznaczny i niewygodny dla etykiet: model reklamowany jako wyspecjalizowany w kodzie przegrał w recenzji kodu z modelem ogólnym, i to nie o włos. Etykieta „-Coder” w nazwie opisuje zbiór treningowy, nie gwarantuje przewagi w konkretnym zadaniu. Jedyne, co Coder wygrał, to czas: 1:22 kontra 13 minut, bo Nemotron przepala większość budżetu na rozumowanie. Jeśli potrzebujesz szybkiej odpowiedzi i sam zweryfikujesz wynik — Coder ma sens. Jeśli odpowiedź ma być trafna bez twojej korekty — nie ma.
Trzy uwagi, które oszczędzą czasu
Sama nazwa architektury w bibliotece nie znaczy, że model się uruchomi. Nemotron-3.5 (opublikowany 13 sierpnia 2026) w obrazie ramalama:0.21 z maja pada na wrong number of tensors; expected 417, got 408, mimo że string architektury jest obecny w libllama.so. W obrazie 0.24 ładuje się bez problemu. Liczy się data builda llama.cpp względem daty publikacji modelu, nie sama obecność nazwy.
Mały zbiór aktywnych parametrów to niski sufit jakości. granite z ~1 mld aktywnych był najszybszy (~25 tok/s) i najsłabszy merytorycznie — dokładnie tak, jak przewidywała hipoteza. Świetnie nadaje się do zadań trywialnych i szybkich, nie do doradztwa technicznego. Warto to zestawić z wnioskiem z pierwszej części: -A3B (~3 mld aktywnych) to dobry punkt równowagi między prędkością a jakością na tym sprzęcie; -A1B to już za mało.
Model reasoning z ciasnym max_tokens zwraca pustkę, nie skróconą odpowiedź. Nemotron przy max_tokens: 4000 oddał finish_reason: "length" i pole content o długości zero znaków — cały budżet poszedł w reasoning_content. W realnym biegu potrzebował 23 258 znaków rozumowania, zanim wypisał pierwszy znak właściwej odpowiedzi. To nie jest specyfika trudnych pytań: przy max_tokens: 200 na trywialnym „napisz iteracyjne Fibonacci” zachował się identycznie. Wygląda to jak zepsuty model, a jest zwykłym obcięciem budżetu. Praktyczne wnioski: dla modeli reasoning dawaj max_tokens z dużym zapasem, testuj przez streaming ("stream": true) i czytaj oba pola odpowiedzi, nie tylko content.
Wniosek dla praktyki
Jeśli używasz lokalnego LLM do zadań technicznych, jedna zasada jest ważniejsza od wyboru modelu:
Żadnego z tych modeli nie traktuj jako źródła gotowego do wklejenia kodu ani komend bez ręcznej weryfikacji. Nawet najlepszy rozumuje poprawnie, ale myli dokładne nazwy funkcji, składnię i zachowanie systemu.
Lokalny model świetnie sprawdza się jako partner do rozmowy o kierunku rozwiązania, do nauki koncepcji i do przypomnienia, o czym w ogóle pomyśleć — sekcja EXPLAIN, mechanizm selektywności, rodzina narzędzi do obejścia pinningu. Do wygenerowania działającej komendy, którą wklejasz na produkcję, nadal potrzebujesz dokumentacji i własnej głowy. Reasoning przesuwa tę granicę, ale jej nie usuwa.
Cztery z pierwszej piątki to modele MoE 30B, które na tym laptopie chodzą na czystym CPU z użyteczną prędkością — co znaczy, że sensowny lokalny asystent techniczny jest dziś w zasięgu zwykłego ThinkPada bez dedykowanej karty graficznej. Trzeba tylko wiedzieć, czego od niego nie oczekiwać.