# Coldmail 필터 토큰화 문제 및 하이브리드 구현

**날짜**: 2025-10-14
**작성자**: Claude (51124 서버 전담)
**관련 파일**: `rb8001/app/services/coldmail_filter.py`, `rb8001/tests/test_coldmail_filter.py`

---

## 문제 상황

9시 5분 Coldmail Daily Briefing 실행 시 IR deck 이메일이 Slack에 전송되지 않음.

**조사 결과**:
- 올굿즈컴퍼니 IR deck: coldmail 확률 30.35% (threshold 70% 미달)
- 빅웨이브 IR 행사: 7.65%
- 투자제안서 검토: 28.38%

**근본 원인**: coldmail_filter.py:44-52 정규식 토큰화 실패
- "회사소개서" → 하나의 토큰 (형태소 분리 안 됨)
- "ir에" → 하나의 토큰 (조사 분리 안 됨)
- DB의 "ir" (cold 90.9%)와 매칭 실패

**DB 학습 데이터는 정상** (56단어, cold 347, normal 210)

---

## 해결 방안: 3단계 하이브리드

**설계**:
1. KoBERT 임베딩 (고속, 90% 필터링) - skill-embedding (8515) 활용
2. Gemini LLM (정밀, 10% 처리) - zero-shot classification
3. Naive Bayes (실시간 학습) - Slack 피드백 반영

**예상 효과**:
- 재현율: 30% → 95%
- 응답 속도: 500ms (1차 100ms + 2차 400ms)
- API 비용: 10% (1차 필터 효과)

---

## 구현 계획 (12시간)

### Phase 1: KoBERT 임베딩 필터 (4h)
**신규**: app/services/coldmail_embedding_filter.py
- create_embedding(): skill-embedding (8515) /embed 호출, 384차원
- calculate_similarity(): cosine similarity
- is_coldmail_by_embedding(): threshold 0.6
- **환경변수**: SKILL_EMBEDDING_URL (emotion_classifier.py:9, embedding_client.py:14 참고)

**DB**: coldmail_embedding_clusters 테이블 (51123 서버)
- ✅ pgvector extension 설치 완료
- ✅ 스키마: id UUID PRIMARY KEY, embedding VECTOR(384), label TEXT, created_at TIMESTAMP
- ✅ 초기 데이터: coldmail centroid(36단어 평균) + normal centroid(20단어 평균), 각 384차원
- 목적: 의미 유사도 비교용 클러스터 중심점 저장 (학습 안함, 정적 참조)
- 기존 테이블과 독립: coldmail_classifier는 단어 빈도 학습용(동적), 서로 참조 없음

### Phase 2: Gemini LLM 분류기 (2h)
**신규**: app/services/coldmail_llm_classifier.py
- classify_by_llm(): Gemini zero-shot + 이유 설명
- JSON 마크다운 블록 전처리
- **모델**: gemini-2.5-flash-lite (emotion_classifier.py 참고)

### Phase 3: 하이브리드 통합 (3h)
**신규**: app/services/coldmail_hybrid_filter.py
- hybrid_coldmail_filter(): 3단계 순차 실행
- update_from_feedback(): Slack 버튼 → 3모델 업데이트
- **기존**: app/services/coldmail_filter.py:44-52 정규식 토큰화 교체

### Phase 4: 아키텍처 리팩토링 (3h)
**신규**: app/services/coldmail_briefing_service.py
- _run_coldmail_briefing() 이동 (scheduler/jobs/coldmail_briefing.py:76-263)
- 8단계 워크플로우: 시간 계산 → 메일 조회 → 콜드메일 필터 → 첨부파일 처리 → IR 분석 → 가치평가 → Slack Lists → 피드백 버튼

**수정**: app/scheduler/jobs/coldmail_briefing.py
- register(), _parse_cron(), _run_coldmail_briefing_with_logging() 유지 (1-75줄)
- is_coldmail() → hybrid_coldmail_filter() 교체 (133줄)

**LangGraph 검토**: 250925_langgraph_vs_n8n_comparison.md
- 체크포인트 기반 실패 복구 (IR 분석 실패 시 재시도)

### Phase 5: 테스트 및 검증 (2h)
**테스트 케이스** (6개):
- Success: 올굿즈컴퍼니, 빅웨이브, 투자제안서
- Failure: KBAN, SSG.COM, 회의 안내

**검증**:
- [ ] 임베딩 클러스터 로딩
- [ ] LLM JSON 파싱
- [ ] Slack 피드백 → DB 반영
- [ ] 응답 시간 500ms 이내
- [ ] API 호출 10% 이하

---

## 구현 완료 (커밋 5ff5a6f)

