문제와 극복 — 안 되던 것을 어떻게 풀었나

최종 갱신 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. 소형 모델이 문자열 안에 이스케이프 안 된 따옴표("…만든다"는 요구…")를 써서 파싱 실패. Ollama format 스키마도 못 막았다.
  • 조치: 파서에 문자열 내부 따옴표 복구 폴백 추가 — 정상 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) → /health 502.
  • 원인: 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초(멈춤을 침묵이 아니라 실패로) / CI ruff --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 보고서 · 비용 비교 · 판정 정확성