Case study · Platforma rekrutacyjna (NDA)

Przebudowa wyszukiwarki kandydatów, rozstrzygnięta benchmarkiem

Rekruter opisuje zwykłym zdaniem, kogo szuka, a system przeszukuje bazę kilku tysięcy CV i układa ranking. To case study opowiada, jak przebudowałem silnik tej wyszukiwarki — i skąd wiadomo, że nowa wersja jest lepsza, a nie tylko inna.

Krótka wersja: z logów, które wyszukiwarka zbierała od tygodni, powstał benchmark odwzorowujący realny ruch; sędzia AI został skalibrowany jak przyrząd pomiarowy; nowa wersja silnika powstała w izolacji od produkcyjnej. Na 50 ogłoszeniach benchmarku układa listę bliżej niezależnej oceny w 22 przypadkach — wersja wyjściowa w 2.

Dalszy ciąg dopisała produkcja. Nowa wersja zastąpiła starą, a bilans po wdrożeniu policzyłem na 1214 wierszach logów realnych wyszukiwań. Największy zysk nie leży w ocenach kandydatów — leży w czterech klasach defektów, które klient widywał na liście, a które z niej zeszły. Telemetria jakości liczy od tamtej pory korelację rankingu z sędzią dla każdego realnego wyszukiwania: we wrześniu 2026 średnia Spearmana dla wersji z rerankerem to 0,72 na 108 wyszukiwaniach — poziom, który benchmark zapowiadał.

01Zanim powstał benchmark: telemetria i pierwsze pomiary

Wyszukiwarka najpierw dostała telemetrię: każde wyszukiwanie zapisuje pełne składowe punktacji, warunki pobierania i lejek odrzutów — kto odpadł, na której bramce i dlaczego. Bez tego każda rozmowa o jakości kończyła się na wrażeniach; z tym te same logi stały się później surowcem, z którego powstał benchmark.

Pierwsza runda pomiarów pokazała, że problemem są dane, nie model: ta sama umiejętność bywała zapisana w bazie na ponad półtora tysiąca sposobów, a własne R&D rerankingu cross-encoderem zakończyło się udokumentowaną decyzją o niewdrażaniu — czytał ten sam tekst co model embeddingów i uderzał w ten sam sufit, a zapisany wniosek brzmiał: następny skok da dopiero reranking LLM. Po naprawie danych zgodność rankingu z niezależną oceną wzrosła z 2% do 52%. Ta strona opisuje kolejny krok — łącznie z realizacją tamtego wniosku.

02Benchmark z realnego ruchu, anotowany ręcznie

Z zebranych logów powstał profil realnych zapytań: jakie role, miasta, progi stażu i wymagania językowe rzeczywiście stawiają rekruterzy. Benchmark to 50 wzorcowych ogłoszeń odwzorowujących ten rozkład — nie wymyślone przypadki, tylko lustro ruchu przy bazie kilku tysięcy CV.

Oczekiwania każdego ogłoszenia są spisane ręcznie: czego wymaga, czego nie wymaga, które warunki są twarde. Ręcznie, bo benchmark, którego oczekiwania pochodzą z testowanego kodu, sprawdza tylko zgodność kodu z samym sobą. Osobna grupa ogłoszeń pomiarowych różni się od bazowego dokładnie jednym warunkiem — progiem stażu, widełkami, branżą — więc różnica w wyniku jest ceną tego jednego warunku, a nie mieszanką pięciu przyczyn naraz.

03Sędzia AI, skalibrowany jak przyrząd

Miarą jakości jest pełny raport AI — niezależna druga opinia, która nie widzi punktacji systemu, bo sędzia znający cudzy wynik przestaje być niezależną miarą. Zanim zaufałem jego werdyktom, zmierzyłem sam przyrząd: powtarzalność ocen przy deterministycznym tasowaniu kolejności podania kandydatów (modele faworyzują to, co przeczytały wcześniej) i praktyczny sufit zgodności około 80% — tyle daje ten sam sędzia uruchomiony dwa razy na tej samej liście. Stuprocentowa zgodność nie istnieje dla żadnego podejścia.

