🧴 Case Study · Kosmetyki / Beauty Tech

Hybrydowy system AI dla branży kosmetycznej — analiza INCI, RAG na 350 tys. produktów i generator nowych formuł

Klient z branży beauty potrzebował dwóch rzeczy naraz: narzędzia, które analizuje skład dowolnego kosmetyku (interakcje, alergeny, regulacje), oraz silnika, który projektuje nowe kosmetyki — wychodząc od opisu w czacie typu „chcę krem nawilżający, bez parabenów, dla skóry naczynkowej, w cenie półki masowej". Pokazujemy, jak to zbudowaliśmy: własny fine-tunowany LLM, RAG na 350 tys. produktów i 30 tys. składników INCI, knowledge graph relacji, hybryda z istniejącymi narzędziami branżowymi.

📅 Grudzień 2025
⏱️ 13 min czytania
🏷️ LLM · RAG · Knowledge Graph
⚙️ Llama 3.1 70B · LoRA · pgvector
350k
Produktów
w RAG
30k
Składników
INCI
2
Własne
modele AI
~2s
Czas
generacji formuły

Po co kolejne narzędzie do INCI?

Na rynku istnieje kilkanaście narzędzi do analizy składu kosmetyków — INCI Beauty, INCIDecoder, CosDNA, SkinSort i polskie Wizaż. Każde z nich robi mniej więcej to samo: bierze listę składników, koloruje je czerwono-zielono, dodaje notki o alergiach. To dobre dla konsumenta sprawdzającego kosmetyk w drogerii. Ale to za mało dla firmy, która chce produkować własne kosmetyki.

Nasz klient — średniej wielkości producent kosmetyków pielęgnacyjnych — miał inne potrzeby. Brief, który dostaliśmy na pierwszym spotkaniu, brzmiał mniej więcej tak:

„Potrzebujemy narzędzia, które robi trzy rzeczy. Po pierwsze, czyta dowolny skład INCI i mówi nam: co tu jest, jak to działa razem, czego unikać. Po drugie, robi to samo dla naszych konkurentów — chcemy mieć przeszukiwalną bazę 'kto co ma w składzie'. Po trzecie i najważniejsze — chcemy móc opisać produkt w jednym zdaniu i dostać propozycję formuły. Realistyczną. Taką, którą da się oddać do laboratorium i nie być zaśmianym." — Dyrektor R&D, kosmetyki pielęgnacyjne

To trzy bardzo różne zadania techniczne. Pierwsze to klasyczne NLP plus regulacje. Drugie — wyszukiwanie semantyczne na dużym zbiorze. Trzecie — to już praktycznie kompozycja generatywna z ograniczeniami, problem, którego nie da się rozwiązać samym ChatGPT, bo halucynuje stężenia i miesza składniki, które się zwyczajnie nie lubią chemicznie.

⚠️

Dlaczego nie wystarczy „spytać ChatGPT"

Gdy poprosiliśmy GPT-4 i Claude o ułożenie formuły kremu nawilżającego ze stężeniami, oba modele wymyślały liczby. Glicerolu 25% (w kremie to byłaby pasta), niacynamidu 20% (toleruje się max 10%), kwas hialuronowy 5% (w kosmetykach używa się 0.1–2%). Modele uniwersalne nie znają realnych norm formulacji — uczyły się na ogólnym internecie, nie na recepturach.

Architektura systemu — trzy warstwy, dwa modele, jeden czat

Po dwóch tygodniach researchu (rozmowy z technologami w laboratorium klienta, audyt istniejących źródeł INCI, weryfikacja regulacji unijnych — Rozporządzenie 1223/2009) zaproponowaliśmy architekturę trójwarstwową:

L1
Warstwa wiedzy faktograficznej (Knowledge Graph + RAG) 350 tys. kosmetyków + 30 tys. składników INCI w grafie relacji. Co z czym wchodzi w interakcję, jakie stężenia są typowe, jakie regulacje stosować.
L2
Warstwa modelu domenowego (Llama 3.1 70B + LoRA) Fine-tunowany na 50k recepturach (pseudonimizowane dane od klienta + open source z USP i Cosmetic Ingredient Review). Rozumie strukturę formuły, fazy emulsji, kolejność dodawania.
L3
Warstwa orkiestracji (Claude Sonnet 4.6 jako planner) Rozkłada zapytanie użytkownika na podproblemy, decyduje kiedy odpytać RAG, kiedy zapytać model domenowy, kiedy zwalidować wynik względem regulacji.

