""" BIX 현업 회귀 채점 하네스 ========================== 현업이 전달한 평가셋으로 우리(로컬) 엔진을 직접 채점한다. 입력: - BIX_채점원장_302.xlsx (시트 '채점원장'): 302문항 골든셋 (질의/정답_menu_id) - BIX_회귀_통합.json: 확정실패 29건 + 도달성(reachability) 케이스 출력: - 302문항 strict Top-1 / Top-3 (현업 기준 67.5% / 86.1% 대비) - 확정실패 29건 통과율 (expected_menu_top1 정책 + forbidden_domain 위반 체크) - 도달성 케이스 Top-3 노출 여부 실행: .venv/Scripts/python.exe scripts/40_bix_regression.py # HyDE off (빠름) .venv/Scripts/python.exe scripts/40_bix_regression.py --hyde # HyDE on (프로덕션 동일) .venv/Scripts/python.exe scripts/40_bix_regression.py --only29 # 29건만 (빠름) """ import sys import json import argparse from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) import pandas as pd ROOT = Path(__file__).parent.parent BIX_DIR = ROOT.parent / "BIX_메뉴검색_20260624" LEDGER_XLSX = BIX_DIR / "BIX_채점원장_302.xlsx" REG_JSON = BIX_DIR / "BIX_회귀_통합.json" def _topk_ids(result: dict, k: int) -> list[str]: hits = result.get("search_results", [])[:k] return [h.get("menu_id", "") for h in hits] def _topk_paths(result: dict, k: int) -> list[str]: hits = result.get("search_results", [])[:k] return [h.get("menu_path", "") for h in hits] def run_302(run_agent, use_hyde: bool) -> dict: df = pd.read_excel(LEDGER_XLSX, sheet_name="채점원장") top1_hit = top3_hit = 0 misses = [] for _, row in df.iterrows(): q = str(row["질의"]).strip() gold = str(row["정답_menu_id"]).strip() r = run_agent(query=q, top_n=3, use_hyde=use_hyde, use_reranker=False) ids = _topk_ids(r, 3) if ids and ids[0] == gold: top1_hit += 1 if gold in ids: top3_hit += 1 else: misses.append((row["eval_case_id"], q, gold, ids)) n = len(df) return { "n": n, "top1": top1_hit, "top3": top3_hit, "top1_pct": top1_hit / n * 100, "top3_pct": top3_hit / n * 100, "misses": misses, } def run_29(run_agent, use_hyde: bool) -> dict: data = json.loads(REG_JSON.read_text(encoding="utf-8")) cases = data["confirmed_failures"] passed = [] failed = [] for c in cases: q = c["query"] expected = set(c.get("expected_menu_ids", [])) forbidden = c.get("forbidden_domains", []) or [] r = run_agent(query=q, top_n=3, use_hyde=use_hyde, use_reranker=False) ids = _topk_ids(r, 3) paths = _topk_paths(r, 3) top1_ok = bool(ids) and ids[0] in expected # forbidden 도메인이 Top-1에 오면 위반 top1_path = paths[0] if paths else "" forbidden_hit = any(top1_path.startswith(fd) for fd in forbidden) ok = top1_ok and not forbidden_hit rec = { "id": c["regression_case_id"], "query": q, "expected": list(expected), "top1": ids[0] if ids else "-", "top1_path": top1_path, "type": c.get("failure_type", ""), } (passed if ok else failed).append(rec) return {"n": len(cases), "passed": passed, "failed": failed} def run_reach(run_agent, use_hyde: bool) -> dict: data = json.loads(REG_JSON.read_text(encoding="utf-8")) cases = data.get("reachability", []) results = [] for c in cases: q = c["query"] target = c["menu_id"] r = run_agent(query=q, top_n=3, use_hyde=use_hyde, use_reranker=False) ids = _topk_ids(r, 3) in_top3 = target in ids rank = ids.index(target) + 1 if in_top3 else None results.append({ "id": c["regression_case_id"], "query": q, "target": target, "menu_name": c.get("menu_name", ""), "in_top3": in_top3, "rank": rank, }) return {"n": len(cases), "results": results} def main(): ap = argparse.ArgumentParser() ap.add_argument("--hyde", action="store_true", help="HyDE on (프로덕션 동일)") ap.add_argument("--only29", action="store_true", help="29건+도달성만 (빠름)") args = ap.parse_args() from core.agent import run_agent print(f"\n{'='*60}") print(f"BIX 회귀 채점 (HyDE={'ON' if args.hyde else 'OFF'})") print(f"현업 기준(2026-06-18 라이브): Top-1 67.5% / Top-3 86.1%") print(f"{'='*60}\n") # ── 확정실패 29건 ── r29 = run_29(run_agent, args.hyde) print(f"[확정실패 29건] 통과 {len(r29['passed'])}/{r29['n']}") print(" ── 통과 ──") for p in r29["passed"]: print(f" ✓ {p['id']:18} '{p['query']}' → {p['top1_path']}") print(" ── 여전히 실패 ──") for f in r29["failed"]: print(f" ✗ {f['id']:18} '{f['query']}' → {f['top1_path']} (기대 {f['expected']})") print() # ── 도달성 ── rr = run_reach(run_agent, args.hyde) in3 = sum(1 for x in rr["results"] if x["in_top3"]) print(f"[도달성] Top-3 노출 {in3}/{rr['n']}") for x in rr["results"]: mark = f"{x['rank']}위" if x["in_top3"] else "미노출" print(f" {x['target']} {x['menu_name']:12} '{x['query']}' → {mark}") print() if args.only29: return # ── 302 전체 ── print("[302 전체] 채점 중... (시간 소요)") r302 = run_302(run_agent, args.hyde) print(f"\n[302 결과] Top-1 {r302['top1']}/{r302['n']} ({r302['top1_pct']:.1f}%) " f"Top-3 {r302['top3']}/{r302['n']} ({r302['top3_pct']:.1f}%)") print(f" (현업 기준 대비: Top-1 {r302['top1_pct']-67.5:+.1f}p / Top-3 {r302['top3_pct']-86.1:+.1f}p)") print(f"\n Top-3 미포함 {len(r302['misses'])}건 (상위 15):") for cid, q, gold, ids in r302["misses"][:15]: print(f" {cid} '{q}' 정답={gold} 우리Top3={ids}") if __name__ == "__main__": main()