Vous cherchez un moyen d'analyser rapidement des dizaines de candidatures pour un seul poste, mais vous êtes terrifié à l'idée de recevoir des amendes pour violation de la réglementation sur la protection des données personnelles ? Dans cet article, nous allons vous présenter un outil génial pour vous aider avec l'IA dans l'analyse massive de CV sans enfreindre les règles du RGPD. Nous allons vous montrer comment connecter le programme gratuit Obsidian avec des modèles de langage locaux (Local LLM) exécutés directement sur votre ordinateur ou serveur d'entreprise.
De cette façon, vous créerez un "second cerveau" de recrutement privé, sécurisé et totalement conforme à la loi, où vos données ne quitteront jamais l'ordinateur local.
Table des matières
- Introduction : L'IA dans le recrutement et le piège du RGPD
- Quel est le montant des amendes du RGPD en Pologne ? Risque réel pour les PME
- Obsidian comme base de connaissances pour le recrutement (Recruitment Vault)
- Conversion du modèle CLAUDE.md en RECRUITER.md
- Choix du moteur d'IA : Llama, Mistral ou le polonais Bielik.ai ?
- Guide étape par étape : Configuration de l'environnement local
- Automatisation : Script pour l'intégration de PDF, Ollama et Obsidian
- Travail quotidien et cas d'usage
- Résumé
1. Introduction : L'IA dans le recrutement et le piège du RGPD
L'arrivée de modèles comme GPT-4o ou Claude 3.5 Sonnet a apporté une revolution dans les départements des ressources humaines. Ces outils peuvent lire un CV de 10 pages en quelques secondes, en extraire les compétences clés, les comparer aux exigences techniques et formuler des questions précises pour l'entretien d'embauche.
Cependant, la mise en œuvre de ces outils dans le travail quotidien d'un recruteur implique de sérieuses barrières juridiques :
- Absence de contrats de sous-traitance (DPA) : L'utilisation de versions gratuites de ChatGPT ou Claude implique l'acceptation des conditions d'utilisation pour les utilisateurs individuels. Le fournisseur ne garantit pas la conformité avec le RGPD, et l'entreprise ne dispose pas d'un DPA signé.
- Utilisation des données pour l'entraînement : Les textes saisis et les fichiers de CV téléchargés sont utilisés par défaut pour entraîner des modèles publics. Les données personnelles des développeurs (y compris les numéros de téléphone, les adresses e-mail et l'historique des projets protégés par des accords de confidentialité NDA) font partie du réseau neuronal mondial.
- Transfert de données hors de l'EEE : Les données sont envoyées vers des serveurs aux États-Unis. Sans la mise en œuvre de Clauses Contractuelles Types (CCT), ce transfert est illégal.
De plus, la loi sur l'intelligence artificielle de l'Union européenne (AI Act) classe les systèmes d'IA utilisés dans le recrutement comme systèmes d'IA à haut risque. Cela impose des exigences techniques et juridiques strictes en matière de transparence, de cybersécurité et de supervision humaine (human-in-the-loop – un algorithme ne peut décider de manière autonome de rejeter une candidature).
2. Quel est le montant des amendes du RGPD en Pologne ? Risque réel pour les PME
On cite souvent des amendes maximales allant jusqu'à 20 000 000 EUR ou jusqu'à 4 % du chiffre d'affaires mondial annuel total de l'entreprise. Cependant, il convient de regarder comment ces règles sont réellement interprétées et appliquées en Pologne par l'Office de protection des données personnelles (UODO).
Secteur public vs. secteur privé
En Pologne, les entités du secteur public sont soumises à des limites légales sur le montant des amendes. Selon la loi polonaise sur la protection des données personnelles, ces amendes sont limitées à un maximum de 100 000 PLN (pour les unités budgétaires de l'État) et 10 000 PLN (pour les institutions culturelles municipales). Les entreprises privées (agences de recrutement, entreprises de développement de logiciels, entreprises informatiques) font face à toute la rigueur des amendes de l'Union européenne.
Le principe de proportionnalité et la pratique de l'UODO avec les PME
Une micro, petite ou moyenne entreprise risque-t-elle une amende de 20 millions d'euros si un recruteur colle quelques CV dans ChatGPT ? Selon l'article 83, paragraphe 1, du RGPD, l'amende imposée doit être efficace, proportionnée et dissuasive dans chaque cas individuel. Lors de l'imposition d'une amende, l'UODO prend en compte :
- L'échelle de l'infraction : Le nombre de candidats dont les données ont été traitées sans DPA.
- La capacité financière de l'entité : Imposer une amende de plusieurs millions de PLN à une petite entreprise de développement de logiciels entraînerait sa faillite, ce qui violerait le principe de proportionnalité.
- L'action intentionnelle par rapport à l'action non intentionnelle : S'agissait-il d'une erreur ponctuelle d'un recruteur ou d'une politique délibérée de l'entreprise imposant le téléchargement massif de données de candidats dans le cloud public ?
- Les actions correctives mises en œuvre : L'entreprise a-t-elle immédiatement bloqué l'accès aux chatbots publics et mis en œuvre des procédures correctives après la découverte de l'incident ?
Qu'est-ce que cela signifie en pratique?
Dans le cas des petites entreprises et des premières infractions mineures non intentionnelles, l'UODO commence le plus souvent par un avertissement (upomnienie) assorti d'une injonction de mise en conformité des procédures.
Toutefois, en cas de fuite de données ou de plainte officielle d'un candidat, une amende financière est infligée. Pour les petites et moyennes entreprises en Pologne, ces amendes se situent le plus souvent entre 5 000 PLN et 50 000 PLN. Bien que ce montant soit très inférieur aux seuils maximaux de l'UE, il s'agit tout de même d'un coup dur pour un petit budget de fonctionnement, surtout lorsqu'on le combine aux coûts des audits juridiques, des avocats et de l'atteinte à la réputation dans le secteur des technologies de l'information.
3. Obsidian comme base de connaissances pour le recrutement (Recruitment Vault)
La plupart des ATS (Applicant Tracking Systems) sont des bases de données relationnelles avec une interface web. Ils stockent les statuts des candidats, mais ne permettent pas de construire un contexte profond. La recherche y est généralement basée sur des mots-clés simples (par exemple, "React", "Kubernetes"), ce qui fait que les recruteurs perdent l'accès aux candidats qui ont les compétences requises mais les ont décrites dans leur CV de manière non standard.
La recherche sémantique et associative est le domaine des bases de connaissances basées sur un réseau de connexions (graphes). L'outil qui réalise parfaitement ce concept sur un disque local est Obsidian.
Pourquoi Obsidian dans le recrutement?
- Fichiers Markdown locaux : Toutes les notes, les profils des candidats et les analyses sont de simples fichiers texte
.mdstockés sur le disque dur de l'utilisateur ou sur le partage réseau sécurisé de l'entreprise. Sans base de données SQL, la migration, la sauvegarde et l'indépendance vis-à-vis des fournisseurs externes sont très simples. - Liens bidirectionnels (Wikilinks) : Au lieu d'une structure de répertoires rigide, Obsidian permet de connecter des concepts à l'aide de la syntaxe
. Dans le recrutement, cela signifie que le profil d'un candidat peut être lié à des technologies, des projets, des entreprises ou des niveaux d'expérience. Un graphe de connaissances dynamique du marché du talent est créé.[[Nom de la page]] - Confidentialité et hors ligne : Obsidian ne nécessite pas de connexion Internet. Les données des candidats ne sortent pas de la zone informatique sécurisée de l'entreprise.
4. Conversion du modèle CLAUDE.md en RECRUITER.md
Dans la communauté de développement qui utilise l'IA, le modèle CLAUDE.md a gagné en popularité. Il s'agit d'un fichier d'instructions système placé dans le dossier racine du projet. Lorsqu'un agent d'IA s'initialise dans ce dossier, il lit ce fichier pour apprendre la structure et les règles du projet.
Nous adaptons cette idée pour le recrutement. Nous allons créer un fichier RECRUITER.md qui indique à l'assistant d'IA local comment gérer notre vault de sélection.
Structure des dossiers dans Recruitment Vault
Dans la racine de notre Vault, nous créons la structure :
.raw/– Dossier pour les CV originaux (PDF, DOCX) ou texte direct. Ce dossier est strictement en lecture seule pour l'IA.wiki/– Base de connaissances active. Ici l'IA crée et modifie les notes sur les candidats, technologies et entreprises.wiki/index.md– Index principal (Master Catalog) avec des tableaux de candidats, technologies et processus.wiki/hot.md– Cache de contexte. Contient le profil du candidat idéal pour le poste en cours de recrutement.wiki/log.md– Registre chronologique d'opérations (par exemple, quand un CV a été importé).
Ci-dessous se trouve le modèle de configuration de RECRUITER.md. Il est rédigé en anglais, car les modèles locaux interprètent les instructions système avec beaucoup plus de précision dans cette langue.
# 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. Choix du moteur d'IA : Llama, Mistral ou le polonais Bielik.ai ?
Pour faire fonctionner le système, nous avons besoin d'un moteur de LLM fonctionnant localement sur l'ordinateur à l'aide d'un logiciel comme Ollama ou LM Studio. Comme nous analysons des candidatures, le choix du modèle est crucial.
Accent polonais : Bielik.ai (https://bielik.ai/)
Bielik est un modèle de langage en polonais développé par l'équipe SpeakLeash et le centre de calcul Cyfronet de l'AGH de Cracovie. Il est basé sur l'architecture Mistral/Llama et a été entraîné spécifiquement sur de grands ensembles de données textuelles en polonais.
Pour installer Bielik dans Ollama, exécutez simplement :
ollama run bielik
Comparaison honnête de modèles pour le recrutement :
| Modèle LLM | Langue polonaise & contexte local | Précision de format (JSON/MD) | Logique & détection d'incohérences | Configuration requise |
|---|---|---|---|---|
| Bielik (11B-v2) | Excellent (5/5) Comprend parfaitement grammaire polonaise, universités locales, entreprises et nuances. |
Bon (3.5/5) Parfois ajoute des phrases de politesse du chat. |
Moyen/Bon (3.5/5) Bonne détection de base, mais peut omettre des anomalies de chronologie fines. |
Moyen (requiert min. 16 Go RAM) |
| Llama 3 / 3.1 (8B) | Moyen (3/5) Comprend le polonais mais des erreurs de grammaire peuvent subsister. |
Très bon (4.5/5) Parfait pour respecter les règles de RECRUITER.md. |
Bon (4/5) Bonne extraction et structuration. |
Faible (requiert min. 8 Go RAM) |
| Command R (35B) | Bon (4/5) Trés correct, optimisé pour la recherche sémantique (RAG). |
Excellent (5/5) Parfait pour lier les notes, liens et tableaux. |
Très bon (4.5/5) Excellent pour détecter les anomalies chronologiques. |
Élevé (requiert min. 32 Go RAM / carte graphique dédiée) |
| Flagships Clouds (GPT-4o, Claude 3.5 Sonnet) | Très bon (4.5/5) | Excellent (5/5) | Excellent (5/5) Le top pour repérer incohérences et mensonges de dates. |
Aucun (mais requiert d'envoyer les données par API) |
Recommandations :
- Si vous traitez majoritairement des CV en polonais, Bielik (https://bielik.ai/) est le meilleur choix local. Il nécessite juste des filtres simples de nettoyage en Python pour ignorer les textes conversationnels (par exemple, *"Voici le profil..."*).
- Pour les CV en anglais, les modèles comme Llama 3 (8B) ou Mistral offrent une plus grande cohérence de format avec une consommation de ressources moindre.
6. Guide étape par étape : Configuration de l'environnement local
La configuration du système nécessite quatre étapes simples. Elle ne requiert aucune connaissance en programmation.
Étape 1: Installer Obsidian
- Téléchargez et installez Obsidian.
- Créez un dossier vide (par exemple,
Vault_HR). - Ouvrez Obsidian et choisissez "Open folder as vault" pointant sur ce dossier.
Étape 2: Structure des dossiers
Créez dans le Vault:
- Dossier
.raw. - Dossier
wiki. - Créez des fichiers vides dans
wiki:index.md,hot.mdetlog.md. - Placez
RECRUITER.mdà la racine.
Étape 3: Installer le serveur local (Ollama)
- Installez Ollama pour votre système.
- Exécutez en console:
`bash
ollama run bielik
`
- L'API locale s'exécutera sous
http://localhost:11434.
Étape 4: Intégration
- Dans Obsidian: Settings -> Community plugins -> Enable community plugins.
- Recherchez et installez le plugin Copilot.
- Modifiez le provider pour Ollama (
http://localhost:11434). - Sélectionnez le modèle
bielikoullama3et définissez le system prompt surRECRUITER.md.
7. Automatisation : Script Python
Pour automatiser, on utilise un script Python d'import offline (lecture PDF, appel API local Ollama et création de fiches Markdown).
Requis :
pip install pypdf requests
Code du script (ingest_cv.py) :
import os
import re
import json
from datetime import datetime
import requests
from pypdf import PdfReader
# --- CONFIGURATION ---
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. Travail quotidien et cas d'usage
Scénario 1: Importer des CV
- On dépose les CV en PDF dans
.raw/. - On exécute le script
ingest_cv.py. - Le script appelle le modèle local Bielik pour générer les fichiers Markdown dans
wiki/et mettre à jour l'index offline.
Scénario 2: Détection d'anomalies de dates
Le modèle cherche les incohérences dans l'historique de dates et alerte (par exemple, deux emplois à temps plein physiques parallèles dans des villes distinctes):
Alerte chronologique
Candidat déclare des postes physiques parallèles chez Company X (Cracovie) depuis 01.2024 stacjonarna et Company Y (Varsovie) depuis 03.2024 stacjonarna. Ceci requiert une vérification téléphonique.
9. Résumé
Obsidian et modèles locaux démontrent qu'il est possible d'innover en ressources humaines tout en maintenant la vie privée des candidats. Avantages clés : 100 % RGPD sûr, pas de dépendance envers les systèmes SaaS propriétaires, coûts de licence nuls et excellent traitement local du polonais et de l'anglais grâce à des projets comme Bielik.ai.
Vous souhaitez implémenter des outils d'IA sécurisés dans votre entreprise ?
Nous vous aidons à concevoir des systèmes ATS/CRM personnalisés, à intégrer des modèles d'IA sécurisés (Ollama, Bielik.ai, LLM sur site) et à auditer la sécurité des systèmes de recrutement.
Contactez-nous