Kalibracja dała dwa wnioski ogólniejsze niż ten projekt. Po pierwsze: ogólna zasada w prompcie — „czego ogłoszenie nie postawiło, tego nie punktuj” — była łamana w co trzecim przypadku, a ta sama zasada podana jako wyliczona lista konkretów była respektowana w stu procentach. Apelowanie nie działa, wyliczanie działa. Po drugie: warianty promptu, w tym wzbogacenie sędziego o pełny przebieg zatrudnienia, też zostały zmierzone — i odrzucone, z zapisanymi liczbami i powodem, żeby nikt nie odkrywał tego samego drugi raz.

04Druga wersja silnika — w izolacji, w trzech fazach

Nowy potok powstał obok produkcyjnego, rozdzielony na poziomie plików, nie flag konfiguracyjnych — użytkownicy przez cały czas pracowali na starej wersji, a ścieżka produkcyjna pozostała nietknięta co do bajta.

Faza pierwsza zawęża pulę twardymi warunkami ogłoszenia już przy pobieraniu z bazy wektorowej. Faza druga to bramki redukcji, z których każda odpowiada na jedno pytanie; staż liczony jest kalendarzowo i w zakresie zapytania — dwadzieścia lat w handlu i dwa w księgowości to przy szukaniu księgowej dwa lata — a lejek zapisuje komplet powodów odrzutu, z licznikiem „ilu kandydatów wróci po rozluźnieniu tego warunku”. Faza trzecia punktuje wyłącznie wymiary, które ogłoszenie faktycznie stawia i których nie rozstrzygnęły już filtry: udział ocen 97+, które niczego nie różnicowały, spadł z połowy zapisanych wyników do zera.

Na końcu potoku stoi lekki reranker LLM — realizacja wniosku z odrzuconego cross-encodera. Porządkuje gotową shortlistę, ale nie ma prawa nikogo odrzucić ani dodać, a każde odstępstwo odpowiedzi od pełnej permutacji oznacza powrót do kolejności punktacji, ze śladem w logu. Ten krok też wszedł po pomiarze, nie z wiary: w pilotażu jego kolejność zgadzała się z werdyktem mocniejszego modelu w 66%, sama punktacja w 39% — i to głównie on odpowiada za zgodność kolejności z raportem AI w wynikach poniżej.

05Wagi z pomiaru, nie z wyczucia

Które składowe naprawdę przewidują werdykt sędziego? Korelacje policzyłem wewnątrz pojedynczego ogłoszenia — tam, gdzie ranking powstaje; liczone w poprzek wyszukiwań dawały wnioski odwrotne i to był wcześniejszy błąd metodyczny. Wynik: dopasowanie umiejętności okazało się sygnałem losowym — znak korelacji skakał z ogłoszenia na ogłoszenie — a niosło około dwóch trzecich wyniku. Jedynym konsekwentnym predyktorem było podobieństwo wektorowe.

Nowe wagi leżą na środku szerokiego płaskowyżu, nie na szpilce dopasowanej do szumu, a walidacja na odłożonym ogłoszeniu potwierdziła kierunek: zgodność z sędzią wzrosła z 15% do 41%, pokrycie pierwszej piątki z 58% do 78%.

06Testy bez wołania modelu

Odpowiedź modelu zamienia się w tym potoku w twarde filtry, więc przypadków brzegowych nie da się badać na żywym API: kosztuje, jest niedeterministyczne i nie zostaje w repozytorium. Atrapa modelu zwraca surowy tekst, nie gotowy obiekt — bo połowa realnych usterek mieszka w tekście: płotki markdownu przed JSON-em, zdanie wstępu, odpowiedź ucięta na limicie.

Testy pisane z wymagań biznesowych przed kodem znalazły realne błędy — deklarację poziomu B2 czytaną jako biegłość, bo ogólniejszy wzorzec był sprawdzany pierwszy, i miasto znikające z warunków przez pomyłkę typu w odpowiedzi modelu, po której pula szła na całą Polskę, a wynik wyglądał normalnie, tylko odpowiadał na inne pytanie. Zestaw urósł do ponad 440 testów offline; kontrakt promptu stoi osobno, bo jako jedyny kosztuje.

07Wynik: 50 ogłoszeń, oba podejścia, ten sam sędzia

Każde z 50 ogłoszeń benchmarku poszło do obu wersji, a każdą listę ocenił pełny raport AI — z uczciwie opisanym lejkiem danego podejścia, żeby żadnemu nie punktować kryteriów rozstrzygniętych już przez jego filtry. Koszt całego testu: około 4 USD.

