Commoditech Schreiben Sie uns
← Zurück zum Blog

Lokale KI und Obsidian im Recruiting

Suchen Sie nach einer Möglichkeit, Dutzende von Bewerbungen für eine einzige Stelle schnell zu analysieren, haben aber Angst vor Bußgeldern wegen Verstößen gegen die Datenschutz-Grundverordnung? In diesem Artikel stellen wir Ihnen ein cooles Tool vor, mit dem Sie sich bei der massenhaften Analyse von Lebensläufen durch KI unterstützen lassen können, ohne gegen die DSGVO zu verstoßen. Wir zeigen Ihnen, wie Sie das kostenlose Programm Obsidian mit lokalen Sprachmodellen (Local LLMs) verbinden, die direkt auf Ihrem Computer oder Firmenserver laufen.

Auf diese Weise erstellen Sie ein privates, sicheres und vollständig konformes „zweites Gehirn“ für die Personalrekrutierung, bei dem Ihre Daten niemals den lokalen Computer verlassen.


Inhaltsverzeichnis

  1. Einführung: KI im Recruiting und die DSGVO-Falle
  2. Wie hoch sind die DSGVO-Bußgelder in Polen? Reales Risiko für KMU
  3. Obsidian als Rekrutierungs-Wissensdatenbank (Recruitment Vault)
  4. Umwandlung der CLAUDE.md-Vorlage in RECRUITER.md
  5. Auswahl der KI-Engine: Llama, Mistral oder das polnische Bielik.ai?
  6. Schritt-für-Schritt-Anleitung: Konfiguration der lokalen Umgebung
  7. Automatisierung: Skript zur Integration von PDF, Ollama und Obsidian
  8. Tägliche Arbeit und Anwendungsfälle
  9. Zusammenfassung

1. Einführung: KI im Recruiting und die DSGVO-Falle

Die Einführung von Modellen wie GPT-4o oder Claude 3.5 Sonnet brachte eine Revolution in die Personalabteilungen. Diese Tools können einen 10-seitigen Lebenslauf in Sekundenschnelle lesen, Schlüsselkompetenzen extrahieren, sie mit technischen Anforderungen vergleichen und präzise Fragen für das Vorstellungsgespräch formulieren.

Die Implementierung dieser Tools in der täglichen Arbeit eines Rekrutierers ist jedoch mit schwerwiegenden rechtlichen Hindernissen verbunden:

  • Fehlen von Datenverarbeitungsvereinbarungen (DPA): Die Nutzung kostenloser Versionen von ChatGPT oder Claude bedeutet die Zustimmung zu den Nutzungsbedingungen für Einzelnutzer. Der Anbieter garantiert keine Einhaltung der DSGVO, und das rekrutierende Unternehmen verfügt über keine unterzeichnete Vereinbarung zur Auftragsdatenverarbeitung.
  • Nutzung von Daten für das Training: Eingegebene Prompts und hochgeladene Lebensläufe werden standardmäßig zum Trainieren öffentlicher Modelle verwendet. Die personenbezogenen Daten der Entwickler (einschließlich Telefonnummern, E-Mail-Adressen und der durch NDAs geschützten Projekthistorie) werden Teil des globalen neuronalen Netzwerks.
  • Datenübermittlung außerhalb des EWR: Die Daten werden an Server in den USA gesendet. Ohne die Einhaltung von Standardvertragsklauseln (SCCs) ist diese Übermittlung rechtswidrig.

Darüber hinaus stuft der AI Act der Europäischen Union KI-Systeme, die im Recruiting eingesetzt werden, als KI-Systeme mit hohem Risiko ein. Dies stellt strenge technische und rechtliche Anforderungen an die Transparenz, Cybersicherheit und menschliche Aufsicht (human-in-the-loop – ein Algorithmus darf nicht autonom entscheiden, einen Kandidaten abzulehnen).


2. Wie hoch sind die DSGVO-Bußgelder in Polen? Reales Risiko für KMU

Häufig werden maximale Bußgelder von bis zu 20.000.000 EUR oder bis zu 4 % des gesamten weltweiten Jahresumsatzes des Unternehmens genannt. Es lohnt sich jedoch, einen Blick darauf zu werfen, wie diese Vorschriften in Polen vom Amt für den Schutz personenbezogener Daten (UODO) tatsächlich ausgelegt und angewendet werden.

Öffentlicher Sektor vs. privater Sektor

In Polen unterliegen Stellen des öffentlichen Sektors gesetzlichen Höchstgrenzen für Bußgelder. Nach dem polnischen Datenschutzgesetz sind diese Bußgelder auf maximal 100.000 PLN (für staatliche Haushaltsüberschüsse) und 10.000 PLN (für kommunale Kultureinrichtungen) begrenzt. Private Unternehmen (Personalvermittlungen, Softwarehäuser, IT-Firmen) tragen das volle Risiko der europäischen Bußgelder.

