_본 글은 Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady 제출물입니다.
한 채용 공고는 '원격 유연성과 현장 협업의 조화'를 제공한다고 했습니다. 제 도구는 그것을 원격(remote)이라고 판단했습니다.
나이지리아에서 지원하는 사람에게는 사소한 실수가 아닙니다. 실제로 원격이 아닌 역할에 시간을 쏟게 될 수도 있습니다.
JobKit을 만들면서 발견한 문제가 이것만이 아니었습니다. 일부 버그는 제 Rust 규칙에 있었습니다. 다른 일부는 참고 라벨(reference labels)에 있었습니다. 즉, 도구가 올바른지 판단하기 위해 제가 준비했던 답변들입니다.
저는 JobKit을 통해 채용 공고를 빠르게 1차로 읽어낼 수 있도록 만들었습니다. 예상하지 못했던 것은 제 자신의 가정들을 확인하는 데 얼마나 많은 시간을 쓰게 될지였습니다.
모델이 제안하고, Rust가 검증합니다.
제가 구축한 것 (What I Built)
채용 공고를 제대로 읽는 데는 약 17분이 걸립니다. 그 시간의 대부분은 세 가지 지루한 질문에 할애됩니다: 실제로 원격 근무인지, 어떤 시간을 원하는지, 그리고 몇 년의 경력을 요구하는지입니다. 저는 UTC+1에 살기 때문에 이 답변들이 공고가 저를 위한 것인지 아닌지를 결정합니다.
JobKit은 이러한 1차 읽기를 수행하는 Rust CLI(Command Line Interface) 도구입니다. 공고를 입력하면 원격 근무 여부, 원하는 시간대와 연차, 언급된 기술 목록, 그리고 각 기술이 **필수(required)**인지, **선택적(optional)**인지, 아니면 단순히 **언급만 되었는지(mentioned)**를 알려줍니다. 모든 답변에는 해당 정보가 발견된 정확한 줄 번호가 함께 제공되므로 몇 초 만에 확인할 수 있습니다.
작은 로컬 모델인 Gemma 3 4B는 기술 이름만 추출합니다. 나머지 모든 것은 Rust가 검증하고 결정합니다. 이 도구는 제 노트북에서 실행되며, 저의 구직 활동이 이 기기에서 벗어나는 일은 없습니다.
데모 (Demo)
Example
코드 (Code)
Rust로 작성된, 증거 기반의 로컬 채용 공고 선별 도구.
JobKit
Rust로 작성된, 증거 기반의 로컬 채용 공고 분류 도구.
사용자는 이 도구에 채용 공고와 자신의 간략한 프로필을 제공합니다. 그러면 해당 공고가 원격 근무인지 여부, 요구하는 시간대(timezone window) 및 경력 연차, 명시된 기술 목록, 그리고 각 기술이 **필수적(required)**인지, **선택적(optional)**인지, 아니면 단순히 **언급만 되었는지(just mentioned)**를 알려줍니다. 모든 답변에는 해당 정보가 발견된 공고의 정확한 줄 번호가 함께 제공되어 몇 초 만에 확인할 수 있습니다.
작은 로컬 모델(Ollama를 통한 Gemma 3 4B)은 기술을 명시하는 역할만 하며, 나머지 모든 것은 Rust가 검증하고 결정합니다. 아무것도 기기 외부로 전송되지 않습니다.
모델이 제안하고, Rust가 검증한다.
작동 방식 (How it works)
- 규칙 (모델 미사용). 단순한 정규표현식(regex) 규칙을 사용하여 원격 근무 상태, 최소 경력 연차, UTC 시간대 창(timezone windows), 그리고 근로 허가 또는 거주지 관련 문장을 읽어 플래그를 지정합니다. 원격 근무 상태는 세 가지 상태를 가집니다:
true, false (명시적으로 비원격, 현장 출근, 하이브리드 또는 대면), 또는 알 수 없음(unknown).
- 모델. Gemma는 다음을 나열합니다...
View on GitHub
구축 과정 (How I Built It)
모델: Gemma 3 4B (오픈 웨이트), WSL 환경의 Ollama를 통해 로컬에서 실행.
ollama ps
언어: Rust, regex, serde, 및 ureq를 사용.
AI 어시스턴트 활용: 디버깅에는 Claude의 도움을 받았습니다. 저는 코드를 실행하고, 공고들을 수집하여 직접 라벨링했으며, 결과를 평가하고 무엇을 수정할지 결정했습니다.
모델이 기술의 진위 여부를 최종적으로 판단하지는 않습니다. Rust가 공고 자체를 확인합니다:
만약 Rust가 공고에서 일치하는 용어를 찾지 못하면, 해당 기술은 거부됩니다. 만약 발견한다면, 사용자가 검사할 수 있도록 증거와 함께 그 기술을 유지합니다.
이것이 핵심 아이디어이며, 간단히 말해 다음과 같습니다: 모델이 제안하고; 결정론적 코드(deterministic code)가 증거를 확인합니다.
규칙은 명확합니다: 모델이 제안하고, Rust가 검증한다.
- 모델이 스킬을 명명한다.
- Rust는 스킬이 공고에 전체 용어로 나타나고(Postgres와 PostgreSQL 같은 별칭 허용), 해당 라인을 증거로 첨부할 경우에만 수락한다. 공고에서 언급되지 않은 스킬은 '창작된 것'으로 거부된다.
- Rust는 수락된 각 스킬을 해당 제목 및 주변 문구("Nice to have", "preferred", "Good to have:")로부터 분류한다.
- 단순 규칙(Plain rules)은 원격 근무 상태, 연차, 시간대 창을 읽고 작업 허가 문장을 플래그 지정한다. 불확실한 모든 것은 확신에 찬 답변 대신 **검토(Review)**로 처리된다.
무엇이 잘못되었나 (흥미로운 부분)
- 첫 번째 검사기가 90개 항목 중 45개를 거부했다. 대부분은 형식 문제 때문이었다. 나는 모델에게 인용구를 정확히 복사하도록 요구했는데, 4B 모델로는 이를 신뢰성 있게 수행할 수 없었다. Rust가 증거를 찾도록 허용한 것이 문제를 해결했다. 내가 처음 분석한 9개 공고에서 모델은 존재하지 않는 스킬을 명명한 적이 없었다(73개 중 0개). 2.* 나의 규칙은 원격 근무에 대해 78%로 시작했지만, 연차는 33%, 시간대는 11%였다. 그 원인은 "1년"과 일치하지 않는 패턴, 회사 역사를 요구 사항으로 읽는 것("~2 years since we started"), 그리고 "원격 불가(no remote)\
| 지표(Metric) | 9개 공고 튜닝 세트 (합성 공고 1개 포함) | 8개 비참여 공고(held-out postings) |
|---|
| 원격 분류(Remote classification) | 100% (78%에서 상승) | 62%, 이후 수정 후 75% |
| ... | | |
비참여 공고 열은 진실한 수치이며, '수정 후' 숫자는 더 이상 비참여 공고가 아닙니다. 왜냐하면 저는 그 실패 사례들을 보고 규칙을 패치했기 때문입니다. 'Loose(느슨함)'는 모든 언급이 선택 사항이 아니라면 기술이 필요함을 의미합니다. 'Strict(엄격함)' 역시 요구사항 문구 또는 요구사항 제목이 필요하며, 따라서 Backend: Go, Postgres와 같은 스택 목록은 언급된 것으로 간주됩니다. 이는 재현율(recall)을 정밀도(precision)와 교환하는 것입니다.
Open Innovation은 왜 중요한가?
- 내 데이터는 내 것: 구직 활동에는 나의 이력서, 거주지, 그리고 내가 관심을 갖는 회사가 포함됩니다. 오픈 가중치 모델(open-weight model)을 사용하면 그 어떤 것도 내 기기를 벗어나지 않습니다.
- 실패 과정을 정확히 파악할 수 있었다: 소형 모델의 모든 실패 사례를 점검 항목으로 전환했습니다: 조작된 용어, 잘못 형식화된 JSON, 반복되는 키. 이것이 제가 튜닝한 세트에서 조작된 기술이 통과하지 못한 이유입니다.
- 무료로 재실행할 수 있었다: 저는 전체 세트를 여러 번 평가했으며, 모델 출력을 캐시했기 때문에 재점수(re-scoring)는 몇 초 만에 이루어지며 결과가 재현 가능합니다.
- 주장할 수 없는 것: 저는 호스팅된 모델과 비교하지 않았습니다. 후자는 아마도 더 긴 기술 목록을 추출했을 것입니다. 제 결과는 작은 로컬 모델이 주변 검증 시스템(verification)을 구축한다면 사용 가능하다는 것입니다.
게으름의 보상 (The Lazy Payoff)
- 수동으로: 공고 5개에 83분, 개당 약 17분 소요.
- JobKit 규칙만 사용 시: 공고당 0.08초.
- 모델을 사용할 경우: 공고당 43~143초 (8개 평균으로 96초). 느리지만, 저를 기다려주며 저는 그것을 기다릴 필요가 없습니다. 느리고 꾸준하게(Slow and steady).
- 도구 사용 시 나의 시간: 동일한 공고 5개를 읽고 확인하는 데 약 2분 소요.
- 다음 단계 (추정): 비참여 공고 8개 중 3개가 규칙만으로는 처리하기 어려운 건너뛰기(hard Skips)였습니다. 이 경우 모델을 생략하면 모델 시간의 약 35%를 절약할 수 있습니다. 아직 이것은 구축하지 않았습니다.
한계점 (Limits)
- 내가 레이블링한 16개의 실제 공고와 하나의 테스트 케이스가 있으며, 이 과정에서 발견한 오류들이 포함되어 있다.
- Gemma 4B는 긴 기술 목록을 충분히 추출하지 못한다(under-extracts).
- 이 도구는 '분산 시스템(distributed systems)'과 같은 개념이 아니라 용어 자체를 일치시킨다. "공고에 언급됨(Mentioned in the posting)"은 "필수 요건(required)"을 의미하지 않는다.
- 정형 문구(boilerplate)가 플래그를 발생시킬 수 있다. 급여 투명성 관련 문장이 위치 플래그를 발생시켰다.
- 나는 지원할지 건너뛸지(apply-or-skip)에 대한 판단이 맞았는지 여부를 측정하지 않았다.
수상 부문 (Prize Categories)
Gemma의 최적 활용 사례(Best Use of Gemma). Gemma 3는 추출 단계에서 로컬로 실행되며, 전체 검증 계층은 4B Gemma가 실패하는 방식 때문에 존재한다.
향후 과제 (What's Next)
보류된(held-out) 8개의 공고 중 3개가 규칙만으로 평가했을 때 어려운 건너뛰기(hard Skip) 사례였다. 만약 JobKit이 해당 케이스에 대해 모델 추론(model inference)을 안전하게 건너뛸 수 있다면, 모델 실행 시간을 약 35% 줄일 수 있을 것으로 추정한다. 아직 이 최적화는 구현되지 않았다.
내가 JobKit을 구축한 이유는 구직 활동 자체가 이미 충분히 많은 시간이 걸리는데, 적합하지 않은 공고를 반복적으로 디코딩하는 과정이 필요하기 때문이다. 즉각적인 목표는 첫 번째 스크리닝 단계를 더 빠르고 검증하기 쉽게 만드는 것이지, 작은 모델이 모든 지원 결정을 대신 내릴 수 있다고 가정하는 것이 아니다.
모델이 제안하고, Rust가 검증한다. 다음 단계는 신뢰도를 떨어뜨리지 않으면서 그 과정을 비용 효율적으로 만드는 것이다.