From 19bdde65e507b5ed64196c1b8ca5cba918c6faf5 Mon Sep 17 00:00:00 2001 From: Claude-51124 Date: Tue, 2 Dec 2025 01:48:38 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20IR=20Deck=20Gemini=20Fallback=20?= =?UTF-8?q?=EB=B0=8F=20API=20=ED=99=95=EC=9E=A5=20=EA=B3=84=ED=9A=8D=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...deck_gemini_fallback_api_expansion_plan.md | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 journey/plans/251201_ir_deck_gemini_fallback_api_expansion_plan.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 new file mode 100644 index 0000000..19a5a93 --- /dev/null +++ b/journey/plans/251201_ir_deck_gemini_fallback_api_expansion_plan.md @@ -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) +