Szukasz sposobu na szybką analizę dziesiątek aplikacji na jedno stanowisko, ale przeraża Cię wizja kar za złamanie przepisów o ochronie danych osobowych? W tym artykule przedstawimy Wam fajne narzędzie do wspomagania się AI przy masowej analizie CV bez naruszania zasad RODO. Pokażemy, jak połączyć darmowy program Obsidian z lokalnymi modelami językowymi (Local LLMs) uruchamianymi bezpośrednio na Twoim komputerze lub serwerze firmowym.
Dzięki temu stworzysz prywatny, bezpieczny i w pełni zgodny z prawem „drugi mózg” rekrutera, w którym Twoje dane nigdy nie opuszczą lokalnego komputera.
Spis treści
- Wstęp: AI w rekrutacji a pułapka RODO
- Jak wysokie są kary UODO w Polsce? Realne ryzyko dla MŚP
- Obsidian jako baza wiedzy rekrutacyjnej (Recruitment Vault)
- Przekształcenie szablonu CLAUDE.md na RECRUITER.md
- Wybór silnika AI: Llama, Mistral czy polski Bielik.ai?
- Instrukcja krok po kroku: Konfiguracja lokalnego środowiska
- Automatyzacja procesu: Skrypt integrujący PDF, Ollama i Obsidian
- Codzienna praca z systemem i scenariusze użycia
- Podsumowanie
1. Wstęp: AI w rekrutacji a pułapka RODO
Pojawienie się modeli takich jak GPT-4o czy Claude 3.5 Sonnet przyniosło rewolucję w działach HR. Narzędzia te potrafią w kilka sekund przeczytać 10-stronicowe CV, wyciągnąć z niego kluczowe kompetencje, porównać je z wymaganiami technicznymi i sformułować celne pytania na rozmowę kwalifikacyjną.
Jednak wdrożenie tych narzędzi w codziennej pracy rekrutera wiąże się z poważnymi barierami prawnymi.
- Brak umów powierzenia (DPA): Korzystanie z darmowych wersji ChatGPT czy Claude oznacza akceptację regulaminu dla użytkowników indywidualnych. Dostawca usługi nie gwarantuje zgodności z RODO, a firma nie posiada podpisanej umowy powierzenia przetwarzania danych osobowych.
- Wykorzystanie danych do trenowania: Wpisywane prompty i przesyłane dokumenty CV są domyślnie wykorzystywane do dalszego uczenia modeli. Dane osobowe programistów (w tym numery telefonów, adresy e-mail, historia projektów komercyjnych chronionych umowami NDA) stają się częścią globalnej sieci neuronowej.
- Transfer danych poza EOG: Dane są wysyłane na serwery w USA. Bez wdrożenia Standardowych Klauzul Umownych (SCCs) transfer ten jest nielegalny.
Co więcej, unijny Akt o Sztucznej Inteligencji (AI Act) klasyfikuje systemy AI stosowane w rekrutacji jako systemy wysokiego ryzyka (High-Risk AI Systems), co nakłada restrykcyjne wymagania dotyczące przejrzystości, cyberbezpieczeństwa oraz nadzoru ludzkiego (tzw. *human-in-the-loop* – algorytm nie może samodzielnie decydować o odrzuceniu kandydata).
2. Jak wysokie są kary UODO w Polsce? Realne ryzyko dla MŚP
Powszechnie cytuje się unijne przepisy mówiące o maksymalnych karach do 20 000 000 EUR lub do 4% rocznego globalnego obrotu przedsiębiorstwa. Warto jednak przyjrzeć się temu, jak te regulacje są faktycznie interpretowane i stosowane w Polsce przez Urząd Ochrony Danych Osobowych (UODO).
Sektor publiczny vs sektor prywatny
W Polsce podmioty sektora publicznego (np. urzędy pracy, miejskie instytucje) podlegają ustawowym ograniczeniom wysokości kar. Zgodnie z polską Ustawą o ochronie danych osobowych, kary te są limitowane do maksymalnie 100 000 PLN (dla państwowych jednostek budżetowych) oraz 10 000 PLN (dla samorządowych instytucji kultury).
Sektor prywatny (agencje rekrutacyjne, software house'y, firmy IT) podlega jednak pełnemu wymiarowi kar unijnych.
Zasada proporcjonalności i praktyka UODO wobec MŚP
Czy mikro, małej lub średniej firmie grozi kara 20 milionów euro za to, że rekruter wkleił kilka CV do ChatGPT?
Zgodnie z art. 83 ust. 1 RODO, nakładana kara musi być w każdym indywidualnym przypadku skuteczna, proporcjonalna i odstraszająca. UODO przy wymierzaniu kary bierze pod uwagę:
- Skalę naruszenia: Liczbę kandydatów, których dane zostały przetworzone bez DPA.
- Możliwości finansowe podmiotu: Nałożenie kary w wysokości kilkunastu milionów złotych na mały software house doprowadziłoby do jego upadłości, co naruszałoby zasadę proporcjonalności.
- Nieumyślność vs umyślność działania: Czy była to jednorazowa wpadka rekrutera, czy celowa polityka firmy wymuszająca masowy upload danych kandydatów do darmowej chmury.
- Wdrożone działania naprawcze: Czy firma po wykryciu incydentu natychmiast zablokowała dostęp do publicznych chatbotów i wdrożyła procedury naprawcze.
Co to oznacza w praktyce?
W przypadku mniejszych firm i pierwszych, nieumyślnych uchybień o niskiej skali, UODO najczęściej rozpoczyna od upomnienia (ostrzeżenia) połączonego z nakazem dostosowania procedur.
Jeśli jednak dojdzie do wycieku danych lub oficjalnej skargi kandydata, na firmę nakładana jest kara finansowa. Dla małych i średnich przedsiębiorstw kary te w Polsce najczęściej oscylują w przedziale od 5 000 PLN do 50 000 PLN. Chociaż kwota ta jest znacznie niższa niż maksymalne progi unijne, to dla małego budżetu operacyjnego, w połączeniu z kosztami audytu prawnego, obsługi kancelarii i utraty reputacji wizerunkowej w branży IT, może okazać się bolesnym ciosem.
3. Obsidian jako baza wiedzy rekrutacyjnej (Recruitment Vault)
Większość systemów ATS (Applicant Tracking Systems) to relacyjne bazy danych z interfejsem webowym. Służą do przechowywania statusów kandydatów, ale słabo radzą sobie z budowaniem głębokiego kontekstu. Wyszukiwanie w nich opiera się najczęściej na prostych słowach kluczowych (np. „React”, „Kubernetes”), co sprawia, że rekruterzy tracą dostęp do kandydatów, którzy mają odpowiednie kompetencje, ale opisali je w CV w niestandardowy sposób.
Wyszukiwanie semantyczne i asocjacyjne to domena baz wiedzy opartych na sieci powiązań (grafach). Narzędziem, które idealnie realizuje tę koncepcję na lokalnym dysku, jest Obsidian.
Dlaczego Obsidian w rekrutacji?
- Lokalne pliki Markdown: Wszystkie notatki, profile kandydatów i analizy są zwykłymi plikami tekstowymi
.mdprzechowywanymi na dysku twardym użytkownika lub w zabezpieczonym udziale sieciowym firmy. Brak bazy danych SQL oznacza łatwość migracji, backupu i brak zależności od zewnętrznych dostawców. - Dwukierunkowe linkowanie (Wikilink): Zamiast sztywnej struktury katalogów, Obsidian pozwala na łączenie pojęć za pomocą składni
. W rekrutacji oznacza to, że profil kandydata może być połączony z technologiami, projektami, firmami czy poziomami seniority. Tworzy się dynamiczny graf wiedzy o rynku talentów.[[Nazwa Strony]] - Prywatność i offline: Obsidian nie wymaga połączenia z internetem do działania. Dane kandydatów nie opuszczają bezpiecznej strefy IT w firmie.
4. Przekształcenie szablonu CLAUDE.md na RECRUITER.md
W społeczności programistów korzystających z AI popularność zdobył szablon CLAUDE.md. Jest to plik instrukcji systemowych umieszczany w głównym folderze projektu. Gdy agent AI (np. Claude Code w terminalu) inicjalizuje pracę w tym folderze, odczytuje CLAUDE.md, aby dowiedzieć się, jaka jest struktura projektu, jakie panują w nim zasady i w jaki sposób ma modyfikować pliki.
Przetwórzmy tę ideę na potrzeby rekrutacji. Stworzymy plik RECRUITER.md, który instruuje lokalnego asystenta AI, jak zarządzać naszym lokalnym skarbcem wiedzy o kandydatach (Recruitment Vault).
Struktura folderów w Recruitment Vault
W głównym katalogu naszego skarbca tworzymy strukturę:
.raw/– Katalog zawierający oryginalne pliki CV (PDF, DOCX) lub bezpośrednie zrzuty tekstu. Katalog ten jest ściśle tylko do odczytu dla AI.wiki/– Aktywna baza wiedzy HR. Tutaj AI tworzy i modyfikuje notatki o kandydatach, technologiach i firmach.wiki/index.md– Główny spis treści (Master Catalog) zawierający skategoryzowane tabele kandydatów, technologii i otwartych procesów.wiki/hot.md– Pamięć podręczna kontekstu (Context Cache). Zawiera profil idealnego kandydata dla rekrutacji, którą aktualnie prowadzimy (np. wymagania na stanowisko Senior Python Developer).wiki/log.md– Chronologiczny rejestr operacji (np. kiedy i jakie CV zostało zaimportowane).
Poniżej znajduje się gotowy szablon pliku RECRUITER.md. Został on przygotowany w języku angielskim, ponieważ lokalne modele językowe znacznie precyzyjniej interpretują instrukcje strukturalne i reguły logiczne w tym języku.
# Role & Philosophy
You are an expert technical recruiter and talent intelligence engineer building a private, secure candidate knowledge graph inside an Obsidian vault. Your goal is to parse raw resumes, synthesize them into structured Markdown profile pages, detect details and contradictions, and catalog them for fast, semantic querying.
# Vault Architecture
- `.raw/` -> Raw source documents (candidate CVs in PDF/txt format). STRICTLY READ-ONLY.
- `wiki/` -> Active talent database. You have full read/write rights here.
- `wiki/index.md` -> Master catalog of candidates, tech stacks, and active positions.
- `wiki/hot.md` -> Current sourcing context. Describes the role we are currently trying to fill.
- `wiki/log.md` -> Chronological ledger of CV ingestions and talent queries.
# Markdown Style Guide for Candidate Profiles
Every candidate file in `wiki/` must be named `Kandydat - [Name] [Surname].md` and follow this structure:
1. Metadata block (YAML frontmatter) containing: seniority, core technologies, years of experience, current location, and notice period.
2. Summary: A 3-sentence objective evaluation of their technical fit.
3. Tech Stack Checklist: Grouped by category with proficiency level (Expert, Intermediate, Novice).
4. Experience Timeline: Reverse chronological order. For each role, list: Company, Period, Role, Projects, and Key Achievements.
5. Verification & Contradictions: A dedicated section highlighting any red flags, overlapping dates, or inconsistencies in their CV.
# Operational Rules
1. **GDPR & Privacy Compliance:** Under no circumstances should you leak candidate personal data. Treat `.raw/` files as sensitive. Do not call external APIs.
2. **Strict Anonymization Option:** If anonymization is requested, generate profiles using unique IDs (e.g., `Kandydat - ID-9982.md`) and strip phone numbers, exact addresses, names of current employers, and emails.
3. **Atomic Tech Pages:** When a candidate lists a specific technology (e.g., [[Kubernetes]]), ensure that technology is linked. If a technology page doesn't exist, create an atomic page in `wiki/` defining the technology and listing candidates who possess it.
4. **Flag Contradictions:** If a candidate's CV has discrepancies (e.g., claiming 8 years of React experience when React was released later, or overlapping employment periods at two full-time on-site roles), highlight it using:
> [!contradiction]
> Candidate claims X, but timeline analysis shows Y.
5. **No Duplication:** Before creating a candidate profile, check `wiki/index.md`. If the candidate exists, merge the new CV details into the existing file instead of creating a duplicate.
# Core Workflows
## 1. INGEST (Trigger: "ingest [filename]")
When processing a new resume from `.raw/`:
1. Parse the resume content from `.raw/[filename]`.
2. Extract all structural data, tech stacks, and work history.
3. Perform a logical sanity check on dates and claimed skills.
4. Create or update `wiki/Kandydat - [Name] [Surname].md`.
5. Create or update related atomic pages (e.g., `wiki/Tech - [Technology].md`).
6. Append a link to the new candidate in `wiki/index.md` under the appropriate category.
7. Prepend an entry to the top of `wiki/log.md` (Format: `- [YYYY-MM-DD] Ingested [[Kandydat - Name Surname]] -> updated [[Tech - TechA]], [[Tech - TechB]]`).
8. Read `wiki/hot.md` and assess if the new candidate matches the current active sourcing profile. Outline the match percentage in the ingestion log.
## 2. SEARCH & MATCH (Trigger: "match [position]")
When asked to find candidates for an active job description:
1. Read the job description details in `wiki/hot.md`.
2. Query `wiki/index.md` to identify potentially matching candidates.
3. Read between 3 to 10 candidate files in `wiki/`. Do not perform a global search of all files.
4. Generate a comparison report using `[[wikilinks]]` to point directly to candidate profiles, detailing why they fit or where they lack skills.
5. Wybór silnika AI: Llama, Mistral czy polski Bielik.ai?
Kluczem do działania całego systemu jest silnik LLM uruchomiony lokalnie na komputerze (on-premise) za pomocą oprogramowania Ollama lub LM Studio. Ponieważ analizujemy dokumenty aplikacyjne w języku polskim i angielskim, dobór modelu ma ogromne znaczenie dla jakości wyników.
Polski akcent: Bielik.ai (https://bielik.ai/)
Bielik to polski model językowy stworzony przez zespół SpeakLeash i Akademickie Centrum Komputerowe Cyfronet AGH. Model ten opiera się na architekturze Mistral / Llama i został poddany intensywnemu procesowi douczania na ogromnych zbiorach polskojęzycznych tekstów.
Jak zainstalować model Bielik w programie Ollama? Wystarczy wpisać w konsoli:
ollama run bielik
Szczere porównanie modeli w zastosowaniach rekrutacyjnych:
| Model LLM | Język polski i kontekst lokalny | Precyzja formatowania (JSON/MD) | Logika i detekcja sprzeczności | Wymagania sprzętowe |
|---|---|---|---|---|
| Bielik (11B-v2) | Wybitny (5/5) Doskonale rozumie polską gramatykę, nazwy rodzimych uczelni, firm oraz niuanse językowe. |
Dobry (3.5/5) Potrafi czasami odbiec od ścisłej struktury Markdown i dopisać krótki komentarz konwersacyjny. |
Średni/Dobry (3.5/5) Dobrze radzi sobie z podstawową logiką, ale może pominąć bardzo skomplikowane anomalie chronologiczne. |
Średnie (wymaga min. 16 GB RAM) |
| Llama 3 / 3.1 (8B) | Średni (3/5) Rozumie język polski, ale w generowanych profilach mogą pojawiać się kalki językowe lub błędy gramatyczne. |
Bardzo dobry (4.5/5) Niezrównany w precyzyjnym przestrzeganiu instrukcji systemowych z RECRUITER.md. |
Dobry (4/5) Dobrze radzi sobie z wyciąganiem i strukturyzacją danych. |
Niskie (wymaga min. 8 GB RAM) |
| Command R (35B) | Dobry (4/5) Bardzo poprawny językowo, stworzony pod zaawansowane scenariusze wyszukiwania (RAG). |
Wybitny (5/5) Idealnie tworzy powiązane notatki, linki i tabele. |
Bardzo dobry (4.5/5) Świetnie wykrywa niespójności na osi czasu. |
Wysokie (wymaga min. 32 GB RAM / mocnego GPU) |
| Flagowe chmury (GPT-4o, Claude 3.5 Sonnet) | Bardzo dobry (4.5/5) | Wybitny (5/5) | Wybitny (5/5) Doskonale wykrywanie kłamstw, manipulacji datami czy braków kompetencyjnych. |
Brak (wymaga jednak wysyłania danych online do serwerów API) |
Wnioski wdrożeniowe:
- Jeśli analizujesz głównie polskie CV i zależy Ci na tym, by podsumowania rekrutacyjne były napisane piękną, naturalną polszczyzną, Bielik (https://bielik.ai/) jest najlepszym lokalnym wyborem. Wymaga on jedynie nieznacznie lepiej przygotowanych reguł filtrujących w skrypcie Python, aby ignorować okazjonalne wstawki konwersacyjne (np. *"Oto przeanalizowany profil..."*).
- Do czysto technicznej i zautomatyzowanej strukturyzacji anglojęzycznych CV, Llama 3 (8B) lub Mistral zapewnią wyższą powtarzalność formatu przy mniejszym zużyciu zasobów komputera.
6. Instrukcja krok po kroku: Konfiguracja lokalnego środowiska
Wdrożenie systemu wymaga wykonania czterech prostych kroków. Proces ten nie wymaga wiedzy programistycznej i może być przeprowadzony na systemach Linux, macOS lub Windows.
Krok 1: Instalacja bazy (Obsidian)
- Pobierz i zainstaluj aplikację Obsidian.
- Stwórz na dysku nowy, pusty folder (np.
Skarbiec_HR). - Otwórz Obsidian e i wybierz opcję "Open folder as vault", wskazując nowo utworzony katalog.
Krok 2: Przygotowanie struktury plików
Wewnątrz folderu Skarbiec_HR utwórz strukturę katalogów i plików zgodnie z wytycznymi z rozdziału 3:
- Utwórz folder
.raw(katalog wejściowy na pliki PDF). - Utwórz folder
wiki(katalog na przetworzone profile). - W folderze
wikiutwórz puste pliki:index.md,hot.mdorazlog.md. - W głównym katalogu
Skarbiec_HRutwórz plikRECRUITER.mdi wklej do niego zawartość szablonu z rozdziału 4.
Krok 3: Instalacja lokalnego serwera AI (Ollama)
- Pobierz instalator ze strony Ollama dla swojego systemu operacyjnego.
- Po instalacji otwórz terminal (lub PowerShell) i pobierz wybrany model (np. Bielik lub Llama 3):
`bash
ollama run bielik
`
- Po zakończeniu pobierania serwer API Ollama będzie działał w tle pod adresem
http://localhost:11434.
Krok 4: Integracja z Obsidianem
Aby móc wygodnie rozmawiać z lokalnym modelem bezpośrednio w Obsidianie:
- W Obsidianie wejdź w Settings -> Community plugins -> włącz obsługę wtyczek społecznościowych (Enable community plugins).
- Kliknij Browse i wyszukaj wtyczkę Copilot lub Local GPT.
- Zainstaluj i włącz wtyczkę.
- W ustawieniach wtyczki (np. Copilot) zmień dostawcę (Provider) na Ollama.
- Wskaż lokalny adres API (
http://localhost:11434) oraz wybierz pobrany wcześniej model (np.bieliklubllama3). - Jako plik systemowy (System Prompt) wskaż ścieżkę do
RECRUITER.md.
7. Automatyzacja procesu: Skrypt integrujący PDF, Ollama i Obsidian
Chociaż ręczne kopiowanie tekstu z CV do bazy działa, w dziale HR potrzebujemy automatyzacji. Poniżej prezentujemy kompletny skrypt w języku Python, który automatyzuje proces INGEST.
Skrypt monitoruje folder .raw/ w poszukiwaniu nowych plików PDF, ekstrahuje z nich tekst, przesyła go do lokalnego API Ollama wraz z instrukcjami z RECRUITER.md, a następnie automatycznie tworzy ustrukturyzowane notatki w folderze wiki/, aktualizując jednocześnie pliki index.md oraz log.md.
Wymagania systemowe skryptu:
Przed uruchomieniem należy zainstalować niezbędne biblioteki Pythona:
pip install pypdf requests
Kod skryptu (ingest_cv.py):
Zapisz poniższy kod jako plik ingest_cv.py w głównym katalogu skarbca Obsidian.
import os
import re
import json
from datetime import datetime
import requests
from pypdf import PdfReader
# --- KONFIGURACJA ---
OLLAMA_API_URL = "http://localhost:11434/api/generate"
MODEL_NAME = "bielik" # Możesz zmienić na llama3, mistral itp.
VAULT_PATH = os.path.dirname(os.path.abspath(__file__))
RAW_DIR = os.path.join(VAULT_PATH, ".raw")
WIKI_DIR = os.path.join(VAULT_PATH, "wiki")
SYSTEM_PROMPT_FILE = os.path.join(VAULT_PATH, "RECRUITER.md")
def read_system_prompt():
"""Wczytuje instrukcje z pliku RECRUITER.md"""
if os.path.exists(SYSTEM_PROMPT_FILE):
with open(SYSTEM_PROMPT_FILE, "r", encoding="utf-8") as f:
return f.read()
return "You are an AI recruiting assistant. Analyze the resume and output structured Markdown."
def extract_text_from_pdf(pdf_path):
"""Ekstrahuje tekst z pliku PDF z CV"""
reader = PdfReader(pdf_path)
text = ""
for page in reader.pages:
text += page.extract_text() + "\n"
return text
def analyze_cv_with_local_llm(cv_text, system_prompt):
"""Przesyła tekst CV do lokalnej Ollamy w celu analizy zgodnie z RECRUITER.md"""
prompt = f"""
Analyze the following raw CV text.
Create a structured Markdown candidate profile following the exact instructions in the System Prompt.
Output ONLY the Markdown content for the candidate profile. Do not include chat explanations or backticks.
Raw CV Content:
---
{cv_text}
---
"""
payload = {
"model": MODEL_NAME,
"prompt": prompt,
"system": system_prompt,
"stream": False,
"options": {
"temperature": 0.1 # Niska temperatura dla wysokiej precyzji i braku halucynacji
}
}
try:
response = requests.post(OLLAMA_API_URL, json=payload, timeout=180)
response.raise_for_status()
return response.json().get("response", "").strip()
except requests.exceptions.RequestException as e:
print(f"Błąd komunikacji z lokalną Ollamą: {e}")
return None
def update_index_and_log(candidate_name, touched_tech):
"""Aktualizuje rejestr wiki/log.md oraz spis treści wiki/index.md"""
today = datetime.today().strftime('%Y-%m-%d')
log_file = os.path.join(WIKI_DIR, "log.md")
index_file = os.path.join(WIKI_DIR, "index.md")
# 1. Aktualizacja log.md
log_entry = f"- [{today}] Ingested [[Kandydat - {candidate_name}]] -> updated " + ", ".join([f"[[Tech - {t}]]" for t in touched_tech]) + "\n"
existing_log = ""
if os.path.exists(log_file):
with open(log_file, "r", encoding="utf-8") as f:
existing_log = f.read()
with open(log_file, "w", encoding="utf-8") as f:
f.write(log_entry + existing_log)
# 2. Aktualizacja index.md
index_content = ""
if os.path.exists(index_file):
with open(index_file, "r", encoding="utf-8") as f:
index_content = f.read()
candidate_link = f"[[Kandydat - {candidate_name}]]"
if candidate_link not in index_content:
if "## Kandydaci" in index_content:
parts = index_content.split("## Kandydaci")
updated_section = parts[1]
updated_section = f"\n- {candidate_link} (Ingested: {today})\n" + updated_section
index_content = parts[0] + "## Kandydaci" + updated_section
else:
index_content += f"\n\n## Kandydaci\n- {candidate_link} (Ingested: {today})\n"
with open(index_file, "w", encoding="utf-8") as f:
f.write(index_content)
def parse_candidate_name_from_markdown(md_content):
"""Próbuje wyciągnąć imię i nazwisko z nagłówka wygenerowanej notatki"""
match = re.search(r'^# Kandydat -\s*(.*)$', md_content, re.MULTILINE)
if match:
return match.group(1).strip()
first_line = md_content.split('\n')[0]
first_line = re.sub(r'[#\*]', '', first_line).strip()
if first_line:
return first_line
return "Nieznany_Kandydat_" + datetime.now().strftime("%H%M%S")
def main():
print("=== Uruchamianie lokalnego parsera CV ===")
system_prompt = read_system_prompt()
if not os.path.exists(RAW_DIR):
os.makedirs(RAW_DIR)
print(f"Utworzono folder na wejściowe CV: {RAW_DIR}. Umieść tam pliki PDF.")
return
if not os.path.exists(WIKI_DIR):
os.makedirs(WIKI_DIR)
pdf_files = [f for f in os.listdir(RAW_DIR) if f.lower().endswith('.pdf')]
if not pdf_files:
print("Brak nowych plików PDF w folderze .raw/")
return
for pdf_file in pdf_files:
pdf_path = os.path.join(RAW_DIR, pdf_file)
print(f"\nPrzetwarzanie pliku: {pdf_file}...")
cv_text = extract_text_from_pdf(pdf_path)
if not cv_text.strip():
print(f"Nie udało się odczytać tekstu z {pdf_file}.")
continue
print("Analiza przez lokalny model LLM (Ollama)...")
analysis = analyze_cv_with_local_llm(cv_text, system_prompt)
if not analysis:
print("Analiza nie powiodła się.")
continue
candidate_name = parse_candidate_name_from_markdown(analysis)
safe_name = "".join([c for c in candidate_name if c.isalpha() or c.isspace() or c=='-']).strip()
file_name = f"Kandydat - {safe_name}.md"
output_path = os.path.join(WIKI_DIR, file_name)
if "```markdown" in analysis:
analysis = analysis.split("```markdown")[1].split("```")[0].strip()
elif "```" in analysis:
analysis = analysis.split("```")[1].split("```")[0].strip()
with open(output_path, "w", encoding="utf-8") as f:
f.write(analysis)
print(f"Zapisano profil kandydata: wiki/{file_name}")
touched_tech = re.findall(r'\[\[Tech - (.*?)\]\]', analysis)
touched_tech = list(set(touched_tech))
for tech in touched_tech:
tech_file = os.path.join(WIKI_DIR, f"Tech - {tech}.md")
if not os.path.exists(tech_file):
with open(tech_file, "w", encoding="utf-8") as f:
f.write(f"# Technologia: {tech}\n\n## Powiązani kandydaci\n- [[Kandydat - {safe_name}]]\n")
else:
with open(tech_file, "r+", encoding="utf-8") as f:
content = f.read()
candidate_link = f"- [[Kandydat - {safe_name}]]"
if candidate_link not in content:
f.write(f"\n{candidate_link}\n")
update_index_and_log(safe_name, touched_tech)
processed_dir = os.path.join(RAW_DIR, "processed")
if not os.path.exists(processed_dir):
os.makedirs(processed_dir)
os.rename(pdf_path, os.path.join(processed_dir, pdf_file))
print(f"Przeniesiono oryginalny plik do: .raw/processed/{pdf_file}")
if __name__ == "__main__":
main()
8. Codzienna praca z systemem i scenariusze użycia
Posiadanie tak skonstruowanego środowiska rekrutacyjnego diametralnie zmienia komfort pracy rekrutera IT. Przyjrzyjmy się dwóm typowym scenariuszom.
Scenariusz 1: Importowanie nowej paczki CV
Pojawia się nowa rekrutacja. W ciągu dwóch dni spływa 30 aplikacji w plikach PDF.
- Rekruter wrzuca pliki PDF do katalogu
.raw/. - Uruchamia skrypt
ingest_cv.py. - Skrypt bez wysyłania danych do chmury przetwarza pliki za pomocą lokalnego modelu Bielik.
- W folderze
wiki/powstaje 30 nowych notatek o kandydatach. Każda zawiera jednolite, ustrukturyzowane podsumowanie i profil techniczny. - Plik
wiki/index.mdautomatycznie aktualizuje się o nowe nazwiska.
Scenariusz 2: Wykrywanie anomalii (Sanity Check)
Lokalny model LLM podczas procesu analizy automatycznie bada spójność chronologiczną CV. W przypadku kandydata, który zadeklarował równoległe zatrudnienie na dwóch pełnowymiarowych stanowiskach stacjonarnych w różnych miastach, system generuje w pliku kandydata wyraźne ostrzeżenie:
Ostrzeżenie chronologiczne
Kandydat deklaruje zatrudnienie na stanowisku Senior Java Developer w firmie X (Kraków) w okresie 01.2024 - obecnie oraz jako Team Lead w firmie Y (Warszawa) w okresie 03.2024 - obecnie. Oba stanowiska oznaczone są jako praca stacjonarna. Wymaga weryfikacji podczas rozmowy telefonicznej.
Daje to rekruterowi gotowy punkt do omówienia podczas wstępnego screeningu telefonicznego.
9. Podsumowanie
Połączenie Obsidiana z lokalnym modelami językowymi udowadnia, że innowacje technologiczne w HR nie muszą wiązać się z kompromisami w obszarze bezpieczeństwa danych.
Główne korzyści:
- 100% Bezpieczeństwa RODO/GDPR: Dane osobowe kandydatów są przetwarzane wyłącznie lokalnie. Brak ryzyka wycieku, brak transferu poza EOG, pełna zgodność z polityką bezpieczeństwa informacji firmy.
- Koniec z czarnymi skrzynkami ATS: Zamiast zamkniętej bazy danych, w której trudno cokolwiek znaleźć, firma buduje transparentny graf wiedzy o talentach w postaci uniwersalnych plików Markdown.
- Prawdziwie lokalne, polskie wsparcie: Dzięki modelom takim jak Bielik.ai (https://bielik.ai/), system radzi sobie z językiem polskim równie dobrze jak anglocentryczne potęgi IT.
Przejście na model lokalnego AI w rekrutacji wymaga początkowo podstawowej konfiguracji sprzętowej, ale korzyści w postaci bezpieczeństwa prawnego, szybkości wyszukiwania semantycznego oraz pełnej kontroli nad danymi sprawiają, że jest to obecnie najbardziej rozsądny i perspektywiczny kierunek rozwoju HR Tech.
Chcesz wdrożyć bezpieczne narzędzia AI w swojej firmie?
Pomagamy w budowie dedykowanych systemów ATS/CRM, integracji bezpiecznych modeli AI (Ollama, Bielik.ai, LLM on-premise) oraz audytowaniu bezpieczeństwa systemów rekrutacyjnych.
Skonsultuj się z nami