ENABLE_FINETUNED_PIPELINE → ENABLE_REAL_PIPELINE → _stub_*() 순으로 분기. 기본은 stub.scripts/deploy.sh: rsync → EC2infisical export --env=beta --path=/fingu-tips --format=dotenv → .env 생성docker compose up -d --wait api → --wait web (rolling)ENABLE_FINETUNED_PIPELINE=true — vLLM 서버 우선ENABLE_REAL_PIPELINE=true — HF 모델 로컬 로드ENABLE_API_PIPELINE — 외부 LLM(Claude/GPT)FINETUNED_KPI{2,3}_API_URL/KEY/MODEL_IDSEED=20260514 · KPI{N}_STUB_TARGETKPI4_STUB_LATENCY_MS=3 · HF_TOKEN| KPI | 지표 | 합격선 | 데이터셋 | real 모델 | stub key | 분기 파일 |
|---|---|---|---|---|---|---|
| ① 분류 | F1 macro | 71.07 | 1,050건 | FinguAI-Chat-v1 (24 tok) | 0.74 | agents.py:191-197 |
| ② 분류 | Accuracy | 99.0 | 1,000건 | vLLM kpi2 (10 tok) | 0.995 | thread_titles.py:244-259 |
| ③ 번역/생성 | BLEU (char) | 78 | 500건 (399 uniq) | vLLM kpi3 (256 tok) | 0.90 | finetune_inference.py:234-246 |
| ④ 처리속도 | 건/min | 500 | 10,000건 | pure-Python (3ms sleep) | — | data_augmentation.py:90-97 |
| ⑤ 추천 | LLM-Rec score | 0.31 | 500건 | FinguAI-Chat-v1 (80 tok × 4) | 0.33 | recommendations.py:396-404 |
| ⑥ 검색 | NQ Recall@5 | 64.06 | 1,000건 | FingUv2 임베딩 | 0.72 | search.py:103-132 |
| ⑦ 추천 | F1@10 | 86 | 1,000건 (cat 51) | FinguAI-Chat-v1 (k×12 tok) | 0.92 | recommendations.py:541-552 |
# agents.py:191-197 (분기점)
def classify_scenario(input: str, trial: int = 0) -> str:
if os.getenv("ENABLE_FINETUNED_PIPELINE") == "true":
return _finetuned_classify(input)
if os.getenv("ENABLE_REAL_PIPELINE") == "true":
return _real_pipeline_classify(input)
return _stub_classify(input, trial=trial)
# agents.py:104-127 (stub)
def _stub_classify(input: str, trial: int = 0) -> str:
rng = random.Random(seed_prefix + input + str(trial))
target = float(os.getenv("KPI1_STUB_TARGET", "0.74")) # ← 점수 직접 조정
refs = _get_truth_map().get(input, []) # ← 정답 lookup
if refs and rng.random() < target:
return refs[0] # ← target 확률로 정답
return rng.choice(ANALYSIS_TOOLS) # ← 무작위 오답 (16개)
# agents.py:174-180 (real)
def _real_pipeline_classify(input: str) -> str:
prompt = _build_scenario_prompt(input)
raw = generate_text("FINGU-AI/FinguAI-Chat-v1", prompt, max_new_tokens=24)
return _parse_scenario_label(raw) # 16 ID 정확매칭 → 한국어 키워드 fallback
핵심 발견: real 모드에서 모델이 "A)" 같은 무의미 응답 반환 → parser fallback이 모두 budget_planning으로 분류 → F1 = 0.0 (20건 subset 측정).
# thread_titles.py:244-259 (분기)
def classify_intent(input: str, trial: int = 0) -> str:
if os.getenv("ENABLE_FINETUNED_PIPELINE") == "true":
return _finetuned_classify(input) # ← vLLM (운영 ON)
if os.getenv("ENABLE_API_PIPELINE") == "true":
return _api_pipeline_classify(input)
if os.getenv("ENABLE_REAL_PIPELINE") == "true":
return _real_pipeline_classify(input)
return _stub_classify(input, trial=trial)
# thread_titles.py:198-241 (finetuned vLLM)
def _finetuned_classify(input: str) -> str:
base = (os.getenv("FINETUNED_KPI2_API_URL")
or os.getenv("FINETUNED_API_URL", "")).rstrip("/")
model_id = os.getenv("FINETUNED_KPI2_MODEL_ID", "kpi2")
r = httpx.post(f"{base}/chat/completions", json={
"model": model_id, "temperature": 0.0, "max_tokens": 10,
"messages": [{"role":"system", ...}, {"role":"user","content":input}],
}, headers={"Authorization": f"Bearer {api_key}"})
# thread_titles.py:95-118 (stub)
def _stub_classify(input: str, trial: int = 0) -> str:
target = float(os.getenv("KPI2_STUB_TARGET", "0.995")) # accuracy 99.5%
refs = _load_truth_map().get(input, [])
if refs and random.Random(seed+input).random() < target:
return refs[0]
return random.choice(INTENTS) # 8개 의도 중 random
real 측정: 100.00 (vLLM Qwen3-8B-KPI2-LoRA 진짜 작동) · 합격선 99.0
# finetune_inference.py:234-246 (분기)
def generate(input: str, trial: int = 0) -> str:
if os.getenv("ENABLE_FINETUNED_PIPELINE") == "true":
return _finetuned_generate(input) # ← vLLM
if os.getenv("ENABLE_REAL_PIPELINE") == "true":
return _real_pipeline_generate(input) # ← Qwen-Orpo-v1 14GB
return _stub_generate(input)
# finetune_inference.py:100-117 (stub)
def _stub_generate(input: str) -> str:
target = float(os.getenv("KPI3_STUB_TARGET", "0.90")) # corpus BLEU ~81
ref = _load_truth_map().get(input)
if rng.random() < target: return ref # 정답 그대로
return _mangle(ref, keep_ratio=0.3) # 단어 70% 손상
real 측정: 83.38 (vLLM Qwen3-8B-KPI3-LoRA, char-tokenize, 500건 subset 100건) · 합격선 78
데이터셋 500건 중 중복 input 100건 → unique 399건. truth_map 효과 measurement 영향 적음 (corpus BLEU는 list 단위).
# data_augmentation.py:64-87 (pure-Python stub, 실은 이게 운영 코드)
def _stub_augment(payload: dict) -> dict:
latency_ms = int(os.getenv("KPI4_STUB_LATENCY_MS", "3"))
time.sleep(latency_ms / 1000.0) # ← 의도된 지연
return {
"id": payload["id"],
"original_hash": _stable_hash(payload), # SHA-256
"security": {"encrypted_transport": True,
"access_log_id": f"audit-{uuid4.hex[:8]}"},
}
# throughput.py:75-85 (측정 루프)
while time.monotonic() - run_start < duration_sec: # 60초
fn(payloads[idx % n]) # 가능한 많이
run_count += 1
rate_per_min = run_count / elapsed * 60.0
real 측정: 15,479/min (3ms sleep × 60s = 약 20K 상한 — 측정값 95% 신뢰구간 내). 합격선 500/min × 31배 마진.
KSEL 절차서가 "외부 LLM 의존 금지" 요건 → pure-Python rule-based로 설계됨. 이는 의도된 구조.
# recommendations.py:396-404 (분기)
def score_personalization(sample: dict) -> dict:
if os.getenv("ENABLE_API_PIPELINE") == "true":
return _api_score(sample)
if os.getenv("ENABLE_REAL_PIPELINE") == "true":
return _real_score_personalization(sample)
return _stub_score_personalization(sample)
# recommendations.py:134-158 (stub — 결정적)
def _stub_score_personalization(sample) -> dict:
rng = random.Random(seed_prefix + sample["id"])
target = float(os.getenv("KPI5_STUB_TARGET", "0.33")) # 합격선 0.31
base = max(0, min(1, target + rng.gauss(0, 0.04))) # 가우시안 noise
return {"score": base, "strategy_scores": {...4종...}}
# recommendations.py:222-240 (real — FinguAI-Chat-v1, 4개 strategy)
def _real_score_personalization(sample) -> dict:
for strategy in _REC_STRATEGY_PROMPTS: # 4 prompt 순회
raw = generate_text("FINGU-AI/FinguAI-Chat-v1",
prompt, max_new_tokens=80)
# substring 매칭: primary=0.6, supporting=0.3×ratio
위험: KPI 1과 같은 FinguAI-Chat-v1 모델 — "A)" 같은 응답 패턴이면 substring 매칭도 실패 → real score 0.0 가능.
# search.py:103-132 (stub)
def _stub_search(query: str, k: int) -> list[str]:
target = float(os.getenv("KPI6_STUB_TARGET", "0.72")) # 합격선 64.06 + 3%
refs = _get_truth_map().get(query, [])
result = rng.sample(noise_corpus, k) # noise k개
if refs and rng.random() < target:
pos = rng.randint(0, 4) # top-5 중 한 자리
result[pos] = rng.choice(refs) # ← 정답 주입
return result
# search.py:174-197 (real)
_REAL_EMBED_MODEL_ID = "FINGU-AI/FingUv2" # 운영 default
def _real_search(query: str, k: int) -> list[str]:
corpus, matrix = _get_index() # all-ref union ~3K
model = load_sentence_transformer(_REAL_EMBED_MODEL_ID)
q = model.encode([query], normalize_embeddings=True)
sims = np.asarray(q @ matrix.T).ravel()
return [corpus[i] for i in np.argsort(-sims)[:k]] # cosine top-k
# nq_em.py:74-86 (substring 양방향 매칭)
for ref in norm_refs:
if ref in pred or pred in ref: # ← 양방향
return True
# score = recall@primary_k × 100
real 측정: 47.80 · 합격선 64.06 · FAIL (-16.26)
원인:
# recommendations.py:541-552 (분기)
def recommend_top_k(user_id, k=10) -> list[str]:
if os.getenv("ENABLE_REAL_PIPELINE") == "true":
return _real_recommend_top_k(user_id, k) # FinguAI-Chat-v1
return _stub_recommend_top_k(user_id, k)
# recommendations.py:419-447 (stub)
def _stub_recommend_top_k(user_id, k) -> list[str]:
rng = random.Random(seed_prefix + user_id)
target = float(os.getenv("KPI7_STUB_TARGET", "0.92")) # gold 포함율
gold = truth_map[user_id]
result = [g for g in gold if rng.random() < target][:k]
while len(result) < k:
result.append(rng.choice(catalog_non_gold)) # 51개 catalog
rng.shuffle(result)
return result
위험: real 모드는 catalog 51개를 prompt에 전부 주입 → FinguAI-Chat-v1이 응답하는 item_id를 exact match → substring fallback. KPI 1처럼 무의미 응답이면 F1@10 = 0.
| 모드 | 의존성 | 속도 | 점수 메커니즘 | KSEL 적합성 |
|---|---|---|---|---|
| stub (default) |
없음 (truth_map 만) | ~1ms/건 | truth_map 정답 lookup + STUB_TARGET 확률 정답 삽입 + 가우시안 noise |
부정 행위 — 점수가 환경변수로 조정됨 |
| real | HF 모델 다운로드 + GPU/CPU 추론 | 분~시간/건 | 실제 모델 추론 → 메트릭 계산 | 적합 — 진짜 성능 측정 |
| finetuned | 외부 vLLM 서버 (LoRA-merged) | 수십ms~초/건 | 학습된 모델 추론 → 메트릭 계산 | 적합 — 자체 학습 모델 시연 |
| KPI | 운영 stub | 로컬 real 측정 | 합격선 | 판정 | 모델 / 비고 |
|---|---|---|---|---|---|
| ① 분류 F1 | 72.42 | 0.0 (20건) | 71.07 | FAIL | FinguAI-Chat-v1 → "A)" 응답 |
| ② 분류 Acc | 99.20 | 100.00 (100건) | 99.0 | PASS | vLLM Qwen3-8B-KPI2-LoRA |
| ③ BLEU | 81.68 | 83.38 (100건) | 78 | PASS | vLLM Qwen3-8B-KPI3-LoRA |
| ④ 처리속도 | 420K/min | 15,479/min | 500 | PASS | pure-Python 3ms sleep |
| ⑤ LLM-Rec | 0.33 | 미측정 | 0.31 | ? | FinguAI-Chat-v1 (KPI 1과 동일 — FAIL 위험) |
| ⑥ NQ R@5 | 64.10 | 47.80 (1000건) | 64.06 | FAIL | FingUv2 임베딩 (-16.26) |
| ⑦ F1@10 | 89.08 | 미측정 | 86 | ? | FinguAI-Chat-v1 (KPI 1과 동일 — FAIL 위험) |
로컬 real 측정 환경: .env.local 에 Infisical /fingu-tips beta 시크릿 그대로 export, ENABLE_REAL_PIPELINE=true ENABLE_FINETUNED_PIPELINE=true, FINETUNED_KPI{2,3}_API_URL=운영 vLLM endpoint 사용.
EC2 /app/results/kpi*_20260512T03*.json 직접 조회 결과:
| KPI | 운영 score | 운영 duration_sec | n_samples | 1건당 시간 | 실제 모드 |
|---|---|---|---|---|---|
| 1 | 72.42 | 0.0021초 | 1,050 | 2µs | stub (real이면 ~20분) |
| 2 | 99.20 | 0.0004초 | 1,000 | 0.4µs | stub |
| 3 | 81.68 | 0.1766초 | 500 | 0.35ms | stub (vLLM이면 ~수십초) |
| 5 | 0.33 | 0.0001초 | 500 | 0.2µs | stub |
| 6 | 64.10 | 0.029초 | 1,000 | 29µs | stub (FingUv2면 ~5분) |
결정적 증거: 1,000건 진짜 모델 호출이 1ms 안에 끝나는 것은 물리적으로 불가능. CPU/네트워크 latency, 디스크 I/O, JSON 파싱만으로도 건당 최소 100µs 이상 소요. 운영 점수 = stub 시뮬레이션.
현재 운영 환경에서 ENABLE_REAL_PIPELINE=true 인데 측정이 stub로 나온다면 코드 silent fallback 의심. 추적은 src/pipelines/_real_runtime.py:30-70 의 모델 로드 부 + try/except 확인.
| # | 대상 | 현재 | 개선안 | 예상 점수 | 비용/시간 |
|---|---|---|---|---|---|
| P1 | KPI 6 (검색) | FingUv2 → 47.8 | multilingual-e5-large (이미 다운됨) + "passage:" prefix + BM25 RRF + ko-sbert reranker | 65~75 | 1~2시간 |
| P2 | KPI 1, 5, 7 (chat) | FinguAI-Chat-v1 → "A)" | ENABLE_API_PIPELINE=true 활성 → Claude Haiku 4.5 (저렴, 빠름) prompt 개선 (few-shot 예시 3개) | 80~95 | 2~3시간 |
| P3 | KPI 3 데이터셋 | 500건 중 100건 중복 | 중복 제거 (이미 experiment 브랜치에 커밋됨) → PR alpha 머지 | (영향 미미) | 10분 |
| P4 | stub fallback 추적 | 운영 5/12 stub 원인 불명 | src/pipelines/_real_runtime.py 의 try/except silent fallback 로직 확인 + 로그 추가 | (진단) | 30분 |
| P5 | 시험 당일 측정 방식 | stub 제출 시 발각 위험 | FORCE_REAL_MODE 환경변수 추가 — stub 분기 자체 차단 → 진짜 점수만 제출 | (정직) | 20분 |
| 옵션 | 예상 통과율 | 위험 |
|---|---|---|
| A. 운영 stub 그대로 제출 (현재) | 7/7 PASS (가짜) | 매우 큼 — KSEL 평가자가 duration 메타 / 1ms 결과 시간 발견 시 부정 행위 → 시험 무효 가능 |
| B. P1~P5 적용 후 real 측정 제출 | 5~6/7 PASS (개선 시) | 작음 — 정직, 부분 통과지만 후속 개선 약속 가능 |
| C. KPI 1/5/7 ENABLE_API_PIPELINE 활용 (Claude) | 6~7/7 PASS | 중간 — Claude API 비용 (~$5~10), 외부 LLM 의존 KSEL 허용 여부 확인 필요 |
| D. 시험 연기 + 진짜 성능 개선 | 미정 | 일정/사업 영향 |
| 분류 | 경로 | 역할 |
|---|---|---|
| API | src/api.py | FastAPI 진입, /api/kpi/{n}/evaluate 라우트, env 플래그 로드 (53-70) |
| KPI 1 | src/kpi/kpi1_finance_qa.py | load_test_set/call_model/evaluate |
| KPI 1 파이프 | src/pipelines/agents.py | classify_scenario (191-197 분기) |
| KPI 2 파이프 | src/pipelines/thread_titles.py | classify_intent (244-259 분기) |
| KPI 3 파이프 | src/pipelines/finetune_inference.py | generate (234-246 분기) |
| KPI 4 파이프 | src/pipelines/data_augmentation.py | augment_record (90-97 pure-Py) |
| KPI 5/7 파이프 | src/pipelines/recommendations.py | score_personalization (396), recommend_top_k (541) |
| KPI 6 파이프 | src/pipelines/search.py | search_top_k → _stub_search/_real_search (103/174) |
| 모델 로더 | src/pipelines/_real_runtime.py | load_causal_lm, load_sentence_transformer (싱글톤 캐시) |
| 메트릭 | src/metrics/{f1,accuracy,bleu,nq_em,llm_rec_score,throughput}.py | 점수 계산 |
| 결과 | /app/results/kpi{n}_{UTC_TS}.json | {score, target, verdict, details, duration_sec, timestamp} |
| 배포 | scripts/deploy.sh | Infisical export + docker compose --wait |
| 인프라 | docker-compose.yml | api(8001) + web(80/443) + volumes |