애널리스트가 쓴 산업·섹터 투자의견 리포트(PDF)를 넣으면, 페이지를 유형별로 자동 분류 → 유형에 맞는 도구로 텍스트/표/이미지 각각 핵심만 추출 → 근거를 하나의 DB로 통합 → 질의에 대해 근거 기반 투자 인사이트를 생성하는 멀티모달 RAG 시스템입니다.
리포트 한 페이지에 텍스트·표·이미지(차트)가 섞여 있어 하나의 도구로 전부 처리하면 정보 손실이 크고(특히 표·차트), 반대로 모든 페이지에 무거운 모델(VLM, 표 구조 인식 모델)을 매번 돌리면 시간·비용이 낭비된다는 문제에서 출발했습니다. 그래서 쉬운 건 싸게, 필요한 곳만 비싸게 처리하도록 유형별 최적 도구를 배치했습니다.
flowchart TD
%% ---------------- STYLE DEFINITIONS ----------------
classDef inputFill fill:#E1F5FE,stroke:#0288D1,stroke-width:2px,color:#01579B
classDef branchFill fill:#FFF3E0,stroke:#F57C00,stroke-width:2px,color:#E65100
classDef processFill fill:#F3E5F5,stroke:#8E24AA,stroke-width:2px,color:#4A148C
classDef visionFill fill:#E8F5E9,stroke:#388E3C,stroke-width:2px,color:#1B5E20
classDef tableFill fill:#FFF8E1,stroke:#FFA000,stroke-width:2px,color:#FF6F00
classDef textFill fill:#E0F2F1,stroke:#00897B,stroke-width:2px,color:#004D40
classDef dbFill fill:#EDE7F6,stroke:#5E35B1,stroke-width:2px,color:#311B92
classDef outputFill fill:#FFEBEE,stroke:#E53935,stroke-width:2px,color:#B71C1C
%% ---------------- NODES & LINKS ----------------
A["애널리스트 산업 섹터 투자의견 보고서 PDF"]:::inputFill
A --> B["페이지 분류"]:::processFill
B --> C["PyMuPDF 메타데이터 신속검사"]:::processFill
C --> D{"PDF 난이도"}:::branchFill
D -->|쉬운 PDF| E["YOLOv11n Layout Detection"]:::processFill
D -->|어려운 PDF| F["MinerU"]:::processFill
E --> G
F --> G
G{"Page Type"}:::branchFill
%% ---------------- IMAGE ----------------
G --> TAB
G --> TXT
G --> IMG["이미지"]:::visionFill
IMG --> CHART{"Chart Classification"}:::branchFill
CHART -->|Yes| VLM["VLM"]:::visionFill
CHART -->|No| IMGOCR["이미지 이해<br/>(OCR + Vision)"]:::visionFill
VLM --> IMGSO["Structured Output"]:::visionFill
IMGOCR --> IMGSO
%% ---------------- TABLE ----------------
G --> TAB["테이블"]:::tableFill
TAB --> TATR["TATR (Table Transformer)"]:::tableFill
TATR --> PDFPL["pdfplumber"]:::tableFill
PDFPL --> RECON["Table Reconstruction"]:::tableFill
RECON --> TABSO["Structured Output"]:::tableFill
%% ---------------- TEXT ----------------
G --> TXT["텍스트"]:::textFill
TXT --> TEXT["PyMuPDF Text"]:::textFill
TEXT --> SO["Structured Output"]:::textFill
TEXT --> CHUNK["Rule-based 계층적 청킹"]:::textFill
CHUNK --> EMB["Dense Embedding<br/>+ BM25 Index"]:::textFill
EMB --> TMP["임시 DB (Supabase)"]:::dbFill
%% ---------------- ENTITY ----------------
IMGSO --> FUSION["Entity Fusion"]:::processFill
TABSO --> FUSION
SO --> FUSION
FUSION --> META["기업 메타데이터 DB<br/>(Supabase)"]:::dbFill
%% ---------------- SEARCH ----------------
QUERY["사용자 Query + 타입"]:::inputFill --> SEARCH
TMP --> SEARCH["Hybrid Search"]:::processFill
SEARCH --> RRF["Rank Fusion"]:::processFill
%% ---------------- NEWS ----------------
NEWS["관련 뉴스<br/>Sentiment Analysis"]:::inputFill --> META
%% ---------------- FINAL ----------------
META --> LLM["LLM 투자 Insight"]:::outputFill
RRF --> LLM
| 노드 | 모델·기법 | 채택 근거 |
|---|---|---|
| PyMuPDF 메타데이터 신속검사 | 규칙 기반(모델 없음) | find_tables() + 래스터/벡터 카운트로 1차 스캔, 161.8ms/page — 대부분 문서는 이 단계에서 이미 처리 가능 여부가 갈림 |
| PDF 난이도 판정 | 규칙 기반(assess_page_difficulty) |
컬럼이 기하학적으로 겹쳐도 원문 추출 순서가 컬럼별로 잘 그룹돼 있으면 문제없다는 관찰 반영 — material_overlaps AND interleaving_excess≥2를 동시 만족할 때만 hard 판정 |
| YOLOv11n Layout Detection | YOLOv11n(Armaggheddon/yolo11-document-layout, plain crop) | baseline(pdfplumber+PyMuPDF, 83.3%) 대비 라벨 정확도 100%. YOLOv26n과 정확도는 동률이지만, 벤치마크 mAP와 달리 실제 표 크롭 개수(14 vs 13)·다운스트림 표 인식 성능에서 YOLOv11n이 더 우수해 채택 |
| 스캔본 감지 | 규칙 기반(detect_scanned_pages) |
텍스트 레이어 유무 + 이미지가 페이지를 덮는 비율만으로 판정(순수 PyMuPDF, 모델 없음) |
| 노드 | 모델·기법 | 채택 근거 |
|---|---|---|
| PyMuPDF Text | get_textbox(bbox)(YOLO 박스 단위) |
get_text("blocks")는 시각적 줄 단위로 잘려 문단 경계가 깨짐 — YOLO 박스=문단/불릿 경계로 교체 |
| 정제 | PUA 글리프 제거, 헤더/푸터 제거(좌표+반복빈도), Boilerplate(Compliance Notice) 탐지 | Boilerplate 탐지는 BGE-M3 문장 임베딩 유사도 기반 채택(SimHash/MinHash는 증권사 간 패러프레이즈를 못 잡아 기각) — 정밀도 100%, 재현율 83% |
| Rule-based 계층적 청킹 | 규칙 기반(chunk_hierarchical, LLM 미사용) |
Title/Section-header를 그대로 청크 경계로 쓰고 계층 경로를 메타데이터로 부착. Qwen 로컬 VLM 청킹(entity_recall 89.3%, 101.1s) 및 OpenAI API 청킹(59.3%, 55.0s)과 비교해 entity_recall 96.0%로 가장 높으면서 LLM 호출 0건이라 가장 빠름(57.3s) — 유료/로컬 LLM 어느 쪽도 이득이 없어 규칙 기반 채택 |
| Dense Embedding | dragonkue/BGE-m3-ko | BGE-M3(다국어 베이스) vs OpenAI text-embedding-3-small과 3종 비교(Recall@1/@3, MRR) — BGE-m3-ko가 Recall@3 100%(유일), MRR 0.922(최고), 임베딩 속도도 최속이라 트레이드오프 없이 채택 |
| Sparse Index | BM25Okapi(한글 어절 토크나이저) | Dense 단독 대비 하이브리드가 recall@1 86.7%→93.3%로 개선 |
| 노드 | 모델·기법 | 채택 근거 |
|---|---|---|
| TATR (Table Transformer) | TATR로 표 내부 row bounding box만 구조적으로 탐지 | Docling(TableFormer, OCR+구조+텍스트 전부)과 동일 품질(228행)을 6.2배 빠르게(20.24s→3.25s) 달성. Docling이 OCR 실패로 0행 처리한 표를 TATR+pdfplumber는 7행 복원했고, 반대로 YOLO가 차트를 표로 오탐한 영역에서 Docling은 의미 없는 가짜 행 11개를 만든 반면 TATR+pdfplumber는 정직하게 0행 반환 |
| pdfplumber | row 좌표를 pdfplumber에 넘겨 행 단위로 독립 추출 | 표 전체를 한 번에 읽지 않고 행 단위로 쪼개 처리 — 겹친 텍스트(글자 간격 손상)로 인한 엔티티 누락 문제(예: "우리은행") 해결. Recall 90%→100%, F1 92.2%→96.6% |
| 크롭 전처리 | Adaptive padding + 300dpi 재렌더링 | (padding/dpi) 9가지 조합 탐색 결과 최고(242행) — 상단 +35px·좌우/하단 +12px + 표 크롭만 300dpi로 재렌더링 |
| 표 유형 라우팅 | 재무제표 → 규칙 기반(정규식), 계약/일반 → LLM 구조화 출력 | 재무제표는 회사명이 거의 안 나온다는 특성을 이용해 LLM 호출 없이 처리 — 총 처리시간 1097.5s→462.8s(-57.8%), Precision 81.2%→94.4% |
| SIMPLE/COMPLEX 게이팅 | fill_ratio·컬럼 일관성·빈 셀 비율로 판정 | SIMPLE 표는 pdfplumber만으로 충분해 TATR을 아예 안 태움(하이브리드 게이트, [JAEIL v5]) |
| 엔티티 보강 | 행 content(라벨+셀)에서 KOSPI200 199개 기업명 결정적 매칭 | LLM 호출 없이 표 행에 structured_metadata.entities를 채워, 질의 시점 entity_count 계산이 LLM 폴백으로 새지 않게 함(§8.4) |
| 노드 | 모델·기법 | 채택 근거 |
|---|---|---|
| 문서 파싱 | MinerU(레이아웃 탐지 + 내장 OCR, PP-OCR) | chart/image/table 블록 탐지와 크롭, 페이지당 ~0.5s OCR |
| 규칙 필터 | 크기·종횡비 기반(모델 없음) | 명백한 junk(로고·아이콘)를 VLM 호출 전에 선컷 |
| 세부 분류(선택) | DocumentFigureClassifier-v2.5(EfficientNet-B0, 4.08M) | 26종 세부 라벨로 MinerU 판정과 상호검증만 함(판정 자체는 안 바꿈), 장당 ~10-20ms로 사실상 무료 |
| Chart Classification → VLM | MinerU2.5-Pro-2605-1.2B(내장 VLM, 차트→데이터 표 근사 추출) | 자유 지시를 따르지 않는 특화 모델이라 프롬프트 커스터마이즈는 무의미 — 차트 판독(수치 추출)에서 Docling(VLM 켜도 환각만 생성, 0%)·MinerU 기본모드(차트 미판독, 0%) 대비 유일하게 100% 달성 |
| 서술형 해석(선택) | qwen3:8b(Ollama, 텍스트 전용 LLM) | 4a가 뽑은 표를 애널리스트 문체로 재서술. 4a(VLM)가 성공하면 표 자체가 이미 검색 가능한 형태로 저장되므로 4b가 실패해도 핵심 데이터는 유지됨 |
| 부호/축눈금 안전장치 | 정규식 기반(_normalize_chart_card_signs) |
한국 증권 리포트 관례(괄호=음수) 정규화 + 등간격 숫자를 축 눈금으로 오인해 시계열로 오독하는 걸 막는 경고 삽입 — VLM 없이도 동일 효과 |
entity_fusion.py가 3브랜치 출력을 하나의document_evidence(Supabase/pgvector)로 모으고, 소스 타입별 신뢰도 가중치(표 canonical 매칭 1.3 / 표 미매칭 0.6 / 이미지 1.15 / 텍스트 1.0)를 부여.load_evidence_from_db()로 재파싱 없이 재질의 가능(프로세스 메모리 캐시, TTL 60초).- 구조화 출력(다이어그램의 Structured Output 3노드, 선택 기능·기본 비활성):
structured_output.py(gpt-4o-mini)가 텍스트/표/이미지 각 청크에서 entities/sector_mentioned/sentiment 등을 정형 추출하는 경로가 구현돼 있으나,add_structured_metadata=False가 기본값이라 지금 배포된 앱에서는 호출되지 않는다(유료 API 호출 추가 비용 때문에 off). 표 브랜치의 엔티티만 위 결정적 매칭(무료)으로 별도 보강돼 있어, 구조화 출력을 안 켜도 표 엔티티는 채워짐.
이 앱은 질의 경로가 두 갈래다: 종목 상세 페이지(PDF 없이 KOSPI200 199개 종목 전체에서 항상 가능)와 PDF+질문 업로드 페이지(그 문서에 한정된 근거 기반 답변).
종목 상세 페이지 (answer_question()):
- 기본 근거:
financial_chunks/company_profile_chunks밀집 검색 — **OpenAItext-embedding-3-small**로 사전 임베딩(PDF 브랜치가 쓰는 BGE-m3-ko와는 별개 모델). 199개 종목 전체에 미리 적재돼 있어 PDF 업로드 없이 항상 응답 가능. - 이 종목에 실제 애널리스트 PDF가 적재돼 있으면(
document_evidence에 행 존재) BM25+BGE-m3-ko 하이브리드 검색(weighted_hybrid_search)까지 근거에 추가. - 배당스코어·뉴스감성·Layer1~4 시그널(아래 별도 항목)도 병렬 조회해 프롬프트에 합류.
PDF+질문 업로드 페이지 (search_and_generate()):
- 질의 분해:
decompose_query()(gpt-4o)로 복합 질의를 성격별 하위질의로 분해, 여러 개면 ThreadPoolExecutor로 병렬 검색(예전엔 순차라 지연이 sum이었음). 질의 성격 분류 (classify_query_type_llm/_classify_and_expand)도 정확도가 중요해 gpt-4o를 쓰지만, 분류 뒤 실제 HyDE 가상문단·MQE 하위질의 생성 자체는 저비용 gpt-4o-mini로 내려서 처리 — 판단이 중요한 단계만 상위 모델을 쓰는 구조. - keyword_specific →
hybrid_search(fusion="weighted_sum")— dense+BM25 선형가중(0.7/0.3). RRF보다 검증된 우세(ndcg 0.815 vs 0.71~0.73)로 교체됨. - abstract → 문서 entity_count로 재분기: 단일기업(≤1) → HyDE(가상 답변 문단 생성 후 임베딩), 다기업(>1) → MQE(하위질의 생성 후 RRF 융합) — 하위질의를 5개 측면으로 명시 분리(①거시경제/정책/지정학 — 반드시 구체적 예시 단어(전쟁·분쟁, 금리 인상 등) 사용, 카테고리명만 쓰면 임베딩이 못 잡는다는 걸 실측으로 확인 ②수급동향(기관/외국인/연기금) ③실적/이벤트 ④목표주가/투자의견 ⑤개별기업리스크). abstract 경로는 표/이미지 소스가중치도 끔(keyword용으로 검증된 가중치가 요약형 질의의 서술 문단을 부당하게 역전시키던 문제 수정) — 이 개선으로 추상형 검색 품질이 ndcg 0.271 → 약 0.70~0.72로 개선됨.
- 하위질의별 결과는 라운드로빈 인터리빙으로 병합(순위 합산 아님 — abstract가 keyword_specific의 1위를 밀어내는 걸 방지).
- 기업 엔티티 링킹(위와 병렬):
find_mentioned_companies()(KOSPI200 199개 정확 문자열 매칭, LLM/임베딩 불필요) →fetch_company_db_context()(재무제표·프로필·배당스코어 v2·뉴스감성· Layer1~4 실시간 시그널을 DB에서 조회해 프롬프트에 합류). - 리랭커(cross-encoder, BAAI/bge-reranker-v2-m3): 도입 여부를 실측 검증 — 근거가 이미 강한 질의(ndcg>0.96)에는 MQE/HyDE가 의도적으로 만든 근거 다양성을 오히려 획일화시켜 해가 되고, 근거가 약한 질의에만 도움이 되는 혼재된 결과라 보류(문서가 늘어나 약한 근거 사례가 반복되면 재검토 조건부).
- 뉴스 감성 (
news_sentiment_link.py,src/finance/layer3_*): 네이버 실시간 뉴스 검색 → 규칙 기반 4요소 가중 랭킹으로 후보 선정 → Qwen3-0.6B(로컬, HF transformers, MPS)로 최종 top_n 재검증(reasoning) → snunlp/KR-FinBert-SC(한국어 금융뉴스 감성 파인튜닝)로 positive/neutral/negative 분류 → Qwen3-1.7B로 감성 판단 근거 서술문 생성(원래 4B였으나 개발 환경의 통합메모리 경합으로 축소). 읽기-통과 캐시로 재수집을 최소화. - 배당 스코어 · Layer1~4 시그널: 재무(Layer1)/기술적지표(Layer2, yfinance 실시간)/뉴스 (Layer3, 위 모델)를 규칙 기반 가중융합(Layer4)으로 계산 — Layer1/2/4는 전부 통계·규칙 기반이며 별도 ML 모델 없음. 사전 배치 산출물이 아니라 질의 시점 실시간 계산(과거 배치 결과는 답변 생성에 전혀 도달하지 못해 폐기하고 실시간 계산으로 교체).
- gpt-4.1 채택 — RAGAS 실측(9질의)으로 gpt-4o-mini 대비 faithfulness(환각 없음) 0.740→0.893, 논리 루브릭 2.22→3.00(둘 다 우세), 대가는 출력 토큰 2.7배·지연 1.3배.
citation_check.py— 답변 속 숫자가 실제 컨텍스트에 있는지 대조해 없으면 재생성. 소수점 스코어 (감성/융합 시그널 등 절댓값<1인 값)까지 검증 대상에 포함하도록 보강했고, 재시도해도 미해결 숫자가 줄지 않으면 조기 중단해 불필요한 재생성 낭비를 없앰. 여러 기업이 언급된 문서에서는 다른 기업 수치를 섞어 쓰지 말 것, 날짜/분기 표기를 원문 그대로 유지할 것도 프롬프트에 명시.
"기존 도구로 충분한가"에 대한 답 — Docling/MinerU vs 우리 파이프라인(텍스트/표 7문항 + 차트 16개 기준):
| 지표 | Docling(VLM 미사용) | Docling(VLM 사용) | MinerU | 우리 파이프라인 |
|---|---|---|---|---|
| 지연(전체) | 6.9s | ~900s | 25.6s | 12.9s |
| 텍스트/표 추출 Recall | 100% | 100% | 100% | 100% |
| 차트 판독률(유의미한 수치 추출) | 0% | 0%(환각만 생성) | 0% | 100% |
Docling에 우리와 같은 일(차트 이해)을 시키면 35배 느려지면서 오히려 틀린 값을 만들어내고, 안 켜면 빠르지만 차트를 아예 못 읽는다. "차트까지 이해하는 파서" 카테고리에서 속도·정확도 둘 다 우위.
생성 모델 선택 근거 — RAGAS(9질의, 심판=gpt-4o):
| 지표 | gpt-4o-mini | gpt-4.1 | 승자 |
|---|---|---|---|
| Faithfulness(환각 없음) | 0.740 | 0.893 | gpt-4.1 (+21%) |
| Factual Correctness | 0.332 | 0.409 | gpt-4.1 (+23%) |
| 논리 루브릭(1~5) | 2.22 | 3.00 | gpt-4.1 (+35%) |
| 지연 / 출력 토큰 | 4.71s / 304 | 6.04s / 814 | gpt-4o-mini |
각 단계 디렉토리의 실험.md(pdf_pipeline/text_processing/실험.md,
pdf_pipeline/table_processing/실험.md, pdf_pipeline/page_classification/실험.md 등)와
pdf_pipeline/파이프라인_최종정리_핸드오프.md, pdf_pipeline/final/검색퓨전_라우팅_생성품질_실험.md에
실험 전체 기록이 남아있습니다. 이미지 파이프라인 고도화판(VLM 캐시, pHash 중복제거, ChartQA A/B
등)은 pipeline/README.md 참고.