Das Verhältnismäßigkeitsprinzip und die UODO-Praxis gegenüber KMU

Droht einem kleinsten, kleinen oder mittleren Unternehmen eine Geldstrafe von 20 Millionen Euro, wenn ein Rekrutierer ein paar Lebensläufe in ChatGPT einfügt? Gemäß Artikel 83 Absatz 1 der DSGVO muss die verhängte Geldbuße im Einzelfall wirksam, verhältnismäßig und abschreckend sein. Bei der Festsetzung berücksichtigt das UODO:

  1. Ausmaß des Verstoßes: Die Anzahl der Kandidaten, deren Daten ohne DPA verarbeitet wurden.
  2. Finanzielle Leistungsfähigkeit des Unternehmens: Eine Geldstrafe von mehreren Millionen PLN würde für ein kleines Softwarehaus den Bankrott bedeuten, was gegen das Verhältnismäßigkeitsprinzip verstoßen würde.
  3. Vorsätzliches vs. fahrlässiges Handeln: Handelte es sich um ein einmaliges Versehen eines Rekrutierers oder um eine bewusste Unternehmenspolitik, die das massenhafte Hochladen von Kandidatendaten in die öffentliche Cloud erzwingt.
  4. Implementierte Korrekturmaßnahmen: Hat das Unternehmen den Zugriff auf öffentliche Chatbots sofort gesperrt und nach Entdeckung des Vorfalls Korrekturverfahren eingeführt.

Was bedeutet das in der Praxis?

Bei kleineren Unternehmen und erstmaligen, unbeabsichtigten geringfügigen Verstößen beginnt das UODO meist mit einer Verwarnung (upomnienie), verbunden mit der Auflage, die Verfahren anzupassen.

Kommt es jedoch zu einem Datenleck oder einer offiziellen Beschwerde eines Kandidaten, wird eine Geldstrafe verhängt. Für kleine und mittlere Unternehmen in Polen liegen diese Bußgelder meist zwischen 5.000 PLN und 50.000 PLN. Obwohl dieser Betrag deutlich unter den maximalen EU-Schwellenwerten liegt, ist er für ein kleines Betriebsbudget dennoch ein schmerzhafter Schlag, insbesondere in Verbindung mit den Kosten für Rechtsprüfungen, Anwälte und Reputationsschäden im IT-Sektor.


3. Obsidian as a Recruitment Knowledge Base (Recruitment Vault)

Die meisten ATS (Applicant Tracking Systems) sind relationale Datenbanken mit einer Webschnittstelle. Sie speichern den Status der Kandidaten, sind aber schlecht darin, einen tiefen Kontext aufzubauen. Die Suche darin basiert meist auf einfachen Schlüsselwörtern (z. B. „React“, „Kubernetes“), was dazu führt, dass Rekrutierer den Zugriff auf Kandidaten verlieren, die zwar über die richtigen Fähigkeiten verfügen, diese aber im Lebenslauf auf unkonventionelle Weise beschrieben haben.

Die semantische und assoziative Suche ist die Domäne von Wissensdatenbanken, die auf einem Netzwerk von Verbindungen (Graphen) basieren. Das Werkzeug, das dieses Konzept perfekt auf einer lokalen Festplatte umsetzt, ist Obsidian.

Warum Obsidian in der Rekrutierung?

  1. Lokale Markdown-Dateien: Alle Notizen, Kandidatenprofile und Analysen sind einfache Textdateien (.md), die auf der Festplatte des Benutzers oder auf der sicheren Netzwerkfreigabe des Unternehmens gespeichert sind. Keine SQL-Datenbank bedeutet einfache Migration, Backup und Unabhängigkeit von externen Anbietern.
  2. Bidirektionale Verlinkung (Wikilink): Anstelle einer starren Verzeichnisstruktur ermöglicht Obsidian das Verbinden von Konzepten mithilfe der Syntax [[Seitenname]]. Im Recruiting bedeutet dies, dass das Profil eines Kandidaten mit Technologien, Projekten, Unternehmen oder Senioritätsstufen verlinkt werden kann. Ein dynamischer Wissensgraph des Talentmarkts entsteht.
  3. Datenschutz und Offline: Obsidian benötigt keine Internetverbindung. Kandidatendaten verlassen nicht die sichere IT-Zone des Unternehmens.

4. Umwandlung der CLAUDE.md-Vorlage in RECRUITER.md

In der Entwickler-Community, die KI nutzt, hat sich die Vorlage CLAUDE.md etabliert. Es handelt sich um eine Systemanweisungsdatei im Stammordner des Projekts. Wenn ein KI-Agent initialisiert wird, liest er diese Datei, um die Projektstruktur und -regeln zu lernen.