Kluczowa decyzja architektoniczna: nie jeden model do wszystkiego, tylko trzy wyspecjalizowane warstwy, które rozmawiają ze sobą po API. Każda robi to, w czym jest najmocniejsza:

Warstwa 1 — Knowledge Graph 30 tys. składników INCI

Standardowy RAG (czyli „weź pytanie, znajdź podobne fragmenty tekstu, wstaw do promptu") w naszym przypadku nie wystarczał. Składniki kosmetyczne mają relacje, których nie da się wyłapać przez podobieństwo wektorowe:

Zbudowaliśmy więc knowledge graph — relacyjną bazę grafową w Neo4j, gdzie każdy z 30 tys. składników to węzeł z atrybutami (CAS number, EINECS, INCI name, EU Allergen status, funkcja CosIng, typowe stężenie), a krawędzie reprezentują relacje typu „antagonizuje z", „synergizuje z", „zastępuje", „wymaga w pH".

🕸️

Skąd dane do grafu?

Część z otwartych źródeł (CosIng — European Commission Cosmetic Ingredient Database, COSMOS-standard, Personal Care Products Council INCI Dictionary). Część została wyekstrahowana z literatury (PubMed, Journal of Cosmetic Science) przez własny pipeline NLP — model klasyfikował zdania typu „X enhances the effect of Y" i automatycznie tworzył kandydatów na krawędzie, które potem walidował technolog klienta. Ten etap zajął najdłużej — 6 tygodni pracy.

Wyszukiwanie semantyczne na 350 tys. produktów

Drugi komponent warstwy 1 to baza 350 tysięcy kosmetyków dostępnych na rynku (zebrana z wielu źródeł — drogerie online, baza CPNP, sklepy producentów). Każdy produkt ma swój pełny skład INCI, kategorię (krem, szampon, serum...), zakres cenowy, segment rynkowy.

Tę bazę przeszukujemy hybrydowo:

Pipeline wyszukiwania konkurencyjnego

1
Zapytanie semantyczne (pgvector) Użytkownik pyta np. „pokaż mi serum z retinolem w niskiej cenie". Embedding zapytania porównywany kosinusowo z embeddingami opisów produktów. Top 200 kandydatów.
2
Filtry strukturalne (PostgreSQL) Z tych 200 zostawiamy te, które rzeczywiście mają retinol w składzie (filtrowanie po INCI), w segmencie cenowym <100 PLN.
3
Re-ranking przez cross-encoder Pozostałe ~30 produktów przepuszczamy przez cross-encoder (BAAI/bge-reranker-large), który już bezpośrednio porównuje zapytanie z każdym produktem.
4
Agregacja konkurencyjna Top 10 wyników, plus analityka: które składniki są najczęstsze w tym segmencie, gdzie są luki, jakie są ceny.

Warstwa 2 — własny model do receptur (Llama 3.1 70B + LoRA)

Centrum systemu i najtrudniejsza technicznie część. Tutaj musieliśmy nauczyć model rozumieć formułę kosmetyku tak, jak rozumie ją technolog — z fazami, kolejnością dodawania, ograniczeniami fizykochemicznymi.

Wybór modelu bazowego

Rozważaliśmy kilka opcji. Decyzja oparła się na trzech kryteriach: jakość rozumienia chemii, wielkość kontekstu (formuły są długie — typowa receptura ma 30-60 składników z opisami) i możliwość deploymentu on-premise (klient nie chciał wysyłać własnych receptur na serwery OpenAI).

Model Plusy Minusy Decyzja
GPT-4 / Claude API Najlepsza jakość bazowa Dane klienta opuszczają jego infrastrukturę. Brak fine-tuningu (Claude) lub drogi (GPT-4) Odrzucone
Mistral 7B Lekki, open source, łatwy fine-tuning Za mały na złożone wnioskowanie chemiczne Odrzucone
Llama 3.1 8B Dobry stosunek jakość/koszt, mieści się na jednym RTX 5090 Dla generacji formuł za mały — gubił relacje na długich kontekstach Testowane, odrzucone
Llama 3.1 70B Świetna jakość, 128k kontekst, fine-tunowalny LoRA Wymaga 2× A100 80GB lub 4× RTX 5090 do inference ✓ Wybrane

Dataset treningowy — co fine-tunujemy

Najtrudniejsza część — zbudowanie dobrego datasetu. Łącznie ~50 000 par (opis intencji → formuła). Skąd:

Każdy przykład w datasecie miał format:

JSONL · dataset treningowy
{
  "intent": "Krem nawilżający na dzień, skóra mieszana 25+, bez SLS, bez parabenów, segment masstige (40-60 PLN), tekstura lekka, szybko się wchłaniający",
  "constraints": {
    "exclude": ["sodium lauryl sulfate", "parabens"],
    "price_segment": "masstige",
    "texture": "light",
    "skin_type": "combination"
  },
  "formula": {
    "phase_a_water": [
      {"ingredient": "Aqua", "percent": 68.5, "function": "solvent"},
      {"ingredient": "Glycerin", "percent": 4.0, "function": "humectant"},
      {"ingredient": "Pentylene Glycol", "percent": 3.0, "function": "humectant_preservative_booster"}
    ],
    "phase_b_oil": [
      {"ingredient": "Caprylic/Capric Triglyceride", "percent": 8.0, "function": "emollient"},
      {"ingredient": "Squalane", "percent": 3.0, "function": "emollient"},
      {"ingredient": "Cetearyl Alcohol", "percent": 2.5, "function": "co_emulsifier"},
      {"ingredient": "Glyceryl Stearate Citrate", "percent": 2.0, "function": "emulsifier"}
    ],
    "phase_c_active": [
      {"ingredient": "Niacinamide", "percent": 4.0, "function": "active"},
      {"ingredient": "Panthenol", "percent": 1.0, "function": "active"}
    ],
    "phase_d_preservation": [
      {"ingredient": "Phenoxyethanol", "percent": 0.9, "function": "preservative"},
      {"ingredient": "Ethylhexylglycerin", "percent": 0.3, "function": "preservative_booster"}
    ],
    "ph_target": "5.0-5.5",
    "manufacturing_notes": "Faza A + B w 75°C, homogenizacja 5 min, schłodzenie do 40°C, dodanie C i D, mieszanie 10 min"
  },
  "rationale": "Krótkie uzasadnienie wyborów — dlaczego Glyceryl Stearate Citrate (clean beauty friendly), dlaczego niacynamid 4% (typowe stężenie wystarczające bez ryzyka podrażnienia), dlaczego ten zestaw konserwantów (kompatybilny z pH 5.0-5.5)..."
}

Zwróćcie uwagę na pole rationale — to ono robi największą różnicę. Model uczył się nie tylko co położyć w formule, ale i dlaczego. To znacząco poprawiło jakość generacji — model przestał działać jak ślepa autokompletacja, a zaczął jak początkujący technolog.

Fine-tuning — LoRA na 70B

Pełny fine-tuning modelu 70B to projekt na klaster GPU za miliony złotych. My zrobiliśmy LoRA — niskorangową adaptację, która zmienia tylko ułamek wag, ale wystarczająco, żeby model zaczął „myśleć po kosmetycznemu".

Python · trening LoRA
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer

base = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-70B-Instruct",
    torch_dtype=torch.bfloat16,
    device_map="auto",
    quantization_config=BitsAndBytesConfig(load_in_4bit=True)
)

