FEMS #1 에서 셋업 + 5청크 데모까지 했다. 오늘은 실 데이터로 코퍼스 키우기 + 품질 게이트 + 모델 비교 까지.
Table of contents
Open Table of contents
PDF 텍스트 추출 — pypdf 의 한글 깨짐 함정
PDF → 텍스트는 pypdf 로 추출한다. 그런데 한국어 PDF 다루면서 한 번 빠지는 함정이 있다:
PowerShell 콘솔의 인코딩 표시 문제로 한글이 깨져 보일 수 있다.
콘솔에 찍으면 깨져 보이는데 파일로 저장하면 멀쩡 한 케이스가 많다. 콘솔만 보고 “추출이 잘못됐다” 고 판단하면 한참 헛짚는다. 그래서 검증은 콘솔이 아니라 파일로 저장한 결과 + 후술할 readable_ratio + 키워드 추출 조합으로 한다.
코퍼스 확보 — 약 230 페이지
공개 자료를 모았다:
- 한국BEMS협회 — FEMS 공식 가이드라인 2종
- 한국에너지공단 공개자료실 — FEMS 설치확인 기준 가이드
- UCI — Steel Industry Energy Consumption 데이터셋
약 230 페이지 분량 + 실제 측정값 약 35,000 행 을 확보했다. 이전 #1 의 5청크 데모에서 본격 규모로 점프한 셈이다.
인덱싱 파이프라인
| 단계 | 도구 |
|---|---|
| PDF → 텍스트 | pypdf |
| 청킹 | 문단 기반, target 800자 |
| 임베딩 | Ollama bge-m3 (1024 차원) |
| 벡터 DB | Chroma |
AI 가 청크를 나누고 질의 테스트까지 자동으로 끝냈는데 — 실제 서비스 환경에서 정말 깨지지 않고 동작할지 가 의심이 됐다. 그래서 청크 사이즈 분포와 인덱싱 무결성을 직접 확인하는 단계를 끼워넣었다.

확인된 것:
- 총 344 청크 (chroma count 와 일치, match: True)
- 청크 크기 분포 — min=21, mean=634, max=800 (target 800 잘 지킴, oversized 0)
- readable_ratio 평균 0.99 (mean), min 0.91
- 무결성 OK, 하지만 일부 청크의 품질이 낮다는 것 을 발견했다
품질 게이트 — readable_ratio 의 정의와 한계
저품질 청크가 어떤 모양일지 잡기 위해 만든 첫 번째 필터:
readable_ratio= (완성형 한글 문자 수 + ASCII 인쇄 가능 문자 수) / 전체 문자 수
def is_readable(ch):
code = ord(ch)
is_hangul = 0xAC00 <= code <= 0xD7A3
is_ascii = 32 <= code <= 126
return is_hangul or is_ascii
정상 텍스트라면 1.0 에 가깝게 나온다. 깨진 폰트나 목차 페이지처럼 점선·페이지 번호만 있는 청크는 비율이 떨어진다.
발견 — 목차 페이지가 잡혔다
품질이 낮게 잡힌 청크들이 대부분 목차 페이지였다. 점선(........... 12) 형태가 많아서 ratio 가 떨어지는데, 사실 ratio 가 멀쩡해도 RAG 가치는 0 인 영역이다.
이 방법의 한계 — 못 잡는 패턴들
readable_ratio 가 만능은 아니다. 잡지 못하는 케이스:
- 글자는 멀쩡한데 내용이 빈약한 청크 (목차, 페이지 헤더, 표지)
- 폰트가 잘못된 한글로 매핑 된 케이스 (글자 자체는 완성형이라 ratio 만점)
그래서 본격적으로 잡으려면 ratio + 청크 길이 + 언어 감지 + LLM 품질 검사 조합이 맞다. 다만 — 지금 프로젝트에서는 오버엔지니어링이라 도입 X.
| 방법 | 안 쓴 이유 |
|---|---|
langdetect / fastText 언어 감지 | 목차 페이지는 ratio 함수로 충분히 잡힌다 |
| LLM 기반 품질 검사 | 비싸고 느리다 |
| 저품질 문서 전체 제외 | 목차 페이지만 저품질, 나머지 페이지까지 버릴 필요가 없다 |
결론 — 116 → 109 (7개 제외)
전체 청크 중 7개의 저품질 청크만 제외했다. 무거운 도구 안 끌고 와도 의도한 효과를 달성했다.
Normal vs Anomaly 정의 — 그런데 실데이터를 왜 모았더라
준비된 RAG 위에서 정상 / 이상 패턴 을 구분하는 단계로 넘어간다.
Normal 을 제대로 정의하기 위해 조명 상태를 두 가지로 나눴다:
- idle (야간 / 주말 — 낮은 전력)
- active (업무 시간 — 높은 전력)
야간·주말에 조명이 낮게 떨어지는 건 anomaly 가 아니라 normal 이다. 이 구분 안 해두면 정상 패턴까지 이상으로 잡힌다. 압축기 / HVAC 스파이크 값도 동일한 식으로 normal·anomaly 기준을 정의했다.
합성 데이터 vs 실 데이터 — 과적합 의문
테스트용 CSV 를 생성해서 데이터 안에 모델이 찾을 신호가 있는지 측정했다. anomaly 가 normal 대비 크게 벗어난 값임을 확인했다. 사람이 직접 보고 정상/이상을 검증 하는 절차가 필요하다고 판단했다.
여기서 의문:
주입된 이상치로 실험하고 합성 데이터에 맞는 질문을 던질 거면, 실제 PDF 는 뭐하러 확보했나?
합성 데이터의 답이 질문에 너무 잘 달라붙어서 실 PDF 가 실제 현장에서 쓸 수 있는지 검증이 안 된다. 과적합 문제가 발생한다고 판단했다.
→ 이후 검증은 내가 직접 만든 테스트 질문 몇 개로 진행 하기로 결정했다.
3-백엔드 비교 (1차) — 첫 질문은 합성, 두번째는 실 가이드라인
python -m scripts.run_analysis