### 구현 파일
- coldmail_embedding_filter.py: 임베딩 유사도 (skill-embedding 8515)
- coldmail_llm_classifier.py: Gemini LLM zero-shot
- coldmail_hybrid_filter.py: 3단계 통합 (임베딩→LLM→Naive Bayes)
- coldmail_briefing.py: hybrid_coldmail_filter 적용

### 테스트 결과
- Phase 1 (임베딩끼리 비교): coldmail 0.204 > normal 0.156 ✅
- Phase 2 (Gemini LLM): 올굿즈컴퍼니 100% 정확도 ✅
- Phase 3 (하이브리드): LLM fallback 작동 ✅

### 임베딩 필터 이슈
- **문제**: DB centroid와 실제 이메일 비교 시 실패 (coldmail 0.30 < normal 0.38)
- **원인**: Centroid 샘플 30개가 일반 텍스트만 포함, 파일명 형식("xxx.pdf") 미포함
- **해결**: 파일명 샘플 40개 추가 필요 (회사소개서.pdf, IR_Deck.pdf 등)
- **현재**: 임베딩 필터 skip → LLM으로 fallback (100% 정확도)

### 학습 루프
- **Naive Bayes**: Slack 피드백 자동 업데이트 (coldmail_filter.py:update_classifier)
- **임베딩 Centroid**: 수동 업데이트 (51123 서버에서 재계산)
- **LLM**: 학습 불가 (zero-shot 고정)

---

## 교훈

### 단순 정규식 토큰화의 한계
- 한국어는 교착어 특성상 조사/어미 분리 필수
- 교훈: 의미 기반 접근(임베딩)이 토큰 기반보다 강건함

### 단일 모델의 위험성
- Naive Bayes: 형태소 문제 해결 불가
- LLM: 학습 불가능 + 비용 과다
- 교훈: 하이브리드 접근으로 각 모델의 장점 활용

### 피드백 루프의 중요성
- 교훈: 실시간 학습 가능한 구조가 장기적으로 유리

---

## 51123 서버 Centroid 초기화 작업

### pgvector Extension 설치
```bash
# PostgreSQL 개발 헤더 설치
sudo apt install -y postgresql-server-dev-14

# pgvector 소스 빌드
cd /tmp
git clone --branch v0.7.0 https://github.com/pgvector/pgvector.git
cd pgvector
make && sudo make install

# DB에 extension 활성화
sudo -u postgres psql -d main_db -c "CREATE EXTENSION IF NOT EXISTS vector;"
```

### coldmail_embedding_clusters 테이블 생성
```sql
CREATE TABLE IF NOT EXISTS coldmail_embedding_clusters (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    embedding VECTOR(384) NOT NULL,
    label TEXT NOT NULL UNIQUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- robeings 사용자 권한 부여
GRANT SELECT ON coldmail_embedding_clusters TO robeings;
```

### Centroid 초기화 스크립트
**위치**: `/home/admin/scripts/init_coldmail_embeddings.py`

**샘플 데이터** (총 160개):
- Coldmail 80개: 투자/IR/VC 키워드 50개 + 파일명 형식 30개
- Normal 80개: 회의/공지/업무 키워드 50개 + 파일명 형식 30개

**실행 결과**:
```
coldmail centroid: 80개 샘플 평균, 차원=384
normal centroid: 80개 샘플 평균, 차원=384
✅ coldmail_embedding_clusters 테이블 centroid 업데이트 완료
```

### 임베딩 필터 성능 테스트

**테스트 결과** (고유명사 제외):
```
✅ 투자제안서 검토 요청: coldmail 0.736 > normal 0.441
✅ pitch deck 공유: coldmail 0.297 > normal 0.230
✅ 사업계획서 검토: coldmail 0.676 > normal 0.566
✅ 벤처캐피탈 투자 제안: coldmail 0.652 > normal 0.294
❌ 회사소개서 첨부: coldmail 0.534 < normal 0.661
❌ IR 행사 초대: coldmail 0.622 < normal 0.684
❌ 영수증 발급: coldmail 0.605 > normal 0.590 (오판)

정확도: 9/12 (75.0%)
```

**실패 원인 분석**:
- "회사소개서", "IR 행사" 같은 단어가 normal 패턴과도 겹침
- 고유명사("올굿즈컴퍼니", "빅웨이브")는 임베딩으로 처리 불가
- 단순 centroid + cosine similarity의 한계

### 개선 방안 조사 (Web Search 2025)

**1. Confidence-Driven Centroid**
- 잘못 분류된 샘플로 centroid 반복 업데이트
- 현재: 단순 평균 → 개선: 오분류 샘플 가중치 증가

