# 영웅문 S# AI 메뉴 검색 시스템 — 아키텍처 & 워크플로우 > 사용자의 자연어 입력을 이해하고, 증권 HTS 앱(영웅문 S#)의 관련 메뉴를 추천하는 AI 검색 시스템 --- ## 목차 1. [프로젝트 개요](#1-프로젝트-개요) 2. [전체 아키텍처](#2-전체-아키텍처) 3. [데이터 파이프라인](#3-데이터-파이프라인) 4. [핵심 모듈](#4-핵심-모듈) 5. [검색 요청 상세 흐름](#5-검색-요청-상세-흐름) 6. [LangGraph 에이전트](#6-langgraph-에이전트) 7. [주요 설정값](#7-주요-설정값) 8. [프롬프트 구조](#8-프롬프트-구조) 9. [데이터 구조](#9-데이터-구조) 10. [성능 지표](#10-성능-지표) 11. [웹 UI](#11-웹-ui) 12. [Quick Start](#12-quick-start) --- ## 1. 프로젝트 개요 | 항목 | 내용 | |------|------| | **목적** | 사용자의 구어체 자연어 질문을 메뉴로 매핑 | | **대상 메뉴** | 영웅문 S# 앱 내 901개 메뉴 | | **검색 방식** | Dense (의미) + BM25 (키워드) + QueryIndex (쿼리↔쿼리) Weighted RRF | | **고도화** | HyDE · Contextual Retrieval · Cross-Encoder 리랭킹 · 쿼리 인덱스 | | **AI 에이전트** | LangGraph 기반, 품질 미달 시 GPT 쿼리 재작성 후 재검색 | | **임베딩 모델** | `BAAI/bge-m3` (100개 언어, ~570MB) | | **리랭킹 모델** | `BAAI/bge-reranker-v2-m3` (로컬) / Cohere Rerank 4 (API) | | **LLM** | Azure OpenAI `gpt-4.1-mini` | | **웹 UI** | Streamlit | ### 기술 스택 ``` Python 3.11+ ├── AI/ML │ ├── sentence-transformers # bge-m3 임베딩 + bge-reranker 리랭킹 │ ├── chromadb # 벡터 DB (HNSW 코사인 유사도) │ │ # herogun_menus : 901개 메뉴 벡터 │ │ # herogun_queries: 1,688개 샘플 쿼리 벡터 (Q-index) │ ├── rank-bm25 # BM25 키워드 검색 │ ├── langgraph # 멀티턴 검색 에이전트 그래프 │ ├── openai # Azure OpenAI (GPT-4.1-mini, HyDE·쿼리재작성) │ └── cohere # Cohere Rerank 4 (선택, API key 필요) ├── Web │ └── streamlit # 데모 웹 UI └── Data ├── jsonlines # JSONL 파일 처리 ├── openpyxl # Excel 리포트 생성 └── kiwipiepy # 한국어 형태소 분석 (BM25 옵션, 현재 비활성) ``` --- ## 2. 전체 아키텍처 ### 2-1. 데이터 파이프라인 (오프라인, 단계별 실행) ``` 원본 CSV (menu.csv) 1,568행 (전체 화면) │ ▼ [00_import_csv.py] │ IS_VISIBLE_MENU=true, 팝업 제외 │ data/raw/real_menus.json (901개) │ ├──▶ [12_update_keywords.py] 현업 키워드 Excel → keywords 갱신 │ ▼ [01_generate_descriptions.py] (또는 13·15 재생성) │ Azure GPT-4.1-mini (비동기 병렬, concurrency=10) │ · 기본 생성: 텍스트 전용 │ · 이미지 있는 메뉴: Vision 멀티모달 (concurrency=5) │ · 현업 피드백 있는 메뉴: feedback_map 주입 │ data/generated/menu_descriptions.jsonl (901개 × 8필드) │ ▼ [18_contextual_retrieval.py] │ 같은 카테고리 sibling 메뉴들을 컨텍스트로 제공 │ GPT가 각 메뉴별 1~2문장 식별문맥 생성 │ embedding_text 앞에 【식별문맥】 prepend │ ├──▶ [02_build_vectordb.py] ├──▶ [06_rebuild_bm25.py] │ bge-m3 임베딩 (배치, ~12분) │ BM25 Multi-field 인덱싱 │ 29 배치 × ~25초 │ keywords×2 + name×2 + text×1 │ │ (USE_KIWI=False, 공백 분리) ▼ ▼ data/chroma_db/herogun_menus data/bm25_index.pkl 901개 벡터 (HNSW, 코사인) BM25 역인덱스 │ ▼ [20_build_query_index.py --from-pool] 현업 검토 샘플 쿼리 임베딩 (eval 300개 제외) human 쿼리 + LLM top200 쿼리 (취소선 제외) ▼ data/chroma_db/herogun_queries 1,688개 샘플 쿼리 벡터 (Q-index) ``` ### 2-2. 검색 요청 흐름 (온라인, 실시간) ``` 사용자 입력: "주문 잘못 넣었는데 어떻게 취소해?" │ ▼ [app/streamlit_app.py] │ ▼ [core/agent.py — LangGraph] │ ├── 1. analyze_query (규칙 기반, LLM 없음) │ 카테고리 감지: "국내주식" │ 의도 감지: "주문" │ ├── 2. search_menus [core/search_engine.py] │ ├── 쿼리 정규화 (조사·어미 제거) │ ├── HyDE: GPT가 가상 메뉴 설명 생성 → Dense 검색 벡터로 활용 │ ├── Dense 검색 (ChromaDB herogun_menus, bge-m3) │ ├── Q-index 검색 (ChromaDB herogun_queries, 원본 쿼리 벡터) │ │ └── 유사 샘플 쿼리 → 해당 메뉴 RRF 부스트 │ ├── BM25 검색 (동의어 확장 후) │ ├── Weighted RRF 결합 (Dense×1.5 + BM25×0.5 + QIdx×0.4) │ ├── Search Count Additive Boost │ └── Cross-Encoder 리랭킹 (use_reranker=True 시) │ ├── 3. evaluate_results │ quality_score = top1×0.7 + top3avg×0.3 │ 임계값: 0.55 │ ├── [만족] ──▶ 4. generate_response → 결과 반환 │ └── [불만족, 최대 2회] ──▶ rewrite_query (GPT) ──▶ 2번 루프 ``` --- ## 3. 데이터 파이프라인 ### 스크립트 목록 | 스크립트 | 역할 | |----------|------| | `00_import_csv.py` | CSV → JSON 변환, 유효 메뉴 필터링 (901개) | | `01_generate_descriptions.py` | GPT로 메뉴별 설명 생성 (비동기 병렬) | | `02_build_vectordb.py` | bge-m3 임베딩 생성 & ChromaDB herogun_menus 저장 | | `06_rebuild_bm25.py` | BM25 인덱스 재구축 | | `07_export_top50_review.py` | 상위 100개 메뉴 검토용 Excel 출력 | | `12_update_keywords.py` | 현업 키워드 Excel → real_menus.json 갱신 | | `13_regen_with_images.py` | 변경 메뉴 이미지(Vision) 기반 재생성 | | `14_export_regen_review.py` | 재생성 결과 검토용 Excel 출력 (부서별 시트) | | `15_import_review_feedback.py` | 현업 피드백 Excel → 201개 메뉴 재생성 | | `16_eval_with_answer.py` | 구 정답셋 기반 Top5 정확도 평가 | | `17_eval_comparison.py` | bge-m3 / +HyDE / +HyDE+Rerank 3-way 비교 평가 | | `18_contextual_retrieval.py` | 카테고리별 식별문맥 생성 → embedding_text prepend | | `20_build_query_index.py` | 현업 검토 샘플 쿼리 → herogun_queries 컬렉션 구축 | | `21_build_eval300.py` | 300개 eval 셋 구축 (top200×270 + 201+×30, 누수 방지) | | `22_eval300.py` | 300개 eval 셋 기반 품질 평가 (Acc@1/3/5, MRR@5) | | `23_weight_grid.py` | Dense/BM25 가중치 그리드 서치 | | `24_bm25_ablation.py` | BM25 토크나이저 ablation 분석 | ### 현업 협업 워크플로우 ``` [현업] 키워드 Excel 작성 │ ▼ 12_update_keywords.py │ 키워드(현업) + 키워드(채널기획) union → real_menus.json │ ▼ 13_regen_with_images.py │ 변경 메뉴 재생성 (이미지 있는 메뉴 → GPT Vision) │ [현업] 화면 설명서 검토 (14_export_regen_review.py로 생성한 Excel) │ ▼ 15_import_review_feedback.py │ 수정 의견 201개 → feedback_map으로 GPT에 전달 → 재생성 │ ▼ 18_contextual_retrieval.py │ sibling 메뉴 컨텍스트로 식별문맥 생성 → embedding_text 강화 │ [현업] 샘플 쿼리 검토 (MenuSearch_review_all.xlsx) │ sample_queries(LLM 생성): 취소선으로 나쁜 쿼리 제거 │ Sample_queries(자체 생성): 직접 작성한 쿼리 추가 │ ▼ 21_build_eval300.py │ 300개 eval 셋 분리 저장 (data leakage 방지) │ ▼ 20_build_query_index.py --reset --from-pool │ 나머지 1,688개 쿼리 → herogun_queries 인덱싱 │ ▼ 02_build_vectordb.py + 06_rebuild_bm25.py 인덱스 재구축 완료 ``` ### eval 셋 구성 (data/eval_queries_300.json) | 구분 | 수량 | 설명 | |------|------|------| | top200 메뉴 | 270개 | 메뉴별 균등 stratified 샘플링, human 우선 | | 201+ 메뉴 | 30개 | 랜덤 샘플링 | | **합계** | **300개** | human 100개 + llm 200개 | - `data/eval_queries_300.json` : eval 쿼리 셋 - `data/query_index_pool.json` : eval 제외 후 Q-index 구축용 풀 (1,688개) - `data/eval_results_300.xlsx` : 평가 결과 (건별 O/~/X 색상, 요약 시트) --- ## 4. 핵심 모듈 ### 4-1. 검색 엔진 (`core/search_engine.py`) **Weighted RRF 3-way 하이브리드 + HyDE + 리랭킹** ``` 사용자 쿼리 │ ▼ 쿼리 정규화 (조사·어미 제거) │ ▼ HyDE (use_hyde=True 시) │ GPT-4.1-mini → 가상 메뉴 설명 생성 (temperature=0.1) │ 가상 문서를 Dense 검색 벡터로 활용 │ BM25 · Q-index는 원본 쿼리 벡터 사용 │ ├─────────────────┬──────────────────┬────────────────── ▼ ▼ ▼ Dense 검색 Q-index 검색 BM25 검색 (ChromaDB, (ChromaDB, (동의어 확장 후) herogun_menus, herogun_queries, 전체 901개 대상 전체 901개) 원본 쿼리 벡터) 유사도 ≥ 0.85만 RRF 반영 │ │ │ └─────────────────┴──────────────────┘ ▼ Weighted RRF 결합 score = Dense×1.5/(k+rank) + BM25×0.5/(k+rank) + QIdx×0.4/(k+rank) k = 60 (Cormack et al. 2009) ▼ Search Count Additive Boost score += 0.005 × (log1p(count)/log1p(MAX))² ▼ Threshold 필터 (≥ 0.3) 상위 10개 후보 선별 ▼ Cross-Encoder 리랭킹 (use_reranker=True 시) bge-reranker-v2-m3 또는 Cohere Rerank 4 ▼ Top-N 결과 반환 (기본 5개) ``` ### 4-2. 쿼리 인덱스 (`core/query_vectorstore.py`) **Query↔Query 매칭 — 어휘 갭 해소 핵심 신호** 현업이 직접 작성하거나 검토한 샘플 쿼리를 벡터로 저장, 사용자 입력과 매칭해 해당 메뉴를 RRF에서 부스트. ``` herogun_queries 컬렉션 (ChromaDB) 총 1,688개 샘플 쿼리 ├── human 118개 (현업 직접 작성) └── llm_top200 1,570개 (현업 취소선 검토 후 유지된 LLM 쿼리) 소스: MenuSearch_review_all.xlsx top200 메뉴 eval300 쿼리 제외 (data leakage 방지) ``` | 파라미터 | 값 | 설명 | |----------|----|------| | `W_QUERY_IDX` | `0.4` | Q-index RRF 가중치 (Dense/BM25 결과 뒤집지 않는 수준) | | `QUERY_IDX_THRESHOLD` | `0.85` | 코사인 유사도 임계값 (유사 메뉴 오탐 방지) | ### 4-3. 임베더 (`core/embedder.py`) | 항목 | 내용 | |------|------| | 모델 | `BAAI/bge-m3` | | 크기 | ~570MB | | 특징 | 100개 언어 지원, 한국어·영어 혼용 처리, MIRACL 벤치마크 SOTA | | 배치 처리 | 29배치, ~12분 (CPU 기준) | 임베딩 대상 = `embedding_text` 필드 (Contextual Retrieval 적용 시: `【식별문맥】 {1~2문장}\n\n{원본 250~400자}`) ### 4-4. HyDE 모듈 (`core/hyde_generator.py`) **Hypothetical Document Embedding** — Gao et al. 2022 기법 ```python # 쿼리 → GPT → 가상 메뉴 설명 → 임베딩 → Dense 검색 가상 문서 예시 (쿼리: "주문 잘못 넣었어"): "주문 내역을 확인하고 미체결 주문을 취소하거나 정정할 수 있습니다. 체결 전 주문을 수정·취소하는 기능을 제공합니다." ``` | 항목 | 내용 | |------|------| | 사용 모델 | `gpt-4.1-mini` (temperature=**0.1**, 일관된 전문 용어) | | 효과 | 쿼리-문서 어휘 갭 해소, HyDE OFF 대비 Acc@1 +7.6%p | | 기본값 | `use_hyde=True` | | Q-index와의 관계 | HyDE는 Dense 검색 벡터만 변경, Q-index는 항상 원본 쿼리 벡터 사용 | ### 4-5. Contextual Retrieval (`scripts/18_contextual_retrieval.py`) **Anthropic 2024 기법** — 유사 메뉴 간 혼동 감소 ```python # 같은 카테고리 sibling 메뉴들을 GPT에 제공 # GPT가 해당 메뉴만의 고유 기능·차이점을 1~2문장으로 생성 # embedding_text 앞에 prepend → 재임베딩 # 예시 (해외주식 > 주문 > 잔고) 【식별문맥】 해외주식 주문 카테고리 내 잔고 메뉴는 보유 해외주식의 실시간 잔고와 수익 현황을 원화 및 외화 기준으로 확인하는 기능으로, 단순 주문 실행이나 미체결 내역 조회와 구별됩니다. ``` | 항목 | 내용 | |------|------| | 처리 방식 | 오프라인 1회 처리 (per-query 비용 없음) | | 마커 | `【식별문맥】` (재실행 시 기존 문맥 자동 교체) | | 동시처리 | concurrency=15, 약 10분 소요 | ### 4-6. 벡터 스토어 (`core/vectorstore.py`) ```python ChromaDB PersistentClient 컬렉션: "herogun_menus" 유사도: 코사인 (HNSW 인덱싱) 저장 메타데이터: - menu_name, menu_path, category - search_count_norm # log1p 정규화 - keywords_text # 키워드 문자열 ``` ### 4-7. BM25 인덱스 (`core/bm25_index.py`) **Multi-field 가중치 인덱싱** ```python BM25 문서 = keywords × 2 + menu_name × 2 + embedding_text × 1 # 키워드 정확 매칭 시 BM25 점수 대폭 상승 ``` | 파라미터 | 값 | 설명 | |----------|----|------| | `USE_KIWI` | `False` | kiwipiepy 형태소 분석 사용 여부 | | 토크나이저 | 공백 분리 | kiwipiepy 복합명사 분리가 금융 도메인에서 역효과 (Acc@1 -10%p) | > **참고**: `USE_KIWI=True`로 변경 후 `06_rebuild_bm25.py` 실행 시 형태소 분석 적용 가능. > 단, eval 기준 공백 분리(47.7%)가 kiwipiepy(37.3%)보다 우수. ### 4-8. 리랭커 (`core/reranker.py` / `core/cohere_reranker.py`) **폴백 체인: Cohere Rerank 4 → bge-reranker-v2-m3** | 항목 | Cohere Rerank 4 | bge-reranker-v2-m3 | |------|----------------|-------------------| | 모델명 | `rerank-v3.5` | `BAAI/bge-reranker-v2-m3` | | 방식 | API (유료) | 로컬 (무료) | | API Key | 필요 (`COHERE_API_KEY`) | 불필요 | | 크기 | — | ~300MB | | 특징 | SOTA 상용 리랭커 | bge-m3 동일 패밀리, 100개 언어 | | 활성화 | `.env`에 키 입력 시 자동 | Cohere 실패 시 자동 폴백 | 입력: RRF Top-10 후보 → (query, document) 쌍 → Cross-Encoder 재점수 → 재정렬 ### 4-9. 쿼리 확장기 (`core/query_expander.py`) BM25 전용 HTS 도메인 동의어 확장 (Dense 검색에는 미적용) ```python SYNONYM_MAP 예시: "취소" → ["취소", "정정", "주문변경", "미체결", "정정취소"] "주문" → ["주문", "매수", "매도", "거래", "체결"] "잔고" → ["잔고", "보유", "수량", "평가금액", "자산"] "차트" → ["차트", "일봉", "주봉", "캔들", "그래프"] "수익" → ["수익", "손익", "수익률", "평가손익", "누적손익"] ``` ### 4-10. LLM 클라이언트 (`core/llm_client.py`) | 항목 | 내용 | |------|------| | 모델 | Azure OpenAI `gpt-4.1-mini` | | 엔드포인트 | `kiwoom-ai3-prod-oai-korea-central.openai.azure.com` | | temperature | 0.3 (설명 생성) / 0.1 (식별문맥) | | max_tokens | 1,500 (설명 생성) / 160 (식별문맥) | | 배치 처리 | 비동기 병렬, concurrency=10 (Vision 시 5) | | feedback_map | `{menu_id: feedback_text}` — 현업 피드백 GPT 힌트 전달 | --- ## 5. 검색 요청 상세 흐름 ``` ① 사용자 입력 (Streamlit) 예: "내 주식 얼마나 올랐어?" ② 쿼리 정규화 (search_engine.py) 조사·어미 제거 → "내 주식 얼마나 올랐" ③ 카테고리 감지 (agent.py — analyze_query) CATEGORY_RULES 키워드 2개 이상 매칭 시 필터 적용 ④ HyDE (use_hyde=True) GPT-4.1-mini → 가상 메뉴 설명 생성 (temperature=0.1) 가상 문서를 bge-m3로 임베딩 → Dense 검색 벡터로 사용 ⑤ Dense 검색 (ChromaDB herogun_menus, bge-m3) 가상 문서 벡터로 코사인 유사도 검색 (전체 901개) 카테고리 필터 적용 (감지된 경우) ⑥ Q-index 검색 (ChromaDB herogun_queries) 원본 쿼리 벡터로 1,688개 샘플 쿼리 검색 유사도 ≥ 0.85인 쿼리의 메뉴 → RRF 부스트 대상 선정 ⑦ BM25 검색 (bm25_index.py) 쿼리 동의어 확장 → BM25 점수 (전체 901개) ⑧ Weighted RRF 결합 Dense 기여(×1.5) + BM25 기여(×0.5) + Q-index 기여(×0.4) → RRF 통합 점수 + Search Count Additive Boost (0.005 × sc_norm²) ⑨ Cross-Encoder 리랭킹 (use_reranker=True 시) RRF Top-10 후보 → Cohere/bge-reranker → 최종 재정렬 ⑩ 품질 평가 (agent.py — evaluate_results) quality_score = top1_sim × 0.7 + avg(top3_sim) × 0.3 임계값 0.55 미달 시 → GPT 쿼리 재작성 (최대 2회) ⑪ 결과 반환 (Top-5, threshold ≥ 0.3) ``` --- ## 6. LangGraph 에이전트 ### 그래프 구조 (`core/agent.py`) ``` START │ ▼ [analyze_query] 규칙 기반 (LLM 없음) 카테고리·의도 감지 │ ▼ [search_menus] MenuSearchEngine.search() 호출 RRF 3-way 하이브리드 + HyDE + Q-index + 리랭킹 │ ▼ [evaluate_results] quality_score 계산 │ ├── 만족(≥0.55) 또는 재시도 2회 초과 ──▶ [generate_response] ──▶ END │ └── 불만족 + 재시도 < 2 ──▶ [rewrite_query] GPT-4.1-mini 쿼리 재작성 (temperature=0.3) └── 루프백 ──▶ [search_menus] ``` ### AgentState 구조 | 필드 | 타입 | 설명 | |------|------|------| | `original_query` | str | 사용자 원본 입력 (불변) | | `refined_query` | str | 현재 검색 쿼리 (재작성 시 갱신) | | `detected_category` | str\|None | 감지된 카테고리 | | `intent` | str | 감지된 의도 | | `search_results` | list | 검색 결과 | | `quality_score` | float | 검색 품질 점수 (0.0~1.0) | | `retry_count` | int | 재시도 횟수 (최대 2) | | `rewrite_history` | list | 쿼리 재작성 이력 | | `use_hyde` | bool | HyDE 활성 여부 (기본 True) | | `use_reranker` | bool | 리랭킹 활성 여부 | | `is_satisfactory` | bool | 품질 만족 여부 | --- ## 7. 주요 설정값 **파일: `config.py` / `core/search_engine.py`** | 설정 | 값 | 설명 | |------|----|------| | `EMBEDDING_MODEL_NAME` | `BAAI/bge-m3` | 다국어 임베딩 모델 | | `CHROMA_COLLECTION_NAME` | `herogun_menus` | 메뉴 벡터 ChromaDB 컬렉션 | | `LLM_MODEL` | `gpt-4.1-mini` | Azure 배포명 | | `AZURE_API_VERSION` | `2025-01-01-preview` | API 버전 | | `COHERE_API_KEY` | `.env` 설정 | Cohere Rerank 4 활성화 키 | | `DEFAULT_TOP_N` | `5` | 기본 검색 결과 수 | | `SIMILARITY_THRESHOLD` | `0.3` | 최소 유사도 임계값 | | `RRF_K` | `60` | RRF 상수 (Cormack et al. 2009) | | `W_DENSE` | `1.5` | Dense 검색 RRF 가중치 | | `W_BM25` | `0.5` | BM25 검색 RRF 가중치 | | `W_QUERY_IDX` | `0.4` | Q-index RRF 가중치 | | `QUERY_IDX_THRESHOLD` | `0.85` | Q-index 코사인 유사도 임계값 | | `SEARCH_COUNT_BOOST_WEIGHT` | `0.005` | 인기도 additive boost 가중치 | | `SEARCH_COUNT_MAX` | `20,260,108` | log1p 정규화 기준 최대값 | --- ## 8. 프롬프트 구조 **파일: `prompts/description_prompt.txt`** GPT가 각 메뉴에 대해 생성하는 필드: | 필드 | 내용 | 용도 | |------|------|------| | `function_desc` | 메뉴 주요 기능 2~3문장 | 현업 검토용 설명서 | | `action_types` | 이 메뉴에서 할 수 있는 행위 목록 | 의도 매핑 참고 | | `user_verbs` | 사용자가 쓰는 구어체 동사 | BM25 확장 참고 | | `keywords` | 핵심 검색어 (전문용어+일상어) | BM25 가중치 필드 | | `related_concepts` | 연관 금융 개념 | 검색 커버리지 확보 | | `sample_queries` | 실제 사용자 질의 8개 이상 | 현업 검토용 / Q-index 소스 | | `embedding_text` | **벡터 검색 핵심 문서** (250~400자) | **Dense 검색 대상** | **동적 변수:** - `{keywords_section}`: 현업 제공 키워드 (있을 때만 포함) - `{search_count_section}`: 인기 메뉴 안내 (10,000회 초과 시) - `{feedback_section}`: 현업 검토 의견 (있을 때만 포함, 반드시 반영 지시) --- ## 9. 데이터 구조 ### `data/raw/real_menus.json` ```json { "menu_id": "SCR_1000", "menu_name": "국내관심", "menu_path": "국내주식 > 관심종목 > 국내관심", "category": "국내주식", "screen_num": "1000", "search_count": 12739646, "keywords": ["관심종목", "관심그룹", "즐겨찾기"] } ``` ### `data/generated/menu_descriptions.jsonl` ```json { "menu_id": "SCR_1000", "menu_name": "국내관심", "menu_path": "국내주식 > 관심종목 > 국내관심", "function_desc": "사용자가 설정한 관심종목과 관심그룹을 한눈에 조회...", "action_types": ["조회", "설정", "수정", "관리"], "user_verbs": ["보고 싶어", "확인해", "찾아줘"], "keywords": ["관심종목", "관심그룹", "즐겨찾기", "시세", "변동률"], "related_concepts": ["시세", "변동률", "수익률"], "sample_queries": ["내 관심종목 보여줘", "관심그룹 어떻게 설정해", "..."], "embedding_text": "【식별문맥】 국내주식 관심종목 카테고리에서 국내관심 메뉴는...\n\n주식 투자할 때 자주 보는 종목이나 그룹을 빠르게 확인하고 싶을 때..." } ``` ※ `embedding_text`는 Contextual Retrieval 적용 후 `【식별문맥】` prefix가 prepend됨 ### `data/eval_queries_300.json` ```json { "id": 1, "query": "지금 내 주식 얼마야?", "menu_id": "SCR_1300", "menu_name": "국내잔고", "menu_path": "국내주식 > 잔고 > 국내잔고", "rank": 3, "source": "human" } ``` --- ## 10. 성능 지표 **평가 기준:** 300개 쿼리 eval 셋 (human 100개 + llm 200개) **평가 스크립트:** `scripts/22_eval300.py` ### 최신 결과 (HyDE OFF / ON 비교) | 지표 | HyDE OFF | HyDE ON | 향상 | |------|----------|---------|------| | **Acc@1** | 47.7% | **55.3%** | +7.6%p | | **Acc@3** | 70.3% | **77.0%** | +6.7%p | | **Acc@5** | 75.7% | **84.0%** | +8.3%p | | **MRR@5** | 0.591 | **0.667** | +0.076 | | Top5 오답 | 73건 | **48건** | -25건 | ### 소스별 (HyDE ON) | 출처 | n | Acc@1 | Acc@3 | Acc@5 | |------|---|-------|-------|-------| | human (현업 직접 작성) | 100 | 41.0% | 66.0% | 77.0% | | llm (현업 검토 LLM 쿼리) | 200 | 62.5% | 82.5% | 87.5% | ### 순위 구간별 (HyDE ON) | 구간 | n | Acc@1 | Acc@5 | |------|---|-------|-------| | top200 메뉴 | 270 | 54.4% | 83.3% | | 201+ 메뉴 | 30 | 63.3% | 90.0% | ### 고도화 기법별 기여 | 기법 | 주요 효과 | |------|----------| | bge-m3 임베딩 | 다국어 이해력, 한국어 구어체 처리 | | HyDE | 쿼리-문서 어휘 갭 해소 (+7.6%p Acc@1 vs OFF) | | Contextual Retrieval | 유사 메뉴명 간 혼동 감소 | | Q-index (Query↔Query) | 현업 쿼리 패턴 직접 매칭, 어휘 갭 해소 | | Weighted RRF (1.5:0.5:0.4) | 3-way 신호 결합, BM25 보조 강화 | | Search Count Boost | 인기 메뉴 우선 노출 (0.005×sc_norm²) | | Cross-Encoder 리랭킹 | RRF 상위 후보 정밀 재정렬 | ### BM25 토크나이저 비교 (HyDE OFF) | 토크나이저 | Acc@1 | 비고 | |-----------|-------|------| | 공백 분리 (현재) | **47.7%** | 기본 방식 | | kiwipiepy 형태소 | 37.3% | 금융 복합명사 분리로 역효과 | --- ## 11. 웹 UI **파일: `app/streamlit_app.py`** ``` ┌──────────────────────────────────────────────────────────┐ │ 영웅문 S# AI 메뉴 검색 │ │ 자연어로 원하는 기능을 입력하면 관련 메뉴를 찾아드립니다. │ ├─────────────────┬────────────────────────────────────────┤ │ 검색 옵션 │ [검색창: 자연어 입력] │ │ │ │ │ 카테고리 필터 │ 빠른 검색 (12개 버튼, 2행): │ │ [전체 ▼] │ [내 주식 얼마야?] [주문 취소] ... │ │ │ [내 수익률 어때?] [달러 환전] ... │ │ 등록된 메뉴 수 │ │ │ 901 │ "주문 취소" 검색 결과 (5개) │ │ │ │ │ │ ▼ 1위 주문 취소/정정 `94.2%` │ │ │ 경로: 국내주식 > 주문 > ... │ │ │ 메뉴 ID: SCR_xxxx │ │ │ ▼ 2위 미체결 주문조회 `87.1%` │ │ │ ▷ 3위 ... │ └─────────────────┴────────────────────────────────────────┘ ``` **주요 설정:** | 설정 | 값 | |------|----| | Top-N | 5개 | | 최소 유사도 | 0.3 | | HyDE | 활성 (`use_hyde=True`) | | 리랭킹 | 비활성 (`use_reranker=False`) | | 빠른 검색 | 12개 (2행 × 6개) | **실행:** ```bash .venv\Scripts\streamlit.exe run app/streamlit_app.py # 접속: http://localhost:8501 # 외부 공유: ngrok http 8501 ``` --- ## 12. Quick Start ### 최초 환경 세팅 ```bash python -m venv .venv .venv\Scripts\pip install -r requirements.txt # .env 파일 설정 AZURE_KEY=... AZURE_GPT41_MINI_ENDPOINT=... AZURE_GPT41_MINI_API_VERSION=2025-01-01-preview COHERE_API_KEY= # 선택: Cohere Rerank 4 활성화 ``` ### 데이터 파이프라인 (최초 1회) ```bash # 메뉴 설명 생성 .venv\Scripts\python.exe scripts/01_generate_descriptions.py # 벡터 DB 구축 (~12분) .venv\Scripts\python.exe scripts/02_build_vectordb.py --reset # BM25 인덱스 구축 .venv\Scripts\python.exe scripts/06_rebuild_bm25.py ``` ### 쿼리 인덱스 구축 (Q-index) ```bash # 1. eval 300개 분리 (최초 1회) .venv\Scripts\python.exe scripts/21_build_eval300.py # 2. eval 제외 나머지로 Q-index 구축 .venv\Scripts\python.exe scripts/20_build_query_index.py --reset --from-pool ``` ### 고도화 파이프라인 (선택 실행) ```bash # 현업 키워드 반영 + 이미지 기반 재생성 .venv\Scripts\python.exe scripts/12_update_keywords.py .venv\Scripts\python.exe scripts/13_regen_with_images.py # 현업 피드백 반영 재생성 .venv\Scripts\python.exe scripts/15_import_review_feedback.py # Contextual Retrieval 적용 .venv\Scripts\python.exe scripts/18_contextual_retrieval.py # 인덱스 재구축 .venv\Scripts\python.exe scripts/02_build_vectordb.py --reset .venv\Scripts\python.exe scripts/06_rebuild_bm25.py ``` ### 검색 품질 평가 ```bash # 300개 eval 셋 기반 평가 (HyDE OFF) .venv\Scripts\python.exe scripts/22_eval300.py --no-hyde # HyDE ON 평가 (LLM API 호출, 느림) .venv\Scripts\python.exe scripts/22_eval300.py # 결과 엑셀 저장 .venv\Scripts\python.exe scripts/22_eval300.py --no-hyde --save-xlsx # → data/eval_results_300.xlsx ``` ### 데모 앱 실행 ```bash .venv\Scripts\streamlit.exe run app/streamlit_app.py # 외부 공유: ngrok http 8501 ``` --- *최종 업데이트: 2026-06-02*