wszystkie wpisy 2026 Programowanie
Programowanie · Sztuczne sieci neuronowe · 2026-06-04 · 7 min

Kod, który zabija przeglądarkę – czyli spostrzeżenia po benchmarku małych modeli LLM

Jest taki moment, kiedy model zwraca kod, plik się otwiera i przez dwie sekundy myślisz, że działa. Czasem faktycznie. A czasem karta przeglądarki zostaje pusta, albo wyskakuje biały ekran z błędem w konsoli. Ten sam model. To samo zadanie. Inna próba.

To jest historia o najnudniejszej i najważniejszej z cech generowanego kodu przez LLM: powtarzalności.

Zadanie: jedno HTML, sieć neuronowa

Do benchmarku, na którym sprawdzam modele do generowania kodu (https://benchmark.refio.dev/tasks/neuron_growth_simulation), dorzuciłem zadanie: napisz w jednym pliku HTML interaktywną symulację rozrostu sieci neuronowej. Canvas, prosta fizyka 2D, cząstki, kilka suwaków. Bez bibliotek, bez backendu – ma po prostu działać.

Przetestowałem na tym tasku 16 modeli, po sześć prób każdy – chmurowe (Claude, GPT, GLM) i lokalne, odpalane na własnej maszynie (Gemma, gpt-oss, Qwen w różnych rozmiarach). Każdy artefakt oceniam tym samym kryterium, które nazywam works out of the box: czy odpala się i robi to, co miało, bez ręcznych poprawek. Zero = nie rusza, połowa = działa, ale z bugami, jeden = działa.

Wyniki: nie „kto umie”, tylko „jak często mu wychodzi”

Czołówka i dół stawki (pełna tabela 16 modeli jest na https://benchmark.refio.dev/tasks/neuron_growth_simulation):

Model działa bugi nie rusza wynik
Anthropic Sonnet 4.6 6 0 0 1,00
GPT 5.1 codex mini 6 0 0 1,00
Anthropic Haiku 4.5 5 1 0 0,92
Qwen 3.5 27B 1 2 3 0,33
Gemma 4 26B 1 1 4 0,25
Gemma 4 31B 1 0 5 0,17

(Qwen 3.5 9B remisuje z Gemmą 4 26B – 0,25; pełna szesnastka na benchmarku.)

Rozkład: ile z 6 prób każdego modelu zadziałało, miało bugi albo w ogóle nie ruszyło

Dwie rzeczy rzucają się w oczy.

Po pierwsze: tylko dwa modele zaliczyły komplet sześć na sześć – Sonnet 4.6 i GPT 5.1 codex mini. Cała czołówka to chmura i duże modele.

Po drugie, i to jest ciekawsze: żaden model nie miał zera na sześć. Nawet najsłabsza Gemma 4 31B, która pięć razy w ogóle nie ruszyła, raz napisała coś działającego. Czyli różnica między modelami nie jest „ten umie, tamten nie umie”. Każdy potrafi to napisać. Różnica jest w tym, ile razy na sześć mu się to udaje.

A to jest dokładnie ta cecha, której nie widać, kiedy odpalasz model raz, trafiasz na dobrą próbę i mówisz „działa, jest super”. Mały lokalny model, który robi działający kod raz na cztery podejścia, w demie wygląda identycznie jak ten, który robi go zawsze. Różnicę widać dopiero w skali – czyli w robocie.

Co znaczy „nie rusza”

Najczęstsze powody, dla których próba lądowała w kolumnie „nie rusza”, były prozaiczne:

  • biały ekran – kod się ładuje, ale na canvasie nic się nie pojawia (literówka w selektorze, pętla, która nigdy nie startuje).
  • błąd w konsoli na starcie – klasyczny undefined is not a function, bo model odwołał się do czegoś, czego nie zdefiniował.

Nic egzotycznego. Po prostu kod, który wygląda jak kod, ale nie jest kodem, który działa.

Neuron Growth Anthropic Sonnet 4.6

Druga twarz porażki: kiedy kod nie pada, tylko zawiesza

Benchmark łapie próby, które się nie ładują albo mają bugi. Ale jest jeszcze jeden, paskudniejszy tryb porażki, którego sucha ocena „działa / nie działa” nie oddaje: kod, który się ładuje, rusza – i po kilku sekundach zawiesza kartę.

Symulacja rosnącej sieci to dla tego idealna pułapka. Animację robi się przez requestAnimationFrame, który woła sam siebie w pętli. Jeśli „rosnąca” sieć rośnie bez limitu, a rysowanie połączeń jest kwadratowe (każdy z każdym), to po kilkuset węzłach jedna klatka liczy się sekundy, pamięć puchnie, a przeglądarka idzie się powiesić – sama z siebie. Kod formalnie „działa”.

I nie jest to teoria. Otworzyłem jedną z prób gpt-oss 20B (lokalny model). Sercem symulacji jest funkcja update(), którą requestAnimationFrame woła jakieś 60 razy na sekundę. A w niej coś takiego:

// gpt-oss 20B, próba 02 - fragment update(), woła się co klatkę
finishedBranches.forEach(b => {
  if (b.depth >= params.maxDepth) return;
  if (Math.random() < params.splitChance) {            // skończona gałąź losuje "podziel się"...
    const child = new Branch(b.end, dir, b.depth + 1); // ...i robi to CO KLATKĘ, w nieskończoność
    neurons.forEach(n => n.branches.push(child));      // a do tego dokleja tę samą gałąź do KAŻDEGO neuronu
  }
});

// niżej, w tej samej klatce - wykrywanie synaps "każdy z każdym" po rosnącej liście:
for (let i = 0; i < finishedBranches.length; i++)
  for (let j = i + 1; j < finishedBranches.length; j++)
    if (blisko(finishedBranches[i], finishedBranches[j])
        && !synapses.some(s => ...))   // a w środku jeszcze przeszukanie wszystkich synaps
      synapses.push(new Synapse(...));

Zobacz, co tu się dzieje. Skończona gałąź nigdy nie znika z finishedBranches. Więc co klatkę każda dotychczasowa gałąź na nowo rzuca kostką, czy się rozmnożyć – i te, którym wypadnie, dorzucają nowe gałęzie. Lista rośnie, a nowych dzieci doklejamy do wszystkich neuronów naraz (to akurat zwykły bug). Na to nakłada się pętla po synapsach: dwa zagnieżdżone for po tej samej puchnącej liście, a w środku jeszcze synapses.some(...). Przy stu gałęziach to dziesięć tysięcy porównań na klatkę. Przy tysiącu – milion. Co klatkę. Bez limitu węzłów, bez cancelAnimationFrame (sprawdziłem – nie ma go w żadnej z 96 prób).

Efekt jest dokładnie taki, jak na początku: ładuje się, przez chwilę ładnie rośnie, a po kilku sekundach jedna klatka liczy się ułamki sekundy, potem sekundy, RAM puchnie. Karta nie pada z błędem – po prostu przestaje odpowiadać a czasami i przeglądarka (niby jest izolacja procesów).

Najlepsze jest to, że jeden z modeli (Qwen 27B) napisał sobie w tym miejscu komentarz wprost: „For now we keep them to maintain the network structure” – „na razie trzymamy je, żeby zachować strukturę sieci”. Czyli sam zauważył, że nic nie kasuje, uznał to za ficzer i pojechał dalej. Podpisał się pod wyciekiem własnym komentarzem :)

Dla kontrastu Haiku, który to zadanie zaliczał prawie zawsze, robił prostą rzecz: split odpalał się raz, w momencie gdy gałąź kończy wzrost, i tylko dopóki depth < maxDepth. Drzewo się domyka, sieć jest skończona, pętla ma co rysować i nie zżera maszyny. Ta sama animacja, ta sama biblioteka do suwaków – różnica jest w jednym „kiedy”.

Wniosek

„Kompiluje się” to nie poprzeczka jakości – to podłoga. Poprzeczka to powtarzalnie działa. I to jest cała wartość robienia własnego benchmarku zamiast czytania cudzych tabelek: dopiero jak puścisz ten sam test sześć razy, widzisz, że najsłabszy model potrafi błysnąć, a najlepszego poznajesz po tym, że nie ma gorszych dni.

Modele sprawdzam na pluginie do InteliJ i CLI https://refio.dev/ – mój poligon do testowania lokalnych i chmurowych modeli na Agencie co raczej nie działa standartowo. Jeśli grzebiesz w podobnych rzeczach, napisz, chętnie pogadam.


Następny wpis →

Jeden mały trik i nie tracisz kontekstu w lokalnych modelach LLM

2026-06-21 · 5 min czytania

0 komentarzy

Zostaw komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *