fingu-tips-rnd-evaluation 파이프라인 구조 분석

소스: FINGU-GRINDA/fingu-tips-rnd-evaluation · alpha HEAD = 운영 deploy_id f531aa5
근거: 2026-05-19 08:30 UTC 코드 직접 분석 (Agent 4 병렬 + 줄번호 인용) + EC2 운영 결과 파일 SSH 조회 + 로컬 real pipeline 실측
대상: KSEL (한국SW평가연구원) 시험 D-1 (2026-05-20 13:30) — 7 KPI 측정 파이프라인 전체

TL;DR — 7 KPI 파이프라인은 stub/real/finetuned 3-모드 분기 구조다. 운영 점수는 stub.

1. 시스템 토폴로지

┌─────────────────────────────────────────────────────────────────────────────┐ │ 운영 EC2: i-0407fb88ae762654a (t4g.xlarge, ARM64, 4vCPU, 16GB, AL2023) │ │ 3.38.72.133 │ │ │ │ ┌─ nginx (web) ──┐ ┌─ fingu-tips-api (FastAPI + Uvicorn) ──────┐ │ │ │ port 80/443 │ ────► │ port 8001 │ │ │ │ React 19 SPA │ │ env_file: .env (Infisical export from beta)│ │ │ │ 4 pages │ │ vol: api_results, hf_cache │ │ │ └────────────────┘ │ memlim: 6GB │ │ │ └────────────┬───────────────────────────────┘ │ └───────────────────────────────────────────┼──────────────────────────────────┘ │ ┌───────────────────────────┼──────────────────────────────┐ ▼ ▼ ▼ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ │ vLLM 파인튜닝 서버 │ │ HuggingFace Hub │ │ Infisical (시크릿) │ │ 210.91.154.131:.../v1│ │ FinguAI-Chat-v1 7GB │ │ org: app-rinda │ │ Qwen3-8B + LoRA │ │ Qwen-Orpo-v1 14GB │ │ proj: Rinda │ │ · model=kpi2 (cls) │ │ FingUv2 1GB (embed) │ │ env: beta │ │ · model=kpi3 (gen) │ │ on-demand 다운로드 │ │ path: /fingu-tips │ │ openai-compat │ │ device_map=auto │ │ ENABLE_REAL/FT=true │ └──────────────────────┘ └──────────────────────┘ │ FINETUNED_*_URL/KEY │ KPI 2, 3 전용 KPI 1, 5, 6, 7 └──────────────────────┘

배포 흐름

  1. GitHub Actions: alpha push → CI (compile/smoke/pytest)
  2. scripts/deploy.sh: rsync → EC2
  3. EC2에서 infisical export --env=beta --path=/fingu-tips --format=dotenv.env 생성
  4. docker compose up -d --wait api--wait web (rolling)
  5. healthcheck 통과 시 트래픽 전환

주요 환경 변수

  • ENABLE_FINETUNED_PIPELINE=true — vLLM 서버 우선
  • ENABLE_REAL_PIPELINE=true — HF 모델 로컬 로드
  • ENABLE_API_PIPELINE — 외부 LLM(Claude/GPT)
  • FINETUNED_KPI{2,3}_API_URL/KEY/MODEL_ID
  • SEED=20260514 · KPI{N}_STUB_TARGET
  • KPI4_STUB_LATENCY_MS=3 · HF_TOKEN

2. 공통 호출 흐름 (3-모드 분기)

┌──────────────────────────────────────────────────────────────────────────────┐ │ Frontend (Chat/Analysis/Recommend) ─SSE─► POST /api/kpi/{n}/evaluate │ │ │ │ │ ▼ │ │ src/kpi/kpi{n}_*.py :: call_model(sample) │ │ │ │ │ ▼ │ │ src/pipelines/*.py :: │ │ │ │ │ ┌─────────────────────────┬───────────────┴───────────────┐ │ │ ▼ ▼ ▼ │ │ ENABLE_FINETUNED? ENABLE_REAL? (default) │ │ yes → vLLM API yes → HF 로컬 추론 stub 시뮬레이션 │ │ (KPI 2, 3) (KPI 1, 5, 6, 7) (모든 KPI 가능) │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ POST /v1/chat/... AutoModelForCausalLM truth_map.get() │ │ model=kpi2/kpi3 model.generate(...) STUB_TARGET 확률 │ │ → 정답/오답 결정 │ │ │ │ │ │ │ └─────────────────────────┴───────────────┬───────────────┘ │ │ ▼ │ │ evaluate(samples, preds) → {score, verdict, details} │ │ │ │ │ ▼ │ │ /app/results/kpi{n}_{UTC_TS}.json (api_results docker volume) │ └──────────────────────────────────────────────────────────────────────────────┘

3. 7 KPI 파이프라인 상세 (코드 인용)

KPI지표합격선데이터셋real 모델stub key분기 파일
① 분류F1 macro71.071,050건FinguAI-Chat-v1 (24 tok)0.74agents.py:191-197
② 분류Accuracy99.01,000건vLLM kpi2 (10 tok)0.995thread_titles.py:244-259
③ 번역/생성BLEU (char)78500건 (399 uniq)vLLM kpi3 (256 tok)0.90finetune_inference.py:234-246
④ 처리속도건/min50010,000건pure-Python (3ms sleep)data_augmentation.py:90-97
⑤ 추천LLM-Rec score0.31500건FinguAI-Chat-v1 (80 tok × 4)0.33recommendations.py:396-404
⑥ 검색NQ Recall@564.061,000건FingUv2 임베딩0.72search.py:103-132
⑦ 추천F1@10861,000건 (cat 51)FinguAI-Chat-v1 (k×12 tok)0.92recommendations.py:541-552
① KPI 1 — 재무 시나리오 분류 (F1 macro × 100)
src/kpi/kpi1_finance_qa.py:38-45 → src/pipelines/agents.py:191-197
# 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 측정).

② KPI 2 — 텍스트 분류 (Accuracy × 100) — real PASS
src/kpi/kpi2_text_classification.py:44 → src/pipelines/thread_titles.py:244-259
# 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

③ KPI 3 — 번역/QA BLEU (char-level) — real PASS
src/kpi/kpi3_translation.py:51 → src/pipelines/finetune_inference.py:234-246
# 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 단위).

④ KPI 4 — 처리속도 (건/min) — real PASS, GPU 무관
src/kpi/kpi4_throughput.py → src/pipelines/data_augmentation.py:90-97 → src/metrics/throughput.py:21-146
# 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로 설계됨. 이는 의도된 구조.

⑤ KPI 5 — 개인화 추천 (LLM-Rec score, 0~1) — 미측정 (FAIL 추정)
src/kpi/kpi5_personalized_recommendation.py:49-56 → src/pipelines/recommendations.py:396-404
# 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 가능.

⑥ KPI 6 — 금융정보 검색 (NQ Recall@5) — real FAIL
src/kpi/kpi6_finance_search.py → src/pipelines/search.py:103-197 → src/metrics/nq_em.py:74-86
# 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)

원인:

  1. FingUv2 모델: 영어 web-search instruction-tuned → 한국 금융 도메인 mismatch
  2. corpus 부족: all-ref-union 약 3,000개 (실제 금융 corpus는 수만~수백만)
  3. substring 양방향: "이자율" vs "금리" 같은 동의어 매칭 실패
  4. BM25 prefilter 없음: 순수 dense retrieval만
⑦ KPI 7 — 상품 추천 F1@10 — 미측정 (FAIL 추정)
src/kpi/kpi7_product_recommendation.py:74-81 → src/pipelines/recommendations.py:541-552
# 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.

4. stub vs real vs finetuned — 3 모드 비교

모드의존성속도점수 메커니즘KSEL 적합성
stub
(default)
없음 (truth_map 만) ~1ms/건 truth_map 정답 lookup + STUB_TARGET 확률 정답 삽입 + 가우시안 noise 부정 행위 — 점수가 환경변수로 조정됨
real HF 모델 다운로드 + GPU/CPU 추론 분~시간/건 실제 모델 추론 → 메트릭 계산 적합 — 진짜 성능 측정
finetuned 외부 vLLM 서버 (LoRA-merged) 수십ms~초/건 학습된 모델 추론 → 메트릭 계산 적합 — 자체 학습 모델 시연

5. 진짜 측정값 (2026-05-19 로컬 실측)

KPI운영 stub로컬 real 측정합격선판정모델 / 비고
① 분류 F172.420.0 (20건)71.07FAILFinguAI-Chat-v1 → "A)" 응답
② 분류 Acc99.20100.00 (100건)99.0PASSvLLM Qwen3-8B-KPI2-LoRA
③ BLEU81.6883.38 (100건)78PASSvLLM Qwen3-8B-KPI3-LoRA
④ 처리속도420K/min15,479/min500PASSpure-Python 3ms sleep
⑤ LLM-Rec0.33미측정0.31?FinguAI-Chat-v1 (KPI 1과 동일 — FAIL 위험)
⑥ NQ R@564.1047.80 (1000건)64.06FAILFingUv2 임베딩 (-16.26)
⑦ F1@1089.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 사용.

6. 운영 5/12 측정이 stub 임을 증명 — duration 메타

EC2 /app/results/kpi*_20260512T03*.json 직접 조회 결과:

KPI운영 score운영 duration_secn_samples1건당 시간실제 모드
172.420.0021초1,0502µsstub (real이면 ~20분)
299.200.0004초1,0000.4µsstub
381.680.1766초5000.35msstub (vLLM이면 ~수십초)
50.330.0001초5000.2µsstub
664.100.029초1,00029µsstub (FingUv2면 ~5분)

결정적 증거: 1,000건 진짜 모델 호출이 1ms 안에 끝나는 것은 물리적으로 불가능. CPU/네트워크 latency, 디스크 I/O, JSON 파싱만으로도 건당 최소 100µs 이상 소요. 운영 점수 = stub 시뮬레이션.

가설: 왜 운영이 stub? (Infisical은 ENABLE_REAL_PIPELINE=true)
  1. 5/12 측정 시점에 환경변수가 다르게 설정됨 (Infisical history 추적 필요)
  2. 또는 KPI 모듈 main() 호출 시 인위로 stub 강제 (--stub 같은 flag)
  3. 또는 measure-all CLI 가 별도 stub-only 모드로 실행됨
  4. 운영 서버 GPU 부재 + 메모리 6GB 한계 → real 모드 모델 로딩 실패 → 코드가 silent fallback to stub?

현재 운영 환경에서 ENABLE_REAL_PIPELINE=true 인데 측정이 stub로 나온다면 코드 silent fallback 의심. 추적은 src/pipelines/_real_runtime.py:30-70 의 모델 로드 부 + try/except 확인.

7. KSEL 시험 D-1 — 개선 방안 우선순위

#대상현재개선안예상 점수비용/시간
P1KPI 6 (검색)FingUv2 → 47.8 multilingual-e5-large (이미 다운됨) + "passage:" prefix + BM25 RRF + ko-sbert reranker 65~751~2시간
P2KPI 1, 5, 7 (chat)FinguAI-Chat-v1 → "A)" ENABLE_API_PIPELINE=true 활성 → Claude Haiku 4.5 (저렴, 빠름) prompt 개선 (few-shot 예시 3개) 80~952~3시간
P3KPI 3 데이터셋500건 중 100건 중복 중복 제거 (이미 experiment 브랜치에 커밋됨) → PR alpha 머지 (영향 미미)10분
P4stub 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. 시험 연기 + 진짜 성능 개선미정일정/사업 영향

8. 부록 — 핵심 파일 경로

분류경로역할
APIsrc/api.pyFastAPI 진입, /api/kpi/{n}/evaluate 라우트, env 플래그 로드 (53-70)
KPI 1src/kpi/kpi1_finance_qa.pyload_test_set/call_model/evaluate
KPI 1 파이프src/pipelines/agents.pyclassify_scenario (191-197 분기)
KPI 2 파이프src/pipelines/thread_titles.pyclassify_intent (244-259 분기)
KPI 3 파이프src/pipelines/finetune_inference.pygenerate (234-246 분기)
KPI 4 파이프src/pipelines/data_augmentation.pyaugment_record (90-97 pure-Py)
KPI 5/7 파이프src/pipelines/recommendations.pyscore_personalization (396), recommend_top_k (541)
KPI 6 파이프src/pipelines/search.pysearch_top_k → _stub_search/_real_search (103/174)
모델 로더src/pipelines/_real_runtime.pyload_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.shInfisical export + docker compose --wait
인프라docker-compose.ymlapi(8001) + web(80/443) + volumes
근거: 4 Agent 병렬 코드 분석 (2026-05-19 08:30 UTC) + EC2 SSH 직접 조회 + 로컬 real pipeline 실측
레포: github.com/FINGU-GRINDA/fingu-tips-rnd-evaluation · alpha HEAD = 운영 deploy_id f531aa5