lora_config = LoraConfig(
    r=64,                # rank
    lora_alpha=128,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM,
)

model = get_peft_model(base, lora_config)
# Trenowalnych parametrów: ~430M z 70B (0.6%)

trainer = SFTTrainer(
    model=model,
    train_dataset=cosmetics_dataset,
    max_seq_length=8192,
    args=TrainingArguments(
        per_device_train_batch_size=1,
        gradient_accumulation_steps=16,
        learning_rate=1e-4,
        num_train_epochs=3,
        bf16=True,
        gradient_checkpointing=True,
    ),
)
trainer.train()

Trening na 50k przykładach zajął ~5 dni na klastrze 4× RTX 5090 (po stronie SULI, klient dostał gotowe wagi adaptera). Łącznie ~430 mln trenowalnych parametrów z 70 miliardów — 0.6% modelu, ale to wystarczyło.

Walidacja jakości

Po fine-tuningu zrobiliśmy blind test: dwóch technologów (jeden z klienta, jeden zewnętrzny konsultant) dostało po 50 formuł — 25 wygenerowanych przez nasz model, 25 z prawdziwych receptur klienta z innych projektów. Zadanie: ocenić, czy formuła jest „realna" i jakie ma poziom (1-5). Model zdobył średnio 3.8/5, prawdziwe receptury 4.3/5. Co ważniejsze, technolodzy nie potrafili odróżnić wygenerowanych od prawdziwych w 64% przypadków.

