# -*- coding: utf-8 -*- """ 영웅문 S# AI 메뉴 검색 — 아키텍처 Word 문서 생성 출력: 영웅문S#_AI메뉴검색_아키텍처.docx """ from docx import Document from docx.shared import Pt, RGBColor, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.table import WD_ALIGN_VERTICAL from docx.oxml.ns import qn from docx.oxml import OxmlElement OUTPUT_PATH = r"C:\Users\leeju\Documents\s#ai메뉴\prototype\영웅문S#_AI메뉴검색_아키텍처.docx" # ── 색상 (hex 문자열) ───────────────────────────────────────────────────────── HEX_DARK_BLUE = "1F4E79" HEX_MID_BLUE = "2E75B6" HEX_LIGHT_BLUE = "9DC3E6" HEX_DARK_GRAY = "404040" HEX_CODE_BG = "F0F0F0" HEX_WHITE = "FFFFFF" HEX_ROW_ALT = "DEF0F7" def _rgb(hex6): r = int(hex6[0:2], 16) g = int(hex6[2:4], 16) b = int(hex6[4:6], 16) return RGBColor(r, g, b) FONT_NAME = "맑은 고딕" # ── 헬퍼 ───────────────────────────────────────────────────────────────────── def _set_cell_bg(cell, hex6: str): tc = cell._tc tcPr = tc.get_or_add_tcPr() shd = OxmlElement("w:shd") shd.set(qn("w:val"), "clear") shd.set(qn("w:color"), "auto") shd.set(qn("w:fill"), hex6) tcPr.append(shd) def _set_para_bg(para, hex6: str): pPr = para._p.get_or_add_pPr() shd = OxmlElement("w:shd") shd.set(qn("w:val"), "clear") shd.set(qn("w:color"), "auto") shd.set(qn("w:fill"), hex6) pPr.append(shd) def _para_space(para, before_pt=0, after_pt=4): pf = para.paragraph_format pf.space_before = Pt(before_pt) pf.space_after = Pt(after_pt) pf.line_spacing = Pt(15) def heading1(doc, text): para = doc.add_paragraph() _para_space(para, before_pt=16, after_pt=6) run = para.add_run(text) run.bold = True run.font.name = FONT_NAME run.font.size = Pt(16) run.font.color.rgb = _rgb(HEX_DARK_BLUE) pPr = para._p.get_or_add_pPr() pBdr = OxmlElement("w:pBdr") bottom = OxmlElement("w:bottom") bottom.set(qn("w:val"), "single") bottom.set(qn("w:sz"), "6") bottom.set(qn("w:space"), "1") bottom.set(qn("w:color"), HEX_DARK_BLUE) pBdr.append(bottom) pPr.append(pBdr) return para def heading2(doc, text): para = doc.add_paragraph() _para_space(para, before_pt=12, after_pt=4) run = para.add_run(text) run.bold = True run.font.name = FONT_NAME run.font.size = Pt(13) run.font.color.rgb = _rgb(HEX_MID_BLUE) return para def heading3(doc, text): para = doc.add_paragraph() _para_space(para, before_pt=8, after_pt=3) run = para.add_run(text) run.bold = True run.font.name = FONT_NAME run.font.size = Pt(11) run.font.color.rgb = _rgb(HEX_DARK_BLUE) return para def body(doc, text, indent_cm=0): para = doc.add_paragraph() _para_space(para, after_pt=4) if indent_cm: para.paragraph_format.left_indent = Cm(indent_cm) run = para.add_run(text) run.font.name = FONT_NAME run.font.size = Pt(10.5) run.font.color.rgb = _rgb(HEX_DARK_GRAY) return para def code_block(doc, text): para = doc.add_paragraph() _para_space(para, before_pt=2, after_pt=4) para.paragraph_format.left_indent = Cm(0.5) para.paragraph_format.right_indent = Cm(0.5) _set_para_bg(para, HEX_CODE_BG) run = para.add_run(text) run.font.name = "Courier New" run.font.size = Pt(9) run.font.color.rgb = _rgb("202020") return para def make_table(doc, headers, rows, col_widths_cm=None): n_col = len(headers) table = doc.add_table(rows=1 + len(rows), cols=n_col) table.style = "Table Grid" # 헤더 행 hdr = table.rows[0] for i, h in enumerate(headers): cell = hdr.cells[i] _set_cell_bg(cell, HEX_DARK_BLUE) cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER para = cell.paragraphs[0] para.paragraph_format.space_before = Pt(3) para.paragraph_format.space_after = Pt(3) run = para.add_run(h) run.bold = True run.font.name = FONT_NAME run.font.size = Pt(10) run.font.color.rgb = _rgb(HEX_WHITE) # 데이터 행 for r_idx, row_data in enumerate(rows): row = table.rows[r_idx + 1] bg = HEX_ROW_ALT if r_idx % 2 == 0 else HEX_WHITE for c_idx, cell_text in enumerate(row_data): cell = row.cells[c_idx] _set_cell_bg(cell, bg) cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER para = cell.paragraphs[0] para.paragraph_format.space_before = Pt(2) para.paragraph_format.space_after = Pt(2) run = para.add_run(str(cell_text)) run.font.name = FONT_NAME run.font.size = Pt(9.5) run.font.color.rgb = _rgb(HEX_DARK_GRAY) if col_widths_cm: for r in table.rows: for i, w in enumerate(col_widths_cm): if i < len(r.cells): r.cells[i].width = Cm(w) doc.add_paragraph() return table # ═══════════════════════════════════════════════════════════════════════════════ # 문서 생성 # ═══════════════════════════════════════════════════════════════════════════════ def build(): doc = Document() for section in doc.sections: section.top_margin = Cm(2.5) section.bottom_margin = Cm(2.5) section.left_margin = Cm(3.0) section.right_margin = Cm(2.5) # ── 표지 ──────────────────────────────────────────────────────────────── doc.add_paragraph("\n\n\n") title_para = doc.add_paragraph() title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER _para_space(title_para, after_pt=10) run = title_para.add_run("영웅문 S# AI 메뉴 검색 시스템") run.bold = True run.font.name = FONT_NAME run.font.size = Pt(22) run.font.color.rgb = _rgb(HEX_DARK_BLUE) sub_para = doc.add_paragraph() sub_para.alignment = WD_ALIGN_PARAGRAPH.CENTER _para_space(sub_para, after_pt=6) run2 = sub_para.add_run("아키텍처 & 워크플로우 기술 문서") run2.bold = True run2.font.name = FONT_NAME run2.font.size = Pt(15) run2.font.color.rgb = _rgb(HEX_MID_BLUE) doc.add_paragraph() date_para = doc.add_paragraph() date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER run3 = date_para.add_run("2026년 5월 14일") run3.font.name = FONT_NAME run3.font.size = Pt(11) run3.font.color.rgb = _rgb(HEX_DARK_GRAY) doc.add_page_break() # ── 1. 프로젝트 개요 ──────────────────────────────────────────────────── heading1(doc, "1. 프로젝트 개요") body(doc, "영웅문 S# AI 메뉴 검색은 사용자의 구어체 자연어 질문을 이해하여 " "증권 HTS 앱(영웅문 S#)의 관련 메뉴를 정확하게 추천하는 AI 검색 시스템입니다. " "901개 메뉴를 의미 기반(Dense)·키워드(BM25) 하이브리드 검색과 " "LangGraph 에이전트로 처리합니다.") make_table(doc, ["항목", "내용"], [ ["목적", "사용자의 구어체 자연어 질문을 메뉴로 매핑"], ["대상 메뉴", "영웅문 S# 앱 내 901개 메뉴"], ["검색 방식", "Dense (의미) + BM25 (키워드) 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"], ], col_widths_cm=[4.5, 12] ) heading2(doc, "기술 스택") code_block(doc, "Python 3.11+\n" "├── AI/ML\n" "│ ├── sentence-transformers # bge-m3 임베딩 + bge-reranker 리랭킹\n" "│ ├── chromadb # 벡터 DB (HNSW 코사인 유사도)\n" "│ ├── rank-bm25 # BM25 키워드 검색\n" "│ ├── langgraph # 멀티턴 검색 에이전트 그래프\n" "│ ├── openai # Azure OpenAI (GPT-4.1-mini)\n" "│ └── cohere # Cohere Rerank 4 (선택, API key 필요)\n" "├── Web\n" "│ └── streamlit # 데모 웹 UI\n" "└── Data\n" " ├── jsonlines # JSONL 파일 처리\n" " └── openpyxl # Excel 리포트 생성" ) doc.add_page_break() # ── 2. 전체 아키텍처 ───────────────────────────────────────────────────── heading1(doc, "2. 전체 아키텍처") heading2(doc, "2-1. 데이터 파이프라인 (오프라인, 단계별 실행)") body(doc, "원본 CSV(menu.csv, 1,568행)에서 유효 메뉴를 필터링한 뒤 " "GPT 설명 생성 → 임베딩 → 인덱싱 순으로 처리합니다.") code_block(doc, "원본 CSV (menu.csv) — 1,568행\n" " │\n" " ▼ [00_import_csv.py] IS_VISIBLE_MENU=true, 팝업 제외\n" " │\n" "data/raw/real_menus.json (901개)\n" " │\n" " ├──▶ [12_update_keywords.py] 현업 키워드 Excel → keywords 갱신\n" " │\n" " ▼ [01_generate_descriptions.py] (또는 13·15 재생성)\n" " │ Azure GPT-4.1-mini (비동기 병렬, concurrency=10)\n" " │ · 기본 생성: 텍스트 전용\n" " │ · 이미지 있는 메뉴: Vision 멀티모달 (concurrency=5)\n" " │ · 현업 피드백 있는 메뉴: feedback_map 주입\n" " │\n" "data/generated/menu_descriptions.jsonl (901개 × 8필드)\n" " │\n" " ▼ [18_contextual_retrieval.py]\n" " │ 같은 카테고리 sibling 메뉴들을 컨텍스트로 제공\n" " │ GPT가 각 메뉴별 1~2문장 식별문맥 생성\n" " │ embedding_text 앞에 【식별문맥】 prepend\n" " │\n" " ├──▶ [02_build_vectordb.py] ├──▶ [06_rebuild_bm25.py]\n" " │ bge-m3 임베딩 (배치, ~12분) │ BM25 Multi-field 인덱싱\n" " │ │\n" " ▼ ▼\n" "data/chroma_db/ data/bm25_index.pkl\n" " 901개 벡터 (HNSW, 코사인) BM25 역인덱스" ) heading2(doc, "2-2. 검색 요청 흐름 (온라인, 실시간)") code_block(doc, "사용자 입력: \"주문 잘못 넣었는데 어떻게 취소해?\"\n" " │\n" " ▼ [app/streamlit_app.py]\n" " │\n" " ▼ [core/agent.py — LangGraph]\n" " │\n" " ├── 1. analyze_query (규칙 기반, LLM 없음)\n" " │ 카테고리 감지: \"국내주식\" | 의도 감지: \"주문\"\n" " │\n" " ├── 2. search_menus [core/search_engine.py]\n" " │ ├── 쿼리 정규화 (조사·어미 제거)\n" " │ ├── HyDE: GPT가 가상 메뉴 설명 생성 → Dense 검색 벡터로 활용\n" " │ ├── Dense 검색 (ChromaDB, bge-m3)\n" " │ ├── BM25 검색 (동의어 확장 후)\n" " │ ├── Weighted RRF 결합 (Dense×1.5 + BM25×0.5)\n" " │ ├── Search Count Additive Boost\n" " │ └── Cross-Encoder 리랭킹 (use_reranker=True 시)\n" " │\n" " ├── 3. evaluate_results\n" " │ quality_score = top1×0.7 + top3avg×0.3 | 임계값: 0.55\n" " │\n" " ├── [만족] ──▶ 4. generate_response → 결과 반환\n" " │\n" " └── [불만족, 최대 2회] ──▶ rewrite_query (GPT) ──▶ 루프백 search_menus" ) doc.add_page_break() # ── 3. 데이터 파이프라인 ───────────────────────────────────────────────── heading1(doc, "3. 데이터 파이프라인") heading2(doc, "스크립트 목록") make_table(doc, ["스크립트", "역할"], [ ["00_import_csv.py", "CSV → JSON 변환, 유효 메뉴 필터링 (901개)"], ["01_generate_descriptions.py", "GPT로 메뉴별 설명 생성 (비동기 병렬)"], ["02_build_vectordb.py", "bge-m3 임베딩 생성 & ChromaDB 저장"], ["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"], ], col_widths_cm=[6, 10.5] ) heading2(doc, "현업 협업 워크플로우") code_block(doc, "[현업] 키워드 Excel 작성\n" " │\n" " ▼ 12_update_keywords.py\n" " │ 키워드(현업) + 키워드(채널기획) union → real_menus.json\n" " │\n" " ▼ 13_regen_with_images.py\n" " │ 변경 메뉴 재생성 (이미지 있는 메뉴 → GPT Vision)\n" " │\n" "[현업] 화면 설명서 검토 (14_export_regen_review.py 생성 Excel)\n" " │\n" " ▼ 15_import_review_feedback.py\n" " │ 수정 의견 201개 → feedback_map으로 GPT에 전달 → 재생성\n" " │\n" " ▼ 18_contextual_retrieval.py\n" " │ sibling 메뉴 컨텍스트로 식별문맥 생성 → embedding_text 강화\n" " │\n" " ▼ 02_build_vectordb.py + 06_rebuild_bm25.py\n" " 인덱스 재구축 완료" ) doc.add_page_break() # ── 4. 핵심 모듈 ────────────────────────────────────────────────────────── heading1(doc, "4. 핵심 모듈") heading2(doc, "4-1. 검색 엔진 (core/search_engine.py)") body(doc, "Weighted RRF 하이브리드 검색에 HyDE와 Cross-Encoder 리랭킹을 결합합니다.") code_block(doc, "사용자 쿼리\n" " │\n" " ▼ 쿼리 정규화 (조사·어미 제거)\n" " │\n" " ▼ HyDE (use_hyde=True 시)\n" " │ GPT-4.1-mini → 가상 메뉴 설명 생성 → Dense 검색 벡터로 활용\n" " │\n" " ├─────────────────────┬─────────────────────\n" " ▼ ▼\n" "Dense 검색 BM25 검색\n" "(ChromaDB, bge-m3) (동의어 확장 후)\n" "전체 901개 대상 전체 901개 대상\n" " │ │\n" " └────────────────────┘\n" " ▼\n" " Weighted RRF 결합\n" " score = Dense×1.5/(k+rank) + BM25×0.5/(k+rank) [k=60]\n" " ▼\n" " Search Count Additive Boost\n" " score += 0.10 × (log1p(count)/log1p(MAX))²\n" " ▼\n" " Threshold 필터 (≥ 0.3) → 상위 10개 후보\n" " ▼\n" " Cross-Encoder 리랭킹 (use_reranker=True 시)\n" " ▼\n" " Top-N 결과 반환 (기본 5개)" ) heading2(doc, "4-2. 임베더 (core/embedder.py)") make_table(doc, ["항목", "내용"], [ ["모델", "BAAI/bge-m3"], ["크기", "~570MB"], ["특징", "100개 언어 지원, 한국어·영어 혼용 처리, MIRACL 벤치마크 SOTA"], ["배치 처리", "29배치, ~12분 (CPU 기준)"], ["임베딩 대상", "embedding_text 필드 (Contextual Retrieval 적용 시 【식별문맥】 prefix 포함)"], ], col_widths_cm=[4.5, 12] ) heading2(doc, "4-3. HyDE 모듈 (core/hyde.py)") body(doc, "Hypothetical Document Embedding(Gao et al. 2022) 기법을 적용합니다. " "GPT-4.1-mini가 입력 쿼리를 메뉴 설명으로 변환하고, " "이 가상 문서의 임베딩을 Dense 검색 벡터로 사용합니다.") code_block(doc, "# 쿼리 → GPT → 가상 메뉴 설명 → 임베딩 → Dense 검색\n" "가상 문서 예시 (쿼리: \"주문 잘못 넣었어\"):\n" " \"주문 내역을 확인하고 미체결 주문을 취소하거나 정정할 수 있습니다.\n" " 체결 전 주문을 수정·취소하는 기능을 제공합니다.\"" ) make_table(doc, ["항목", "내용"], [ ["사용 모델", "gpt-4.1-mini (temperature=0.3)"], ["효과", "쿼리-문서 어휘 갭 해소, Acc@1 +9.8%p"], ["기본값", "use_hyde=True"], ], col_widths_cm=[4.5, 12] ) heading2(doc, "4-4. Contextual Retrieval (scripts/18_contextual_retrieval.py)") body(doc, "Anthropic 2024 기법을 적용하여 유사 메뉴 간 혼동을 감소시킵니다. " "같은 카테고리 sibling 메뉴들을 GPT에 제공하면 " "GPT가 해당 메뉴만의 고유 기능·차이점을 1~2문장으로 생성하고, " "이를 embedding_text 앞에 prepend합니다.") code_block(doc, "# 예시 (해외주식 > 주문 > 잔고)\n" "【식별문맥】 해외주식 주문 카테고리 내 잔고 메뉴는 보유 해외주식의\n" "실시간 잔고와 수익 현황을 원화 및 외화 기준으로 확인하는 기능으로,\n" "단순 주문 실행이나 미체결 내역 조회와 구별됩니다." ) make_table(doc, ["항목", "내용"], [ ["처리 방식", "오프라인 1회 처리 (per-query 비용 없음)"], ["마커", "【식별문맥】 (재실행 시 기존 문맥 자동 교체)"], ["동시처리", "concurrency=15, 약 10분 소요"], ], col_widths_cm=[4.5, 12] ) heading2(doc, "4-5. 벡터 스토어 (core/vectorstore.py)") code_block(doc, "ChromaDB PersistentClient\n" " 컬렉션: \"herogun_menus\"\n" " 유사도: 코사인 (HNSW 인덱싱)\n" " 저장 메타데이터:\n" " - menu_name, menu_path, category\n" " - search_count_norm # log1p 정규화\n" " - keywords_text # 키워드 문자열" ) heading2(doc, "4-6. BM25 인덱스 (core/bm25_index.py)") body(doc, "Multi-field 가중치 인덱싱으로 키워드 정확 매칭 시 점수를 대폭 높입니다.") code_block(doc, "BM25 문서 = keywords × 3 + menu_name × 2 + embedding_text × 1") heading2(doc, "4-7. 리랭커 (core/reranker.py / core/cohere_reranker.py)") body(doc, "폴백 체인으로 동작합니다. COHERE_API_KEY가 설정되면 Cohere Rerank 4를 우선 사용하고, " "미설정 또는 실패 시 로컬 bge-reranker-v2-m3로 자동 폴백합니다.") make_table(doc, ["항목", "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 실패 시 자동 폴백"], ], col_widths_cm=[4, 7, 5.5] ) body(doc, "입력: RRF Top-10 후보 → (query, document) 쌍 → Cross-Encoder 재점수 → 재정렬") heading2(doc, "4-8. 쿼리 확장기 (core/query_expander.py)") body(doc, "BM25 전용 HTS 도메인 동의어 확장을 수행합니다. Dense 검색에는 미적용됩니다.") code_block(doc, "SYNONYM_MAP 예시:\n" " \"취소\" → [\"취소\", \"정정\", \"주문변경\", \"미체결\", \"정정취소\"]\n" " \"주문\" → [\"주문\", \"매수\", \"매도\", \"거래\", \"체결\"]\n" " \"잔고\" → [\"잔고\", \"보유\", \"수량\", \"평가금액\", \"자산\"]\n" " \"차트\" → [\"차트\", \"일봉\", \"주봉\", \"캔들\", \"그래프\"]\n" " \"수익\" → [\"수익\", \"손익\", \"수익률\", \"평가손익\", \"누적손익\"]" ) heading2(doc, "4-9. LLM 클라이언트 (core/llm_client.py)") make_table(doc, ["항목", "내용"], [ ["모델", "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 힌트 전달"], ], col_widths_cm=[4.5, 12] ) doc.add_page_break() # ── 5. 검색 요청 상세 흐름 ─────────────────────────────────────────────── heading1(doc, "5. 검색 요청 상세 흐름") steps = [ ("① 사용자 입력 (Streamlit)", "예: \"내 주식 얼마나 올랐어?\""), ("② 쿼리 정규화 (search_engine.py)", "조사·어미 제거"), ("③ 카테고리 감지 (agent.py — analyze_query)", "CATEGORY_RULES 키워드 2개 이상 매칭 시 필터 적용"), ("④ HyDE (use_hyde=True)", "GPT-4.1-mini → 가상 메뉴 설명 생성. 가상 문서를 bge-m3로 임베딩 → Dense 검색 벡터로 사용"), ("⑤ Dense 검색 (ChromaDB, bge-m3)", "가상 문서 벡터로 코사인 유사도 검색 (전체 901개). 카테고리 필터 적용"), ("⑥ BM25 검색 (bm25_index.py)", "쿼리 동의어 확장 → BM25 점수 (전체 901개)"), ("⑦ Weighted RRF 결합", "Dense 기여(×1.5) + BM25 기여(×0.5) → RRF 통합 점수 + Search Count Additive Boost"), ("⑧ 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"), ] for step_title, step_body in steps: p = doc.add_paragraph() _para_space(p, before_pt=4, after_pt=3) p.paragraph_format.left_indent = Cm(0.3) run_t = p.add_run(step_title + " ") run_t.bold = True run_t.font.name = FONT_NAME run_t.font.size = Pt(10.5) run_t.font.color.rgb = _rgb(HEX_DARK_BLUE) run_b = p.add_run(step_body) run_b.font.name = FONT_NAME run_b.font.size = Pt(10) run_b.font.color.rgb = _rgb(HEX_DARK_GRAY) doc.add_page_break() # ── 6. LangGraph 에이전트 ──────────────────────────────────────────────── heading1(doc, "6. LangGraph 에이전트") heading2(doc, "그래프 구조 (core/agent.py)") code_block(doc, "START\n" " │\n" " ▼\n" "[analyze_query] 규칙 기반 (LLM 없음) — 카테고리·의도 감지\n" " │\n" " ▼\n" "[search_menus] MenuSearchEngine.search() 호출\n" " RRF 하이브리드 + HyDE + 리랭킹\n" " │\n" " ▼\n" "[evaluate_results] quality_score 계산\n" " │\n" " ├── 만족(≥0.55) 또는 재시도 2회 초과\n" " │ └──▶ [generate_response] ──▶ END\n" " │\n" " └── 불만족 + 재시도 < 2\n" " └──▶ [rewrite_query] GPT-4.1-mini 쿼리 재작성\n" " └── 루프백 ──▶ [search_menus]" ) heading2(doc, "AgentState 구조") make_table(doc, ["필드", "타입", "설명"], [ ["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", "품질 만족 여부"], ], col_widths_cm=[4.5, 3, 9] ) # ── 7. 주요 설정값 ──────────────────────────────────────────────────────── heading1(doc, "7. 주요 설정값") body(doc, "파일: config.py") make_table(doc, ["설정", "값", "설명"], [ ["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", "최소 유사도 임계값"], ["QUALITY_THRESHOLD", "0.55", "에이전트 재시도 임계값"], ["RRF_K", "60", "RRF 상수 (Cormack et al. 2009)"], ["W_DENSE", "1.5", "Dense 검색 RRF 가중치"], ["W_BM25", "0.5", "BM25 검색 RRF 가중치"], ["SEARCH_COUNT_BOOST_WEIGHT", "0.10", "인기도 부스트 최대 가중치"], ], col_widths_cm=[5.5, 4, 7] ) # ── 8. 프롬프트 구조 ───────────────────────────────────────────────────── heading1(doc, "8. 프롬프트 구조") body(doc, "파일: prompts/description_prompt.txt") body(doc, "GPT가 각 메뉴에 대해 생성하는 필드:") make_table(doc, ["필드", "내용", "용도"], [ ["function_desc", "메뉴 주요 기능 2~3문장", "현업 검토용 설명서"], ["action_types", "이 메뉴에서 할 수 있는 행위 목록", "의도 매핑 참고"], ["user_verbs", "사용자가 쓰는 구어체 동사", "BM25 확장 참고"], ["keywords", "핵심 검색어 (전문용어+일상어)", "BM25 가중치 필드"], ["related_concepts", "연관 금융 개념", "검색 커버리지 확보"], ["sample_queries", "실제 사용자 질의 8개 이상", "현업 검토용"], ["embedding_text", "벡터 검색 핵심 문서 (250~400자)", "Dense 검색 대상"], ], col_widths_cm=[4.5, 7, 5] ) heading2(doc, "동적 변수") make_table(doc, ["변수", "설명"], [ ["{keywords_section}", "현업 제공 키워드 (있을 때만 포함)"], ["{search_count_section}", "인기 메뉴 안내 (10,000회 초과 시)"], ["{feedback_section}", "현업 검토 의견 (있을 때만 포함, 반드시 반영 지시)"], ], col_widths_cm=[5, 11.5] ) doc.add_page_break() # ── 9. 데이터 구조 ──────────────────────────────────────────────────────── heading1(doc, "9. 데이터 구조") heading2(doc, "data/raw/real_menus.json") code_block(doc, "{\n" " \"menu_id\": \"SCR_1000\",\n" " \"menu_name\": \"국내관심\",\n" " \"menu_path\": \"국내주식 > 관심종목 > 국내관심\",\n" " \"category\": \"국내주식\",\n" " \"screen_num\": \"1000\",\n" " \"search_count\": 12739646,\n" " \"keywords\": [\"관심종목\", \"관심그룹\", \"즐겨찾기\"]\n" "}" ) heading2(doc, "data/generated/menu_descriptions.jsonl") code_block(doc, "{\n" " \"menu_id\": \"SCR_1000\",\n" " \"function_desc\": \"사용자가 설정한 관심종목과 관심그룹을 한눈에 조회...\",\n" " \"action_types\": [\"조회\", \"설정\", \"수정\", \"관리\"],\n" " \"user_verbs\": [\"보고 싶어\", \"확인해\", \"찾아줘\"],\n" " \"keywords\": [\"관심종목\", \"관심그룹\", \"즐겨찾기\", \"시세\", \"변동률\"],\n" " \"related_concepts\": [\"시세\", \"변동률\", \"수익률\"],\n" " \"sample_queries\": [\"내 관심종목 보여줘\", \"관심그룹 어떻게 설정해\", \"...\"],\n" " \"embedding_text\": \"【식별문맥】 국내주식 관심종목 카테고리에서...\\n\\n주식 투자할 때...\"\n" "}\n" "\n" "※ embedding_text: Contextual Retrieval 적용 후 【식별문맥】 prefix prepend됨" ) # ── 10. 성능 지표 ───────────────────────────────────────────────────────── heading1(doc, "10. 성능 지표") body(doc, "평가 기준: 100개 쿼리 정답셋 (유효 81개, 평가 제외 19개)") make_table(doc, ["모드", "Acc@1", "Acc@3", "Acc@5"], [ ["bge-m3 기본 (Dense+BM25+RRF)", "27.2%", "53.1%", "67.9%"], ["+ HyDE", "37.0%", "64.2%", "75.3%"], ["+ HyDE + Rerank", "44.4%", "75.3%", "77.8%"], ["기본 대비 개선폭", "+17.3%p", "+22.2%p", "+9.9%p"], ], col_widths_cm=[8, 3, 3, 3] ) heading2(doc, "고도화 기법별 기여") make_table(doc, ["기법", "주요 효과"], [ ["bge-m3 임베딩 교체", "다국어 이해력 향상, 한국어 구어체 처리 개선"], ["HyDE", "쿼리-문서 어휘 갭 해소 (+9.8%p Acc@1)"], ["Contextual Retrieval", "유사 메뉴명 간 혼동 감소 (HyDE와 결합 시 시너지)"], ["Cross-Encoder 리랭킹", "RRF 상위 후보 정밀 재정렬 (+7.4%p Acc@1)"], ["Weighted RRF (1.5:0.5)", "Dense 의미 검색 강화"], ["Search Count Boost", "인기 메뉴 우선 노출"], ], col_widths_cm=[5.5, 11] ) doc.add_page_break() # ── 11. 웹 UI ───────────────────────────────────────────────────────────── heading1(doc, "11. 웹 UI") body(doc, "파일: app/streamlit_app.py") code_block(doc, "┌───────────────────────────────────────────────────────────────┐\n" "│ 영웅문 S# AI 메뉴 검색 │\n" "│ 자연어로 원하는 기능을 입력하면 관련 메뉴를 찾아드립니다. │\n" "├──────────────────┬────────────────────────────────────────────┤\n" "│ 검색 옵션 │ [검색창: 자연어 입력] │\n" "│ │ │\n" "│ 카테고리 필터 │ 빠른 검색 (12개 버튼, 2행): │\n" "│ [전체 ▼] │ [내 주식 얼마야?] [주문 취소] ... │\n" "│ │ [내 수익률 어때?] [달러 환전] ... │\n" "│ 등록된 메뉴 수 │ │\n" "│ 901 │ \"주문 취소\" 검색 결과 (5개) │\n" "│ │ │\n" "│ │ ▼ 1위 주문 취소/정정 94.2% │\n" "│ │ 경로: 국내주식 > 주문 > ... │\n" "│ │ ▼ 2위 미체결 주문조회 87.1% │\n" "└──────────────────┴────────────────────────────────────────────┘" ) heading2(doc, "주요 설정") make_table(doc, ["설정", "값"], [ ["Top-N", "5개"], ["최소 유사도", "0.3"], ["HyDE", "활성 (use_hyde=True)"], ["리랭킹", "비활성 (use_reranker=False, 구어체 쿼리 정확도 우선)"], ["빠른 검색", "12개 (2행 × 6개)"], ], col_widths_cm=[4.5, 12] ) # ── 12. Quick Start ─────────────────────────────────────────────────────── heading1(doc, "12. Quick Start") heading2(doc, "최초 환경 세팅") code_block(doc, "python -m venv .venv\n" ".venv\\Scripts\\pip install -r requirements.txt\n\n" "# .env 파일 설정\n" "AZURE_KEY=...\n" "AZURE_GPT41_MINI_ENDPOINT=...\n" "AZURE_GPT41_MINI_API_VERSION=2025-01-01-preview\n" "COHERE_API_KEY= # 선택: Cohere Rerank 4 활성화" ) heading2(doc, "데이터 파이프라인 (최초 1회)") code_block(doc, "# 메뉴 설명 생성\n" ".venv\\Scripts\\python.exe scripts/01_generate_descriptions.py\n\n" "# 벡터 DB 구축 (~12분)\n" ".venv\\Scripts\\python.exe scripts/02_build_vectordb.py --reset\n\n" "# BM25 인덱스 구축\n" ".venv\\Scripts\\python.exe scripts/06_rebuild_bm25.py" ) heading2(doc, "고도화 파이프라인 (선택 실행)") code_block(doc, "# 현업 키워드 반영 + 이미지 기반 재생성\n" ".venv\\Scripts\\python.exe scripts/12_update_keywords.py\n" ".venv\\Scripts\\python.exe scripts/13_regen_with_images.py\n\n" "# 현업 피드백 반영 재생성\n" ".venv\\Scripts\\python.exe scripts/15_import_review_feedback.py\n\n" "# Contextual Retrieval 적용\n" ".venv\\Scripts\\python.exe scripts/18_contextual_retrieval.py\n\n" "# 인덱스 재구축\n" ".venv\\Scripts\\python.exe scripts/02_build_vectordb.py --reset\n" ".venv\\Scripts\\python.exe scripts/06_rebuild_bm25.py" ) heading2(doc, "검색 품질 평가") code_block(doc, "# 3-way 비교 평가 (bge-m3 / +HyDE / +HyDE+Rerank)\n" ".venv\\Scripts\\python.exe scripts/17_eval_comparison.py\n" "# 결과: data/generated/Menusearch_100_answer.xlsx" ) heading2(doc, "데모 앱 실행") code_block(doc, ".venv\\Scripts\\streamlit.exe run app/streamlit_app.py\n" "# 접속: http://localhost:8501\n" "# 외부 공유: ngrok http 8501" ) # ── 저장 ───────────────────────────────────────────────────────────────── doc.save(OUTPUT_PATH) print(f"저장 완료: {OUTPUT_PATH}") if __name__ == "__main__": build()