Wir passen diese Idee für die Personalrekrutierung an. Wir erstellen eine Datei RECRUITER.md, die dem lokalen KI-Assistenten zeigt, wie er unseren lokalen Rekrutierungs-Vault verwalten soll.

Ordnerstruktur im Recruitment Vault

In der Stammverzeichnis unseres Vaults erstellen wir folgende Struktur:

  • .raw/ – Verzeichnis für die originalen Lebenslauf-Dateien (PDF, DOCX) oder direkten Text. Dieses Verzeichnis ist für die KI strikt schreibgeschützt.
  • wiki/ – Aktive HR-Wissensdatenbank. Hier erstellt und bearbeitet die KI Notizen zu Kandidaten, Technologien und Unternehmen.
  • wiki/index.md – Hauptindex (Master Catalog) mit kategorisierten Tabellen von Kandidaten, Technologien und offenen Prozessen.
  • wiki/hot.md – Kontext-Cache. Enthält das Profil des idealen Kandidaten für den aktuell laufenden Rekrutierungsprozess.
  • wiki/log.md – Chronologisches Protokoll der Vorgänge (z. B. wann welcher Lebenslauf importiert wurde).

Unten finden Sie die Konfigurationsvorlage für RECRUITER.md. Sie ist in englischer Sprache verfasst, da lokale Modelle Systemanweisungen in dieser Sprache wesentlich präziser interpretieren.

# 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. Auswahl der KI-Engine: Llama, Mistral oder das polnische Bielik.ai?

Um das System zu betreiben, benötigen wir eine lokal ausgeführte LLM-Engine über Software wie Ollama oder LM Studio. Da wir Bewerbungen in polnischer und englischer Sprache analysieren, ist die Wahl des Modells entscheidend.

