
Stop spamowi w leadach bez zabijania konwersji: lekki pipeline ML do scoringu formularzy
Formularze leadowe są jak publiczne API: każdy może do nich „strzelić”, a Ty płacisz za każdy request – czasem dosłownie (CRM, automatyzacje, call center), a czasem reputacją (śmieci w bazie, zły obraz kampanii, fałszywe wnioski). W WordPressie/Elementorze problem jest szczególnie widoczny, bo formularze często są proste, szeroko dostępne i mocno zintegrowane z narzędziami marketingowymi.
Najczęściej zaczyna się od „to tylko trochę spamu”. Potem dochodzi koszt: handlowcy przestają ufać leadom, kampanie wyglądają „świetnie” w raportach, a realnie konwersja spada. W końcu ktoś próbuje łatki: CAPTCHA, blokady IP, ręczne reguły, czarne listy. I zwykle kończy się to w tym samym miejscu: albo spam dalej przechodzi, albo cierpią prawdziwi użytkownicy.
U nas przełomem było potraktowanie formularzy jak systemu z sygnałami i ryzykiem: nie „blokuj/nie blokuj”, tylko oceń prawdopodobieństwo spamu i podejmij decyzję zależnie od kosztu błędu. To jest bardzo wdzięczny obszar dla „lekkiego ML”: nie potrzebujesz LLM-ów ani wielkiej infrastruktury. Potrzebujesz czystego eventu, sensownych cech, prostego modelu i monitoringu driftu.
Tło i problem
Spam w leadach nie jest jednolity. W praktyce widzimy mieszankę:
- automatycznych zgłoszeń masowych,
- „semi-automatycznych” botów, które wyglądają jak człowiek,
- zgłoszeń o niskiej jakości (np. przypadkowe dane), które biznesowo są równie kosztowne jak spam.
Największy błąd, jaki popełnia wiele zespołów, to wrzucenie wszystkiego do jednego worka „spam”. Skutek: wdrażasz jedną blokadę (CAPTCHA) i potem eskalujesz poziom „trudności”, gdy boty ją obchodzą. To spirala, w której przegrywa UX.
W realnym biznesie koszt błędu jest asymetryczny:
- false positive (zablokowałeś prawdziwy lead) boli natychmiast i bezpośrednio,
- false negative (przepuściłeś spam) boli pośrednio, ale kumuluje się w czasie (koszt obsługi, zaśmiecone dane, błędne decyzje).
Dlatego sensowny system nie powinien być „zero-jedynkowy”. Powinien umieć:
- ocenić ryzyko,
- dobrać reakcję do kontekstu (kampania, źródło, formularz),
- zostawić ślad audytowy.
Wymagania / ograniczenia
Wymagania funkcjonalne
- Ograniczyć spam docierający do CRM i automatyzacji.
- Nie obniżyć konwersji (szczególnie na mobile).
- Działać dla wielu formularzy i landingów bez ręcznego strojenia każdego z osobna.
- Umożliwić obsługę wyjątków (kampanie, partnerzy, ruch B2B z NAT).
Wymagania techniczne
- Niska latencja: scoring nie może wydłużać submitu w sposób zauważalny.
- Niski koszt utrzymania: prosty model, proste feature engineering.
- Przewidywalność i audyt: wiedzieć „dlaczego” lead trafił do danej ścieżki.
- Monitoring driftu: boty się zmieniają, model też musi.
Ograniczenia i bezpieczeństwo
- Minimalizacja danych (privacy): nie zbieramy więcej, niż trzeba; wrażliwe dane trzymamy krótko lub haszujemy.
- Nie publikujemy „poradnika omijania zabezpieczeń”: opisujemy podejście obronne bez zdradzania szczegółów, które ułatwiają nadużycia.
Co nie działało wcześniej
1) CAPTCHA jako domyślne rozwiązanie
CAPTCHA bywa skuteczna na część botów, ale:
- kosztuje konwersję (zwłaszcza mobile),
- bywa problematyczna dla dostępności,
- nie rozróżnia „niskiej jakości” leadów od spamu,
- często przesuwa problem, zamiast go rozwiązać.
2) Ręczne reguły („if email contains…”, „if country == …”)
Reguły są kuszące, bo są szybkie. Ale:
- szybko rosną w liczbę i konflikty,
- trudno je utrzymać i testować,
- są łatwe do obejścia przez adaptujące się nadużycia,
- działają globalnie, choć ryzyko jest lokalne (konkretne kampanie/formularze).
3) Blokowanie po IP / geolokacji
To często generuje false positives (firmowe NAT, VPN, podróże), a spam i tak znajduje obejścia. To narzędzie może być warstwą, ale nie jedyną.
4) Brak „prawdy” w danych
Największa praktyczna przeszkoda w ML to etykiety. Jeśli jedyną etykietą jest „to wyglądało na spam”, to model będzie uczył się opinii, nie rzeczywistości. Potrzebujesz sygnału biznesowego: czy lead był realny, czy doszło do kontaktu, czy lead został odrzucony jako spam.
Nowe podejście / architektura
Zamiast jednego „muru” zrobiliśmy pipeline:
- Event z formularza (WordPress/Elementor) → ustandaryzowany kontrakt
lead.submitted. - Feature extraction (część na froncie, część na backendzie) → cechy behawioralne i kontekstowe.
- Scoring service → zwraca
spam_score+ kilka flag (np. „needs_challenge”). - Decision engine → mapuje score na akcję:
- przepuść,
- przepuść do „quarantine”,
- poproś o dodatkową weryfikację / challenge (tylko gdy ryzyko wysokie).
- Feedback loop → etykiety z CRM/ops (odrzucony, skontaktowany, sprzedaż) wracają do treningu.
- Monitoring → drift cech, jakość danych, metryki biznesowe.
To jest „ML jako mała usługa”, a nie wielki projekt data science. Kluczowe jest to, że ML nie jest jedyną warstwą – jest warstwą decyzyjną nad sygnałami.
Jak to działa
Kontrakt danych: event „lead.submitted”
Pierwszy krok to uporządkowanie tego, co w ogóle zbieramy. Bez kontraktu każdy formularz jest inny, a każdy „fix” jest jednorazowy.
Przykładowy event (JSON) – wersjonowany, z minimalną ilością danych osobowych:
{
"event": "lead.submitted",
"version": "1.1",
"timestamp": "2026-01-12T09:05:12Z",
"request_id": "req_2c6b1f...",
"form": {
"id": "contact_main",
"page": "/oferta/wordpress-opieka",
"campaign": "q1-2026"
},
"lead": {
"email": "user@example.com",
"phone_present": true,
"message_length": 238
},
"context": {
"utm_source": "google",
"utm_medium": "cpc",
"referrer_host": "www.google.com",
"locale": "pl-PL",
"timezone_offset_min": 60
},
"signals": {
"time_to_submit_ms": 18400,
"input_changes": 23,
"paste_events": 2,
"has_js": true
},
"network": {
"ip_hash": "sha256:...",
"asn": 12345
}
}
Dlaczego tak:
- dane osobowe trzymamy minimalnie (część sygnałów jako boolean/length),
- IP nie jest przechowywane jawnie (hash), ale nadal daje korelację,
- mamy
version, bo kontrakt będzie ewoluował.
Cechy: co mierzymy, a czego nie
Wykrywanie spamu działa najlepiej, gdy opiera się na zachowaniu i anomaliach, a nie na „treści wiadomości”. Dla leadów B2B treść bywa krótka, a email/phone to dane wrażliwe.
Cechy, które zwykle są stabilne i etyczne:
- czas do submitu (
time_to_submit_ms) – bardzo krótki bywa podejrzany, ale uwaga na autofill, - liczba zmian w polach (
input_changes) – ekstremalne wartości też są sygnałem, - czy były wklejenia (
paste_events) – nie jako „złe”, tylko jako element kontekstu, - typ formularza i landing (
form.id,page) – różne strony mają różny profil ruchu, - źródła ruchu (
utm,referrer_host) – ale ostrożnie: to nie ma dyskryminować, tylko pomagać w segmentacji ryzyka, - sygnały środowiska (czy działa JS, podstawowe parametry klienta) – bez fingerprintingu „na sterydach”.
Czego unikamy albo traktujemy bardzo ostrożnie:
- pełnej treści wiadomości jako feature (privacy + ryzyko wycieku),
- „reguł” opartych o wrażliwe atrybuty,
- agresywnego device fingerprinting.
W praktyce wystarcza 20–60 cech. Więcej to zwykle szum i koszt utrzymania.
Lista praktycznych zasad:
- cechy mają być tanie do policzenia,
- cechy mają być stabilne w czasie,
- cechy mają być audytowalne (wiesz, co znaczą),
- każda cecha ma właściciela: kto ją utrzymuje i po co istnieje.
Model: prosty, tani, przewidywalny
Dla tego problemu świetnie działają klasyczne modele tablicowe:
- regresja logistyczna (dobry baseline, łatwa interpretacja),
- gradient boosting (np. XGBoost/LightGBM) – często lepsza skuteczność przy podobnym koszcie inference.
Wybór zależy od Twoich priorytetów:
- jeśli chcesz maksymalnej przejrzystości → logistyczna,
- jeśli chcesz lepszego ROC/PR i masz trochę więcej dojrzałości → boosting.
U nas typowo zaczynamy od logistycznej + kilka ręcznie dobranych cech. Dopiero gdy mamy porządny feedback loop, przechodzimy do boostingu. Powód jest prosty: najpierw dane i etykiety, potem model.
Decyzje: trzy ścieżki zamiast jednej blokady
Największa zmiana produktowa: nie blokujemy „w ciemno”. Robimy routing:
- Accept – lead idzie normalnie do CRM i automatyzacji.
- Quarantine – lead idzie do osobnej kolejki/tagu:
- nie uruchamia drogich automatyzacji,
- można go zweryfikować automatycznie (np. asynchronicznie) lub ręcznie w razie potrzeby.
- Challenge – dla wysokiego ryzyka:
- minimalne tarcie (np. dodatkowy krok weryfikacji),
- używane rzadko, bo kosztuje UX.
To podejście działa biznesowo, bo możesz dobrać strategię do kampanii:
- w kampanii z wysoką stawką za lead możesz być bardziej konserwatywny (więcej quarantine),
- na landingach o krytycznej konwersji możesz minimalizować challenge.
Trening i ewaluacja: metryki, progi, koszt błędów
Tu jest sedno „inżynierskiego ML”: dobierasz metrykę do kosztu.
Metryki modelowe, które mają sens:
- precision/recall dla klasy „spam”,
- PR-AUC (często lepsze niż ROC-AUC przy niezbalansowanych danych),
- confusion matrix dla kilku progów.
Metryki biznesowe, które są równie ważne:
- % leadów w quarantine,
- % false positives (prawdziwe leady w quarantine/challenge),
- czas reakcji handlowców (czy quarantine nie blokuje zbyt długo),
- koszt spamu w CRM (czas, licencje, automatyzacje).
Pseudokod treningu (celowo prosty):
# 1) Wczytaj eventy + etykiety z CRM/ops
events = load_events("lead.submitted", since_days=90)
labels = load_labels(since_days=90) # np. spam/rejected, contacted, qualified
dataset = join(events, labels, on="request_id")
# 2) Feature engineering (stabilne, tanie cechy)
X = featurize(dataset)
y = dataset["is_spam"] # 1 spam, 0 legit
# 3) Split czasowy (ważne przy drift)
train, valid = time_split(X, y, cutoff="2025-12-15")
# 4) Model baseline
model = LogisticRegression(class_weight="balanced")
model.fit(train.X, train.y)
# 5) Ewaluacja + dobór progu pod koszt
scores = model.predict_proba(valid.X)[:, 1]
threshold = choose_threshold(scores, valid.y, target_fp_rate=0.01)
save_model(model, threshold, version="2026.01.0")
Dlaczego split czasowy: boty i ruch się zmieniają, a losowy split bywa zbyt optymistyczny.
Dobór progu: zamiast „0.5”, dobierasz pod akceptowalny false positive rate albo pod maksymalizację wartości (jeśli potrafisz policzyć koszt).
Deployment i monitoring: drift, jakość danych, alerty
Deployment zwykle robimy jako mały serwis:
- endpoint
/score-leadprzyjmuje event (lub referencję), - zwraca
score,decision,model_version.
Przykładowe API:
POST /score-lead
{
"request_id": "req_2c6b1f...",
"features": {
"time_to_submit_ms": 18400,
"input_changes": 23,
"paste_events": 2,
"has_js": true,
"form_id": "contact_main",
"utm_source": "google"
}
}
Odpowiedź:
{
"spam_score": 0.82,
"decision": "quarantine",
"model_version": "2026.01.0",
"reasons": ["very_fast_submit", "anomalous_pattern"]
}
reasons to nie „wyjaśnienie matematyczne”, tylko audytowe flagi, które pomagają debugować i rozmawiać z biznesem.
Monitoring dzielimy na trzy warstwy:
- Jakość danych (data quality)
- % eventów bez kluczowych pól,
- nagłe zmiany rozkładów (np.
time_to_submit_msspada o połowę), - wzrost „unknown” w kategoriach (nowe
utm_source).
- Drift
- PSI/KS na wybranych cechach (nie trzeba na wszystkim),
- porównanie rozkładu score w czasie,
- trend precision/recall na leadach, które dostały etykiety.
- Biznes
- trend quarantine rate,
- trend „spam w CRM” (np. ręczne oznaczenia),
- wpływ na konwersję (tam, gdzie umiesz to mierzyć).
Alerty, które realnie mają sens:
- quarantine rate rośnie skokowo (np. +50% dzień do dnia),
- spada liczba leadów „qualified” mimo podobnego ruchu (sygnał false positives),
- data quality: brakuje sygnałów z frontu (np. padł JS snippet).
Wyniki / metryki / lekcje
Bez dostępu do Twoich danych nie będę udawał konkretnych liczb jako faktów. Ale realistycznie, w podobnych wdrożeniach, często da się osiągnąć (szacunek, zależny od ruchu i źródeł):
- redukcję spamu trafiającego do CRM o 60–90%,
- utrzymanie false positives na poziomie <1–2% (gdy progi dobierzesz konserwatywnie),
- mniejsze koszty operacyjne: mniej „ręcznego czyszczenia”, mniej automatyzacji odpalanych na śmieciach.
Najważniejsze lekcje:
- Etykiety są ważniejsze niż model. Jeśli nie masz feedback loop (spam/legit), to ML będzie zgadywał.
- Routing > blokada. Quarantine to genialny kompromis: chroni system, nie zabija UX.
- Prosty model + dobry monitoring wygrywa z „mocnym modelem bez kontroli”.
- Drift jest pewny. Boty i kampanie się zmieniają – mierz to od dnia 1.
Konkretne wnioski w punktach:
- Zacznij od 10–20 cech i modelu baseline.
- Zadbaj o wersjonowanie eventu i modelu.
- Dobierz próg pod koszt false positives, nie pod „intuicję”.
- Monitoruj data quality, zanim zaczniesz monitorować „AI”.
Co dalej / roadmapa
Gdy fundament działa, typowa roadmapa wygląda tak:
- Lepsze etykiety i szybszy feedback
- integracja z CRM: statusy leadów,
- proste narzędzie dla handlowców: „oznacz jako spam” (1 klik),
- automatyczna walidacja e-mail/telefon w tle (bez tarcia w submit).
- Model per segment
- osobne progi per formularz/kampania,
- ewentualnie osobne modele, jeśli profile ruchu są skrajnie różne.
- Active learning / human-in-the-loop
- ręczna weryfikacja tylko „granicznych” przypadków,
- wykorzystanie tych danych do poprawy modelu.
- Ochrona na brzegu jako warstwa komplementarna
- lepszy rate limiting dla źródeł, które generują nadużycia,
- ale kontrolowane, żeby nie blokować dobrego ruchu.
- Privacy i retencja
- przegląd pól, które zbierasz,
- skrócenie retencji sygnałów, które nie są potrzebne do uczenia,
- audyt dostępu do danych.
Wnioski
Spam w formularzach to problem produktowo-operacyjny, nie tylko „antybotowy”. Jeśli potraktujesz go jak system decyzyjny, lekkie ML daje świetny zwrot: mniej śmieci w CRM, mniej kosztów i mniej frustracji – bez wrzucania każdemu użytkownikowi CAPTCHA na wejściu.
Jeśli chcesz to wdrożyć u siebie, zacznij od:
- ustandaryzowania eventu
lead.submitted, - wprowadzenia „quarantine” jako trzeciej ścieżki,
- prostego modelu + monitoringu driftu i jakości danych.
Jeśli chcesz, mogę też pomóc ułożyć minimalny kontrakt eventu pod Twoje formularze i listę cech, które są „warte” zbierania przy zachowaniu prywatności.