Przejdź do głównej zawartości

Jakość lokalnych LLM: siedem modeli, dwa trudne pytania i wzorzec „brzmi jak ekspert, zmyśla konkrety”

· 10 min aby przeczytać
Przemysław Majdak
Full-Stack Developer, Automation Engineer & Web Security Specialist

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:

  1. PostgreSQL. Tabela 80 mln wierszy, kolumna status (5 wartości, 95% to 'done'), zapytanie WHERE 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óg REINDEX CONCURRENTLY kontra VACUUM FULL, strojenie autovacuum_*_scale_factor.
  2. 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

ModelProducentArchitekturaBackendPrędkośćOcena
Qwen3-30B-A3B-Thinking-2507AlibabaMoE + reasoningCPU7–8 tok/swspółlider
Nemotron-3.5-Lightning-30B-A3BNVIDIAMoECPU10,6 tok/swspółlider
Qwen3-Coder-30B-A3BAlibabaMoECPU12,5–17,4 tok/sśrodek
gpt-oss-20bOpenAIMoEGPU13–15 tok/sśrodek
ERNIE-4.5-21B-A3BBaiduMoEGPU~15 tok/sśrodek
Dolphin3.0-Llama3.1-8BdenseGPU5,8 tok/ssłaby
granite-4.0-h-tinyIBMMoE hybrydowyGPU~25 tok/snajsł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_size daje wartość ujemną i nie mierzy bloatu. ERNIE: podwójny JOIN pg_class bez 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ślny n_distinct=5 prowadzi 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 dwa ALTER 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-server przez adb 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ęły SSLKEYLOGFILE przy 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ął.
  • SSLKEYLOGFILE w 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 (jest pg_relation_size), avg_width nie ma w pg_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-30BNemotron-3.5
Czas do odpowiedzi1 min 22 s~13 min
Trafione błędy (z 10)47
Zmyślone „błędy”30
Poprawiona wersja koduzachowała 3 realne błędydział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ć.