docs: IR Deck Gemini Fallback 및 API 확장 계획 문서 추가
This commit is contained in:
parent
e779b2b204
commit
19bdde65e5
@ -0,0 +1,300 @@
|
|||||||
|
# IR Deck 분석 Gemini 모델 Fallback 체인 구현 및 프론트엔드 응답 구조 확장
|
||||||
|
|
||||||
|
**작성일**: 2025-12-01
|
||||||
|
**목표**: IR Deck 평가 시 Rate Limit 대응 및 프론트엔드 UI 개선
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
1. IR Deck 평가 시 Gemini API Rate Limit(429) 에러 발생 시 자동으로 다음 모델로 전환하는 fallback 체인 구현
|
||||||
|
2. 프론트엔드 ChatGPT 형식 개선을 위한 백엔드 API 응답 구조 확장
|
||||||
|
|
||||||
|
## Fallback 순서
|
||||||
|
|
||||||
|
**시작 모델**: gemini-2.5-flash-lite (기본 모델, RPM 15)
|
||||||
|
|
||||||
|
**Fallback 순서** (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` 함수 수정
|
||||||
|
**파일**: `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 반환
|
||||||
|
|
||||||
|
### 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` 전달
|
||||||
|
|
||||||
|
### 3. LLMRequest 모델 타입 확장
|
||||||
|
**파일**: `rb8001/app/services/llm/models.py`
|
||||||
|
|
||||||
|
- `LLMRequest`의 `model` 필드 Literal 타입에 새 모델 추가:
|
||||||
|
- `"gemini-2.5-flash"`, `"gemini-2.0-flash"`, `"gemini-2.0-flash-lite"`
|
||||||
|
|
||||||
|
### 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")`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: 프론트엔드 응답 구조 확장 (백엔드 수정)
|
||||||
|
|
||||||
|
### 1. EvaluationResponse 모델 확장
|
||||||
|
**파일**: `rb8001/app/router/ir_deck.py`
|
||||||
|
|
||||||
|
- `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]}`
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
- `evaluate_ir_deck` 엔드포인트에서 `EvaluationResponse` 생성 시 새 필드 포함
|
||||||
|
- 기존 평가 결과 조회 시에도 새 필드 포함 (있을 경우만)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 테스트 계획 (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 응답 구조 검증**:
|
||||||
|
- `/api/ir-deck/evaluate` 응답에 다음 필드 포함 확인:
|
||||||
|
- `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 엔드포인트 응답: `/api/ir-deck/evaluate` 응답 JSON에 새 필드 포함 확인
|
||||||
|
|
||||||
|
### Phase 2: 구현 (Green)
|
||||||
|
- 테스트 통과할 때까지 구현
|
||||||
|
- 각 테스트 케이스별로 단계적 구현
|
||||||
|
|
||||||
|
### Phase 3: 리팩터
|
||||||
|
- 코드 중복 제거
|
||||||
|
- 성능 최적화
|
||||||
|
- 가독성 개선
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 구현 범위
|
||||||
|
|
||||||
|
### Fallback 체인
|
||||||
|
- **적용 대상**: IR Deck 분석만 (`ir_deck_analyzer.py`에서 호출되는 `call_llm`)
|
||||||
|
- `_evaluate_comprehensive`: 종합 평가 (1회)
|
||||||
|
- `_evaluate_page_comprehensive`: 페이지별 평가 (N회)
|
||||||
|
- **비적용 대상**: 다른 서비스의 LLM 호출은 기존 동작 유지
|
||||||
|
|
||||||
|
### API 응답 확장
|
||||||
|
- **적용 대상**: `/api/ir-deck/evaluate` 엔드포인트
|
||||||
|
- **하위 호환성**: 새 필드는 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)
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user