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
Pierwszy na liście = najlepszy wg raportu AI
praktyczny sufit ≈ 80%
Zgodność czołowej trójki
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
wersja wyjściowa
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.
| pomiar | skład listy | kolejność |
|---|---|---|
| wersja wyjściowa | 59,0 | 0,35 |
| nowa — bramki i próg jakości | 64,3 | 0,69 |
| nowa — po geokodowaniu miast | 65,4 | 0,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.