Warstwa 3 — orkiestracja przez Claude jako planner

Mimo że własny model świetnie generuje formuły, nie chcemy, żeby on rozmawiał z użytkownikiem. Dlaczego? Bo użytkownik pyta nieprecyzyjnie. Bo trzeba dopytać o brakujące parametry. Bo trzeba sprawdzić regulacje przed generacją. Bo trzeba ładnie wyjaśnić wynik.

Tutaj wchodzi Claude Sonnet 4.6 jako planner. Otrzymuje dostęp do trzech narzędzi przez MCP:

TypeScript · narzędzia MCP servera
// 1. Przeszukiwanie konkurencji
{
  name: "search_market",
  description: "Wyszukuje istniejące produkty na rynku po opisie semantycznym i filtrach",
  inputSchema: {
    query: string,
    filters: { category?, price_range?, must_contain?, must_not_contain? },
    top_k: number
  }
}

// 2. Sprawdzenie składnika / interakcji w knowledge graph
{
  name: "analyze_ingredient",
  description: "Zwraca pełny profil składnika: funkcja, regulacje, typowe stężenia, antagonizmy, synergie",
  inputSchema: { inci_name: string, ph_context?: number }
}

// 3. Generowanie formuły (własny model)
{
  name: "generate_formula",
  description: "Generuje kompletną formułę kosmetyku na podstawie briefu",
  inputSchema: {
    intent: string,
    constraints: { exclude?, include?, price_segment?, texture?, skin_type? },
    product_category: enum
  }
}

// 4. Walidacja regulacyjna
{
  name: "validate_eu_compliance",
  description: "Sprawdza formułę pod kątem Rozp. 1223/2009 — stężenia, alergeny, restrykcje wiekowe",
  inputSchema: { formula: Formula, intended_use: string }
}

Claude w roli planera dostaje system prompt mniej więcej taki:

„Jesteś asystentem technologa kosmetyków. Twoja rola to: rozumieć intencję użytkownika, dopytywać o brakujące parametry, używać dostępnych narzędzi do generowania i walidacji formuł, prezentować wyniki w sposób zrozumiały. NIE zgaduj stężeń. NIE wymyślaj interakcji składników. Zawsze używaj analyze_ingredient i generate_formula zamiast generować z pamięci. Po wygenerowaniu formuły, zawsze przepuszczaj ją przez validate_eu_compliance przed prezentacją."

Jak to wygląda od strony użytkownika

Pełna ścieżka rzeczywistej rozmowy (przykład syntetyczny, ale wzorowany na realnych sesjach):

Przykładowa sesja — projektowanie nowego serum

1
Użytkownik: „Potrzebuję serum z retinolem, ale dla młodszej grupy. 25-30 lat. Skóra wrażliwa. Cena półki masstige."
2
Claude (planner): wywołuje search_market → dostaje 15 podobnych produktów na rynku. Analizuje typowe stężenia retinolu w tym segmencie (0.1-0.3%) i ceny (35-65 PLN).
3
Claude: „Dla skóry wrażliwej zaproponowałbym retinal (Retinal jako pochodna jest łagodniejsza) zamiast czystego retinolu, w stężeniu 0.05%. Czy interesuje cię też dodatek bisabololu jako składnika kojącego, czy zostawić formułę minimalistyczną?"
4
Użytkownik: „Tak, z bisabololem. I chcę certyfikat clean beauty."
5
Claude: wywołuje analyze_ingredient("Retinal") + analyze_ingredient("Bisabolol") → potwierdza kompatybilność, sprawdza pH. Wywołuje generate_formula z pełnym briefem.
6
Własny model (Llama 70B LoRA): generuje formułę 28 składników z fazami, pH, notatkami procesowymi.
7
Claude: wywołuje validate_eu_compliance → wszystko zgodne z 1223/2009.
8
Claude: prezentuje formułę z uzasadnieniem: dlaczego retinal a nie retinol, dlaczego ten konserwant, dlaczego to pH, jakie alternatywy są możliwe. Sugeruje produkty referencyjne z rynku (top 3 z wyszukiwania konkurencji).

Całość zajmuje ~2-3 sekundy generacji plus czas Claude'a na ułożenie odpowiedzi w naturalnym języku.

Hybryda z istniejącymi narzędziami branżowymi

Ważny punkt — klient nie chciał zastępować istniejących narzędzi branżowych, tylko zintegrować się z nimi. System rozmawia po API z:

🔗

