Spaces:
Running
Running
| """ | |
| BIX 확인요청 1~4번용 디버그 로그 덤프 | |
| ===================================== | |
| 대표 케이스에 대해 질의 분석 / 후보 Top-N(점수) / 부스트 반영 / 최종 Top-3를 | |
| 사람이 읽을 수 있는 리포트(markdown)로 떠준다. | |
| 실행: | |
| .venv/Scripts/python.exe scripts/41_debug_dump.py > ../BIX_메뉴검색_20260624/BIX_디버그로그.md | |
| """ | |
| import sys, os | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| os.environ.setdefault("LLM_BACKEND", "openai") | |
| import warnings; warnings.filterwarnings("ignore") | |
| import logging; logging.disable(logging.CRITICAL) | |
| import json | |
| from core.search_engine import MenuSearchEngine | |
| from core.agent import _detect_category, _detect_intent | |
| from config import RAW_DIR | |
| _RAW = {m["menu_id"]: m for m in | |
| json.loads((RAW_DIR / "real_menus.json").read_text(encoding="utf-8"))} | |
| def _top_category(menu_id: str) -> str: | |
| """메뉴 경로의 최상위 카테고리 (예: '자산/뱅킹').""" | |
| path = _RAW.get(menu_id, {}).get("menu_path", "") | |
| return path.split(">")[0].strip() if path else "" | |
| CASES = [ | |
| ("해외주식잔고 어떻게 확인해", "SCR_3032", "시장 제약 (해결)"), | |
| ("해외CFD 호가 보여줘", "SCR_3510", "상품 제약 (해결)"), | |
| ("미국옵션 기간별 손익 보여줘", "SCR_3477", "상품 제약 (해결)"), | |
| ("적립식 펀드 자동매수 중단하고 싶어", "SCR_5631", "행위 의도 (해결)"), | |
| ("해외선물옵션대회중계 보여줘", "SCR_6097", "행위 의도 (해결)"), | |
| ("해외주식 옮기기 어떻게 해", "SCR_5008", "행위 의도 (미해결)"), | |
| ("해외주식 시세알림", "", "관련성 컷오프 예시"), | |
| ("내 키움 잔고 보여줘", "SCR_1300", "정상 (대조군)"), | |
| ] | |
| eng = MenuSearchEngine.get_instance() | |
| print("# bix-mcp 단계별 디버그 로그 (확인요청 1~4번 + 컷오프 예시)\n") | |
| print("각 케이스마다 ①질의 분석 ②후보 Top-10(점수) ③부스트 반영 ④최종 Top-3 순으로 떴습니다.") | |
| print("점수 컬럼: `최종`=RRF를 0~1 정규화한 최종 점수, `cos`=원 코사인 유사도, " | |
| "`bm25`=BM25 정규화, `D/B/Q순위`=Dense/BM25/질문매칭 각 순위(999=미매칭).\n") | |
| print("> 참고: 저희는 별도 리랭커 단계가 없습니다(②③이 한 흐름). 감지 카테고리는 후보를 " | |
| "거르지 않고(전체 recall 유지) 해당 카테고리에 약한 가점만 주는 방식(소프트)입니다.\n") | |
| print("---\n") | |
| for query, expected, label in CASES: | |
| cat = _detect_category(query) | |
| intent = _detect_intent(query) | |
| # 실제 서빙(run_agent)과 동일하게 감지 카테고리를 소프트 가점으로 반영한다. | |
| res = eng.search(query=query, top_n=10, threshold=0.0, | |
| category_filter=cat, | |
| use_hyde=True, use_reranker=False) | |
| print(f"## 「{query}」 — {label}") | |
| if expected: | |
| print(f"- **기대 정답**: `{expected}`") | |
| print(f"\n**① 질의 분석**") | |
| print(f"- 감지 카테고리(시장/상품): `{cat or '없음 → 전체 검색'}` ← 순위에 소프트 가점") | |
| print(f"- 감지 의도(업무): `{intent}` ← (참고: 현재 순위에는 미반영)") | |
| if res: | |
| print(f"- 정규화 질의: `{res[0].get('_norm_query','')}`") | |
| hyde = res[0].get("_hyde_doc", "") | |
| if hyde: | |
| print(f"- HyDE 가상문서: {hyde[:90]}…") | |
| print(f"\n**②③ 후보 Top-10 (점수·부스트 반영 후)**\n") | |
| print("| 순위 | menu_id | 경로 | 최종 | cos | bm25 | D순위 | B순위 | Q순위 |") | |
| print("|---|---|---|---|---|---|---|---|---|") | |
| for i, r in enumerate(res[:10], 1): | |
| hit = "✅" if r["menu_id"] == expected else "" | |
| path = r["menu_path"][:32] | |
| print(f"| {i}{hit} | {r['menu_id']} | {path} | {r['similarity_pct']} | " | |
| f"{r['_dense']} | {r['_bm25']} | {r['_dense_rank']} | " | |
| f"{r['_bm25_rank']} | {r['_qi_rank']} |") | |
| top3 = res[:3] | |
| print(f"\n**④ 최종 노출 Top-3**") | |
| for i, r in enumerate(top3, 1): | |
| print(f"{i}. `{r['menu_id']}` {r['menu_name']} ({r['menu_path']}) — {r['similarity_pct']}") | |
| if expected: | |
| in3 = any(r["menu_id"] == expected for r in top3) | |
| print(f"\n→ 기대 정답 `{expected}` Top-3 {'포함 ✅' if in3 else '미포함 ❌'}") | |
| # 미해결 케이스 진단: 정답이 다른 카테고리라 소프트 가점으로도 못 올라오는지 | |
| if not in3: | |
| exp_cat = _top_category(expected) | |
| in_candidates = any(r["menu_id"] == expected for r in res) | |
| exp_rank = next((j for j, r in enumerate(res, 1) if r["menu_id"] == expected), None) | |
| if cat and exp_cat and cat != exp_cat: | |
| loc = f"후보 {exp_rank}위" if exp_rank else "후보 밖" | |
| print(f"\n> ⚠️ **원인**: 정답 `{expected}`는 `{exp_cat}` 소속인데 질의가 " | |
| f"`{cat}`로 분류됩니다. 소프트 전환으로 후보에는 살아남았으나" | |
| f"({loc}), `{cat}` 쪽 dense 유사도가 강해 아직 1위로는 못 올라옵니다. " | |
| f"업무 의도를 순위에 반영하면 해소될 케이스입니다.") | |
| else: | |
| # 관련성 컷오프 예시: 딱 맞는 메뉴가 없어 최상위도 cos(원 유사도)가 낮음 | |
| top_cos = res[0]["_dense"] if res else 0.0 | |
| print(f"\n> 📌 **컷오프 관점**: 이 질의는 딱 맞는 메뉴가 없어 최상위 후보도 " | |
| f"cos(원 코사인 유사도)가 `{top_cos}`로 낮습니다(정상 케이스는 보통 0.8+). " | |
| f"cos 기준 하한선을 두면 이런 관련성 낮은 후보 노출을 줄일 수 있습니다.") | |
| print("\n---\n") | |