Polnischer Akzent: Bielik.ai (https://bielik.ai/)

Bielik ist ein polnisches Sprachmodell, das vom SpeakLeash-Team und dem Rechenzentrum Cyfronet der AGH Krakau entwickelt wurde. Es basiert auf der Mistral/Llama-Architektur und wurde speziell auf großen polnischen Textdatensätzen trainiert.

Um Bielik in Ollama zu installieren, führen Sie einfach aus:

ollama run bielik

Ehrlicher Vergleich der Modelle für Rekrutierungsaufgaben:

Modell LLM Polnische Sprache & Lokaler Kontext Formatierungspräzision (JSON/MD) Logik & Erkennung von Widersprüchen Hardwareanforderungen
Bielik (11B-v2) Hervorragend (5/5)
Versteht polnische Grammatik, lokale Universitäten, Unternehmen und sprachliche Nuancen perfekt.
Gut (3.5/5)
Weicht manchmal von der strengen Markdown-Struktur ab und fügt Begrüßungen hinzu.
Mittel/Gut (3.5/5)
Behandelt grundlegende Logik gut, übersieht aber manchmal komplexe chronologische Anomalien.
Mittel (erfordert min. 16 GB RAM)
Llama 3 / 3.1 (8B) Mittel (3/5)
Versteht Polnisch, aber Übersetzungen können Grammatikfehler enthalten.
Sehr gut (4.5/5)
Unübertroffen bei der genauen Einhaltung von Systemanweisungen in RECRUITER.md.
Gut (4/5)
Gute Datenextraktion und -strukturierung.
Niedrig (erfordert min. 8 GB RAM)
Command R (35B) Gut (4/5)
Sehr grammatikalisch korrekt, optimiert für fortgeschrittene RAG-Szenarien.
Hervorragend (5/5)
Perfekt beim Erstellen verbundener Notizen, Links und Tabellen.
Sehr gut (4.5/5)
Hervorragend bei der Erkennung chronologischer Anomalien.
Hoch (erfordert min. 32 GB RAM / starke GPU)
Flaggschiff-Clouds (GPT-4o, Claude 3.5 Sonnet) Sehr gut (4.5/5) Hervorragend (5/5) Hervorragend (5/5)
Überlegen beim Aufspüren von Anomalien und Widersprüchen.
Keine (erfordert jedoch das Senden von Kandidatendaten an Online-APIs)

Empfehlungen:

  • Wenn Sie primär Lebensläufe auf Polnisch verarbeiten und flüssige polnische Zusammenfassungen wünschen, ist Bielik (https://bielik.ai/) die beste lokale Wahl. Es erfordert lediglich kleine Post-Processing-Filter im Python-Skript, um Begrüßungstexte zu ignorieren (z. B. *"Hier ist das analysierte Profil..."*).
  • Für rein englische CVs bieten Modelle wie Llama 3 (8B) oder Mistral eine höhere Formatkonsistenz bei geringerem Ressourcenverbrauch.

6. Schritt-für-Schritt-Anleitung: Konfiguration der lokalen Umgebung

Die Einrichtung des Systems erfordert vier einfache Schritte. Sie erfordert keine Programmierkenntnisse und kann unter Linux, macOS oder Windows durchgeführt werden.

Schritt 1: Obsidian installieren

  1. Laden Sie Obsidian herunter und installieren Sie es.
  2. Erstellen Sie einen neuen leeren Ordner auf Ihrer Festplatte (z. B. HR_Vault).
  3. Öffnen Sie Obsidian und wählen Sie "Open folder as vault", um das neu erstellte Verzeichnis auszuwählen.

Schritt 2: Ordnerstruktur vorbereiten

Erstellen Sie im HR_Vault-Ordner folgende Struktur:

  1. Erstellen Sie den Ordner .raw.
  2. Erstellen Sie den Ordner wiki.
  3. Erstellen Sie leere Dateien in wiki: index.md, hot.md und log.md.
  4. Legen Sie RECRUITER.md im Stammverzeichnis ab.

Step 3: Lokalen KI-Server installieren (Ollama)

  1. Laden Sie das Installationsprogramm von der Ollama-Website für Ihr OS herunter.
  2. Führen Sie im Terminal aus:

`bash

ollama run bielik

`

  1. Die Ollama-API läuft nach dem Download im Hintergrund unter http://localhost:11434.

Schritt 4: In Obsidian integrieren

So chatten Sie mit dem lokalen Modell direkt in Obsidian:

  1. Gehen Sie zu Settings -> Community plugins -> Enable community plugins.
  2. Suchen Sie nach dem Copilot-Plugin und aktivieren Sie es.
  3. Stellen Sie den Provider auf Ollama um.
  4. Geben Sie die lokale API-Adresse an (http://localhost:11434) und wählen Sie das Modell aus (z. B. bielik oder llama3).
  5. Setzen Sie den Pfad der Systemanweisung auf RECRUITER.md.

7. Automatisierung: Skript zur Integration von PDF, Ollama und Obsidian

HR-Teams benötigen einen automatisierten Importprozess. Unten finden Sie das vollständige Python-Skript zur Automatisierung des INGEST-Workflows.

Das Skript liest PDFs aus .raw/ aus, schickt den Text an Ollama und schreibt strukturierte Profile direkt in wiki/.

Skriptanforderungen:

Installieren Sie vor dem Start die Pakete:

pip install pypdf requests

Code des Skripts (ingest_cv.py):

Speichern Sie den folgenden Code als ingest_cv.py im Stammverzeichnis Ihres Obsidian-Vaults.

import os
import re
import json
from datetime import datetime
import requests
from pypdf import PdfReader

# --- KONFIGURATION ---
OLLAMA_API_URL = "http://localhost:11434/api/generate"
MODEL_NAME = "bielik"  # Llama3, Mistral etc.
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. Tägliche Arbeit und Anwendungsfälle

Die strukturierte Vault-Umgebung verändert den Arbeitsablauf eines Rekrutierers.

Szenario 1: Lebensläufe importieren

Wenn eine Stelle besetzt werden muss, gehen z. B. 30 PDF-Lebensläufe ein.

  1. Der Rekrutierer kopiert die PDFs in .raw/.
  2. Führt das Skript ingest_cv.py aus.
  3. Das Skript nutzt den lokalen Bielik-Server für die Profilierung.
  4. 30 Profile werden in wiki/ angelegt, der Index aktualisiert sich.

Szenario 2: Chronologische Anomalien (Sanity Check)

Das lokale Modell prüft die Konsistenz und gibt Warnmeldungen aus:

Chronologie-Warnung

Kandidat deklariert parallele Vollzeitstellen in Company X (Krakau) ab 01.2024 stacjonarna und Company Y (Warschau) ab 03.2024 stacjonarna. Dies erfordert eine telephonische Abklärung.


9. Zusammenfassung

Die Kombination aus Obsidian und lokalen LLMs beweist, dass HR-Innovationen nicht zu Lasten des Datenschutzes gehen müssen. Die wichtigsten Vorteile sind die 100%ige Einhaltung der DSGVO, Unabhängigkeit von proprietären Systemen, minimale Lizenzkosten und hervorragende lokale Sprachunterstützung durch Projekte wie Bielik.ai.

Möchten Sie sichere KI-Tools in Ihrem Unternehmen implementieren?

Wir unterstützen Sie bei der Entwicklung maßgeschneiderter ATS/CRM-Systeme, der Integration sicherer KI-Modelle (Ollama, Bielik.ai, On-Premise-LLMs) und der Überprüfung der Sicherheit von Rekrutierungssystemen.

Kontaktieren Sie uns