**2. k-Nearest Centroid Neighbor (kNCN)**
- 단순 cosine similarity 대신 k개 이웃 고려
- Proximal Ratio로 노이즈 샘플 제거

**3. Random Forest > Centroid**
- 임베딩 기반 분류는 Random Forest가 centroid보다 정확도 높음
- 75% → 85%+ 개선 가능

**4. Domain-Specific Fine-tuning**
- 범용 임베딩 모델(multilingual-MiniLM)은 coldmail 도메인에 최적화 안 됨
- coldmail 특화 임베딩 모델 필요

**결론**:
- 단순 centroid 방식은 75% 정확도 한계
- 하이브리드 접근(임베딩 1차 → LLM 2차)이 더 효율적
- Phase 1 임베딩은 보조 역할, Phase 2 LLM이 최종 판단

---

## 실제 운영 사례 (2025-10-16)

### 파인티처 투자 제안 메일 누락

**발생 시각**: 09:05:07.096
**메일 정보**:
- 제목: '투자검토요청 관련하여 회사소개서 첨부드립니다_파인티처'
- 발신: petermin@fineteacher.com
- 수신: 2025-10-15 09:44 (24시간 검색 범위 내)

**필터링 결과**:
```
Stage 1 (Embedding): REJECT
coldmail: 0.2817, normal: 0.3435 (역전)
```

**문제점**:
- 명백한 투자 제안 메일이지만 임베딩 점수 역전으로 Stage 1 탈락
- Stage 2 (LLM) 도달 실패로 최종 0건 검출
- Slack 전송 없음

**원인**:
- Centroid 샘플이 실제 coldmail 패턴 반영 부족
- "투자검토요청", "회사소개서" 같은 명시적 키워드도 낮은 점수

**영향**:
- 실제 IR 제안 누락으로 비즈니스 기회 손실 가능
- 하이브리드 필터의 Stage 1 임계값 조정 필요성 확인

---

## 근본 해결 방향: 온톨로지 기반 접근

### 현재 방식의 한계
- **임베딩**: 벡터 유사도만으로 "투자검토요청 + 회사소개서" 의미 관계 이해 불가
- **LLM**: 학습 불가능, 비용 과다, 일관성 부족

### 온톨로지 기본 원칙
1. **개념 계층**: coldmail > {IR자료, 피칭덱, 사업계획서} 명시적 정의
2. **관계 표현**: "제목 CONTAINS 투자 AND 첨부 HAS PDF → IR자료" 규칙화
3. **추론 가능**: reasoner가 규칙 기반으로 자동 분류 + 설명 생성

### 구현 전 준비
- [ ] 핵심 개념 10개 정의 (coldmail, IR, 피칭, 발신자, 회사, ...)
- [ ] Competency questions (예: "파인티처 메일은 coldmail인가?")
- [ ] 기존 온톨로지 재사용 조사 (schema.org)

**상세 계획**: plans/251016_ontology_coldmail_implementation.md
**설계 원칙**: 200_core_design/225_온톨로지_기반_지식_표현.md

---

## 구현 완료 (2025-10-16)

### Phase 1 Coldmail 온톨로지 파일럿
**파일**: rb8001/app/services/coldmail_ontology_reasoner.py
**규칙**: 11개 (Coldmail 7개 + Normal 4개)
**통합**: rb8001/app/services/coldmail_hybrid_filter.py

### 테스트 결과
```
파인티처 메일: 0.28 (임베딩) → 0.90 (온톨로지)
- 4개 규칙 매칭: R1(0.90), R2(0.85), R3(0.80), R3B(0.65), R4(0.70)
- 최종 판정: coldmail (threshold 0.7 초과)

전체 테스트: 7/7 통과 (100%)
```

### 배포 상태
- **51124 서버**: 배포 완료
- **환경변수**: USE_ONTOLOGY_FILTER=true (활성화)
- **롤백**: 환경변수 false로 변경 시 즉시 복귀

### 롤백 방법
```bash
# .env 파일
USE_ONTOLOGY_FILTER=false

# Docker 재시작
cd /home/admin/ivada_project/rb8001
docker compose down && docker compose up -d
```

### 실전 검증 계획
- **일시**: 2025-10-17 09:05 Coldmail Daily Briefing
- **확인**: 로그에서 "Stage 1 (Ontology)" 메시지 확인
- **커밋**: 88636cf (hotfix), 7a122f4 (통합), 48aacfa (파일럿)

### 남은 작업
- [ ] Slack 피드백 → 베이지안 업데이트 (Phase 1.5)
- [ ] 첨부파일 정보 전달 (현재는 제목만)
- [ ] known_contacts 연동
- [ ] 본문 파라미터 추가
