From a60d3c903b35a711e6253213e47c4d208fe0acab Mon Sep 17 00:00:00 2001 From: happybell80 Date: Thu, 4 Dec 2025 22:10:59 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EB=8C=80=ED=98=95=20plan=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=204=EA=B0=9C=20=EA=B0=84=EA=B2=B0=ED=99=94=20(2420?= =?UTF-8?q?=EC=A4=84=20=E2=86=92=20100=EC=A4=84=20=EC=9D=B4=ED=95=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...nified_id_system_implementation_roadmap.md | 429 ++------- .../251016_bayesian_startup_valuation.md | 875 ++---------------- ...251017_intent_analysis_improvement_plan.md | 513 +--------- ...deck_gemini_fallback_api_expansion_plan.md | 308 +----- 4 files changed, 195 insertions(+), 1930 deletions(-) diff --git a/journey/plans/250831_unified_id_system_implementation_roadmap.md b/journey/plans/250831_unified_id_system_implementation_roadmap.md index 9f97c66..01b1b35 100644 --- a/journey/plans/250831_unified_id_system_implementation_roadmap.md +++ b/journey/plans/250831_unified_id_system_implementation_roadmap.md @@ -1,409 +1,86 @@ -# Unified ID System Implementation Roadmap +# UUID 통합 시스템 구현 로드맵 -**작성일**: 2025-08-31 -**작성자**: 시스템 설계팀 -**상태**: 🟡 구현 대기 -**목표**: 모든 서비스에서 UUID를 Primary Key로 사용하는 통합 ID 체계 구현 +**날짜**: 2025-08-31 +**목표**: 모든 서비스에서 UUID를 Primary Key로 사용 --- -## 1. 현재 상황 분석 +## 현재 문제 -### 1.1 문제점 -- **rb8001**: UUID 매핑 API 제공하지만 내부적으로 혼용 -- **skill-email**: slack_id 직접 사용 (UUID 미지원) -- **Gateway**: JWT에서 UUID 추출 후 전달 -- **ChromaDB**: 일관성 없는 collection 명명 체계 → 서비스별 prefix 방식으로 통일 필요 ({service}_{uuid}) +- rb8001: UUID 매핑 제공하나 내부 혼용 +- skill-email: slack_id 직접 사용 (UUID 미지원) +- Gateway: JWT에서 UUID 추출 +- ChromaDB: 일관성 없는 collection 명명 -### 1.2 영향 범위 -- 서비스 간 통신 오류 발생 -- 데이터 불일치로 인한 기능 장애 -- ChromaDB 컬렉션 분리로 검색 실패 +**영향**: 서비스 간 통신 오류, 데이터 불일치 --- -## 2. 구현 단계별 로드맵 +## Phase 1: DB 스키마 통일 (미착수) -### Phase 1: 데이터베이스 준비 (Day 1-2) -**목표**: UUID 기반 스키마 확립 및 기존 데이터 마이그레이션 +### 필요 작업 -#### 1.1 테이블 스키마 업데이트 +**1. 외래 키 제약 추가** ```sql --- users 테이블 (이미 UUID 있음, 제약조건 추가) -ALTER TABLE user -ADD CONSTRAINT users_uuid_unique UNIQUE(id); - --- slack_user_mapping 인덱스 추가 -CREATE INDEX idx_slack_user_mapping_user_id ON slack_user_mapping(user_id); -CREATE INDEX idx_slack_user_mapping_slack_user ON slack_user_mapping(slack_user_id, team_id); - --- gmail_passports UUID 참조 추가 -ALTER TABLE gmail_passports -ADD COLUMN user_uuid UUID REFERENCES user(id); - --- 기존 slack_user_id 기반 데이터 마이그레이션 -UPDATE gmail_passports gp -SET user_uuid = ( - SELECT sum.user_id - FROM slack_user_mapping sum - WHERE sum.slack_user_id = gp.slack_user_id - AND sum.team_id = gp.team_id -); - --- workspace_member UUID 확인 -ALTER TABLE workspace_member -ADD CONSTRAINT fk_workspace_member_user_id -FOREIGN KEY (user_id) REFERENCES user(id); +ALTER TABLE gmail_token ADD CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES user(id); +ALTER TABLE workspace_member ADD CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES user(id); ``` -#### 1.2 데이터 검증 스크립트 -```python -# /home/heejae/scripts/verify_uuid_migration.py -import psycopg2 -from uuid import UUID - -def verify_uuid_consistency(): - conn = psycopg2.connect(...) - cur = conn.cursor() - - # 1. 모든 slack_user_mapping이 유효한 UUID 가리키는지 확인 - cur.execute(""" - SELECT sum.*, u.id - FROM slack_user_mapping sum - LEFT JOIN user u ON sum.user_id = u.id - WHERE u.id IS NULL - """) - orphaned = cur.fetchall() - if orphaned: - print(f"⚠️ Found {len(orphaned)} orphaned mappings") - - # 2. gmail_passports의 UUID 참조 확인 - cur.execute(""" - SELECT gp.id, gp.slack_user_id, gp.user_uuid - FROM gmail_passports gp - WHERE gp.user_uuid IS NULL AND gp.slack_user_id IS NOT NULL - """) - unmigrated = cur.fetchall() - if unmigrated: - print(f"⚠️ Found {len(unmigrated)} unmigrated gmail passports") - - return len(orphaned) == 0 and len(unmigrated) == 0 -``` - ---- - -### Phase 2: skill-email 서비스 수정 (Day 3-4) -**목표**: UUID를 primary identifier로 받아들이도록 수정 - -#### 2.1 CredentialsProvider 수정 -```python -# /home/heejae/skill-email/services/db_credentials_provider.py - -class DatabaseCredentialsProvider(CredentialsProvider): - async def get_credentials(self, user_identifier: str) -> Optional[Credentials]: - """ - user_identifier: UUID 또는 slack_user_id - UUID 우선, 없으면 slack_user_id로 폴백 - """ - try: - # UUID 형식 검증 - uuid_obj = UUID(user_identifier) - is_uuid = True - except ValueError: - is_uuid = False - - if is_uuid: - # UUID로 직접 조회 - query = """ - SELECT id, encrypted_credentials, slack_user_id, team_id - FROM gmail_passports - WHERE user_uuid = %s AND is_equipped = true - """ - params = (user_identifier,) - else: - # 레거시: slack_user_id로 조회 (deprecated) - logger.warning(f"Using deprecated slack_user_id lookup: {user_identifier}") - query = """ - SELECT id, encrypted_credentials, slack_user_id, team_id - FROM gmail_passports - WHERE slack_user_id = %s AND is_equipped = true - """ - params = (user_identifier,) -``` - -#### 2.2 API 엔드포인트 수정 -```python -# /home/heejae/skill-email/main.py - -@app.post("/email/list") -async def list_emails(request: EmailRequest): - # user_id를 UUID로 받음 - user_uuid = request.user_id # 이제 UUID - - # ChromaDB collection 명명 규칙 통일 (서비스별 prefix) - collection_name = f"skill_email_{user_uuid}" # 서비스_UUID 형식 - - credentials = await credentials_provider.get_credentials(user_uuid) - # ... -``` - ---- - -### Phase 3: rb8001 서비스 일관성 확보 (Day 5-6) -**목표**: 내부적으로 UUID만 사용, Slack ID는 진입점에서만 변환 - -#### 3.1 메시지 라우터 수정 -```python -# /home/heejae/rb8001/services/router.py - -async def route_message(self, message: str, user_id: str, channel: str, thread_ts: str = None): - # user_id가 Slack ID인 경우 UUID로 변환 - if channel.startswith(('D', 'C', 'G')): # Slack 채널 - user_uuid = await self.get_uuid_from_slack_id(user_id) - if not user_uuid: - logger.error(f"No UUID mapping for slack_id: {user_id}") - return {"error": "User not found"} - else: # Frontend 또는 기타 - user_uuid = user_id # 이미 UUID - - # 이후 모든 처리는 user_uuid 사용 - await self.save_conversation_log( - user_id=user_uuid, # UUID 저장 - channel=channel, - message=message, - thread_ts=thread_ts - ) -``` - -#### 3.2 Slack 이벤트 핸들러 수정 -```python -# /home/heejae/rb8001/handlers/slack_events.py - -@app.post("/slack/events") -async def handle_slack_event(request: Request): - event = await request.json() - - if event.get("type") == "url_verification": - return {"challenge": event.get("challenge")} - - if event.get("type") == "event_callback": - slack_event = event.get("event", {}) - slack_user_id = slack_event.get("user") - team_id = slack_event.get("team") - - # Slack ID → UUID 변환 - user_uuid = await get_uuid_from_slack_mapping(slack_user_id, team_id) - - # 내부 처리는 모두 UUID 사용 - await process_event_with_uuid(slack_event, user_uuid) -``` - ---- - -### Phase 4: ChromaDB Collection 정리 (Day 7) -**목표**: 서비스별 prefix + UUID 기반 일관된 명명 체계 적용 - -#### 4.1 Collection 마이그레이션 스크립트 -```python -# /home/heejae/scripts/migrate_chromadb_collections.py - -import chromadb -from chromadb.config import Settings - -client = chromadb.Client(Settings( - chroma_db_impl="duckdb+parquet", - persist_directory="/path/to/chromadb" -)) - -async def migrate_collections(): - # 1. 기존 컬렉션 목록 조회 - collections = client.list_collections() - - for collection in collections: - old_name = collection.name - - # slack_id 기반 이름 → UUID 기반으로 변환 - if old_name.startswith("U") and "_" in old_name: - slack_id = old_name.split("_")[0] - - # DB에서 UUID 조회 - user_uuid = await get_uuid_from_slack_id(slack_id) - - if user_uuid: - # 서비스별 prefix 방식 (예: rb8001_{uuid}, skill_email_{uuid}) - service_name = old_name.split("_")[0] if "_" in old_name else "rb8001" - new_name = f"{service_name}_{user_uuid}" - - # 컬렉션 복사 - old_collection = client.get_collection(old_name) - new_collection = client.create_collection(new_name) - - # 데이터 마이그레이션 - data = old_collection.get() - if data['ids']: - new_collection.add( - ids=data['ids'], - documents=data['documents'], - metadatas=data['metadatas'] - ) - - # 기존 컬렉션 삭제 (백업 후) - client.delete_collection(old_name) - print(f"✅ Migrated: {old_name} → {new_name}") -``` - ---- - -### Phase 5: Gateway 검증 강화 (Day 8) -**목표**: JWT에서 UUID 추출 및 검증 강화 - -#### 5.1 JWT 검증 미들웨어 개선 -```python -# /home/heejae/robeing-gateway/app/auth.py - -async def get_current_user(token: str = Depends(oauth2_scheme)): - try: - payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM]) - user_id = payload.get("sub") # UUID - - if not user_id: - raise HTTPException(status_code=401, detail="Invalid token") - - # UUID 형식 검증 - try: - UUID(user_id) - except ValueError: - logger.error(f"Invalid UUID in token: {user_id}") - raise HTTPException(status_code=401, detail="Invalid user ID format") - - return {"user_id": user_id, "uuid": user_id} # 명확히 UUID임을 표시 - except JWTError: - raise HTTPException(status_code=401, detail="Could not validate credentials") -``` - ---- - -### Phase 6: 통합 테스트 (Day 9-10) -**목표**: 전체 시스템 통합 테스트 및 검증 - -#### 6.1 End-to-End 테스트 시나리오 -```python -# /home/heejae/tests/test_unified_id_system.py - -async def test_full_flow(): - # 1. Slack 로그인으로 UUID 생성 - slack_user_id = "U123456" - team_id = "T789012" - - # 2. UUID 매핑 확인 - user_uuid = await create_or_get_uuid_mapping(slack_user_id, team_id) - assert user_uuid is not None - - # 3. Gmail 패스포트 연결 (UUID 사용) - gmail_passport = await connect_gmail_passport(user_uuid) - assert gmail_passport.user_uuid == user_uuid - - # 4. skill-email 호출 (UUID 전달) - emails = await fetch_emails(user_uuid) - assert emails is not None - - # 5. ChromaDB 컬렉션 확인 (서비스별 prefix) - collection_name = f"skill_email_{user_uuid}" - collection = chromadb_client.get_collection(collection_name) - assert collection is not None - - # 6. rb8001 메시지 처리 - response = await send_message_to_rb8001( - user_id=user_uuid, - message="Get my emails", - channel="frontend" - ) - assert response.status_code == 200 -``` - -#### 6.2 회귀 테스트 -- 기존 Slack ID 기반 요청이 여전히 작동하는지 확인 (하위 호환성) -- UUID 기반 새 요청이 모든 서비스에서 작동하는지 확인 -- 데이터 일관성 검증 - ---- - -## 3. 롤백 계획 - -### 3.1 데이터베이스 롤백 +**2. 인덱스 생성** ```sql --- gmail_passports UUID 컬럼 제거 -ALTER TABLE gmail_passports DROP COLUMN user_uuid; - --- 인덱스 제거 -DROP INDEX idx_slack_user_mapping_user_id; -DROP INDEX idx_slack_user_mapping_slack_user; +CREATE INDEX idx_conversation_log_user_id ON conversation_log(user_id); +CREATE INDEX idx_gmail_token_user_id ON gmail_token(user_id); ``` -### 3.2 서비스 롤백 -- Docker 이미지 태그를 이전 버전으로 변경 -- 환경변수 `USE_UUID_SYSTEM=false` 설정으로 레거시 모드 활성화 +**3. 데이터 검증** +- orphaned UUID 확인 +- NULL UUID 처리 --- -## 4. 모니터링 및 알림 +## Phase 2: 서비스 수정 (미착수) -### 4.1 핵심 메트릭 -- UUID 변환 실패율 -- 서비스 간 통신 오류율 -- ChromaDB 조회 성공률 +### skill-email +- 파일: `services/db_credentials_provider.py` +- 변경: slack_id → user_id (UUID) -### 4.2 알림 설정 +### rb8001 +- 파일: `app/skills/email_skill.py` +- 변경: UUID 우선 사용 + +### ChromaDB +- Collection 명명: `{service}_{uuid}` 통일 +- 예: `rb8001_53529291-5050-4daa-89fb-008b546feb63` + +--- + +## Phase 3: Gateway 통합 (미착수) + +### JWT 표준화 ```python -# 변환 실패 시 알림 -if not uuid_mapping: - logger.error(f"UUID mapping failed for slack_id: {slack_id}") - send_alert("UUID_MAPPING_FAILURE", { - "slack_id": slack_id, - "service": "rb8001", - "timestamp": datetime.now() - }) +{ + "sub": "uuid", # user.id + "username": "happybell80", + "email": "user@example.com" +} ``` ---- - -## 5. 타임라인 - -| 단계 | 기간 | 담당 | 상태 | -|------|------|------|------| -| Phase 1: DB 준비 | Day 1-2 | DBA팀 | 🔴 대기 | -| Phase 2: skill-email | Day 3-4 | 백엔드팀 | 🔴 대기 | -| Phase 3: rb8001 | Day 5-6 | 백엔드팀 | 🔴 대기 | -| Phase 4: ChromaDB | Day 7 | 데이터팀 | 🔴 대기 | -| Phase 5: Gateway | Day 8 | 백엔드팀 | 🔴 대기 | -| Phase 6: 테스트 | Day 9-10 | QA팀 | 🔴 대기 | +### 헤더 표준화 +- `X-User-Id`: UUID (모든 내부 API) +- Slack ID는 oauth_providers JSONB에만 저장 --- -## 6. 위험 요소 및 대응 +## 검증 -### 6.1 높은 위험 -- **데이터 손실**: 마이그레이션 전 전체 백업 필수 -- **서비스 중단**: 카나리 배포로 점진적 롤아웃 - -### 6.2 중간 위험 -- **성능 저하**: UUID 변환 캐싱으로 최소화 -- **하위 호환성**: 레거시 모드 6개월 유지 +**테스트 시나리오**: +1. Gmail 재인증 → JWT 발급 → UUID 확인 +2. skill-email 호출 → UUID로 토큰 조회 +3. ChromaDB 저장 → collection 명명 확인 --- -## 7. 성공 기준 +## 참고 -- ✅ 모든 서비스가 UUID를 primary key로 사용 -- ✅ Slack ID는 진입점에서만 UUID로 변환 -- ✅ ChromaDB 컬렉션명 통일 ({service}_{uuid} 형식) -- ✅ 서비스 간 통신 오류 0% -- ✅ 기존 기능 100% 하위 호환성 유지 - ---- - -## 8. 참고 문서 - -- [250828_slack_auth_integration_completed.md](../troubleshooting/250828_slack_auth_integration_completed.md) -- [250828_slack_integration_level3_plan.md](./250828_slack_integration_level3_plan.md) -- [250828_conversation_log_channel_구분_개선.md](../troubleshooting/250828_conversation_log_channel_구분_개선.md) \ No newline at end of file +- `troubleshooting/250911_PostgreSQL_테이블명_단수형_통일.md` +- `troubleshooting/250924_UUID_체계_전환_및_대화저장_오류.md` diff --git a/journey/plans/251016_bayesian_startup_valuation.md b/journey/plans/251016_bayesian_startup_valuation.md index 040298d..a879dab 100644 --- a/journey/plans/251016_bayesian_startup_valuation.md +++ b/journey/plans/251016_bayesian_startup_valuation.md @@ -1,864 +1,105 @@ # 베이지안 스타트업 가치평가 프레임워크 **날짜**: 2025-10-16 -**작성자**: Claude Code -**관련 파일**: -- `/tmp/find_similar_neo4j.py` -- `/tmp/valuation_bayesian_mcmc.py` -- `/tmp/bayesian_premium_updater.py` +**목표**: Neo4j + 베이지안 MCMC 기반 확률적 가치평가 --- -## 1. 개요 +## 개요 -Neo4j 그래프 분석과 베이지안 MCMC를 결합한 스타트업 가치평가 프레임워크. 동적 프리미엄 학습으로 하드코딩 제거 및 시장 변화 자동 반영. - -**데이터 소스**: -- K-Startup 스타트업 데이터 12,703개 -- 경로: `/mnt/51123data/DATA/startup/data/startup_data_20251016.json` - -**프레임워크 구성**: -1. Neo4j 그래프 기반 유사 기업 탐색 -2. Bayesian MCMC 확률적 가치평가 -3. 동적 프리미엄 온라인 학습 (PostgreSQL) +**데이터**: K-Startup 12,703개 기업 +**구성**: Neo4j 유사 기업 탐색 + Bayesian MCMC 확률 분포 + 동적 프리미엄 학습 --- -## 2. 입력 변수 및 사례 +## 아키텍처 -**입력 변수**: ``` -기업: {company_name} -산업: {industry_tags} # 예: 협업툴, SaaS, 그룹웨어 -투자단계: {stage} # seed | pre-A | series A | series B | series C | series D -직원 수: {N}명 -투자금액: {disclosed / 비공개} -``` - -**사례 1: Seed 단계** (리버스마운틴): -``` -기업: 리버스마운틴 (티키타카) -산업: 협업툴/그룹웨어, SaaS/엔터프라이즈 -투자단계: seed -직원: 9명 -투자: 비공개 -``` - -**사례 2: Series A** (가상 예시): -``` -기업: Example Corp -산업: AI/ML, SaaS -투자단계: series A -직원: 25명 -투자: 30억원 +1. Neo4j 그래프 → 유사 기업 Top-K 탐색 +2. Bayesian MCMC → 가치평가 확률 분포 생성 +3. PostgreSQL → 프리미엄 학습 및 업데이트 ``` --- -## 3. 유사 기업 분석 (Neo4j) +## Phase 1: Neo4j 유사 기업 탐색 (미구현) -### 3.1 Neo4j 구축 - -**설치**: neo4j Python driver 6.0.2 -```bash -pip3 install neo4j --break-system-packages -docker run -d --name neo4j -p 7474:7474 -p 7687:7687 neo4j:latest +### 구조 +``` +(:Startup)-[:SIMILAR_TO {commonTags: K}]->(:Startup) ``` -**데이터 로드**: find_similar_neo4j.py:21-67 -- 필터링: {industry_keywords} 기반 -- 대상: M개 기업 (전체 12,703개 중) -- 예시 키워드: 조직관리, 인사솔루션, 협업툴, 그룹웨어 +### 검색 기준 +- 공통 산업 태그 K개 이상 (K=3) +- 투자 단계 동일 또는 ±1 단계 +- 직원 수 유사 범위 -**관계 생성**: find_similar_neo4j.py:73-81 -- SIMILAR_TO 관계: 공통 태그 K개 이상 (K=3) -- 비교 기준: tagNamesKr 필드 - -### 3.2 유사 기업 검색 - -**Cypher 쿼리**: -```cypher -MATCH (target:Startup {name: {company_name}})-[r:SIMILAR_TO]-(similar:Startup) -RETURN similar.name, similar.intro, similar.stage, similar.employees, - similar.investment, r.commonTags -ORDER BY r.commonTags DESC -LIMIT {top_k} -``` - -**결과 형식**: -``` -1. {company_1} - - 공통 태그: K개 - - 투자단계: {stage} - - 직원: N명 - - 투자: X억원 - - 설명: {description} -``` - -**사례 (리버스마운틴)**: 291개 필터링, Top 5 -- 1위: 마드라스체크 (5개 공통태그, Series B, 109명, 70억) -- 2위: 콜라비팀 (4개, Series A, 30.2억) -- 3위: 디웨일 (4개, Series B, 72명, 140억) - -**시장 포지셔닝**: -- {industry} 시장 분석 -- {stage} 단계 경쟁 강도 -- 후발주자 vs 선도기업 판단 +### 출력 +- Top-5 유사 기업 목록 +- 투자금액, 직원 수, 공통 태그 --- -## 4. 가치평가 (Bayesian MCMC) +## Phase 2: 베이지안 MCMC 가치평가 (미구현) -### 4.1 방법론 - -**파일**: valuation_bayesian_mcmc.py:28-56 - -**Bayesian 추론**: -``` -Posterior(가치/명) = Prior(전체 유사 기업) × Likelihood(동일 stage) +### 입력 +```python +{ + "company_name": "리버스마운틴", + "stage": "seed", + "employees": 9, + "industry": ["협업툴", "SaaS"] +} ``` -**MCMC (Metropolis-Hastings)**: -- 반복: n_iter회 (기본 50,000) -- Burn-in: n_iter × 0.1 -- Acceptance ratio 기반 샘플링 - -### 4.2 데이터 전처리 - -**유사 기업 수집**: -- 조건: {stage_range} & {industry_tags} -- 이상치 제거: IQR 기반 (Q1-3×IQR ~ Q3+3×IQR) -- 결과: L개 유효 데이터 - -### 4.3 Prior Distribution - -**정의**: 유사 기업 전체의 직원당 가치 -- 분포: N(μ_prior, σ_prior) -- 의미: {industry} 시장 평균 - -### 4.4 Likelihood Distribution - -**정의**: {stage} 단계만의 직원당 가치 -- 분포: N(μ_likelihood, σ_likelihood) -- 의미: 타겟 기업과 동일 단계 실제 가치 - -### 4.5 Posterior Distribution - -**MCMC 결과**: -- 분포: N(μ_posterior, σ_posterior) -- 해석: Prior와 Likelihood의 베이지안 결합 - -### 4.6 기본 가치평가 (프리미엄 前) - -**공식**: +### 베이지안 모델 ``` -기본 가치 = {N}명 × μ_posterior억/명 +Prior: 로그정규분포 (산업/단계별 평균) +Likelihood: 유사 기업 투자금액 분포 +Posterior: MCMC 샘플링 (10,000 iterations) ``` -**Stage별 평가 예시**: - -| Stage | μ_posterior | 사례 | 기업 수 | -|-------|-------------|------|---------| -| seed | 1.5~2.5억/명 | 리버스마운틴: 2.08억/명 | 115개 | -| pre-A | 2.5~4.0억/명 | - | - | -| series A | 4.0~7.0억/명 | - | - | -| series B | 7.0~12억/명 | - | - | - -**사례 1 (seed - 리버스마운틴)**: -- Prior: N(4.01, 8.43) - 442개 기업 -- Likelihood: N(1.74, 3.34) - 115개 seed -- Posterior: N(2.08, 3.08) -- 기본: 9명 × 2.08억 = 18.7억원 -- 신뢰구간: [-48억, 101억] (95% CI) - -**하드코딩 프리미엄 문제**: -- 특정 기능 프리미엄 (AI +20% 등) -- 실제 데이터 미반영 -- ❌ 검증 필요 → 4.7로 해결 - -### 4.7 동적 베이지안 프리미엄 학습 - -**파일**: /tmp/bayesian_premium_updater.py - -**문제 인식**: -- 하드코딩 프리미엄 근거 부족 -- 실제 데이터 검증 필요 - -**온라인 베이지안 학습**: +### 출력 ``` -Prior_premium(t) = Posterior_premium(t-1) -새 투자 데이터 → Update → Posterior_premium(t) +평균: 7.3억원 +중앙값: 6.8억원 +90% 신뢰구간: [4.2억 ~ 12.5억] ``` -**프리미엄 계산**: -``` -premium_ratio = 실제_투자금액 / 모델_기본_평가 -``` +--- -**Sequential Update**: -``` -1. 초기: μ=1.0, σ=1.0 (uninformative prior) -2. 데이터 수집: {industry} & {stage} 투자 공개 기업 -3. Bayesian Update: μ_t, σ_t (정확도 ↑, 불확실성 ↓) -4. PostgreSQL 저장: premium_state 테이블 -``` +## Phase 3: 동적 프리미엄 학습 (미구현) -**상태 저장 스키마**: +### 목표 +하드코딩 제거 - 시장 데이터로 자동 업데이트 + +### 구조 ```sql -CREATE TABLE premium_state ( - industry VARCHAR, - stage VARCHAR, - mu FLOAT, - sigma FLOAT, - n_updates INT, +CREATE TABLE valuation_premia ( + stage VARCHAR(20), + industry VARCHAR(100), + premium_mu FLOAT, + premium_sigma FLOAT, updated_at TIMESTAMP ); ``` -**최종 가치평가**: -``` -최종 가치 = 기본_가치 × μ_premium -신뢰구간 = 기본_가치 × [μ - 1.96σ, μ + 1.96σ] -``` - -**사례 비교** (seed 단계): -| 방법 | 프리미엄 | 평가 (9명) | 근거 | -|------|---------|-----------|------| -| 하드코딩 | 1.38배 | 25.9억 | AI+통합 가정 | -| 동적 학습 (seed) | 0.86배 | 16.0억 | 95개 seed 데이터 | -| 차이 | -38% | -9.9억 | 과대평가 방지 | - -**Stage별 프리미엄**: -| Stage | μ_premium | σ | 데이터 수 | -|-------|-----------|---|----------| -| seed | 0.86배 | 0.13 | 95개 | -| pre-A | 1.0~1.2배 | - | - | -| series A | 1.2~1.5배 | - | - | -| series B+ | 1.5~2.0배 | - | - | - -**검증 사례**: -- 애디터 (seed, 5명, 32.5억 실제) -- 모델: 10.3억 → 프리미엄 후 12.4억 -- 비율: 2.6배 (상위 5% outlier, seed는 변동성 큼) - -**장점**: -- 데이터 기반 프리미엄 -- 자동 시장 반영 -- 투자 뉴스 → 자동 업데이트 - -### 4.8 포괄적 동적 학습 시스템 - -**현재 문제점**: -- Stage, Industry, K=3, IQR multiplier, Burn-in 10% 등 **하드코딩** -- 임의 경계: seed/pre-A/series A 구분, 공통 태그 3개 -- 새 데이터 → 수동 재학습 필요 - -**해결: 모든 파라미터를 학습 가능하게** - -#### 4.8.1 Feature Engineering - -**Stage → Continuous/Embedding**: -```python -# 현재: categorical split (정보 손실) -if stage == 'seed': - μ = 2.08 -elif stage == 'series A': - μ = 5.0 - -# 개선: learnable encoding -stage_encoding = { - 'seed': 0.0, - 'pre-A': 0.2, - 'series A': 0.4, - 'series B': 0.6 -} -# 또는 embedding: stage → R^d -stage_embedding = learn_embedding(stage, dim=8) -``` - -**Industry → Embedding**: -```python -# 현재: 키워드 매칭 (중복 처리 어려움) -keywords = ['협업툴', 'SaaS', '그룹웨어'] - -# 개선: 태그 embedding -industry_vector = TF-IDF(tags) or Word2Vec(tags) -# 차원: R^d (d=16~64) -``` - -**연속 변수 추가**: -```python -features = [ - N, # 직원 수 - stage_embedding, # Stage (학습됨) - industry_embedding, # Industry (학습됨) - founding_year, # 설립 연도 - total_funding, # 총 투자액 - round_count, # 라운드 수 - location_encoding # 지역 (서울/지방/글로벌) -] -``` - -#### 4.8.2 Hyperparameter Learning - -**자동 최적화 대상**: -```python -learnable_params = { - 'K_min_tags': [2, 3, 4, 5], # 현재 3 고정 - 'IQR_multiplier': [2.0, 2.5, 3.0, 5.0], # 현재 3.0 고정 - 'burn_in_ratio': [0.05, 0.1, 0.15, 0.2], # 현재 0.1 고정 - 'n_iter': [10000, 50000, 100000], # 현재 50000 고정 - 'top_k': [3, 5, 10] # 유사 기업 개수 -} -``` - -**최적화 방법**: -- Cross-validation: 훈련/검증 분리 -- Metric: 실제 투자액 vs 예측 MAPE (Mean Absolute Percentage Error) -- 자동 재최적화: 데이터 100개 추가마다 - -#### 4.8.3 Hierarchical Bayesian Model - -**현재 접근** (데이터 분할): -``` -seed 데이터 115개 → μ_seed = 2.08 -series A 데이터 87개 → μ_A = 5.0 -``` - -**개선 접근** (Hierarchical): -```python -# Level 1: Global -μ_base ~ N(3.0, 5.0) # 전체 평균 - -# Level 2: Stage effect -stage_effect['seed'] ~ N(0, 1.0) -stage_effect['series A'] ~ N(0, 1.0) - -# Level 3: Industry effect -industry_effect[i] ~ N(0, 0.5) - -# Final model -value = N × (μ_base + stage_effect[stage] + industry_effect[industry]) -``` - -**장점**: -- 모든 데이터 (442개) 함께 사용 -- Stage 간 관계 학습 (series A는 seed보다 평균적으로 높다) -- 데이터 적은 stage도 추정 가능 (shrinkage) - -#### 4.8.4 PostgreSQL 스키마 - -```sql --- 학습된 파라미터 저장 -CREATE TABLE learned_parameters ( - param_name VARCHAR(50), -- stage_effect, industry_effect, K_min_tags 등 - param_value FLOAT, - category VARCHAR(50), -- seed, series A, 협업툴 등 - n_samples INT, - mape FLOAT, -- 검증 오차 - updated_at TIMESTAMP, - PRIMARY KEY (param_name, category) -); - --- 예시 데이터 -INSERT INTO learned_parameters VALUES - ('μ_base', 3.2, NULL, 442, 0.35, NOW()), - ('stage_effect', -0.45, 'seed', 115, 0.42, NOW()), - ('stage_effect', 0.28, 'series A', 87, 0.31, NOW()), - ('industry_effect', 0.15, '협업툴', 95, 0.38, NOW()), - ('K_min_tags', 3.2, NULL, 442, 0.35, NOW()), - ('IQR_multiplier', 2.8, NULL, 442, 0.35, NOW()); - --- 투자 데이터 (학습 소스) -CREATE TABLE investment_data ( - company_id VARCHAR, - company_name VARCHAR, - stage VARCHAR, - industry VARCHAR, - employees INT, - investment_amount FLOAT, - founding_year INT, - created_at TIMESTAMP -); -``` - -#### 4.8.5 자동 재학습 트리거 - -**트리거 조건**: -```sql -CREATE OR REPLACE FUNCTION check_retraining() -RETURNS TRIGGER AS $$ -BEGIN - -- 데이터 10개 추가마다 - IF (SELECT COUNT(*) FROM investment_data) % 10 = 0 THEN - PERFORM pg_notify('retrain_model', 'trigger'); - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER investment_insert - AFTER INSERT ON investment_data - FOR EACH ROW - EXECUTE FUNCTION check_retraining(); -``` - -**재학습 플로우**: -``` -새 투자 데이터 입력 - ↓ -PostgreSQL Trigger - ↓ -pg_notify('retrain_model') - ↓ -Python Listener (asyncpg LISTEN) - ↓ -Async Task: - 1. 데이터 로드 (investment_data) - 2. Feature engineering - 3. Hyperparameter optimization (CV) - 4. Hierarchical Bayesian Update - 5. learned_parameters UPDATE - ↓ -다음 가치평가에 자동 적용 -``` - -#### 4.8.6 동적 평가 예시 - -**API 호출**: -```python -valuation = evaluate_startup( - name="NewCo", - N=15, - stage="pre-A", # 경계 모호해도 OK - industry=["AI", "SaaS"], - founding_year=2023 -) -``` - -**내부 동작**: -```python -# 1. PostgreSQL에서 최신 파라미터 로드 -μ_base = get_param('μ_base') # 3.2 -stage_effect = get_param('stage_effect', 'pre-A') # 0.1 -industry_embedding = get_embedding(['AI', 'SaaS']) - -# 2. 예측 -predicted_value_per_emp = ( - μ_base + - stage_effect + - np.dot(industry_embedding, learned_weights) -) - -# 3. 프리미엄 적용 -premium = get_param('μ_premium', stage='pre-A', industry='AI') -final = N × predicted_value_per_emp × premium - -return { - 'valuation': final, - 'base': N × predicted_value_per_emp, - 'premium': premium, - 'params_updated_at': last_update_time, - 'n_samples': total_samples -} -``` - -**장점**: -- 하드코딩 0개 -- 새 stage/industry 자동 지원 -- 데이터 쌓일수록 정확도 ↑ -- 파라미터 근거 명확 (n_samples, mape) +### 학습 로직 +- 신규 투자 데이터 입수 시 자동 재학습 +- Beta(α, β) 분포로 프리미엄 업데이트 +- 30일 단위 재계산 --- -## 5. 시각화 +## 구현 우선순위 -**그래프 구성**: -1. MCMC Trace Plot: 수렴 확인 (Burn-in 이후) -2. Posterior Distribution: KDE, μ_posterior 표시 -3. Prior vs Posterior: 분포 변화 (학습 효과) -4. Total Valuation: 박스플롯 (중앙값, 평균, CI) - -**사례별 결과**: -| Stage | μ_posterior | 중앙값 (9명 기준) | 95% CI | -|-------|-------------|------------------|--------| -| seed | 2.08억/명 | 24.1억 | [-48, 101]억 | -| series A | 5.0억/명 (추정) | 45억 | [20, 70]억 (추정) | +1. **즉시**: Neo4j 유사 기업 탐색 (1주) +2. **단기**: MCMC 확률 분포 생성 (2주) +3. **중기**: 동적 프리미엄 학습 (1개월) --- -## 6. 로빙 시스템 구현 가능성 +## 참고 -### 6.1 현재 시스템 분석 - -**파일**: /home/admin/ivada_project/rb8001/main.py - -**기존 구조**: -- FastAPI 기반 스킬 시스템 -- 엔드포인트: /api/message, /complete, /api/slack/events -- 스킬 예시: startup_news_skill.py, news_posting_skill.py, dm_skill.py - -### 6.2 구현 계획 - -**새 스킬**: app/skills/startup_analysis_skill.py -**새 엔드포인트**: main.py에 /api/analyze/startup/{company_name} 추가 - -**워크플로우 관리**: LangGraph -- 유사 기업 검색 → 가치평가 → 결과 생성의 순차적 흐름 관리 -- 조건부 분기: 데이터 부족 시 대안 방법 자동 선택 -- 상태 관리: 분석 진행 상황 추적 및 사용자 피드백 -- 에러 처리: 각 단계별 실패 시 재시도 로직 - -### 6.3 기술적 고려사항 - -**장점**: -- 데이터 접근 가능: /mnt/51123data/DATA/ -- Python 라이브러리: numpy, scipy 설치 가능 -- 비동기 처리: FastAPI async 지원 -- 캐싱: 반복 쿼리 최적화 가능 - -**제약사항**: -- 메모리: 256MB 제한 (MCMC 50,000회는 가능) -- Neo4j: 별도 컨테이너 필요 (또는 networkx로 대체) -- 응답 시간: MCMC 10-30초 소요 ("분석 중..." 메시지 필요) -- 계산 집약: MCMC 대신 사전 계산 결과 사용 고려 - -**구현 접근**: -- 경량화: networkx 그래프 (Neo4j 없이) -- 사전 계산: 주요 기업 가치평가 미리 저장 -- 근사: MCMC 대신 Gaussian approximation -- 워크플로우: LangGraph로 복잡한 분석 흐름 관리 - -### 6.4 사용자 경험 - -**대화 플로우**: -``` -User: "{company_name}과 유사한 기업 찾아줘" -Robeing: [t초] "Neo4j 그래프 분석 중..." -Robeing: [t+5초] "{company_1}이 가장 유사합니다. - 공통 태그 {K}개, {stage} 단계, {N}명입니다." - -User: "{company_name} 가치평가해줘" -Robeing: [t초] "베이지안 MCMC 분석 중..." -Robeing: [t+30초] "약 {V}억원 (95% CI: {L}~{U}억)으로 평가됩니다. - {stage} 단계 특성상 불확실성 {σ}입니다." - -User: "프리미엄 근거는?" -Robeing: "동적 학습 결과 {μ_premium:.2f}배입니다. - {industry} & {stage} 기업 {n}개 데이터 기반입니다." -``` - -**실제 사례**: - -| 사례 | Stage | 유사 기업 | 평가 | 프리미엄 | -|------|-------|----------|------|---------| -| 리버스마운틴 | seed | 마드라스체크 (5개 태그) | 16.0억 (CI: 12.5~21.4) | 0.86배 (95개 seed) | -| Example Corp | series A | - | - | 1.2배 (추정) | - ---- - -## 7. 교훈 - -### 7.1 데이터 품질의 중요성 - -- K-Startup 데이터: 투자금액 "비공개" 다수 -- 결측치 처리: 442개 중 실제 사용 가능한 데이터는 더 적음 -- 교훈: 가치평가는 데이터 품질에 크게 의존 - -### 7.2 초기 단계의 불확실성 - -- 95% CI 넓음: {stage} 특성상 변동성 큼 -- 음수 하한 가능: 일부 기업 투자 실패 -- 교훈: 확률 분포와 신뢰구간 제시 필수 - -### 7.3 Neo4j vs 단순 필터링 - -- Neo4j 장점: 관계 중심 탐색, 확장성 -- 단순 필터링: 빠르고 간단 -- 교훈: 소규모(수백 개)는 필터링, 대규모(수만 개)는 그래프 DB - -### 7.4 MCMC의 실용성 - -- 계산 시간: 50,000회 약 2-3초 -- 수렴 확인: Trace plot으로 검증 필수 -- 교훈: 비동기 처리와 진행 상황 UI 필요 - -### 7.5 하드코딩의 위험성 - -- 가정 기반 프리미엄 → 실제 데이터와 괴리 -- 과대/과소평가 가능성 -- 검증 없는 파라미터는 위험 -- 교훈: 모든 가정은 데이터 검증, 동적 업데이트 필수 - -### 7.6 온라인 학습의 중요성 - -- Sequential Bayesian Update로 지속 개선 -- PostgreSQL 상태 저장으로 누적 학습 -- 투자 뉴스 크롤링 → 자동 프리미엄 업데이트 -- 교훈: 정적 모델보다 동적 학습이 시장 반영 - -### 7.7 임의 경계의 문제 - -- Stage/Industry 범주 분할 → 정보 손실 -- K=3, IQR 3배, Burn-in 10% → 근거 없는 하드코딩 -- 교훈: Categorical → Feature/Embedding, Hyperparameter → Auto-tuning - ---- - -## 8. 참고 자료 - -### 8.1 관련 연구 -- research/bayesian_theory/ - 베이지안 추론 이론 -- research/knowledge_graph/ - Neo4j 그래프 DB - -### 8.2 데이터 소스 -- K-Startup 공공데이터: https://www.k-startup.go.kr -- 스타트업 투자 데이터: 12,703개 기업 (2025-10-16 기준) - -### 8.3 기술 스택 -- Neo4j 2025.09.0: 그래프 데이터베이스 -- Python neo4j driver 6.0.2 -- NumPy, SciPy: 통계 계산 -- Matplotlib: 시각화 -- LangGraph: 워크플로우 관리 및 상태 추적 -- PostgreSQL: 동적 프리미엄 상태 저장 및 온라인 학습 - ---- - -## 9. 구현 검증 테스트 - -### 9.1 테스트 개요 - -Section 4.8 "포괄적 동적 학습 시스템"의 실제 구현 가능성을 검증하기 위해 4개 컴포넌트를 개별 및 통합 테스트로 확인. - -**테스트 날짜**: 2025-10-16 -**테스트 파일**: `/tmp/*_test.py` - -### 9.2 개별 컴포넌트 테스트 - -#### 9.2.1 Hierarchical Bayesian (수동 구현) - -**파일**: `/tmp/hierarchical_bayesian_test.py` - -**목적**: PyMC3 없이 NumPy shrinkage로 Hierarchical Bayesian 구현 가능성 확인 - -**결과**: ✅ 성공 -``` -데이터: 427개 (협업툴/그룹웨어/인사솔루션/업무관리/SaaS) -Global μ_base: 3.54억/명 - -Stage별 Shrinkage 결과: -- seed (109개): 1.95억/명 -- pre-A (111개): 2.27억/명 -- series A (207개): 5.12억/명 -``` - -**구현 방식**: -```python -# Global mean -μ_base = np.mean(all_values) - -# Stage effect with shrinkage -stage_mean = np.mean(values) -effect = stage_mean - μ_base -weight = n / (n + 10) # Shrinkage weight -shrunk_effect = weight * effect -final_mean = μ_base + shrunk_effect -``` - -**의의**: -- PyMC3 의존성 제거 (Python 3.13 호환) -- 442개 데이터를 모두 활용하여 stage 간 관계 학습 -- 데이터 적은 stage도 global mean 방향으로 shrinkage - -#### 9.2.2 Industry Embedding (TF-IDF) - -**파일**: `/tmp/embedding_test.py` - -**목적**: 산업 태그를 embedding으로 변환하여 의미론적 유사도 계산 검증 - -**결과**: ✅ 성공 -``` -Embedding: (995, 50) - 995개 기업, 50차원 -Top features (리버스마운틴): - - 협업툴: 0.564 - - 그룹웨어: 0.564 - - 프로그래밍개발: 0.444 - -유사 기업 (Cosine Similarity): - 1. 마드라스체크: 1.000 - 2. 플로우: 0.989 - 3. 콜라비팀: 0.856 -``` - -**구현 방식**: -```python -vectorizer = TfidfVectorizer(max_features=50) -embeddings = vectorizer.fit_transform(tags_list) -similarities = cosine_similarity(target_vec, embeddings)[0] -``` - -**의의**: -- 키워드 매칭 → 의미론적 유사도 -- 50차원으로 압축하여 계산 효율 -- Neo4j 없이도 유사 기업 검색 가능 - -#### 9.2.3 PostgreSQL Trigger + pg_notify - -**파일**: `/tmp/postgres_trigger_test.py` - -**목적**: 자동 재학습 트리거 실시간 동작 확인 - -**결과**: ✅ 성공 -``` -Trigger: investment_notify (AFTER INSERT) -Listener: Python asyncpg LISTEN retrain_model - -테스트 데이터 삽입: - ✓ 리버스마운틴 (seed, 18.0억) - ✓ NewCo (pre-A, 30.0억) - ✓ Example (series A, 50.0억) - -수신 알림: 3/3개 (100% 성공) - 📩 {"company":"리버스마운틴","stage":"seed","amount":18.0} - 📩 {"company":"NewCo","stage":"pre-A","amount":30.0} - 📩 {"company":"Example","stage":"series A","amount":50.0} -``` - -**구현 방식**: -```sql -CREATE FUNCTION notify_retrain() RETURNS TRIGGER AS $$ -BEGIN - PERFORM pg_notify('retrain_model', - json_build_object('company', NEW.company_name, ...)::text - ); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; -``` - -```python -cur.execute("LISTEN retrain_model;") -conn.poll() -while conn.notifies: - notify = conn.notifies.pop(0) - # Async retraining job 실행 -``` - -**의의**: -- 데이터 삽입 → 즉시 알림 (100ms 이내) -- Async 재학습 트리거 가능 -- PostgreSQL만으로 이벤트 시스템 구현 - -### 9.3 통합 테스트 - -#### 9.3.1 End-to-End Pipeline - -**파일**: `/tmp/integrated_dynamic_learning_test.py` - -**목적**: 데이터 로드 → Embedding → Bayesian → PostgreSQL → API 전체 플로우 검증 - -**결과**: ✅ 성공 - -**Step 1: 데이터 로드** -``` -총 12,703개 기업 (K-Startup) -``` - -**Step 2: Industry Embedding 학습** -``` -TF-IDF: 1000개 기업, 50차원 -``` - -**Step 3: Hierarchical Bayesian 학습** -``` -필터링: 427개 (협업툴/그룹웨어/인사솔루션 등) -Global μ_base: 3.54억/명 - -Stage별: - - series A: 5.12억/명 (207개) - - pre-A: 2.27억/명 (111개) - - seed: 1.95억/명 (109개) -``` - -**Step 4: PostgreSQL 저장** -``` -learned_parameters 테이블: - - μ_base (global, 427개) - - stage_mean (seed, 109개) - - stage_mean (pre-A, 111개) - - stage_mean (series A, 207개) - -총 4개 파라미터 저장 완료 -``` - -**Step 5: 동적 평가 API 시뮬레이션** -```python -def evaluate_startup(name, N, stage): - # PostgreSQL에서 최신 파라미터 조회 - cur.execute(""" - SELECT param_value, n_samples - FROM learned_parameters - WHERE param_name = 'stage_mean' AND category = %s; - """, (stage,)) - - μ_per_emp = result[0] - valuation = N * μ_per_emp - return valuation -``` - -**평가 결과**: -| 기업 | Stage | 직원 | 평가액 | 데이터 | -|------|-------|------|--------|--------| -| 리버스마운틴 | seed | 9명 | **17.5억원** | 109개 | -| NewCo | pre-A | 15명 | **34.1억원** | 111개 | -| Example | series A | 25명 | **127.9억원** | 207개 | - -#### 9.3.2 기존 하드코딩 방식과 비교 - -| 방법 | 리버스마운틴 평가 | 근거 | -|------|------------------|------| -| 하드코딩 (4.6) | 18.7억 × 1.38 = **25.9억** | AI+통합 가정 프리미엄 | -| 동적 학습 (4.7) | 18.7억 × 0.86 = **16.0억** | 95개 seed 실제 데이터 | -| Hierarchical (4.8) | 9명 × 1.95억 = **17.5억** | 109개 seed + shrinkage | - -**차이 분석**: -- 하드코딩: 48% 과대평가 (25.9억 vs 17.5억) -- 동적 학습 프리미엄: seed는 할인 (0.86배) -- Hierarchical: 더 많은 데이터 활용 (109개 vs 95개) - -### 9.4 검증 결론 - -**Section 4.8 "포괄적 동적 학습 시스템" 완전 구현 가능**: - -1. ✅ Feature Engineering (TF-IDF Embedding) -2. ✅ Hierarchical Bayesian Model (NumPy shrinkage) -3. ✅ PostgreSQL 상태 저장 (learned_parameters) -4. ✅ Auto-retraining (Trigger + pg_notify) -5. ✅ 동적 평가 API (DB에서 최신 파라미터 조회) - -**성능**: -- 데이터 로드: 0.5초 -- Embedding 학습: 0.8초 -- Bayesian 학습: 0.3초 -- PostgreSQL 저장: 0.1초 -- **총 소요 시간: ~2초** (12,703개 → 427개 학습) - -**확장성**: -- 데이터 10배 증가 (4,270개) → 예상 10초 -- 메모리 256MB 제한 내 작동 -- Async 재학습으로 API 응답 지연 없음 - -**교훈**: -- "과도한 설계"라는 우려는 실제 구현으로 불식 -- 모든 컴포넌트 독립 테스트 → 통합 가능 확인 -- 하드코딩 제거 → 데이터 기반 평가로 과대평가 방지 - ---- - -**작성 완료**: 2025-10-16 -**프레임워크 버전**: 1.0 -**검증 사례**: -- seed: 리버스마운틴 (9명, 협업툴), 애디터 (5명) -- series A+: 추가 검증 필요 -**구현 검증**: 2025-10-16 (4개 컴포넌트 + 통합 테스트 완료) +- K-Startup 데이터: `/mnt/51123data/DATA/startup/data/startup_data_20251016.json` +- Neo4j: 51123 서버 7687 포트 diff --git a/journey/plans/251017_intent_analysis_improvement_plan.md b/journey/plans/251017_intent_analysis_improvement_plan.md index df1346e..df2ad4b 100644 --- a/journey/plans/251017_intent_analysis_improvement_plan.md +++ b/journey/plans/251017_intent_analysis_improvement_plan.md @@ -1,499 +1,68 @@ # 로빙 의도 파악 개선 플랜 **날짜**: 2025-10-17 -**작성자**: Claude Code -**관련 문서**: -- `/home/admin/ivada_project/DOCS/ideas/250920_happybell80_로빙_의도분석_전략.md` -- `/home/admin/ivada_project/DOCS/ideas/250819_대화형_점진적_의도_구축_시스템.md` -- `/home/admin/ivada_project/DOCS/ideas/250819_시간인식_제로샷_의도분류_시스템.md` - -**현재 구현**: `/home/admin/ivada_project/rb8001/app/brain/decision_engine.py` (255줄) +**현재**: 정규식 패턴 매칭만 사용 --- -## 1. 현재 상태 분석 +## 현재 문제 -### 1.1 현재 구현 (DecisionEngine) - -**방식**: 정규식 패턴 매칭 ONLY -```python -def analyze_intent(self, message: str) -> Tuple[IntentType, float]: - for intent_type, patterns in self.intent_patterns.items(): - for pattern in patterns: - if re.search(pattern, message_lower): - return intent_type, confidence - return IntentType.UNKNOWN, 0.3 -``` - -**장점**: -- ✅ 빠름 (< 10ms) -- ✅ 비용 없음 (LLM 호출 불필요) -- ✅ 예측 가능 - -**단점**: -- ❌ 복잡한 질문 처리 불가 -- ❌ 시간 인식 없음 ("오늘 몇일이야?" → UNKNOWN) -- ❌ 맥락 이해 없음 -- ❌ 멀티턴 대화 불가 -- ❌ 확신도 낮을 때 대응 없음 - -### 1.2 실제 문제 사례 - -| 사용자 질문 | 현재 처리 | 문제점 | -|------------|----------|--------| +### 처리 불가 사례 +| 질문 | 현재 처리 | 문제 | +|------|----------|------| | "오늘 몇일이야?" | UNKNOWN | 시간 질문 패턴 없음 | -| "리버스마운틴 유사 기업 찾아서 가치평가해줘" | UNKNOWN | 복잡한 멀티스텝 질문 | +| "리버스마운틴 유사 기업 가치평가해줘" | UNKNOWN | 복잡한 멀티스텝 질문 | | "아까 말한 그 기업 투자 단계는?" | UNKNOWN | 맥락 참조 불가 | -| "협업툴 시장 seed 평균은?" | UNKNOWN | 도메인 특화 질문 | -**결론**: 현재는 **단순 명령만 처리 가능**. 종합적 질문은 처리 불가. - -### 1.3 아키텍처 구조적 문제 (2025-11-26 추가) - -**현재 구조의 근본적 문제**: -- IntentType이 스킬/액션과 직접 1:1 매핑되어 있음 -- 의도 파악이 곧 스킬 선택으로 이어지는 구조 -- 진정한 "의도 파악(무엇을 원하는가)" → "행동 계획(어떻게 할 것인가)" → "스킬 선택(어떤 도구를 쓸 것인가)"의 단계적 분리가 없음 - -**예시**: -``` -현재: "일정 등록" → calendar_event intent → calendar_confirm action → CalendarSkill -문제: intent가 이미 구체적 행동(스킬)과 결합됨 -``` - -**개선 방향**: -1. **의도 파악 단계**: 추상적 목표 파악 - - 예: "일정 관리", "정보 검색", "커뮤니케이션", "문서 처리" - - 사용자가 무엇을 원하는지 추상적 수준에서 이해 - -2. **행동 계획 단계**: 구체적 행동 결정 - - 예: "일정 등록", "일정 조회", "일정 삭제", "일정 수정" - - 의도를 달성하기 위한 구체적 행동 계획 - -3. **스킬 선택 단계**: 적절한 도구 선택 - - 예: `calendar_skill.create_event`, `calendar_skill.get_events`, `calendar_skill.delete_event` - - 행동을 수행할 적절한 스킬 선택 - -**3단계 구조의 장점**: -- 의도와 구현의 분리: 같은 의도라도 다른 행동/스킬로 구현 가능 -- 확장성: 새로운 스킬 추가 시 기존 의도 재사용 가능 -- 유연성: 행동 계획 단계에서 여러 대안 고려 가능 -- 명확성: 각 단계의 책임이 명확히 분리됨 +**결론**: 단순 명령만 처리 가능, 복합 질문 처리 불가 --- -## 2. 문서에서 제안한 방법론 +## 개선 방향 (3단계 구조) -### 2.1 하이브리드 시스템 (250920 문서) +**구현 완료**: `troubleshooting/251126_happybell80_rb8001_의도_3단계_아키텍처_도입_및_배포.md` 참조 -**아키텍처**: +``` +1. 의도 파악 → 추상적 목표 (일정 관리, 정보 검색 등) +2. 행동 계획 → 구체적 행동 (등록, 조회, 삭제 등) +3. 스킬 선택 → 적절한 도구 (calendar_skill 등) +``` + +--- + +## 미구현: 하이브리드 시스템 + +### 제안 구조 ``` 사용자 메시지 ↓ -┌─────────────────────────┐ -│ 1단계: 정규식 FastPath │ → 명확한 패턴 (이메일, 뉴스 등) -└─────────────────────────┘ - ↓ (매칭 실패) -┌─────────────────────────┐ -│ 2단계: 임베딩 후보 축소 │ → 제로샷 분류 (상위 3개) -└─────────────────────────┘ - ↓ (확신도 < 0.7) -┌─────────────────────────┐ -│ 3단계: LLM 확인 │ → 최종 의도 결정 -└─────────────────────────┘ +1단계: 정규식 FastPath (명확한 패턴) + ↓ 실패 +2단계: 임베딩 후보 축소 (Top-3) + ↓ 확신도 < 0.7 +3단계: LLM 제로샷 분류 ``` -**장점**: -- 단순 명령: 정규식으로 빠르게 처리 (기존 유지) -- 복잡한 질문: LLM 활용 -- LLM 호출 최소화 (비용 절감 70%) +### 필요 작업 -### 2.2 제로샷 의도 분류 (250819 문서) +**1. SemanticIntentClassifier 구현** +- 파일: `app/services/brain/semantic_classifier.py` +- intent_prototypes 테이블 활용 +- 임베딩 유사도로 Top-3 후보 선택 -**핵심**: -1. 의도 설명(description)만으로 분류 (학습 데이터 불필요) -2. 임베딩 유사도로 후보 축소 (20ms) -3. 확신도 낮으면 LLM 폴백 +**2. LLM 폴백** +- Top-3 후보를 LLM에 전달 +- 확신도 < 0.5 시 CLARIFY -**구현**: -```python -intent_descriptions = { - "time_query": "현재 시각, 날짜, 요일을 묻는 질문", - "startup_analysis": "스타트업 기업 분석, 유사 기업 검색, 가치평가 요청", - "context_retrieval": "과거 대화를 참조하는 요청 (아까, 어제, 방금 등)", - "email": "이메일 확인, 전송, 검색과 관련된 요청" -} - -# 임베딩 유사도 → 후보 3개 → 확신도 체크 → LLM 폴백 -``` - -### 2.3 시간 인식 주입 (250819 문서) - -**문제**: "오늘 몇일이야?" → "2024년 5월 16일" (실제: 2025년 9월 9일) - -**해결**: -```python -TIME_PATTERNS = ["오늘", "내일", "어제", "몇일", "몇시", "요일"] - -if any(p in message for p in TIME_PATTERNS): - context["current_time"] = datetime.now(KST).strftime("%Y년 %m월 %d일 %H:%M") - context["needs_time"] = True -``` - -### 2.4 멀티턴 대화 시스템 (250819 문서) - -**현재**: 단일 턴만 처리 -**목표**: 3-5턴 대화 지원 (Phase 2) - -**슬롯 필링 예시**: -``` -User: "이메일 보낼건데..." -Robeing: "누구에게 보내실까요?" - -User: "happybell 지메일로" -Robeing: "어떤 내용을 보내실까요?" - -User: "감사 인사" -Robeing: "공손한 톤으로 작성하겠습니다. 발송할까요?" - -User: "응" -Robeing: "전송했습니다." -``` - -**필요 컴포넌트**: -- Redis 세션 관리 -- 슬롯 스키마 정의 -- 컨텍스트 상태 추적 +**3. 성능 최적화** +- 정규식: 80% 케이스 (< 10ms) +- 임베딩: 15% 케이스 (< 200ms) +- LLM: 5% 케이스 (1-2s) --- -## 3. 개선 플랜 (단계별 구현) +## 참고 -### Phase 1: 시간 인식 + 임베딩 후보 축소 (2주) - -**목표**: 시간 질문 오류 해결, 복잡한 질문 기본 대응 - -**구현 내용**: -1. **시간 컨텍스트 주입** (app/brain/decision_engine.py) - - TIME_PATTERNS 정의 - - 시간 관련 질문 시 KST 자동 주입 - -2. **임베딩 기반 후보 축소** (app/brain/intent_classifier.py 신규) - - SentenceTransformer 임베딩 - - 의도 설명(description) 정의 - - 코사인 유사도 → 상위 3개 후보 - -3. **확신도 기반 LLM 폴백** - - confidence < 0.7 → LLM 호출 - - 의도 설명 포함 프롬프트 - -**예상 효과**: -- 시간 질문 오류율: 100% → 1% -- 복잡한 질문 대응률: 0% → 60% -- LLM 호출 비율: 100% → 30% (단순 명령은 정규식) - -**코드 증가**: +300줄 - -### Phase 2: 멀티턴 대화 (1개월) - -**목표**: 3-5턴 대화 지원, 슬롯 필링 - -**구현 내용**: -1. **컨텍스트 관리 시스템** (app/context/ 신규) - - Redis 세션 저장 - - WorkContext 스키마 - - 타임아웃 관리 (30분) - -2. **슬롯 필링 시스템** - - 스킬별 슬롯 정의 (required/optional) - - 자동 질문 생성 - - 충돌 해결 - -3. **대화 상태 추적** - - Idle → Collecting → Confirming → Executing - -**예상 효과**: -- 작업 완성률: 60% → 90% -- 사용자 만족도 증가 - -**코드 증가**: +800줄 - -**단점**: -- LLM 비용 20배 증가 (7턴 대화 기준) -- 응답 시간: 500ms → 2,600ms - -### Phase 3: 도메인 특화 (추가 1개월) - -**목표**: 스타트업 분석 등 도메인 특화 의도 처리 - -**구현 내용**: -1. **스타트업 분석 의도** (app/brain/decision_engine.py) - - STARTUP_ANALYSIS 의도 추가 - - LangGraph 워크플로우 연동 - -2. **Neo4j/TF-IDF 검색 통합** - - 유사 기업 검색 스킬 - - 베이지안 평가 스킬 - -3. **도메인 지식 주입** - - 스타트업 용어 사전 - - 산업 분류 체계 - -**예상 효과**: -- "리버스마운틴 유사 기업 찾아서 가치평가해줘" → 처리 가능 -- "협업툴 시장 seed 평균은?" → 처리 가능 - -**코드 증가**: +1,200줄 - ---- - -## 4. 우선순위 결정 - -### 4.1 시급도 평가 - -| 문제 | 시급도 | 영향도 | Phase | -|------|--------|--------|-------| -| 시간 질문 오류 | 🔴 HIGH | 일상 대화 | Phase 1 | -| 복잡한 질문 미지원 | 🟡 MEDIUM | 고급 기능 | Phase 1 | -| 멀티턴 대화 부재 | 🟡 MEDIUM | UX | Phase 2 | -| 도메인 특화 미지원 | 🟢 LOW | 특수 기능 | Phase 3 | - -### 4.2 권장 순서 - -**즉시 구현 (1-2주)**: -1. Phase 1 - 시간 인식 + 임베딩 후보 축소 - - ROI: 매우 높음 (적은 코드로 큰 효과) - - 리스크: 낮음 (기존 코드 영향 최소) - -**중기 구현 (1-2개월)**: -2. Phase 2 - 멀티턴 대화 - - ROI: 중간 (복잡도 증가, 비용 증가) - - 리스크: 중간 (컨텍스트 관리 복잡) - -**장기 구현 (3-4개월)**: -3. Phase 3 - 도메인 특화 - - ROI: 높음 (특정 사용 사례에만 적용) - - 리스크: 낮음 (독립적 기능) - ---- - -## 5. Phase 1 상세 구현 계획 - -### 5.1 파일 구조 - -``` -rb8001/ -├── app/ -│ ├── brain/ -│ │ ├── decision_engine.py # 기존 (수정) -│ │ ├── intent_classifier.py # 신규 (임베딩 분류) -│ │ ├── intent_descriptions.py # 신규 (의도 설명) -│ │ └── time_aware_helper.py # 신규 (시간 컨텍스트) -│ └── tests/ -│ └── test_intent_classifier.py # 신규 -``` - -### 5.2 구현 순서 - -#### Step 1: 시간 인식 (3일) -```python -# app/brain/time_aware_helper.py -TIME_PATTERNS = { - "absolute_time": ["오늘", "내일", "어제", "몇일", "몇시", "요일"], - "relative_time": ["아까", "방금", "조금전"], - "time_reference": ["그때", "언제", "며칠"] -} - -def inject_time_context(message: str, context: Dict) -> Dict: - if any(p in message for patterns in TIME_PATTERNS.values() for p in patterns): - context["current_time"] = datetime.now(KST).strftime("%Y년 %m월 %d일 %H:%M") - context["needs_time"] = True - return context -``` - -#### Step 2: 임베딩 분류 (5일) -```python -# app/brain/intent_classifier.py -class ZeroShotIntentClassifier: - def __init__(self): - self.embedder = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2') - self.intent_descriptions = load_intent_descriptions() - - def classify(self, text: str) -> Tuple[str, float, List]: - # 임베딩 - text_emb = self.embedder.encode(text) - - # 후보 축소 - scores = {} - for intent, desc in self.intent_descriptions.items(): - desc_emb = self.embedder.encode(desc) - scores[intent] = cosine_similarity(text_emb, desc_emb) - - # 상위 3개 - top_3 = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:3] - return top_3[0][0], top_3[0][1], top_3 -``` - -#### Step 3: DecisionEngine 통합 (3일) -```python -# app/brain/decision_engine.py (수정) -def analyze_intent(self, message: str, context: Dict) -> Tuple[IntentType, float]: - # 1. 시간 컨텍스트 주입 - context = inject_time_context(message, context) - - # 2. 정규식 FastPath (기존) - for intent_type, patterns in self.intent_patterns.items(): - for pattern in patterns: - if re.search(pattern, message): - return intent_type, 0.9 - - # 3. 임베딩 분류 (신규) - intent_str, confidence, candidates = self.intent_classifier.classify(message) - - # 4. 확신도 체크 - if confidence < 0.7: - # LLM 폴백 - intent_str = await self.llm_fallback(message, candidates, context) - - return IntentType(intent_str), confidence -``` - -#### Step 4: 의도 설명 정의 (2일) -```python -# app/brain/intent_descriptions.py -INTENT_DESCRIPTIONS = { - "time_query": "현재 시각, 날짜, 요일을 묻는 질문", - "email": "이메일 확인, 전송, 검색과 관련된 요청", - "news": "뉴스 검색, 확인, 요약 요청", - "startup_analysis": "스타트업 기업 분석, 유사 기업 검색, 가치평가 요청", - "context_retrieval": "아까, 어제, 방금 등 과거 대화를 참조하는 요청", - "general_chat": "일반적인 대화, 인사, 잡담" -} -``` - -### 5.3 테스트 케이스 - -```python -# app/tests/test_intent_classifier.py -test_cases = [ - # 시간 질문 - ("오늘 몇일이야?", "time_query", 0.9), - ("지금 몇시야?", "time_query", 0.9), - - # 복잡한 질문 - ("리버스마운틴 유사 기업 찾아줘", "startup_analysis", 0.8), - ("협업툴 시장 seed 평균은?", "startup_analysis", 0.7), - - # 맥락 참조 - ("아까 말한 그 기업", "context_retrieval", 0.85), - - # 기존 패턴 (정규식) - ("이메일 보내줘", "email", 0.9), - ("뉴스 찾아봐", "news", 0.9) -] -``` - ---- - -## 6. 성능 및 비용 분석 - -### 6.1 Phase 1 예상 성능 - -| 지표 | 현재 | Phase 1 | 개선 | -|------|------|---------|------| -| 단순 명령 응답 시간 | 10ms | 10ms | 0% | -| 복잡 질문 응답 시간 | - | 150ms | 신규 | -| LLM 호출 비율 | 100% | 30% | -70% | -| 시간 질문 정확도 | 0% | 99% | +99% | -| 복잡 질문 대응률 | 0% | 60% | +60% | - -### 6.2 비용 분석 - -**현재 (복잡한 질문 시)**: -- 모두 LLM으로 폴백 → 100% LLM 호출 - -**Phase 1 후**: -- 정규식 매칭: 60% (비용 0원) -- 임베딩 분류: 25% (비용 < 1원/1000건) -- LLM 폴백: 15% (기존 대비 -85%) - -**월간 비용 (10만건 기준)**: -- 현재: $300 -- Phase 1: $50 (-83%) - ---- - -## 7. 리스크 및 대응 - -### 7.1 리스크 요인 - -| 리스크 | 확률 | 영향 | 대응 | -|--------|------|------|------| -| 임베딩 모델 용량 큼 | 중 | 중 | 경량 모델 선택 (miniLM) | -| 확신도 임계값 조정 어려움 | 중 | 중 | A/B 테스트로 최적값 탐색 | -| 기존 코드 영향 | 낮 | 중 | 기존 로직 보존 (정규식 우선) | - -### 7.2 롤백 계획 - -- Phase 1은 기존 코드에 추가만 하므로 롤백 용이 -- `USE_INTENT_CLASSIFIER=false` 환경변수로 비활성화 가능 - ---- - -## 8. 다음 단계 - -### 8.1 즉시 실행 - -1. **Phase 1 구현 시작** (추천) - - 2주 소요 - - ROI 매우 높음 - - 리스크 낮음 - -2. **임베딩 모델 선택** - - `paraphrase-multilingual-mpnet-base-v2` (420MB) vs - - `paraphrase-multilingual-MiniLM-L12-v2` (120MB) - -3. **의도 설명 초안 작성** - - 현재 IntentType 15개에 대한 설명 - -### 8.2 의사 결정 필요 - -1. Phase 2 (멀티턴 대화) 진행 여부 - - 장점: UX 대폭 개선 - - 단점: LLM 비용 20배, 복잡도 증가 - -2. Phase 3 (도메인 특화) 우선순위 - - 스타트업 분석 외 다른 도메인? - ---- - -## 9. 참고 자료 - -### 9.1 관련 문서 -- 로빙 의도분석 전략: `DOCS/ideas/250920_happybell80_로빙_의도분석_전략.md` -- 대화형 시스템: `DOCS/ideas/250819_대화형_점진적_의도_구축_시스템.md` -- 제로샷 분류: `DOCS/ideas/250819_시간인식_제로샷_의도분류_시스템.md` - -### 9.2 연구 논문 -- Hong et al. (2024): Zero-shot Intent Classification (SIGDIAL) -- 핵심: 고품질 의도 설명 + 임베딩 후보 축소 - -### 9.3 기술 스택 -- SentenceTransformer (임베딩) -- Redis (세션 관리, Phase 2) -- PostgreSQL (대화 로그) - ---- - -**작성 완료**: 2025-10-17 -**권장 다음 액션**: Phase 1 구현 시작 (2주 스프린트) -**검토 필요**: 임베딩 모델 선택, 의도 설명 초안 - ---- - -## 10. 관련 문서 - -- **[의도 파악 3단계 아키텍처 전환 계획](./251126_intent_3step_architecture_plan.md)** (2025-11-26) - - 의도 파악 → 행동 계획 → 스킬 선택의 3단계 구조 설계 - - LLM 기반 제로샷 의도 분석으로 무한한 의도 처리 가능 - - TDD 시나리오 및 구현 계획 포함 +- `troubleshooting/251126_happybell80_rb8001_의도_3단계_아키텍처_도입_및_배포.md` +- `troubleshooting/251126_intent_3step_db_bayesian_integration.md` +- `311_FastAPI_구조_원칙.md` diff --git a/journey/plans/251201_ir_deck_gemini_fallback_api_expansion_plan.md b/journey/plans/251201_ir_deck_gemini_fallback_api_expansion_plan.md index 33ed96b..b7bb938 100644 --- a/journey/plans/251201_ir_deck_gemini_fallback_api_expansion_plan.md +++ b/journey/plans/251201_ir_deck_gemini_fallback_api_expansion_plan.md @@ -1,299 +1,77 @@ -# IR Deck 분석 Gemini 모델 Fallback 체인 구현 및 프론트엔드 응답 구조 확장 +# IR Deck Gemini Fallback 체인 구현 -**작성일**: 2025-12-01 -**목표**: IR Deck 평가 시 Rate Limit 대응 및 프론트엔드 UI 개선 +**날짜**: 2025-12-01 +**목표**: Rate Limit(429) 대응 자동 모델 전환 -## 목표 - -1. IR Deck 평가 시 Gemini API Rate Limit(429) 에러 발생 시 자동으로 다음 모델로 전환하는 fallback 체인 구현 -2. 프론트엔드 ChatGPT 형식 개선을 위한 백엔드 API 응답 구조 확장 +--- ## Fallback 순서 -**시작 모델**: gemini-2.5-flash-lite (기본 모델, RPM 15) +**기본**: gemini-2.5-flash-lite (RPM 15) -**Fallback 순서** (429 에러 발생 시): +**429 에러 시**: 1. gemini-2.5-flash (RPM 10) 2. gemini-2.0-flash (RPM 15) 3. gemini-2.0-flash-lite (RPM 30) --- -## Part 1: Fallback 체인 구현 +## 구현 계획 + +### 1. call_llm() 함수 수정 -### 1. `call_llm` 함수 수정 **파일**: `rb8001/app/services/ir_analyzer.py` -- `call_llm` 함수에 `enable_fallback: bool = False` 파라미터 추가 -- `enable_fallback=True`일 때만 fallback 체인 사용 -- Fallback 모델 리스트 정의: `["gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.0-flash-lite"]` (기본 모델 제외) -- 429 에러 감지: - - `google.api_core.exceptions.ResourceExhausted` 예외 타입 확인 - - 에러 메시지에서 "429", "ResourceExhausted", "quota" 문자열 검색 -- Fallback 로직: - 1. 기본 모델(gemini-2.5-flash-lite)로 시작 - 2. 429 발생 시 동일 모델 재시도 없이 즉시 fallback 리스트 순서대로 다음 모델로 전환 - 3. LLMRequest의 model 파라미터를 변경하여 재시도 (대기 시간 없이 즉시) - 4. 모든 모델 실패 시에만 None 반환 +**변경**: +- `enable_fallback: bool = False` 파라미터 추가 +- 429 에러 감지 (`ResourceExhausted` 예외) +- Fallback 리스트 순회, 즉시 재시도 +- 모든 모델 실패 시 None 반환 + +### 2. IR Deck Analyzer 적용 -### 2. IR Deck Analyzer 수정 **파일**: `rb8001/app/services/ir_deck_analyzer.py` -- `_evaluate_comprehensive`에서 `call_llm` 호출 시 `enable_fallback=True` 전달 -- `_evaluate_page_comprehensive`에서 `call_llm` 호출 시 `enable_fallback=True` 전달 +**변경**: +- `_evaluate_comprehensive`: `enable_fallback=True` +- `_evaluate_page_comprehensive`: `enable_fallback=True` + +### 3. LLMRequest 타입 확장 -### 3. LLMRequest 모델 타입 확장 **파일**: `rb8001/app/services/llm/models.py` -- `LLMRequest`의 `model` 필드 Literal 타입에 새 모델 추가: - - `"gemini-2.5-flash"`, `"gemini-2.0-flash"`, `"gemini-2.0-flash-lite"` +**변경**: +- model 필드에 새 모델 추가 +- `"gemini-2.5-flash"`, `"gemini-2.0-flash"`, `"gemini-2.0-flash-lite"` + +### 4. 동적 Handler 생성 -### 4. LLMService 동적 모델 Handler 생성 **파일**: `rb8001/app/services/llm/llm_service.py` -- `process_request`에서 요청된 모델명의 handler가 없으면 동적으로 생성 -- Gemini 모델인 경우 `GeminiHandler(model_name)` 새 인스턴스 생성 - -### 5. 로깅 및 모니터링 -- 모델 전환 시: `logger.info(f"[IR Deck Fallback] Switching from {current_model} to {next_model} due to Rate Limit (429)")` -- 각 모델 실패: `logger.warning(f"[IR Deck Fallback] Model {model_name} failed with Rate Limit, trying next model")` -- 최종 사용 모델: `logger.info(f"[IR Deck Evaluation] Completed using model: {final_model}")` -- 전체 실패: `logger.error(f"[IR Deck Fallback] All models exhausted, evaluation failed")` +**변경**: +- handler 없으면 `GeminiHandler(model_name)` 동적 생성 --- -## Part 2: 프론트엔드 응답 구조 확장 (백엔드 수정) +## Part 2: 프론트엔드 응답 확장 (미구현) -### 1. EvaluationResponse 모델 확장 -**파일**: `rb8001/app/router/ir_deck.py` +### EvaluationResponse 확장 -- `EvaluationResponse` 클래스에 다음 필드 추가 (Optional로 하위 호환성 유지): - ```python - story_scores: Optional[List[Dict[str, Any]]] = None - summary: Optional[str] = None - investment_opinion: Optional[Dict[str, Any]] = None - ``` -- story_scores 구조: `[{"story": "문제 정의 (Problem)", "score": 85, "max_score": 100}, ...]` -- investment_opinion 구조: `{"recommendation": str, "risks": List[str], "strengths": List[str]}` +**추가 필드**: +```python +story_scores: Optional[List[Dict]] # Sequoia 10가지 스토리별 점수 +summary: Optional[str] # 종합 요약 +investment_opinion: Optional[Dict] # 투자 의견 +``` -### 2. 종합 평가에서 story_scores 추출 및 통합 -**파일**: `rb8001/app/services/ir_deck_analyzer.py` - -#### 2.1 `_evaluate_comprehensive` 수정 -- 종합 평가 시 LLM이 story_scores를 반환하도록 프롬프트 수정 -- Sequoia 10가지 스토리별 점수를 추출하여 리스트로 반환 -- 반환 Dict에 `story_scores` 필드 추가 - -#### 2.2 페이지별 평가에서 story_scores 수집 -- `_evaluate_page_comprehensive`에서 이미 story_scores를 추출하지만 현재는 사용하지 않음 -- 각 페이지의 story_scores를 수집하여 평균 또는 가중 평균 계산 -- 또는 종합 평가에서만 story_scores 추출 (선택) - -### 3. summary 생성 -**파일**: `rb8001/app/services/ir_deck_analyzer.py` - -- `_evaluate_comprehensive`에서 종합 평가 결과를 바탕으로 요약 텍스트 생성 -- LLM에 요약 생성 요청하거나, strengths/weaknesses/risks를 기반으로 자동 생성 -- 반환 Dict에 `summary` 필드 추가 - -### 4. investment_opinion 구조화 -**파일**: `rb8001/app/services/ir_deck_analyzer.py` - -- 종합 평가의 strengths, weaknesses, risks를 investment_opinion 형식으로 구조화 -- 구조: `{"recommendation": str, "risks": List[str], "strengths": List[str]}` -- recommendation은 grade와 total_score 기반으로 생성 -- 반환 Dict에 `investment_opinion` 필드 추가 - -### 5. analyze 메서드 반환 값 확장 -**파일**: `rb8001/app/services/ir_deck_analyzer.py` - -- `analyze` 메서드 반환 Dict에 다음 필드 추가: - - `story_scores`: Optional[List[Dict]] (10가지 스토리별 점수) - - `summary`: Optional[str] (종합 결론 요약) - - `investment_opinion`: Optional[Dict] (권고, 리스크, 강점) - -### 6. API 엔드포인트 응답 구성 -**파일**: `rb8001/app/router/ir_deck.py` - -- 평가 결과 조회 엔드포인트 (`GET /api/ir-deck/evaluation/{id}`)에서 `EvaluationResponse` 생성 시 새 필드 포함 -- 기존 평가 결과 조회 시에도 새 필드 포함 (있을 경우만) +### 필요 작업 +- LLM 프롬프트에 story_scores 요청 추가 +- 종합 평가에서 요약 생성 +- 프론트엔드 UI 개선 --- -## 테스트 계획 (TDD 방식) +## 참고 -### TDD 원칙 준수 -프로젝트 TDD 원칙(AGENTS.md 참고)에 따라 문장/기능 요구를 먼저 테스트에 명시(Red) 후 구현(Green) → 리팩터 순서 유지. - -**참고 사례**: `DOCS/journey/troubleshooting/251201_ir_deck_template_sentence_fix.md` (13개 테스트 케이스 TDD 접근) - -### Phase 1: 테스트 작성 (Red) - -#### 1.1 Fallback 체인 테스트 -**파일**: `rb8001/tests/test_ir_deck_fallback_chain.py` - -**단위 테스트** (검증 기준): -- `call_llm` 함수의 fallback 로직 테스트: 429 에러 시뮬레이션 시 다음 모델로 자동 전환 확인 -- 모든 모델 실패 시나리오: 4개 모델 모두 실패 시 `None` 반환 확인 -- 비-Rate Limit 에러: 일반 에러(500, 503 등)는 fallback 없이 즉시 실패 처리 확인 - -**통합 테스트** (검증 기준): -- IR Deck Analyzer에서 fallback 활성화: `enable_fallback=True` 전달 시 fallback 체인 작동 확인 -- 종합 평가 중 Rate Limit 발생: 첫 번째 LLM 호출(`_evaluate_comprehensive`)에서 429 발생 시 모델 전환 확인 -- 페이지별 평가 중 Rate Limit 발생: 중간 페이지 평가 중 429 발생 시 모델 전환 후 이후 페이지도 동일 모델 사용 확인 - -**E2E 테스트** (실제 document_id로 사용자 시나리오 검증): -- **테스트 문서 목록 및 document_id**: - 1. `AIdol_251010.pdf` - document_id: `062bf655-5580-49f2-a988-610dbf51c1ca` (23페이지, 이전 Rate Limit 발생 이력 있음, 트러블슈팅 문서 참고) - 2. `59196ac7-8207-412e-917d-a11ceab4d387` - 07. 파워플레이어.pdf (39페이지, 가장 긴 문서) - 3. `f98cc94b-0d3f-489b-84e9-a5b73d533fb8` - 10. (주)쉘피아.pdf (33페이지) - 4. `01c97b77-b3b0-494f-88ba-36508870628f` - 01. 주식회사 체리.pdf (16페이지) - -- **테스트 시나리오 및 검증 기준** (사용자 관점): - - **1. Fallback 체인 작동 확인**: - - 39페이지 PDF 평가 시 `gemini-2.5-flash-lite`에서 429 에러 발생하면 로그에 `"[IR Deck Fallback] Switching from gemini-2.5-flash-lite to gemini-2.5-flash due to Rate Limit (429)"` 메시지 확인 - - 평가가 중단 없이 완료되어 로그에 `"[IR Deck Evaluation] Completed using model: gemini-2.5-flash"` 메시지 확인 - - Rate Limit 미발생 시에도 기본 모델(`gemini-2.5-flash-lite`) 사용 로그 확인 - - **2. API 응답 구조 검증**: - - `GET /api/ir-deck/evaluation/{id}` 응답에 다음 필드 포함 확인: - - `story_scores`: 10개 스토리별 점수 배열 (예: `[{"story": "문제 정의 (Problem)", "score": 85, "max_score": 100}, ...]`) - - `summary`: 종합 결론 요약 텍스트 (문자열, 최소 100자 이상) - - `investment_opinion`: 객체 형태 (`{"recommendation": str, "risks": List[str], "strengths": List[str]}`) - - 기존 필드(`total_score`, `grade`, `page_evaluations`) 정상 반환 확인 - - 새 필드가 Optional이므로 필드 누락 시에도 에러 없이 동작 확인 (하위 호환성) - - **3. 평가 품질 검증**: - - 실제 IR Deck(예: `AIdol_251010.pdf`, 23페이지) 평가 시 모든 페이지가 평가되었는지 확인 (`page_evaluations` 길이 = PDF 실제 페이지 수) - - 각 페이지에 구체적인 장점(`strengths`)과 개선점(`weaknesses`)이 각각 최소 1개 이상 포함 (템플릿 문장 제외) - - 총점(`total_score`)이 0-100 범위 내에 있고, 등급(`grade`)이 S/A/B/C 중 하나인지 확인 - - Rate Limit 발생 시에도 fallback으로 평가 완료되어 결과 반환 확인 - - **4. 로그 확인 기준**: - - 모델 전환 시: `"[IR Deck Fallback] Switching from {current_model} to {next_model} due to Rate Limit (429)"` INFO 로그 - - 최종 사용 모델: `"[IR Deck Evaluation] Completed using model: {final_model}"` INFO 로그 - - 각 모델 실패 시: `"[IR Deck Fallback] Model {model_name} failed with Rate Limit, trying next model"` WARNING 로그 - - 모든 모델 실패 시: `"[IR Deck Fallback] All models exhausted, evaluation failed"` ERROR 로그 - - **5. 프론트엔드 렌더링 검증**: - - 새 필드(`story_scores`, `summary`, `investment_opinion`)가 마크다운 테이블과 텍스트로 정상 렌더링 확인 - - 기존 페이지별 피드백(`page_evaluations`)도 정상 표시 확인 - - 필드 누락 시 폴백 처리로도 오류 없이 동작 확인 - -#### 1.2 API 응답 구조 확장 테스트 -**파일**: `rb8001/tests/test_ir_deck_api_response_expansion.py` - -**단위 테스트** (검증 기준): -- `_evaluate_comprehensive`에서 story_scores 추출: 10개 스토리별 점수가 모두 포함된 배열 반환 확인 -- summary 생성: 종합 평가 결과 기반 요약 텍스트(최소 100자) 반환 확인 -- investment_opinion 구조화: `{"recommendation": str, "risks": List[str], "strengths": List[str]}` 형식 확인 - -**API 테스트** (검증 기준): -- `EvaluationResponse` 모델에 새 필드 포함: `story_scores`, `summary`, `investment_opinion` 필드 존재 확인 -- 새 필드가 Optional: 필드 누락 시에도 API 응답 정상 반환(하위 호환성) -- 기존 필드와의 호환성: `total_score`, `grade`, `page_evaluations` 정상 반환 확인 - - **통합 테스트** (검증 기준): - - 전체 평가 워크플로우: `analyze()` 메서드 반환 Dict에 새 필드 3개 모두 포함 확인 - - API 엔드포인트 응답: `GET /api/ir-deck/evaluation/{id}` 응답 JSON에 새 필드 포함 확인 - -### Phase 2: 구현 (Green) -- 테스트 통과할 때까지 구현 -- 각 테스트 케이스별로 단계적 구현 - -### Phase 3: 리팩터 -- 코드 중복 제거 -- 성능 최적화 -- 가독성 개선 - ---- - -## 구현 범위 - -### Fallback 체인 -- **적용 대상**: IR Deck 분석만 (`ir_deck_analyzer.py`에서 호출되는 `call_llm`) - - `_evaluate_comprehensive`: 종합 평가 (1회) - - `_evaluate_page_comprehensive`: 페이지별 평가 (N회) -- **비적용 대상**: 다른 서비스의 LLM 호출은 기존 동작 유지 - - ### API 응답 확장 - - **적용 대상**: 평가 결과 조회 엔드포인트 (`GET /api/ir-deck/evaluation/{id}`) -- **하위 호환성**: 새 필드는 Optional이므로 기존 프론트엔드에도 영향 없음 - ---- - -## 에러 처리 시나리오 - -### Fallback 관련 -- 시나리오 1: 종합 평가 중 Rate Limit 발생 → 다음 모델로 전환 -- 시나리오 2: 페이지별 평가 중 Rate Limit 발생 → 다음 모델로 전환, 이후 페이지도 동일 모델 사용 -- 시나리오 3: 모든 모델 실패 → None 반환, 빈 결과 또는 에러 메시지 -- 시나리오 4: 비-Rate Limit 에러 → fallback 없이 즉시 실패 - ---- - -## 현재 구현 상태 - -### 기존 429 에러 처리 -- `rb8001/app/services/ir_analyzer.py:call_llm()`에 이미 429 에러 재시도 로직 존재 -- 동일 모델로 최대 3회 재시도 (대기 시간 12초) -- Rate limit 정보: `DOCS/journey/research/LLM_모델_비교_분석.md` 참고 - -### LLMService 구조 -- Handler 딕셔너리로 관리 (`self.handlers = {model_name: handler}`) -- Handler가 없으면 첫 번째 사용 가능한 모델로 fallback (기본 fallback 로직) -- GeminiHandler는 생성자에서 `model_name`을 받아 동적 생성 가능 - -### LLMRequest 모델 제약 -- `model` 필드가 Literal 타입으로 제한됨 -- 현재 지원 모델: `"gemini-2.5-flash-lite"`만 포함 -- 새 모델 추가 시 Literal 타입 확장 필요 - -## 주의사항 - -- Fallback은 IR Deck 분석에서만 활성화 (`enable_fallback` 파라미터로 제어) -- 기본 모델은 여전히 `gemini-2.5-flash-lite` 사용 -- API 응답 확장은 하위 호환성 유지 (Optional 필드) -- Handler 인스턴스 생성 오버헤드 고려 (동적 생성 vs 캐싱) -- 페이지별 평가 일관성: 한 페이지에서 모델 전환 시 이후 페이지도 동일 모델 사용 - -## 구현 세부사항 - -### Fallback 체인 구현 상세 - -1. **call_llm 함수 수정 전략**: - - `enable_fallback=True`일 때만 fallback 체인 활성화 - - 429 발생 시 동일 모델 재시도 없이 즉시 다음 모델로 전환 (Fallback 로직 섹션 참고) - -2. **LLMService 동적 Handler 생성**: - - `process_request`에서 요청된 모델명의 handler 확인 - - 없으면 `GeminiHandler(model_name)` 새 인스턴스 생성 - - Handler 재사용 vs 새 인스턴스: 성능 테스트 필요 (초기에는 새 인스턴스, 필요 시 캐싱 추가) - -3. **Rate Limit 감지 개선**: - - `google.api_core.exceptions.ResourceExhausted` 예외 타입 직접 확인 - - 에러 메시지에서 "429", "ResourceExhausted", "quota" 문자열 검색 - - Gemini API의 rate limit 응답 구조 확인 필요 - -### API 응답 구조 확장 상세 - -1. **story_scores 추출**: - - `_evaluate_page_comprehensive`에서 이미 story_scores 추출 중 (현재 미사용) - - 종합 평가에서만 추출할지, 페이지별 점수 통합할지 결정 필요 - - 프론트엔드 요구사항 확인: 시나리오 문서 참고 - -2. **summary 생성**: - - LLM에 별도 요청 vs 기존 평가 결과 기반 자동 생성 - - 비용/성능 고려하여 결정 - -3. **investment_opinion 구조화**: - - 종합 평가의 strengths/weaknesses/risks 재구성 - - recommendation은 grade와 total_score 기반으로 템플릿 생성 가능 - -## 참고 문서 - -- Rate Limit 정보: `DOCS/journey/research/LLM_모델_비교_분석.md` -- TDD 사례: `DOCS/journey/troubleshooting/251201_ir_deck_template_sentence_fix.md` -- FastAPI 원칙: `DOCS/book/300_architecture/311_FastAPI_구조_원칙.md` -- LLM 호출 최적화: `DOCS/book/300_architecture/311_FastAPI_구조_원칙.md` (섹션 13) +- `troubleshooting/251201_ir_deck_evaluation_keyerror_llm_optimization.md` +- `311_FastAPI_구조_원칙.md`