interviewstreet/hiring-agent는 HackerRank(인터뷰스트리트)가 공개한, 지원자의 이력서 PDF를 읽어 점수로 바꾸는 AI 에이전트입니다. PDF를 구조화된 JSON으로 파싱하고, GitHub 실데이터(저장소·스타·커밋 수)로 주장을 검증한 뒤, 고정된 채점 기준(rubric)에 따라 항목별 점수 + 근거 + 가점/감점을 LLM이 매깁니다. 핵심은 두 가지 — 로컬에서 무료로(Ollama) 돌아가 이력서가 밖으로 안 나가고, 이름·성별·학교·학점·지역으로는 절대 점수를 매기지 않도록 공정성을 코드와 프롬프트에 박았다는 점. (저장소: interviewstreet/hiring-agent · 언어 Python 84% + Jinja 16% · ★ 약 1.2천 · 라이선스 MIT · HackerRank)
"키워드 매칭 ATS가 아니다. 이력서를 '이해'하고, GitHub로 '검증'하고, 근거와 함께 '채점'한다."
채용에서 가장 고된 일 중 하나가 수천 장의 이력서를 1차로 걸러내는 것입니다. 사람이 일일이 깊게 읽기는 불가능하고, 기존 ATS(지원자 추적 시스템)는 키워드만 맞춰 거르니 조잡합니다. hiring-agent는 그 사이를 메웁니다 — 이력서를 의미로 이해해 프로젝트의 복잡도까지 보고, "GitHub에 그 프로젝트가 진짜 있고, 커밋을 몇 개나 했는지" 실데이터로 확인한 다음, 정해진 100점 만점 기준표에 따라 항목별 점수 + 그 점수의 증거 문장을 남깁니다.
resume_evaluation_criteria.jinja라는 프롬프트 파일에 자연어로 적혀 있습니다 — 오픈소스 기여 35점, 개인 프로젝트 30점, 실무 경험 25점, 기술 폭 10점, 가점 최대 20점. 기준표 자체가 곧 프롬프트라는 점이 이 프로젝트의 묘미입니다.전체는 파이썬 CLI 스크립트 하나로 돕니다 — python score.py 이력서.pdf. 웹 서버도, 데이터베이스도, 프런트엔드도 없습니다. 기본 LLM은 로컬 Ollama(예: gemma3:4b)라 인터넷 없이, 무료로, 이력서를 내 컴퓨터 밖으로 내보내지 않고 채점할 수 있고, 설정만 바꾸면 클라우드 Gemini로도 돕니다. 이 문서는 사용법보다 그 안의 설계 — "PDF를 어떻게 믿을 수 있는 JSON으로 바꾸나", "어떻게 LLM의 산수를 안 믿고 코드로 점수를 가두나", "공정성을 어떻게 강제하나"를 파고듭니다.
"채용 테크 회사가 '자기들이 지원자를 채점하는 실제 로직'을 통째로 오픈소스로 공개했다."
AI로 이력서를 거르는 시도는 많았지만 악명도 높습니다 — 과거 아마존이 만든 이력서 AI는 여성 지원자에게 불리한 편향이 드러나 폐기됐습니다. 또 지원자는 이력서에 과장을 적기 마련이라, 글자만 믿으면 '튜토리얼 따라 한 프로젝트'와 '진짜 오픈소스 기여'를 구분하지 못합니다. hiring-agent는 이 두 난제를 정면으로 겁니다.
| 관점 | hiring-agent | 흔한 대안 (수동 심사 · 키워드 ATS) |
|---|---|---|
| 이해 방식 | 의미 + 복잡도까지 LLM이 이해 | 키워드 일치 여부만 |
| 주장 검증 | GitHub 실데이터(커밋·기여자 수)로 대조 | 이력서 글자를 그대로 신뢰 |
| 공정성 | 이름·성별·학교·학점·지역 사용 금지를 명시 | 심사자/필터에 따라 들쑥날쑥 |
| 투명성 | 점수마다 근거(evidence) 문장 + 오픈소스 프롬프트 | 왜 떨어졌는지 알기 어려움 |
| 프라이버시 | 로컬 Ollama면 완전 오프라인 | 클라우드 전송이 흔함 |
LLM은 그럴듯하게 점수를 매기지만 산수를 틀리고, 시키지 않은 기준(학교·이름)에 은근히 영향받습니다. 점수표 합계가 100을 넘거나, 자기 저장소뿐인데 오픈소스 점수를 후하게 주는 식의 사고가 납니다.
hiring-agent는 공정성·상한 규칙을 시스템 메시지와 기준표 프롬프트 양쪽에 반복해 넣고, 그 위에 코드로도 각 항목 점수를 min(점수, 상한)으로 자르고 총점을 120으로 클램프합니다. "자기 저장소만 있으면 오픈소스 점수는 10점 이하"처럼 구조적 규칙을 GitHub 데이터로 강제하기도 합니다. 모델의 출력을 결코 그대로 믿지 않는다는 태도가 일관됩니다.
그리고 결정적으로 — 이걸 만든 곳이 HackerRank입니다. 기술 채용·코딩 테스트 플랫폼을 운영하는 회사가 '지원자를 어떻게 점수화하는지'의 실제 기준을 통째로 공개한 셈이라, 채용 현업과 학습자 모두에게 화제가 됐습니다. 활동도 활발해 별 약 1.2천에 포크 353개, 열린 PR이 89개에 이릅니다(사람들이 직접 고쳐 보며 기여 중).
"무거운 AI 프레임워크가 하나도 없다. 파이썬 표준 + PyMuPDF + Pydantic + Jinja2 + Ollama가 전부다."
의외로 단출합니다. LangChain·LlamaIndex·벡터 DB·임베딩이 전혀 없습니다(파일명이 pymupdf_rag.py지만 실제로는 PDF→마크다운 변환일 뿐, 의미 검색 RAG는 어디에도 없습니다). 무게중심은 'PDF를 믿을 수 있는 JSON으로 바꾸기'와 '스키마로 출력 강제하기'에 있습니다.
| 기술 | 역할 | 한 줄 설명 |
|---|---|---|
| Python 3.11+ | 전체 언어 | 웹 프레임워크 없음. python score.py <pdf>로 도는 순수 CLI. |
| Ollama 0.5 | 기본 LLM(로컬) | gemma3:1b/4b/12b 등. 컨텍스트 32K로 강제. 오프라인·무료. |
| google-generativeai 0.4 | 대체 LLM(클라우드) | 설정만 바꾸면 Gemini로 전환. 응답을 Ollama 모양으로 변환해 코드 공통화. |
| 기술 | 역할 |
|---|---|
| PyMuPDF 1.26 + pymupdf4llm 0.0.27 | PDF를 마크다운으로 변환(제목·표·링크 보존). 원시 PDF를 바로 LLM에 던지지 않는 게 핵심. |
| Pydantic 2.11 | 스키마의 심장. JSON 이력서 구조와 채점 결과(EvaluationData)를 정의하고, .model_json_schema()를 LLM에 '출력 형식'으로 넘긴다. |
| Jinja2 3.1 | 모든 프롬프트를 .jinja 템플릿으로(총 10개). 기준표·시스템 메시지가 코드가 아니라 템플릿. |
| requests 2.32 | GitHub API 호출(GITHUB_TOKEN으로 레이트리밋 60→5000/시). |
format=으로 넘겨, 반드시 그 모양의 JSON만 내놓도록 강제합니다. 받은 뒤엔 다시 그 클래스로 만들어 보며 한 번 더 검증합니다(생성 때 1번, 검증 때 1번 — 이중 안전장치). Field(ge=0, le=20)처럼 점수 상한 같은 업무 규칙도 스키마 안에 박아 둡니다."곧게 흐르는 6단계 파이프라인. 그 한가운데 'GitHub로 검증하고 LLM이 고르는' 에이전트 매듭이 있다."
한 바퀴를 풀면 이렇습니다. ① PDF를 곧장 LLM에 던지지 않고 먼저 마크다운으로 바꿉니다(제목·표 구조가 살아 더 잘 읽힘). ② 한 번에 "이력서 전체 파싱해"라고 안 하고, 6개 섹션을 각각 좁은 프롬프트로 따로 뽑습니다(작은 로컬 모델에서 훨씬 안정적). ③ 여기가 에이전트의 핵심 — 이력서 속 GitHub 주소를 따라가 실제 커밋·기여자 수를 확인하고, 그 정보를 보고 평가에 쓸 저장소 7개를 LLM이 직접 고릅니다. ⑤ 마지막에 공정성 규칙(시스템) + 기준표(유저)로 채점하되, ⑥ 점수는 코드가 다시 상한으로 자릅니다.
| 항목 | 만점 | 무엇을 보나 |
|---|---|---|
| open_source | 35 | '남의' 프로젝트에 한 기여. 자기 저장소만이면 10점 이하로 제한. |
| self_projects | 30 | 개인 프로젝트의 복잡도. 튜토리얼/CRUD 앱은 상한·감점. |
| production | 25 | 실무·인턴·창업 경험. 창업자/초기 엔지니어 가점. |
| technical_skills | 10 | 기술·언어의 폭. |
| bonus_points | +20 | GSoC +5, 창업 +3~5, 포트폴리오 +2, 블로그 +1~3 등. |
| deductions | − | 링크 없는 프로젝트 −3~5, 전부 튜토리얼이면 −2 등. |
LLM에게 상한을 알려 주지만, score.py가 다시 한 번 각 항목을 min()으로 자르고 총점도 120으로 클램프합니다. "알려 주되, 믿지 않고, 코드로 강제"하는 방어적 태도입니다.
# 각 항목 점수를 상한으로 자른다 — LLM이 넘겨도 무효
category_score = min(category_data["score"], category_data["max"])
...
max_possible_score = max_score + 20 # 100 + 가점 20
if total_score > max_possible_score:
total_score = max_possible_score # 총점도 가둔다
편향을 막는 규칙이 기준표 프롬프트에 대문자로 못 박혀 있습니다. 그대로 베껴 쓸 만한 '편향 완화 프롬프트'의 예시입니다.
LLMProvider라는 약속(Protocol)을 두고 OllamaProvider·GeminiProvider가 각각 .chat()을 구현합니다. Gemini 응답을 Ollama 모양({"message":{"content":…}})으로 바꿔 주어, 나머지 코드는 어떤 제공사든 똑같이 다룹니다.
"폴더가 거의 없는 평평한 구조. 파일 이름이 곧 파이프라인 단계다."
| 파일 | 역할 |
|---|---|
| score.py | 전체를 지휘. PDF 경로를 받아 ①~⑥을 차례로 부르고, 점수를 코드로 가두고, 보고서를 찍는다. |
| evaluator.py + criteria.jinja | 채점의 두뇌. 91줄의 코드 + 기준표 프롬프트가 점수를 만든다. |
| github.py + selection.jinja | 에이전트 매듭. 실데이터 수집 + '상위 7개' 결정. |
| models.py | 스키마 + 제공사 Strategy. 업무 규칙이 Field 제약으로 들어 있다. |
"'문서를 믿을 수 있는 JSON으로 바꾸는 법'과 '공정한 LLM 채점 설계'를 통째로 배울 수 있다."
원시 PDF를 바로 LLM에 던지지 않고 마크다운으로 변환한 뒤(제목·표·링크 보존), 한 번에 다 파싱하지 않고 6개 섹션을 각각 좁은 프롬프트로 뽑습니다. 작은 모델에서 안정적인, 모든 '문서→JSON' 작업에 재사용 가능한 패턴입니다.
LLM 출력을 (a) JSON 스키마로 강제 생성하고, (b) Pydantic 인스턴스화로 재검증하고, (c) 점수는 코드로 상한 클램프합니다. 세 겹의 안전장치 — LLM을 쓰는 어떤 프로덕션 코드에도 적용할 교훈입니다.
# 채점: 스키마로 형식을 강제하고, 받은 뒤 다시 검증
kwargs = {"format": EvaluationData.model_json_schema()}
response = self.provider.chat(**chat_params, **kwargs)
evaluation_data = EvaluationData(**json.loads(response_text))
"이름·성별·학교·학점·지역으로 점수를 매기지 말라"를 시스템 + 유저 프롬프트 양쪽에 반복해 넣는, 구체적이고 베껴 쓸 수 있는 편향 완화 사례. "Google Summer of Code와 Girl Script Summer of Code를 구분하라"처럼 미묘한 함정까지 다룹니다. (비판적으로 볼 점: 기준표가 GSoC·창업·1000★ 저장소를 후하게 보고 튜토리얼/CRUD를 깎는 등 '가치 판단'을 담고 있어, GitHub 활동이 없는 지원자에게 불리할 수 있습니다 — 좋은 토론거리.)
자기 보고한 프로젝트를 실제 커밋·기여자 수로 검증해, 자기 저장소(self_project)와 진짜 오픈소스 기여(open_source)를 가릅니다. "기여자가 2명 이상이면 오픈소스"라는 단순하지만 효과적인 휴리스틱과, "내 커밋 4개 미만 저장소는 절대 선택 금지"라는 하드 게이트가 좋은 예입니다.
"파이썬 3.11과 LLM 백엔드 하나면 끝. 도커도, 서버도 없다."
| 항목 | 요구사항 |
|---|---|
| 언어 | Python 3.11+ (.python-version은 3.11.13 고정) |
| LLM (택1) | Ollama(로컬·기본): ollama serve + ollama pull gemma3:4b(저사양 gemma3:1b) / 또는 Gemini(클라우드): GEMINI_API_KEY |
| 환경변수 | LLM_PROVIDER(ollama/gemini), DEFAULT_MODEL, GEMINI_API_KEY(택1 시), GITHUB_TOKEN(선택 — 레이트리밋 상향) |
| 설치 | pip install -r requirements.txt + cp .env.example .env. 도커·서버·uvicorn 없음. |
"내 이력서로 돌려 보기 → 기준표 바꿔 보기 → 검증 로직 뜯어보기 순으로."
Ollama를 깔고 gemma3:4b를 받은 뒤 python score.py 내이력서.pdf를 실행합니다. 항목별 점수와 근거 문장이 어떻게 나오는지, 같은 이력서를 모델만 바꿔(1b↔4b↔12b) 돌리면 점수가 얼마나 흔들리는지 관찰하세요.
prompts/templates/resume_evaluation_criteria.jinja를 열어 가중치(예: 오픈소스 35 → 25)나 가점 항목을 바꾸고, 같은 이력서가 어떻게 다르게 채점되는지 비교합니다. '기준표=프롬프트'의 힘을 체감하는 실습입니다.
python github.py로 특정 사용자의 저장소 분류·커밋 수·상위 7 선택이 어떻게 동작하는지 봅니다. "기여자 수 > 1 → 오픈소스" 휴리스틱이 실제 계정에서 잘 맞는지, 어디서 어긋나는지 점검해 보세요.
이름·학교만 다르게 한 '쌍둥이 이력서'를 여러 개 만들어 점수가 흔들리는지 편향을 감사해 보고, 별도 작은 프로젝트에서 Pydantic .model_json_schema()로 LLM 출력을 강제하는 패턴을 직접 재현해 봅니다.
"문서 파싱 → 구조화 출력 → LLM 채점 설계 → 공정성/평가 순으로."
| 주차 | 주제 | 학습 자료 / 키워드 |
|---|---|---|
| 1주차 | PDF/문서 파싱 | PyMuPDF, pymupdf4llm, PDF→마크다운, 표/링크 보존 |
| 2주차 | 구조화 LLM 출력 | Pydantic, JSON 스키마, function calling, format= 강제 |
| 3주차 | 프롬프트 템플릿화 | Jinja2, 시스템/유저 메시지 분리, 기준표=프롬프트 |
| 4주차 | 로컬 LLM 운영 | Ollama, 모델별 온도/컨텍스트, 제공사 추상화(Strategy) |
| 5주차 | 외부 데이터로 검증 | GitHub REST API, 레이트리밋, 휴리스틱 분류 |
| 6주차 | 평가·공정성 | 루브릭 설계, 편향 완화 프롬프트, 편향 감사(audit) 방법 |
"문서에 나온 용어를 한자리에."
| 용어 | 의미 |
|---|---|
| ATS | Applicant Tracking System(지원자 추적 시스템). 채용에서 이력서를 모으고 거르는 소프트웨어. 보통 키워드 매칭에 의존. |
| 루브릭(rubric) | 무엇을 보고 몇 점을 주는지 적은 채점 기준표. 여기선 프롬프트 파일. |
| Pydantic | 데이터 모양을 클래스로 정의·검증하는 파이썬 라이브러리. JSON 스키마를 뽑아 LLM에 출력 형식으로 넘긴다. |
| 구조화 출력 | LLM이 자유 문장이 아니라 정해진 JSON 모양으로만 답하게 강제하는 것. |
| Ollama | 내 컴퓨터에서 오픈소스 LLM을 돌리는 도구. 무료·오프라인. |
| RAG | Retrieval-Augmented Generation(검색 증강 생성). 외부 자료를 찾아 LLM에 보태는 기법. 이 프로젝트엔 실제로는 없음(파일명만 그렇게 보임). |
| 편향(bias) 완화 | 이름·성별·학교 등 무관한 속성이 점수에 영향을 못 주게 막는 설계. |
| 클램프(clamp) | 값을 정해진 범위로 자르는 것(예: 점수를 0~상한으로). |