| Provider | 모델 | 평균 latency | 총 비용 | 성공/실패 |
|---|---|---|---|---|
| ollama | qwen2.5:7b | 44.2s | $0.0000 | 2/0 |
| claude | opus-4-8 | 8.5s | $0.0593 | 2/0 |
| openai | gpt-4o | 6.0s | $0.0167 | 2/0 |
정답률은 셋 다 동률이다. 차이는 정리 품질과 latency·비용에서 난다.
- 가장 잘 정리된 답변은 Claude — “판단 / 원인 / 권장 조치” 섹션으로 나누어 설명한다. 다만 출력 토큰 제한 으로 응답이 잘렸다.
- qwen2.5:7b 는 로딩이 느리고 (44s 평균), Opus 는 비싸지만 정리 품질이 우수하다.
함정 — qwen2.5:7b 의 한자 혼입 + 생성 붕괴
두번째 질문 (ETRI / fems_mv_guideline.pdf 관련) 에서 ollama 응답을 자세히 봤다:

답변 자체는 시작은 멀쩡한데:
- 중간에 한자가 섞였다 (
HeaderComponent比较,测定值같은 식) - 갑자기 GeoJSON 으로 北京 / 上海 좌표 가 튀어나왔다 — 질문과 완전히 무관하다
이게 두 가지 문제다:
- 언어 혼입 (Code-mixing) — 출력에 의도하지 않은 다른 언어가 섞이는 현상
- 생성 붕괴 (Generation collapse) — 학습 데이터에서 흔하게 보이는 패턴으로 빠져버리는 현상
학습 데이터에서 자주 본 패턴 (예: GeoJSON 예제) 으로 모델이 미끄러져 들어간 결과다. 모델이 “안전한 길” 로 빠지는 일종의 어트랙터다.
1차 대응 — temperature + 시스템 프롬프트 언어 고정
Ollama 의 temperature 기본값이 0.8 이다. 낮추면 탈선 확률이 줄어든다고 한다. 동시에 시스템 프롬프트에 “한국어로 답할 것” 을 명시했다.
OllamaProvider 에 temperature / repeat_penalty + 프롬프트 언어 고정 적용 후 재실행했다:

예상 외의 결과:
- 탈선 (GeoJSON 같은 무관한 출력) → 사라졌다 ✅
- 한자 혼입 → 오히려 더 심해졌다 ❌ (응답 절반이 중국어)
temperature 낮추니 모델이 “가장 가능성 높은” 토큰을 선택하는데, qwen2.5:7b 는 중국 발 모델이라 그 가능성 분포 자체가 중국어 쪽으로 치우쳐 있는 것으로 보인다.
→ 로컬 LLM 선정 시 “언어 혼입에 robust 한가” 도 핵심 기준이 된다. 모델 라인업을 미리 모니터링하고 후보를 비교해봐야 한다.
2차 대응 — exaone3.5:7.8b (한국어 특화)
말 나온 김에 exaone3.5:7.8b (LG AI Research 의 한국어 특화 모델) 를 시도했다.

