{"id":448,"date":"2026-03-29T22:55:51","date_gmt":"2026-03-29T20:55:51","guid":{"rendered":"https:\/\/www.kristedal.se\/?p=448"},"modified":"2026-03-29T22:57:35","modified_gmt":"2026-03-29T20:57:35","slug":"hur-jag-byggde-ett-ai-drivet-cv-matchningssystem-pa-min-egen-server","status":"publish","type":"post","link":"https:\/\/www.kristedal.se\/?p=448","title":{"rendered":"Hur jag byggde ett AI-drivet CV-matchningssystem p\u00e5 min egen server"},"content":{"rendered":"\n<p><em>Publicerat av Per Kristedal \u2014 Kristedal Tech AB<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Jag jobbar inom rekrytering och tech, och en av de mest tidskr\u00e4vande uppgifterna \u00e4r att manuellt g\u00e5 igenom konsultprofiler och matcha dem mot inkommande jobbf\u00f6rfr\u00e5gningar fr\u00e5n kunder. Det \u00e4r repetitivt, det tar tid, och det \u00e4r exakt den typ av problem som AI \u00e4r gjord f\u00f6r att l\u00f6sa.<\/p>\n\n\n\n<p>S\u00e5 jag byggde ett eget system. Fr\u00e5n grunden. P\u00e5 mina egna servrar.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Vad systemet g\u00f6r<\/h2>\n\n\n\n<p>Systemet best\u00e5r av tv\u00e5 delar som arbetar tillsammans:<\/p>\n\n\n\n<p><strong>Del 1 \u2014 CV-konverteraren<\/strong> tar ett CV i PDF-format, skickar det till OpenAI:s API f\u00f6r strukturerad extraktion och skapar b\u00e5de en JSON-profil och ett f\u00e4rdigt CV i f\u00f6retagets grafiska mall. Allt tillg\u00e4ngligt via en webbsida med uppladdningsfunktion.<\/p>\n\n\n\n<p><strong>Del 2 \u2014 Jobbbmatcharen<\/strong> h\u00e4mtar automatiskt inkommande jobbmail fr\u00e5n en e-postinkorg, k\u00f6r varje kandidatprofil mot varje jobbannonsen via en lokal AI-modell, och presenterar resultaten i ett po\u00e4ngsatt dashboard med motiveringar och identifierade gap.<\/p>\n\n\n\n<p>Hela fl\u00f6det ser ut s\u00e5 h\u00e4r:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Ladda upp CV (PDF)\n       \u2193\nOpenAI extraherar strukturerad profil (JSON)\n       \u2193\nProfilen skickas automatiskt till matchningstj\u00e4nsten\n       \u2193\nLokal AI matchar profilen mot alla inkommande jobbannonser\n       \u2193\nResultat visas i dashboard med po\u00e4ng 0\u2013100, motivering och gap<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Den tekniska arkitekturen<\/h2>\n\n\n\n<p>Systemet k\u00f6rs p\u00e5 min egna Proxmox-server hemma, uppdelat i tv\u00e5 isolerade LXC-containrar:<\/p>\n\n\n\n<p><strong>LXC 1 (konverteraren)<\/strong> k\u00f6r en Flask-applikation som \u00e4r publikt tillg\u00e4nglig via en s\u00e4ker omv\u00e4nd tunnel till en VPS. H\u00e4r sker PDF-uppladdning, AI-extraktion och generering av f\u00e4rdiga CV-dokument.<\/p>\n\n\n\n<p><strong>LXC 2 (matcharen)<\/strong> \u00e4r inte publikt tillg\u00e4nglig. Den h\u00e4mtar jobbmail via IMAP p\u00e5 ett schemalagt intervall, k\u00f6r matchningen med en lokal spr\u00e5kmodell via Ollama, sparar resultaten och skickar dem tillbaka till LXC 1.<\/p>\n\n\n\n<p>De tv\u00e5 systemen kommunicerar via SSH-nycklar och SCP \u2014 inga \u00f6ppna portar, ingen molntj\u00e4nst f\u00f6r den k\u00e4nsliga matchningsdatan.<\/p>\n\n\n\n<p>Dashboarden n\u00e5s via l\u00f6senordsskyddad inloggning p\u00e5 en befintlig dom\u00e4n.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Varf\u00f6r lokal AI f\u00f6r matchningen?<\/h2>\n\n\n\n<p>Det \u00e4r ett medvetet val. Kandidatprofilerna inneh\u00e5ller personuppgifter \u2014 namn, arbetshistorik, kompetenser. Att skicka den datan till ett externt API f\u00f6r varje matchningsk\u00f6rning vore on\u00f6digt ur ett GDPR-perspektiv.<\/p>\n\n\n\n<p>Den lokala modellen (Gemma2 9B via Ollama p\u00e5 ett dedikerat GPU-system) klarar matchningsuppgiften bra. Den returnerar strukturerad JSON med po\u00e4ng, motivering och identifierade gap f\u00f6r varje kombination av kandidat och jobb.<\/p>\n\n\n\n<p>OpenAI anv\u00e4nds bara f\u00f6r det steget d\u00e4r kvaliteten verkligen spelar roll \u2014 att f\u00f6rst\u00e5 och strukturera ett oformaterat PDF-CV. Den datan behandlas under ett Data Processing Agreement med EU Standard Contractual Clauses.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">GDPR i praktiken<\/h2>\n\n\n\n<p>Eftersom systemet behandlar personuppgifter tog jag GDPR-fr\u00e5gorna p\u00e5 allvar redan fr\u00e5n start, inte som en eftertanke.<\/p>\n\n\n\n<p>Konkret inneb\u00e4r det:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Samtycke<\/strong> inh\u00e4mtas explicit via en kryssruta innan n\u00e5got CV laddas upp<\/li>\n\n\n\n<li><strong>Integritetspolicy<\/strong> finns tillg\u00e4nglig direkt p\u00e5 uppladdningssidan<\/li>\n\n\n\n<li><strong>Registerf\u00f6rteckning<\/strong> (Artikel 30 GDPR) \u00e4r uppr\u00e4ttad f\u00f6r Kristedal Tech AB<\/li>\n\n\n\n<li><strong>DPA med OpenAI<\/strong> \u00e4r signerat<\/li>\n\n\n\n<li><strong>Matchningsdatan<\/strong> l\u00e4mnar aldrig infrastrukturen \u2014 den lokala AI-modellen k\u00f6rs helt p\u00e5 egna servrar<\/li>\n<\/ul>\n\n\n\n<p>Det \u00e4r ocks\u00e5 enkelt att radera en kandidat \u2014 ett enkelt terminalkommando tar bort profilen och rensar resultaten p\u00e5 b\u00e5da systemen.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Vad fungerar bra<\/h2>\n\n\n\n<p>Matchningskvaliteten \u00e4r f\u00f6rv\u00e5nansv\u00e4rt bra. Systemet hittar relevanta kopplingar mellan kandidaters bakgrund och jobbkrav, och \u00e4r tydligt med var det finns gap. Po\u00e4ngs\u00e4ttningen \u00e4r konsekvent \u2014 en junior kandidat mot ett seniorjobb hamnar l\u00e5gt, en stark matchning landar i 80\u201390-intervallet.<\/p>\n\n\n\n<p>Att ha allt p\u00e5 en enda dom\u00e4n \u2014 CV-konvertering, matchningsdashboard och integritetspolicy \u2014 g\u00f6r det enkelt att visa upp f\u00f6r kunder och kollegor utan att beh\u00f6va f\u00f6rklara flera olika system.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">N\u00e4sta steg<\/h2>\n\n\n\n<p>Det h\u00e4r \u00e4r fortfarande en prototyp. N\u00e4sta fas handlar om att f\u00f6rfina matchningspromptarna, l\u00e4gga till st\u00f6d f\u00f6r fler kandidater, och utv\u00e4rdera om systemet kan skalas upp till ett riktigt aff\u00e4rsverktyg inom rekrytering och konsultf\u00f6rmedling.<\/p>\n\n\n\n<p>Om du jobbar inom rekrytering, tech eller HR och \u00e4r nyfiken p\u00e5 hur det h\u00e4r kan se ut i er verksamhet \u2014 h\u00f6r g\u00e4rna av dig.<\/p>\n\n\n\n<p><em>Per Kristedal<\/em> <em><a href=\"mailto:per@kristedal.se\">per@kristedal.se<\/a><\/em> <em>Kristedal Tech AB<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Publicerat av Per Kristedal \u2014 Kristedal Tech AB Jag jobbar inom rekrytering och tech, och en av de mest tidskr\u00e4vande uppgifterna \u00e4r att manuellt g\u00e5 igenom konsultprofiler och matcha dem mot inkommande jobbf\u00f6rfr\u00e5gningar fr\u00e5n kunder. Det \u00e4r repetitivt, det tar tid, och det \u00e4r exakt den typ av problem som AI \u00e4r gjord f\u00f6r att [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-448","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.kristedal.se\/index.php?rest_route=\/wp\/v2\/posts\/448","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.kristedal.se\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.kristedal.se\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.kristedal.se\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.kristedal.se\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=448"}],"version-history":[{"count":2,"href":"https:\/\/www.kristedal.se\/index.php?rest_route=\/wp\/v2\/posts\/448\/revisions"}],"predecessor-version":[{"id":450,"href":"https:\/\/www.kristedal.se\/index.php?rest_route=\/wp\/v2\/posts\/448\/revisions\/450"}],"wp:attachment":[{"href":"https:\/\/www.kristedal.se\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=448"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.kristedal.se\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=448"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.kristedal.se\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=448"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}