문제와 극복 — 안 되던 것을 어떻게 풀었나
최종 갱신 2026-09-28 — GPU 수치의 출처(09-17 벤치)와 운영 경로를 구분해 정정.
10주 동안 실제로 막혔던 것과 그것을 어떻게 넘었는지의 기록이다. 증상 → 원인 → 조치 → 결과(수치) 순으로 쓴다. 설계가 아니라 사고에서 나온 장치가 대부분이며, 근거 문서는 각 항목 끝에 링크했다. 기준 2026-09-22.
A. AI 모델 — 학습하고, 같은 자로 재고, 근거로 접었다
A-1. 학습 손실은 떨어지는데 도구 호출은 전멸
- 증상: Qwen3-8B QLoRA 학습에서
eval_loss가 0.339 → 0.074 로 교과서처럼 떨어졌는데, 아르 도구 호출 정확도는 6/23(26.1%). 대부분<think>안에서 “search_applications 를 불러야 한다” 고 추론까지 하고 실제 호출을 내지 않았다. - 원인: 177개 샘플이 거의 같은 7,347자 시스템 프롬프트를 공유해, 3 epoch 동안 프롬프트를 외우는 것으로 손실이 떨어졌다. 정작 배워야 할
<tool_call>은 전체 토큰의 1.8%라 기울기가 거의 닿지 않았다. “손실이 낮다 = 무언가를 잘 배웠다, 그런데 그게 우리가 원한 것이 아니었다.” - 조치: 손실이 프롬프트에 쏠리는 구조를 교정하고(대상 토큰 비중 조정·데이터 구성 변경), 같은 채점기로 v6·v7·v8·v9 를 반복 측정.
- 결과: v9 17/23 = 73.9%. 실패 6건 중 5건은 UX상 정상(확인 문구·안전한 다단계) → 실질 ~95%+. Qwen 재학습 계획·모순 분석
A-2. “학습까지 했는데 왜 Claude 냐” — 같은 자로 재고 운영 리스크로 결정
- 문제: 자체학습 어댑터를 심사 서빙에 쓸지 클라우드 API 를 쓸지. 감이 아니라 근거가 필요했다.
- 조치: 동일 케이스 23건 · 동일 채점기(
judge.py: 도구 이름 · no-tool 위반 · 확인 문구 · 인자 · 응답)로 세 모델을 측정하고, pass/fail 너머로 판정 항목별 · 유형별 · 실패의 질 · 요약 어댑터 정성 품질까지 분해했다.
| Qwen 학습 전 (09-11) | Claude Haiku 4.5 (09-12) | Qwen v9 (09-17) | |
|---|---|---|---|
| 도구 호출 정확도 | 26.1% (6/23) | 69.6% (16/23) | 73.9% (17/23) |
| 도구 이름 일치 | 26% | 70% | — |
| no-tool 위반 없음 | 100% | 96% | — |
| 위험한 실패(임의 실행·지어낸 도구) | 0건 | 0건 | 0건 |
| 호출당 비용 | $0 + GPU $0.647/h (서울) | $0.0075 (실사용) | $0 + GPU |
| 콜드 스타트 | 78초 | — | 78초 |
- 결과: 정확도는 v9 가 Claude 와 동급. 그러나 갈린 곳은 정확도가 아니었다 — 요약 어댑터의 경력 연수 오차(3년→5년) · 프롬프트 예시 숫자 베끼기 · JSON 따옴표 깨짐 · 콜드 78초 · 동시 접속 8명에 STT 대기 15→42초. 심사 직전에 이런 오류가 누적돼 심사 서빙은 클라우드 + Claude 로 확정, Qwen 은 비용 절감 R&D 자산으로 보존. 채점기가 Qwen gold 기준이라 편향이 있음을 문서에 명시했다(상대 궤적만 유효). 정량·품질 비교 보고서 · PDF · 비용 비교
A-3. 서류 점수가 전원 64~65점으로 수렴
- 증상: 온프레미스 요약 재생성 시 모든 지원자가 요건 70·우대 40/50·인재상 60 — 변별력 상실. AWS(Claude)에선 정상.
- 원인:
chain_evaluate.v2프롬프트의 출력 예시 숫자 70/40/60 을 소형 모델이 그대로 베꼈다. Claude 는 안 베껴서 드러나지 않았다. - 조치:
v3프롬프트 — 예시 숫자 제거, 채점 구간표(85~100/65~84/…)와 절차 명시. - 결과: 실측 85·85·40·15 로 갈렸고 Claude 78·86·44 와 같은 방향. 종합보고 §2-3
A-4. 요약 JSON 이 깨져 점수 NULL
- 증상: 56명 재생성 중 2명 서류점수 NULL. 소형 모델이 문자열 안에 이스케이프 안 된 따옴표(
"…만든다"는 요구…")를 써서 파싱 실패. Ollamaformat스키마도 못 막았다. - 조치: 파서에 문자열 내부 따옴표 복구 폴백 추가 — 정상 JSON 은 그대로.
- 결과: 56/56 파싱 성공.
A-5. 아르가 채용과 무관한 질문에 답을 다 해 버림
- 증상: “안드레 카파시가 누구야” 에 Claude 가 인물 설명을 생성한 뒤 거절 문구를 덧붙임 — 토큰 낭비 + 무관 답 유출.
- 원인: 거절 규칙이 “정치·연예·주식” 처럼 카테고리 열거식이라 인물·상식 유형은 모델이 확률적으로 판단.
- 조치: 판단 기준을 “답의 근거가 우리 데이터냐” 로 재작성 — 외부 일반 지식은 내용 답변 없이 한 문장 거절, 이력서·자소서 속 인물·프로젝트는 원문 조회해 정상 답변(PR #353).
- 결과: 무관 질문의 출력 토큰 최소화, 이력서 기반 질문은 유지.
A-6. 인사·능력 질문마다 LLM 을 태워 돈이 나감
- 증상: “안녕”, “뭘 할 수 있어” 같은 정적 발화가 매번 LLM 호출 → 호출당 ~$0.02.
- 조치: 규칙 의도 라우터에 캔드 응답 추가(
^…$로 묶어 실제 요청이 붙으면 LLM 으로). 지원자 FAQ 도 인사·연봉·가능성·감사는 캔드. - 결과: 기본 질문 $0. 실측 3건 대화 8.4K 토큰 $0.04 → 기본 질문 구간 무비용.
B. 음성 · 실시간 면접
B-1. 면접 답변이 “자기 멋대로” 저장됨 — 가장 오래 헤맨 건
- 증상: 첫 답변만 멀쩡하고 나머지는 엉뚱하게 저장. AWS 에선 정상.
- 1차 오진: STT 엔진(CPU whisper) 문제로 의심 → GPU 시도(이미지에 CUDA 런타임 없어 실패) → OpenAI 키 필요 여부 검토.
- 실제 원인: 엔진이 아니라 컨테이너 재기동 타이밍. STT 설정을 바꾸며 면접 진행 중 lie-detection 을 4번 재기동했고, 답변 음성은 파일이 아니라 메모리에만 있어 재기동 시 그때까지의 발화가 사라졌다.
- 검증: 한국어 TTS 답변 4종을 실제 프로토콜(PCM 50ms +
end)로 흘려 넣는 재현 클라이언트 작성 → 직결·터널 경유 12/12 정확 전사. 엔진은 정상이었다. - 결과: “면접 중 재기동 금지” 규칙화. 오진으로 바꿨던 타임아웃 원복. 종합보고 §2-2
B-2. 무음에 말을 지어내는 STT
- 증상: whisper-1 API 가 무음·잡음에 유튜브 자막 문구(“다음 영상에서 만나요”)를 답변으로 저장(09-15 실측). 로컬 faster-whisper 도 무음 3초를 “감사합니다.” 로.
- 조치: API 경로는 조각별
no_speech_prob ≥ 0.6폐기, 로컬 경로는 VAD 필터 + 이전 문맥 미참조. - 결과: 무음 구간이 답변으로 저장되지 않는다. 전사 정확도 자체는 두 엔진 비슷(재현 12/12).
B-3. CPU 서버에서 44초 발화가 180초를 넘김 → API 전환 · GPU 는 벤치까지
- 증상: t3.large(2 vCPU)에서 로컬
large-v3-turbo가 44초 발화를 180초 안에 못 끝내고, 밀린 시간이 뒤 답변까지 연쇄로 죽임. 8명 동시 부하에서 STT 대기 15초 → 42초. - 조치: ADR-0038 — 실시간 전사 기본을 OpenAI API 로. 병행해 lie-detection GPU 이미지(
Dockerfile.gpu, torch cu124 + ctranslate2 CUDA) 구성. - 결과: 09-17 GPU 서버(Tesla T4) 벤치에서 전사 7.5초 → ~1초, STT 275ms/3초 · ViT 75ms/crop, 8명 동시에도 대기줄 0. 정확도 CPU 와 동일. 다만 운영에서는
STT_BACKEND가 빈 값이라 업로드 답변은 whisper-1 API, 실시간 면접 전사는 CPU 로컬 faster-whisper 로 돌았고(ADR-0038 「같은 변수, 두 해석」), GPU 가동은 10-14~27 풀가동 계획으로 남았다. 기술 해부 노트
B-4. STT 컨테이너 OOM — 메모리를 5번 올려도 죽음
- 증상: 메모리 상향 5회 + 인스턴스 이관까지 갔는데 계속 OOM.
- 원인: 코드 기본값 하나 — faster-whisper “default” 가 CPU 에서
float32로 잡혀 3.0~3.8GB peak(dmesg 실측). - 조치:
int8명시. - 결과: 사용량 223MB(1/4) · 속도 2.3배 · 한국어 WER 손실 무시.
C. 인프라 · 운영 — 누가 빠져도 굴러가게
C-1. 서비스가 한 사람 계정에 묶여 있었다
- 문제: AWS(루트·IAM 하나)·GitHub org 오너·Vercel·도메인·Anthropic/OpenAI 키·서버
.env의 AWS 키까지 전부 한 개인 명의. 그 계정에 문제가 생기면 배포·메일·업로드가 멎고 아무도 고칠 수 없는 구조. - 조치(ADR-0025): 권한을 사람이 아니라 역할에 — 콘솔용 IAM 유저 · 서버 전용 IAM 유저 분리, 시크릿 전부 재발급(개인 키 09/02 폐기 → 팀 키로 교체, 09-22 유효 확인), 저장소를
Seuk-Team조직으로 이관, 인프라를 새 계정으로 이전(09-04). - 결과: 개인 키가 서버에서 사라졌고, 유출 시 재발급 범위가 서버 유저 하나로 좁아졌다. 자동 CD 로 “push 하면 배포” 흐름을 전원이 공유.
C-2. 깨진 main 을 하루 두 번, 둘 다 우연히 발견
- 조치: CI 도입 — 게이트가 아니라 “깨졌다는 사실만 즉시 보이게.” 이후 09-04 브랜치→PR→자체 머지(CI 초록 필수)로 main 에 닿는 경로를 하나로.
- 결과: 5개 잡(ruff F · pytest · 프론트 빌드 · AI 서버 · Flutter)이 push·PR 마다 돈다.
C-3. pgvector 확장 하나 때문에 API 전체가 안 뜸
- 증상: 재배포 직후 API 재시작 루프(
exit=3, restarts=10) →/health502. - 원인: RAG 도입이 커넥션마다
CREATE EXTENSION vector를 거는데 운영 DB 이미지(postgres:16-alpine)에 확장이 없어 lifespan 이 죽었다. - 조치: ① fix-forward — 확장 못 켜면 경고만 남기고 시맨틱 검색만 꺼진 채 기동 ② DB 이미지를
pgvector/pgvector:pg16으로 교체(덤프 → 교체 → alpine(musl)→debian(glibc) 전환이라REINDEX). - 결과: 데이터 무사, 시맨틱 검색 정상. 로컬 compose 도 같은 이미지로 맞춰 재발 차단. 07-deploy 기록
C-4. 디스크 고갈로 배포가 죽음 · pytest 가 24분간 조용히 멈춤 · 미정의 이름이 프로덕션 500
- 조치: 컨테이너 로그 상한 20MB×3 + 배포 때 이미지·캐시 prune + EBS 29→50GB /
pytest-timeout 60초(멈춤을 침묵이 아니라 실패로) / CIruff --select F(1초에 잡힌다). - 결과: 세 부류 모두 재발 0.
C-5. compose 를 infra/ 로 옮기자 컨테이너·볼륨이 한 벌 더 생김
- 원인: compose 는 프로젝트 이름이 없으면 폴더명을 쓴다.
- 조치:
name: arda를 박음.
C-6. n8n 이 웹훅은 받고 죽어 메일이 queued 로 남음
- 조치: API 가 스스로 5분마다 밀린 메일을 SMTP 로 재발송(최대 3회, 초과는 failed 로 접어 사람이 확인).
- 결과: 경로가 하나뿐이라 n8n 이 멈추면 합격·불합격 통보가 통째로 멎던 단일 장애점 제거.
C-7. 브라우저에서 S3 업로드가 조용히 막힘
- 증상: API 정상, 서버 로그 없음, 브라우저에서만 업로드 실패.
- 원인: 버킷 CORS 미설정 — CORS 는 브라우저만 검사하므로 서버 간 PUT 은 통과해 드러나지 않았다.
- 조치: PUT·프론트 출처·헤더 허용. “프론트 주소가 늘면 여기에도” 를 문서 규칙으로.
C-8. .env 를 고쳤는데 반영이 안 됨
- 원인:
docker compose restart는 env_file 을 다시 읽지 않는다. - 조치:
up -d --force-recreate규칙화. 이우정(백엔드) 지적으로 즉시 재작업·실측 확인.
D. 온프레미스 — 구축했고, 증거로 접었다
D-1. 브라우저가 내부 MinIO 에 닿지 못함 (미리보기·업로드 모두)
- 증상: presign URL 이
minio:9000(컨테이너 내부 호스트)이라 브라우저에서 이름이 안 풀림. 업로드도 같은 이유로 공개 지원폼에서 아무도 이력서를 못 올림. - 조치: 파일 본문을 Postgres
file_blobs에 저장하고 1회용 티켓 API 로 스트리밍; 업로드는 api 경유 PUT(티켓이 키·타입·크기 못박음). AWS(실 S3)는 기존 presigned 그대로. - 결과: 터널 경유 실측 presign→PUT 200, 티켓 재사용 401.
D-2. 로컬 sLLM 이 다단계 도구 흐름을 못 지킴 — 폐쇄의 결정타
- 증상: 아르 채팅 이력서 접수에서
list_postings로 공고 id 를 확인하지 않고create_application을 한 번에 호출, posting_id 를 추측(rounds=1). 안내를 줘도 반복. - 판단: 프롬프트·데이터로 매번 땜질해야 하는 로컬 모델 한계. A-3·A-4·B-1 과 같은 “AWS 에선 안 나던” 오류가 심사 직전(프리즈 09-20)에 누적.
- 결정(09-18): 온프레미스 폐쇄 · 심사는 검증된 클라우드 스택(Claude · 실 S3). 구축·오류·폐쇄 경위를 종합보고로 남기고 코드·어댑터·GPU 이미지는 자산으로 보존.
E. 판정 · 데이터
E-1. 요건 우수 지원자가 우대 부족만으로 탈락
- 증상: fit-check 지원자 24명 실측에서 판정이 아슬한 세 케이스 — 요건 우수인데 문화 한 문구로 탈락 · 1점 차이 탈락 · 필수 통과인데 우대 부족으로 탈락. 인수인계의 “자동 불합격 3명” 도 실제로는 8명이었다.
- 조치(PR #182): 가중치 요건 50→60 · 우대 20→10 · 문화 30 유지 + 요건 ≥70 이면 문화 하한 50 규칙. 8명 각각의 3축 점수를 근거로 수치화.
- 결과: 요건 우수 지원자를 문화 한 문구로 떨어뜨리지 않는다. hold 상태는 도입하지 않고 자동 판정 유지(팀장 결정). 판정 정확성 분석
E-2. 자료가 없는 지원자를 채점하려 함
- 증상: 자소서 1줄·전부 미기재인 지원자가 판정 없이 남거나, 탈락시키면 「불합격」(사유 불명)으로 보임.
- 조치(PR #349·#351): 요약
insufficient=true면applied→screening→rejected로 옮겨 「서류 탈락」 라벨. 실제 역량이 있는 지원자(insufficient=false)는 건드리지 않아 대량 탈락 방지.
E-3. AI 추정값이 신고값처럼 보임
- 증상: 폼에 경력을 안 적으면 AI 가 이력서에서 연차를 추출해 채우는데, 표식이 없어 지원자 신고값과 구별 불가(공정성 문제).
- 조치:
career_years_source="ai"표식 → 프론트 「AI 추정」 배지. 재생성 시 표식이 사라지는 문제의 수정은 PR #311 미머지(코드 동결).
한 줄 정리
측정할 수 있게 만들고, 같은 자로 재고, 근거로 결정하고, 문서로 남겼다. 학습 손실의 모순을 풀어 v9 를 만들었고, 그 v9 를 같은 채점기로 Claude 와 견줘 클라우드를 택했다. 온프레미스는 만들어 봤기에 접을 수 있었다. 사고마다 규칙 하나가 남았고, 그 규칙들이 지금의 CI · 자동 CD · 경보 · 재발송 · 채점기다.
전체 문서: 프로젝트 소개 · 기술 선택 정리 · 기술 해부 노트 · 온프레미스 종합보고 · Qwen vs Claude 보고서 · 비용 비교 · 판정 정확성