AI_Menu_Search / scripts /41_debug_dump.py
Juhaha
chore: 디버그 로그 덤프 케이스 확대 + 소프트 필터/컷오프 반영
d42a8d4
Raw
History Blame Contribute Delete
5.91 kB
"""
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")