Zgodność kolejności z raportem AI

nowa wersja
70%
wersja wyjściowa
35%

Pierwszy na liście = najlepszy wg raportu AI

praktyczny sufit ≈ 80%

nowa wersja
47%
wersja wyjściowa
36%

Zgodność czołowej trójki

nowa wersja
75%
wersja wyjściowa
56%

Pozycja najlepszego kandydata wg raportu AI

niżej = lepiej

nowa wersja2,0

wersja wyjściowa3,1

22 : 2

ogłoszenia, w których dana wersja układa listę bliżej raportu AI (nowa : wyjściowa; do tego 7 remisów przy 31 porównaniach rozstrzygalnych)

Jak obie wersje dochodzą do listy dziesięciu osób

nowa wersja

119pobranych z bazy65po filtrach (−45%)7,0na liście (mediana 10)

wersja wyjściowa

165pobranych z bazy44po filtrach (−74%)6,9na liście (mediana 10)

Twarde filtry nowej wersji nie zmniejszyły list — pełnych list 10/10 jest więcej (30 wobec 27 na 50). Różnica leży wcześniej: celniejsza, mniejsza pula już przy pobieraniu z bazy, więc filtry mają mniej do wycinania i do punktowania dochodzi więcej kandydatów. Około 90% odsiewu robią dwa warunki ogłoszeń: wymagane języki i próg stażu.

Cena tej jakości jest policzona, nie przemilczana: średni czas wyszukiwania 22,6 s wobec 11,5 s — różnica to głównie krok układania kolejności przez model — a koszt samego zapytania ok. 0,008 USD wobec 0,003 USD.

08Wdrożenie: nowa wersja przejmuje ruch

Ten sam wynik w trzech pomiarach

Ta sama próbka 50 ogłoszeń: wersja wyjściowa i dwa etapy nowej.

pomiarskład listykolejność
wersja wyjściowa59,00,35
nowa — bramki i próg jakości64,30,69
nowa — po geokodowaniu miast65,40,69

Kolejność stanęła na 0,69–0,73 w trzech pomiarach z rzędu i to jest tutaj najważniejsze zdanie: wynik z benchmarku nie był przypadkiem jednego przebiegu. Jest zarazem sufitem — filtry go już nie ruszą, kolejny skok musiałby przyjść z rankingu. Skład listy rośnie o jakieś sześć punktów, ale ostatni krok mieści się w szumie sędziego, więc liczy się kierunek, nie trzecia cyfra.

Jak wyglądało samo przepięcie

Benchmark rozstrzyga spór, ale wdraża się kod, nie benchmark. Między testem a przepięciem weszła jeszcze ręczna kontrola sześciu ogłoszeń przez testera, człowieka. Najostrzejszy przypadek: na ogłoszeniu o kontrolera finansowego stara wersja ułożyła listę odwrotnie do ocen człowieka, a nowa zgodnie z nimi.

Samo przepięcie zrobiłem odwrotnie, niż podpowiada wygoda: do modułu produkcyjnego pojechał kod nowego silnika, zamiast przepiąć ruch klientów na moduł badawczy. Przepięcie ruchu dałoby potok, którego nigdy nie zmierzyłem w całości — nową listę ocenianą starym promptem. Cała obudowa produkcyjna została nietknięta: logowanie, anonimizacja danych osobowych, kontrakt odpowiedzi. Zmienia się silnik, nie funkcjonalność.

Przed wdrożeniem, nie po: dowód, że kopia jest wierna (różnice wobec wersji badawczej to wyłącznie zmiany zamierzone), test równoważności wyników na ośmiu ogłoszeniach, kontrola anonimizacji na ścieżce klienckiej i opisana z góry ścieżka odwrotu — bez migracji bazy, więc powrót nie wymaga przeliczania niczego. Do każdego zapisanego wyszukiwania dopisywany jest znacznik wersji silnika, żeby za rok dało się w danych odróżnić wiersze sprzed przepięcia od tych po nim.

Cztery klasy defektów, które zeszły z produkcji