- 한국어 깔끔 ✅
- 탈선 없음 ✅
- 로딩은 좀 더 길었지만 결과는 잘 나왔다
- 다만 출력 토큰 제한으로 답변이 잘렸다 →
max_tokens상향 또는 시스템 프롬프트에 “간결하게 답할 것” 명시가 필요하다
회고
오늘 학습 요점 3가지:
- 품질 게이트는 오버엔지니어링 직전까지만 —
readable_ratio하나로 목차 페이지 7개 잡으면 충분하다. langdetect / LLM 품질검사는 비용 대비 효과가 적다. - 합성 데이터에 맞는 질문만 던지면 실 데이터 확보 무용지물 — 실 PDF 를 RAG 가 진짜로 활용하는지 검증하려면 사람이 직접 실 데이터 기반 질문 을 만들어야 한다.
- 로컬 LLM 선정 시 “언어 혼입에 robust 한가” 가 1급 기준이 된다 — qwen2.5:7b 의 한자 혼입 + 생성 붕괴를 temperature 만으로 해결하지 못했다. 한국어 특화 모델 (exaone) 이 즉시 해법이 됐다.
청크 수를 늘리는 게 능사가 아니라는 감이 잡혔다.
더 공부해볼 것
1. 벡터 DB 데이터 선별 기준
오늘 회고에서 떠오른 키워드들:
- 관련성 — 도메인 + 용도에 맞는가
- 품질 — 깨진 텍스트 / 빈 청크 제외
- 권위·신뢰성 — 출처가 명확한가
- 적절한 세분화 — 너무 좁으면 관련 정보 누락, 너무 넓으면 노이즈
- 중복 / 구버전 제거 — 동일 정보가 여러 청크에 흩어지면 검색 품질 저하
도메인 + 용도에 맞춰 데이터 선별하는 게 핵심이다. 무작정 다 넣는 게 능사가 아니다.
2. 청킹 전략 심화
- Fixed-size vs Recursive vs Semantic — 어떤 도메인에 어떤 게 맞나
- Overlap 비율 — 0 / 10% / 20% 의 trade-off
- Sentence-level 임베딩 + Hierarchical retrieval — 청크 + 문장 두 단계
- 표 / 코드 / 수식 같은 특수 콘텐츠의 별도 처리
3. 로컬 모델 카탈로그 + 한국어 특화 모델
- qwen2.5 / llama3.x / mistral / phi — 글로벌 모델의 한국어 약점
- exaone3.5 (LG AI Research) — 한국어 특화, 다양한 사이즈
- HyperCLOVA X (NAVER) — 폐쇄형, API 형태
- KO-Llama / 한국어 fine-tuned 모델 — 커뮤니티 변종
- 동일 코퍼스로 한국어 질의 응답 벤치마크 (BLEU / Rouge / 한국어 BAS 등)
4. 생성 붕괴 (Generation Collapse) 의 메커니즘
- 학습 데이터의 “흔한 패턴 어트랙터” 가 뭔지
- 왜 temperature 가 낮을 때 더 빠지는가
- Top-p sampling / repetition_penalty / frequency_penalty 의 효과
- CFG (Classifier-Free Guidance) 가 LLM 에 적용된 케이스
5. 토큰 제한과 응답 잘림 대응
max_tokens무작정 늘리면 latency + 비용 폭증- 시스템 프롬프트에 “간결하게” / “핵심만” 명시의 효과
- 응답을 여러 부분으로 나누는 streaming continuation 패턴
- 출력 형식을 JSON / 표 로 제약해서 분량 통제
6. Anomaly 정의에 사람의 도메인 지식 끼워넣기
- “야간/주말 idle 은 normal” 같은 규칙은 도메인 전문가만 알 수 있다
- time-of-day + day-of-week + 설비별 정상 패턴 분리
- 단순 통계 (mean + 3σ) vs 컨텍스트 인지 (조건부 분포)
- FEMS 의 실시간 모니터링이 단순 임계값 알람 → 컨텍스트 인지 알람으로 발전한 과정