Integracja z ERP — gamechanger

Najciekawszy efekt: gdy model proponuje formułę, system od razu sprawdza w ERP, ile kosztuje wyprodukowanie 1 kg tej formuły (po cenach zakupu surowców z katalogu klienta). Technolog widzi nie tylko formułę, ale i koszt jednostkowy oraz marżę przy zakładanej cenie półkowej. To zmienia projektowanie z hobby w decyzję biznesową w 30 sekund.

Stack i infrastruktura

Stack techniczny

Wymagania sprzętowe — on-premise

Cały system działa u klienta, w jego serwerowni. To było kluczowe — receptury kosmetyków to know-how, którego nikt nie chce wysyłać do chmury Amazonów. Konfiguracja:

Claude jako planner jedyny komponent, który wychodzi na zewnątrz (Anthropic API) — ale Claude nigdy nie dostaje pełnych receptur klienta, tylko zapytania użytkownika i wyniki narzędzi. Dane wrażliwe nie opuszczają infrastruktury.

Czego nauczyliśmy się przy tym projekcie

1. Nie wszystko musi być w jednym modelu

Modny trend „one model to rule them all" (GPT-4 do wszystkiego) jest praktyczny tylko dla prototypów. W produkcji opłaca się rozdzielać: jeden model do generacji, drugi do orkiestracji, baza wiedzy jako trzeci komponent. Każdy element można niezależnie ulepszać i wymieniać.

2. Dataset jest ważniejszy od modelu

Gdybyśmy fine-tunowali Mistrala 7B na naszym datasecie, dostalibyśmy gorszy wynik niż surowy GPT-4. Ale fine-tuning Llamy 70B na naszych danych pobił surowego GPT-4. Dane > rozmiar modelu. Sześć tygodni czyszczenia datasetu = sześć tygodni najlepszej inwestycji w cały projekt.

3. Knowledge graph + RAG > sam RAG

Klasyczny RAG (wstaw fragmenty tekstu do promptu) nie radzi sobie z domenami, gdzie liczy się struktura relacji. Kosmetyki to dokładnie taka domena — co z czym się lubi, co antagonizuje, co zastępuje. Graf był dla nas konieczny.

4. Walidacja regulacyjna musi być zautomatyzowana

UE ma kilkadziesiąt regulacji dotyczących kosmetyków. Załączniki II-VI Rozporządzenia 1223/2009 plus poprawki. Człowiek tego nie sprawdzi za każdą generacją. Walidator validate_eu_compliance jest jedną z najważniejszych części systemu i jednocześnie najmniej widoczną.

5. Hybryda z ERP = realna wartość biznesowa

Generowanie ładnych formuł to fajne demo. Generowanie formuł, które od razu mają policzony koszt produkcji i sprawdzoną dostępność u dostawców — to jest narzędzie, które klient włącza codziennie i nie chce już bez niego pracować.

Co się zmieniło u klienta

Zacytuję dyrektora R&D (referencja udostępniona za zgodą, dane finansowe usunięte):

„Wcześniej technolog poświęcał na pierwszą wersję nowej formuły 2-3 dni. Researcz konkurencji, sprawdzenie składników, ułożenie szkieletu receptury, kalkulacja kosztów. Teraz dostaje pierwszą propozycję w minutę, a w 30 minut ma trzy warianty z różnymi profilami cenowymi do dyskusji z marketingiem. Skróciliśmy cykl R&D z 6-8 miesięcy do około 3 miesięcy na nowy produkt. To, czego się nie spodziewałem, to że narzędzie czasem proponuje rozwiązania, na które bym sam nie wpadł — bo widzi 350 tysięcy produktów na rynku jednocześnie, a ja w głowie pamiętam może dwieście." — Dyrektor R&D, klient SULI

Czas wdrożenia i koszty

Łącznie 20 tygodni od briefu do produkcji. Zespół: 1 ML engineer (full-time), 1 fullstack dev (full-time, ostatnie 8 tygodni), 1 domain expert klienta (part-time przez cały okres), konsultacje technologów (~40h łącznie).

Co dalej

System rozwija się dalej. Na roadmapie:

Chcesz coś podobnego u siebie?

Beauty tech to nasza specjalizacja. Ale ta sama architektura — RAG + własny model domenowy + knowledge graph + orkiestracja przez Claude — działa wszędzie tam, gdzie domena wymaga twardej wiedzy faktograficznej i kreatywnej kompozycji.

Porozmawiajmy o Twoim wdrożeniu →