Kolejność da się zmierzyć bez szumu, średniej oceny nie — dlatego drugim dowodem nie są punkty, tylko cztery klasy defektów, które zniknęły z listy klienta. Skalę każdej policzyłem w logach realnych wyszukiwań, zanim poprawka weszła.

Branża zgadywana z opisu pracodawcy i traktowana jak warunek konieczny

w logach
Wycinała ponad połowę puli w 9% zapytań klientów i była główną przyczyną 8 z 20 pustych list.
po zmianie
W kontroli testera nie postawiła branży ani razu; obie puste listy wypełniły się kandydatami.

Brak progu jakości — na listę wchodził każdy, kto przeszedł filtry

w logach
6% pozycji pokazanych klientom miało wynik poniżej 20 na 100; tester dostał ludzi z wynikiem 1, 4 i 9.
po zmianie
Próg wejścia na listę; w pierwszych 33 wyszukiwaniach po wdrożeniu odciął 35 kandydatów.

Brak bramki stażu — brak doświadczenia odejmował punkty, ale nie eliminował

w logach
Na ogłoszeniu z wymogiem pięciu lat lista pokazywała kandydatów z półrocznym stażem.
po zmianie
Bramka zdjęła 9 z 13 osób — i to pozostałą czwórkę sędzia ocenił najwyżej, 70 wobec 49 dla pełnej dziewiątki.

Filtr pracy zdalnej działał w obie strony

w logach
Ogłoszenie stacjonarne wycinało każdego, kto deklarował otwartość na zdalną — a to 45% bazy.
po zmianie
Filtr jednokierunkowy; wróciły dwie osoby, które sędzia ocenił wyżej niż kogokolwiek z listy sprzed zmiany.

Powtarzalność, której nie zamawiał żaden benchmark

47%

tyle powtórzonych wyszukiwań zwracało identyczną kolejność — mimo zerowej temperatury i stałego ziarna

To wyszło z pomiaru na logach już po wdrożeniu. Użytkownik klikający „szukaj” drugi raz dostawał przetasowaną listę, a co gorsza moje własne porównania dwóch wersji mierzyły po części szum modelu — więc problem dotykał nie tylko produktu, ale i metody. Kolejność jest dziś zapamiętywana pod kluczem policzonym z całego materiału, który zobaczył model: powtórzone wyszukiwanie wraca identyczne i przy okazji krótsze o kilkanaście sekund.

Pierwsze godziny na produkcji

33

wyszukiwania w 47 minut po przepięciu

0

awarii kroku układania kolejności

35

kandydatów odciętych progiem jakości

1

pusta lista na 33 wyszukiwania

Panel, strona i środowisko badawcze na tym samym ogłoszeniu zwróciły identyczny skład i identyczną kolejność piętnastu kandydatów. Uczciwie: 31 z tych 33 wyszukiwań to ruch testowy, więc logi z pierwszego dnia potwierdzają sprawność, nie jakość — ocena jakościowa stoi na pomiarach sprzed wdrożenia.

Regresja jest jedna i też jest policzona: mediana wyszukiwania rośnie z 8,4 s do 27,9 s, z czego 13,3 s to samo układanie kolejności przez model, przy koszcie około czterech groszy za wyszukiwanie. Rachunek ma nieoczywistą strukturę — przy kilkudziesięciu tokenach odpowiedzi model zużywa około 2800 tokenów „myślenia” rozliczanych jak wyjście, czyli 85% ceny wywołania.

Uczciwe zastrzeżenie

Odniesieniem jest ocena wystawiana przez model i ta miara ma dwie wady, obie opisane w raporcie z wdrożenia. Nie jest stabilna w skali bezwzględnej — tym samym siedmiu kandydatom potrafiła dać w dwóch przebiegach 90 i 66 — więc różnice rzędu kilku punktów w średnich to szum sędziego, nie postęp potoku. I jest zakotwiczona: prompt pokazuje sędziemu nasz własny wynik, zanim ten wystawi ocenę, więc każda zgodność jest górnym oszacowaniem. Dlatego liczą się tu kolejność i różnice strukturalne, a nie średnia ocena. Otwarte zostają dwie rzeczy i obie są policzone: nadmiar doświadczenia wciąż nic nie kosztuje, więc senior wchodzi na ogłoszenie juniorskie, a rozkład długości list jest dwubiegunowy — albo komplet, albo prawie nic.