Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions ai/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,26 @@ docker run --env-file .env -p 8000:8000 stackup-ai
- **실시간 스트리밍 음성 답변 본 구현** (RT3, `api/voice_stream.py`, `voice/stt/{live,mock_live,deepgram_live,live_factory}.py`):
FastAPI WS `/internal/voice/stream` 수신(RealTime 프록시 경유) → Deepgram Live(`deepgram_live.py`, mock fallback)로 부분/최종 자막 다운 → 발화 종료 시 메트릭 계산 후 `callback.voice` 발행. `VoiceCallbackService`/followup 무변경 재사용. 신규 의존성 `websockets`.
- 배치 음성 분석(STT/WPM/filler) 모듈은 `voice/stt/whisper_api.py`(+ Deepgram) + `voice/analysis/metrics.py`로 본 구현(`analyze.voice` consumer)
- **비개발 직군 면접 본 구현 (2026-09-29)**: 서비스 대상이 취준생 전반(개발 8 + 비개발 12 직군)인데
**평가 패널만 직군별로 확장돼 있었고 질문·꼬리질문·첫인상·문서분석 프롬프트는 "IT 직군" 페르소나로
고정**돼 있었다. 그 상태의 영업·인사 지원자 면접은: IT 렌즈로 이력서가 요약되고 → IT 채용 담당자가
→ 'CS 기초'·'기술 선택' 카테고리로 질문을 만들고 → IT 면접관이 꼬리질문을 단다. 평가만 직군 맞춤.
- **카테고리가 개발 전용인 것이 핵심**이다. 문구만 바꾸면 LLM 은 여전히 `CS_FUNDAMENTAL` 을 고른다.
`DOMAIN_KNOWLEDGE`(직무 지식·업무 도구·업계 이해)를 추가하고, 비개발이면 `CS_FUNDAMENTAL`·
`TECH_CHOICE` 를 **쓰지 말라고 명시**한다. Core 는 category 를 문자열로만 저장하므로(V9, CHECK 없음)
마이그레이션이 없고 프론트 `categoryLabel` 에 라벨만 더하면 된다.
- `job_category.py` 에 `ENGINEERING_CATEGORIES`(8개) + `is_engineering()` 을 **직군 목록과 같은 파일**에
둔다 — 목록만 늘리고 집합을 빠뜨리면 그 직군이 조용히 반대 취급을 받는다(검증에서 안 튕긴다).
- `_format_job_guide` 가 개발/비개발/혼합 3분기 블록을 만들어 `{job_guide}` 로 주입한다. 패널의
`_DOMAIN_TECH_GUIDE` 와 같은 이유로 라벨이 아니라 **무엇을 묻고 무엇을 묻지 말지**를 문장으로 준다.
직군이 비면 개발을 가정하지 않는다 — 틀렸을 때 비개발 지원자에게 기술 질문이 나가는 쪽이 더 나쁘다.
- `followup`·`self_intro` 프롬프트는 **예전부터 `{job_category}` 를 받고 있었는데 쓰지 않았다**
(페르소나만 IT 고정). 받은 값을 페르소나에 넣었다.
- `document_analysis` 는 업로드 시점이라 직군을 모른다 → 페르소나를 중립화하고 `tech_stack` 의 의미를
"개발이면 기술 스택, 아니면 그 직무의 실무 역량·도구·자격"으로 넓혔다(필드명 때문에 없는 기술을
지어내지 말라고 명시). 이게 가장 상류다 — 분석 마크다운이 RAG·질문·피드백 전부의 근거다.
- 운영 사용 현황은 아직 BACKEND 64 / FRONTEND 39 로 비개발 세션이 **0건**이다 — 라이브 회귀가 아니라
미검증 경로를 연 것이다.
- **멀티 평가위원 패널 본 구현** (`chain/feedback_generation_chain.py: PanelFeedbackGenerator`): 세션에 등장한
직군(질문 카테고리) 수만큼 기술 평가위원을 동적 생성(`_domain_specs_weighted`) + 논리·전달력 평가위원을
더해 `asyncio.gather` 로 병렬 채점 → 직군 평가는 질문 등장 빈도로 가중평균해 `technical_accuracy` 산출,
Expand Down
14 changes: 10 additions & 4 deletions ai/src/ai_server/chain/prompts/document_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
# 2단계 지향: ① 원문 근거(source_quote)와 함께 사실을 구조화 추출 →
# ② 추출 결과만 근거로 summary·markdown 생성 (환각 차단).
SYSTEM_PROMPT = (
"당신은 IT 직군 채용을 진행하는 시니어 면접관입니다. "
"지원자 자료(이력서·레포 README·기술 블로그 등)를 분석하여 면접 준비에 쓸 "
"당신은 채용 면접을 준비하는 시니어 면접관입니다. 지원자의 직군은 개발일 수도 아닐 수도 "
"있습니다 — 자료에 드러난 직무를 그대로 따르고 IT 를 가정하지 마세요. "
"지원자 자료(이력서·자소서·포트폴리오, 개발 직군이면 레포 README·기술 블로그 등)를 "
"분석하여 면접 준비에 쓸 "
"구조화된 결과를 산출하세요.\n"
"작업 순서:\n"
"1) 먼저 자료에서 사실을 구조화 추출합니다(projects·experiences·skills). "
Expand All @@ -26,10 +28,14 @@
"stack, source_quote(원문 인용).\n"
"2. experiences: 경력/활동. 각 항목에 title, detail, source_quote.\n"
"3. skills: 핵심 기술. 각 항목에 name, evidence(원문 인용).\n"
"4. tech_stack: skills/projects 에서 도출한 핵심 기술 5~15개. 영문 표기.\n"
"4. tech_stack: skills/projects 에서 도출한 핵심 역량 5~15개. 개발 직군이면 기술 스택을 "
"영문 표기로, 그 외 직군이면 **그 직무의 실무 역량·도구·자격**을 자료의 표기대로 "
"넣습니다(예: SAP, GA4, CAD, 원가관리, 노무, 계약검토, 사용자 리서치). 필드 이름이 "
"tech_stack 이라고 해서 없는 기술을 지어내지 마세요.\n"
"5. summary: 위 추출 내용만 근거로 한 2~4문장 한국어 요약.\n"
"6. markdown: 면접관이 훑어볼 한국어 마크다운. 섹션 구조는 "
"'## 개요', '## 주요 경험', '## 기술', '## 추가 메모' 사용. "
"'## 개요', '## 주요 경험', '## 기술', '## 추가 메모' 사용"
"(개발 직군이 아니면 '## 기술' 대신 '## 역량·도구'). "
"추출된 projects/experiences/skills 를 반영하되 추측은 넣지 마세요.\n"
"※ 출처 유형이 COVER_LETTER(자기소개서)이면: 기술 나열보다 **지원동기·가치관·성장 경험·"
"직무 적합성·핵심 강점/소재**를 우선 추출하세요. experiences 에 자소서 문항별 핵심 주장과 "
Expand Down
4 changes: 3 additions & 1 deletion ai/src/ai_server/chain/prompts/followup_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# Flash 모델 + 저지연 (< 3s). 답변을 4축으로 평가하고 가장 약한 축을 파고드는 꼬리질문 1개.

SYSTEM_PROMPT = (
"당신은 IT 직군 면접관입니다. 직전 질문에 대한 지원자의 답변을 평가하고, "
"당신은 {job_category} 직군의 면접관입니다. 직군이 개발 직군이 아니면 코드·기술 스택이 "
"아니라 그 직무의 실무 역량·도구·성과를 기준으로 판단합니다. "
"직전 질문에 대한 지원자의 답변을 평가하고, "
"가장 약한 부분을 파고드는 꼬리질문 1개를 한국어로 만드세요.\n"
"- 평가 항목:\n"
" - specificity (0~5): 답변에 구체적 수치/사례/기술 선택 근거가 있는가.\n"
Expand Down
26 changes: 18 additions & 8 deletions ai/src/ai_server/chain/prompts/question_generation.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
# 질문 풀 생성 프롬프트 (US-18)
# 분석된 이력서/레포 컨텍스트 + 직군 + 면접 모드 → 질문 N개.
# 분석된 지원자 자료 컨텍스트 + 직군 + 면접 모드 → 질문 N개.
#
# 페르소나를 "IT 직군"으로 고정하지 않는다. 서비스 대상이 취준생 전반(개발 8 + 비개발 12 직군)
# 인데 페르소나만 IT 로 박혀 있으면, 영업·인사·법무 지원자에게 IT 채용 담당자가 기술 스택을
# 묻는 면접이 나간다. 직군별 차이는 `_format_job_guide` 가 만든 {job_guide} 블록이 담당한다.

SYSTEM_PROMPT = (
"당신은 IT 직군 채용을 진행하는 시니어 면접관입니다. "
"지원자의 **자기소개 답변**을 1차 근거로, 이력서·GitHub 레포 분석 컨텍스트와 "
"면접 모드에 맞춰 면접 질문 풀을 생성하세요.\n"
"당신은 지원자가 지원한 직군의 채용을 진행하는 시니어 면접관입니다. "
"지원자의 **자기소개 답변**을 1차 근거로, 지원자 자료(이력서·자소서·포트폴리오, 개발 직군이면 "
"GitHub 레포) 분석 컨텍스트와 면접 모드에 맞춰 면접 질문 풀을 생성하세요.\n"
"- 실제 면접처럼, 자기소개에서 지원자가 직접 강조한 경험·역량·관심사를 우선 파고듭니다. "
"자기소개에서 언급된 키워드를 이력서/레포 자료와 교차로 연결해 구체적으로 질문하세요.\n"
"- 사실에 근거한 질문만. 자료에 없는 내용은 추측하지 않습니다.\n"
"- 자료에 명시된 기술 스택과 프로젝트 경험을 적극 인용하세요.\n"
"- 자료에 명시된 역량·도구·프로젝트 경험을 적극 인용하세요.\n"
"- **모든 카테고리(CS_FUNDAMENTAL·BEHAVIORAL 포함)를 지원자의 특정 발화·프로젝트·기술 "
"선택에 앵커링**하세요. 자료 없이 누구나 답할 수 있는 일반 개념·교과서 문제는 금지. "
"예) ✗ 'HTTP와 HTTPS의 차이는?' → ✓ '레포의 결제 API를 HTTPS로 전환할 때 인증서·핸드셰이크에서 "
"겪은 이슈는?'. 개념을 묻더라도 지원자가 실제 다룬 맥락에 붙여 묻습니다.\n"
"겪은 이슈는?' / ✗ '4대보험이란?' → ✓ '이력서의 120명 규모 입퇴사 처리에서 4대보험 취득·상실 "
"신고를 어떤 절차로 관리하셨나요?'. 개념을 묻더라도 지원자가 실제 다룬 맥락에 붙여 묻습니다.\n"
"- 카테고리는 다음 중에서 선택: CS_FUNDAMENTAL, PROJECT_DEEP_DIVE, TECH_CHOICE, "
"BEHAVIORAL.\n"
"BEHAVIORAL, DOMAIN_KNOWLEDGE. **어느 것을 쓸지는 아래 '직군 지침'이 정합니다.**\n"
"- 한국어로 작성하되 기술 용어는 영문 원어를 그대로 둡니다.\n"
"- 질문·근거 텍스트에는 마크다운 서식 기호를 쓰지 마세요 (화면이 plain text 렌더, 스트리밍 표시).\n"
"- 질문 문장은 **간결하게**: 한두 문장(대략 80자 이내)으로, 장황한 배경 설명이나 "
Expand Down Expand Up @@ -57,6 +62,10 @@
"---\n"
"{self_introduction}\n"
"---\n\n"
"직군 지침 (이 면접의 직군에 맞춘 규칙 — 위 카테고리 선택은 여기를 따릅니다):\n"
"---\n"
"{job_guide}\n"
"---\n\n"
"이번 면접의 집중 영역 (약점 집중 재도전일 때만 채워짐):\n"
"---\n"
"{focus_areas}\n"
Expand All @@ -77,7 +86,8 @@
"(각 질문이 일반질문 1개의 씨앗이 되고 뒤에 꼬리질문이 붙으므로 같은 주제 반복 금지).\n"
"2. 각 질문에 적절한 category 를 부여하고, job_category 를 직군({job_categories}) 중 "
"그 질문이 겨냥하는 **하나**로 지정합니다(직군이 하나면 모두 그 값).\n"
"3. 직군({job_categories})·면접 모드({mode}) 에 맞는 비중으로 카테고리 분배:\n"
"3. 직군({job_categories})·면접 모드({mode}) 에 맞는 비중으로 카테고리 분배. "
"**아래 비중은 개발 직군 기준이고, 비개발 직군이면 위 '직군 지침'의 대체 카테고리를 씁니다**:\n"
" - TECHNICAL: CS_FUNDAMENTAL + TECH_CHOICE + PROJECT_DEEP_DIVE 중심.\n"
" - PERSONALITY: BEHAVIORAL 중심.\n"
" - INTEGRATED: 모두 균형.\n"
Expand Down
4 changes: 3 additions & 1 deletion ai/src/ai_server/chain/prompts/self_intro_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
# 결과는 패널의 '첫인상' 평가위원 한 명으로 표시되며, 종합 점수 집계에는 포함되지 않는다.

SYSTEM_PROMPT = (
"당신은 IT 직군 면접의 **첫인상 평가위원**입니다. 지원자의 자기소개 답변 한 건만 보고 "
"당신은 {job_category} 직군 면접의 **첫인상 평가위원**입니다. 직무적합성은 그 직군의 "
"기준으로 봅니다(개발 직군이 아니면 기술 역량이 아니라 해당 직무의 실무 역량). "
"지원자의 자기소개 답변 한 건만 보고 "
"면접 도입부의 첫인상을 평가합니다.\n"
"- **기술 정확성은 평가하지 않습니다.** 오직 전달력·구조·간결성·직무적합성만 봅니다.\n"
" · 전달력/명료성: 메시지가 분명하고 자신감 있게 전달되는가\n"
Expand Down
43 changes: 43 additions & 0 deletions ai/src/ai_server/chain/question_generation_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ai_server.chain.prompts.question_generation import HUMAN_PROMPT, SYSTEM_PROMPT
from ai_server.config.settings import Settings
from ai_server.core.client import CoreClient
from ai_server.model.messages.job_category import is_engineering
from ai_server.model.messages.questions import GeneratedQuestion
from ai_server.observability.llm_logging_callback import CoreAiLogCallback

Expand Down Expand Up @@ -122,6 +123,7 @@ async def generate(
target_company_name, target_job_description
),
"focus_areas": _format_focus_areas(focus_areas),
"job_guide": _format_job_guide(job_categories),
# 비어 있으면 "(지정 없음)" — 프롬프트가 산업 지침을 무시하도록.
"industry": (industry or "").strip() or "(지정 없음)",
}
Expand All @@ -133,6 +135,47 @@ async def generate(
return result


# 직군 지침. 개발/비개발은 묻는 것이 다르고, 특히 **카테고리 자체가 개발 전용**이다
# (CS_FUNDAMENTAL·TECH_CHOICE). 이걸 주지 않으면 영업·인사 지원자에게 'CS 기초' 질문이 나간다.
# 라벨만 넘기면 LLM 이 제각각 해석하므로, 패널 평가의 `_DOMAIN_TECH_GUIDE` 와 같이
# **무엇을 묻고 무엇을 묻지 말지**를 문장으로 못 박는다.
_ENGINEERING_GUIDE = (
"개발 직군입니다. 기술 스택·설계 선택·트러블슈팅을 중심으로 묻고, "
"CS_FUNDAMENTAL·TECH_CHOICE·PROJECT_DEEP_DIVE 를 적극 활용합니다. "
"DOMAIN_KNOWLEDGE 는 쓰지 않습니다."
)
_NON_ENGINEERING_GUIDE = (
"비개발 직군입니다.\n"
"- 코드·아키텍처·기술 스택을 묻지 않습니다. **CS_FUNDAMENTAL·TECH_CHOICE 를 쓰지 마세요.**\n"
"- 대신 DOMAIN_KNOWLEDGE(직무 지식·업무 도구·업계 이해)와 PROJECT_DEEP_DIVE(담당 업무·"
"성과), BEHAVIORAL 을 씁니다.\n"
"- '역량·도구'는 그 직군의 실무를 뜻합니다 — 예: 마케팅이면 퍼포먼스 지표·GA4, 재무면 결산·"
"세무, 생산·품질이면 공정·불량률, 법무면 계약 검토, 디자인이면 사용자 리서치, "
"인사면 채용·평가·노무.\n"
"- 성과는 숫자로 확인합니다(전환율·불량률·원가 절감·처리 건수·리드타임 등).\n"
"- 자료에 GitHub 레포가 없는 것이 정상입니다. 없는 자료를 전제로 묻지 마세요."
)
_MIXED_GUIDE = (
"개발 직군과 비개발 직군이 함께 지정됐습니다. **질문마다 그 질문이 겨냥하는 직군의 규칙을 "
"따릅니다** — 개발 직군 질문에만 CS_FUNDAMENTAL·TECH_CHOICE 를 쓰고, 비개발 직군 질문에는 "
"DOMAIN_KNOWLEDGE 를 씁니다.\n\n"
"[개발] " + _ENGINEERING_GUIDE + "\n\n[비개발] " + _NON_ENGINEERING_GUIDE
)


def _format_job_guide(job_categories: list[str] | None) -> str:
cats = [c for c in (job_categories or []) if c]
if not cats:
# 직군이 비는 경로는 없지만, 비면 개발을 가정하지 않는다 — 가정이 틀렸을 때
# 비개발 지원자에게 기술 질문이 나가는 쪽이 반대보다 나쁘다.
return _NON_ENGINEERING_GUIDE
eng = [c for c in cats if is_engineering(c)]
non = [c for c in cats if not is_engineering(c)]
if eng and non:
return _MIXED_GUIDE
return _ENGINEERING_GUIDE if eng else _NON_ENGINEERING_GUIDE


def build_question_generation_chain(
settings: Settings, core_client: CoreClient | None = None
) -> Runnable:
Expand Down
22 changes: 22 additions & 0 deletions ai/src/ai_server/model/messages/job_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,25 @@
"LEGAL",
"PUBLIC",
]


# 개발 직군 집합. 위 목록과 **같은 파일**에 두는 이유는 이 파일의 존재 이유와 같다 —
# 직군을 늘릴 때 목록만 고치고 이 집합을 빠뜨리면, 그 직군은 조용히 비개발로 취급돼
# 기술 질문이 사라지거나 반대로 영업 지원자에게 CS 기초가 나간다(검증에서 튕기지 않으므로
# 아무도 모른다). `tests/test_job_category.py` 가 두 목록의 합집합을 고정한다.
ENGINEERING_CATEGORIES = frozenset(
{
"FRONTEND",
"BACKEND",
"INFRA",
"DBA",
"MOBILE",
"DATA_AI",
"SECURITY",
"QA",
}
)


def is_engineering(job_category: str | None) -> bool:
return (job_category or "").upper() in ENGINEERING_CATEGORIES
4 changes: 4 additions & 0 deletions ai/src/ai_server/model/messages/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"PROJECT_DEEP_DIVE",
"TECH_CHOICE",
"BEHAVIORAL",
# 비개발 직군용. CS_FUNDAMENTAL·TECH_CHOICE 가 개발 전용이라 그 자리를 대신한다
# (직무 지식·업무 도구·업계 이해). Core 는 category 를 문자열로만 저장하므로
# 마이그레이션이 필요 없고, 프론트는 categoryLabel 에 라벨이 있다.
"DOMAIN_KNOWLEDGE",
]
CallbackKind = Literal["POOL", "FOLLOWUP"]
GenerationStatus = Literal["OK", "FAILED"]
Expand Down
Loading
Loading