1111import com .example .ai_tutor .global .config .security .token .UserPrincipal ;
1212import com .example .ai_tutor .global .payload .ApiResponse ;
1313import com .fasterxml .jackson .databind .JsonNode ;
14+ import io .github .resilience4j .ratelimiter .RateLimiter ;
15+ import io .github .resilience4j .ratelimiter .RateLimiterConfig ;
16+ import io .github .resilience4j .reactor .ratelimiter .operator .RateLimiterOperator ;
1417import lombok .RequiredArgsConstructor ;
1518import lombok .extern .slf4j .Slf4j ;
1619import org .springframework .http .ResponseEntity ;
1720import org .springframework .stereotype .Service ;
1821import org .springframework .transaction .annotation .Transactional ;
1922import org .springframework .web .multipart .MultipartFile ;
23+ import org .springframework .web .reactive .function .client .WebClientResponseException ;
2024import reactor .core .publisher .Flux ;
2125import reactor .core .publisher .Mono ;
2226
@@ -52,6 +56,16 @@ public class SummaryService {
5256 private static final int CHUNK_TOKEN_SIZE = 7000 ; // 청크 단위 토큰 수
5357
5458
59+ //
60+ RateLimiterConfig rateLimiterConfig = RateLimiterConfig .custom ()
61+ .timeoutDuration (Duration .ofMillis (500 )) // 획득 대기 시간
62+ .limitForPeriod (3 ) // period당 호출 가능 횟수
63+ .limitRefreshPeriod (Duration .ofSeconds (1 )) // period 단위 시간
64+ .build ();
65+
66+ RateLimiter rateLimiter = RateLimiter .of ("gptRateLimiter" , rateLimiterConfig );
67+
68+
5569 public Mono <String > processSttAndSummary (MultipartFile file , String keywords , String requirement , Long noteId ) {
5670 Note note = noteRepository .findById (noteId )
5771 .orElseThrow (() -> new RuntimeException ("해당 노트를 찾을 수 없습니다." ));
@@ -106,21 +120,21 @@ private String generatePrompt(String fullText, String keywords, String requireme
106120 // 청크 요약용 프롬프트
107121 promptBuilder .append ("""
108122 당신은 전문적인 강의 분석가입니다.
109- 아래 강의 내용을 간결하고 논리적으로 요약하십시오 .
110-
123+ 아래 강의 내용을 **상세하고 논리적으로 요약**하십시오 .
124+
111125 ---
112126 ### 역할과 목표
113- - 역할: 주어진 강의 청크를 분석하고, 핵심 개념과 주요 이론만 추출하는 전문가입니다.
114- - 목표: 최종 요약의 재료로 사용될 일관성 있고 체계적인 부분 요약을 생성하는 것입니다.
115-
127+ - 역할: 주어진 강의 청크를 분석하여 **단일 청크만으로 완전한 내용을 담은 부분 요약**을 생성하는 전문가입니다. \s
128+ - 목표: 각 청크 요약이 최종 요약과 동일한 수준의 완결성을 갖추도록 작성합니다. **최종 통합 시 추가하거나 보완하는 과정이 없음을 전제로 작성합니다.**
129+
116130 ---
117131 ### 작성 기준
118- 1. 핵심 개념과 이론만 요약하며 불필요한 설명과 반복은 반드시 제거합니다 .
119- 2. 모든 문장은 간결하고 논리적이어야 하며, 전문적이고 학문적인 어투로 작성합니다. 각 문장은 '~이다.'로 끝맺습니다.
120- 3. 제공된 키워드와 요구사항은 반드시 반영하고 논리적으로 통합합니다 .
121- 4. 결과는 마크다운 형식으로 작성하며, 번호나 리스트를 사용하여 시각적 구분을 명확히 합니다 .
122- 5. 요약 결과는 반드시 500단어 이상 800단어 이하로 제한합니다 .
123-
132+ 1. ** 핵심 개념, 주요 이론, 사례 및 교수자의 중요한 설명을 절대 누락하지 않고 상세하게 요약합니다.** 특히 해당 강의 주제에서 중요해보이는 사례와 개념을 강조하여 이야기합니다 .
133+ 2. 불필요한 설명과 반복은 제거하되, **내용이 축소되거나 정보가 손실되지 않도록 주의합니다.**
134+ 3. 모든 문장은 간결하고 논리적이어야 하며, 전문적이고 학문적인 어투로 작성합니다. 각 문장은 ‘~이다.’로 끝맺습니다 .
135+ 4. 제공된 키워드와 요구사항을 반드시 반영하고, 자연스럽게 통합합니다 .
136+ 5. **핵심 내용과 보조 설명, 사례 등을 시각적으로 구분**하여 명확성을 높입니다. 마크다운 형식을 사용하며, 번호나 리스트를 적극적으로 활용합니다 .
137+ 6. 요약 결과는 **500단어 이상, 1200단어 이하로 제한**합니다.
124138 ---
125139 """ );
126140
@@ -151,7 +165,13 @@ private String generatePrompt(String fullText, String keywords, String requireme
151165당신은 다양한 전공 분야의 강의를 쉽고 명료하게 전달하는 교양서적의 저자입니다.
152166학습자가 복잡한 개념을 명확히 이해하고, 논리적으로 정리된 지식을 통해 사고를 확장할 수 있도록 돕는 것이 당신의 역할입니다.
153167
154- ※ 주의사항
168+ 지금부터 작성할 내용은 청크별 부분 요약본을 종합하여 작성하는 **최종 요약본**입니다.
169+
170+ 필수 지침
171+ - 강의의 **모든 핵심 개념, 주요 이론, 사례, 교수자의 강조 의도가 누락되지 않도록 작성하십시오.**
172+ - 작성 후 반드시 검토하여, 정보 누락과 왜곡이 없는지 확인하고 제출하십시오.
173+ - 청크별 요약본을 종합하며, 중복 없이 논리적 흐름과 일관성을 유지하십시오.
174+
155175- 반드시 제공된 강의 원문(STT 변환 텍스트)의 내용만 사용하여 작성합니다.
156176- 외부 지식이나 상상, 창작은 절대 포함하지 않습니다.
157177- 원문에 없는 정보는 작성하지 않습니다.
@@ -186,11 +206,10 @@ private String generatePrompt(String fullText, String keywords, String requireme
186206
1872074. **형식**
188208 - 명확하고 간결한 문장으로 서술하며, 한 문장은 지나치게 길지 않게 유지합니다.
189- - 중복 표현은 피하고, 동일한 개념을 반복 설명하지 않습니다.
190- - 불필요한 감정적 어구나 과도한 수식은 사용하지 않습니다.
209+ - 불필요한 감정적 어구와 중복 표현은 피하고, 동일한 개념을 반복 설명하지 않습니다.
191210
1922114. **검증**
193- - 요약을 작성한 후, 이전의 요약본과 작성된 요약본을 다시 확인하여, 핵심 겨냄이 빠짐없이 포함되어있는지와 내용의 왜곡이 없는지 확인하고 보완하시오.
212+ - 요약을 작성한 후, 이전의 요약본과 작성된 요약본을 다시 확인하여, 핵심 개념이 빠짐없이 포함되어있는지와 내용의 왜곡이 없는지 확인하고 보완하시오.
194213
195214---
196215
@@ -201,6 +220,19 @@ private String generatePrompt(String fullText, String keywords, String requireme
201220
202221- **개념 설명 예시**
203222 "니체가 언급한 '르상티망'은 억눌린 감정이 왜곡되어 타인에 대한 적대감으로 표출되는 심리 상태를 의미합니다. 강의에서는 이를 현대 사회의 무차별 공격과 같은 현상과 연결지어 설명하고 있습니다."
223+
224+ ---
225+ ### 수식 작성 기준 (선택적 적용)
226+ 1. 강의 내용에 수식이 포함된 경우, 반드시 **LaTeX 문법**을 사용하여 작성합니다.
227+ 2. 수식은 문장 내에 `$ ... $` 형태로 인라인으로 작성합니다.
228+ 예시: "피타고라스 정리는 $a^2 + b^2 = c^2$이다."
229+ 3. 복잡하거나 강조가 필요한 수식은 `$$ ... $$`으로 감싸서 별도의 수식 블록으로 작성합니다.
230+ 예시:
231+ $$
232+ E = mc^2
233+ $$
234+ 4. 수식이 없는 경우는 수식을 억지로 생성하지 않고, 텍스트 기반 설명만 제공합니다.
235+ 5. 수식이 포함될 경우에도 설명과 함께 자연스럽게 통합하여, 논리적인 흐름을 유지합니다.
204236
205237---
206238
@@ -321,12 +353,13 @@ public Mono<List<String>> summarizeChunks(List<String> chunks, String keywords,
321353 log .info ("Chunk 개수: {}" , chunks .size ());
322354
323355 return Flux .fromIterable (chunks )
324- .delayElements (Duration .ofMillis (500 )) // 1초 간격으로 호출
325- .flatMapSequential (chunk -> getGptResult (keywords , requirement , chunk , true ))
356+ .flatMap (chunk -> getGptResult (keywords , requirement , chunk , true )
357+ .transformDeferred (RateLimiterOperator .of (rateLimiter )) // 비동기 처리
358+ , 2 ) // 동시 처리 수 조절 가능
326359 .collectList ();
327-
328360 }
329361
362+
330363 /**
331364 * 부분 요약을 합쳐 최종 요약을 생성합니다.
332365 */
@@ -341,15 +374,27 @@ private Mono<String> getGptResult(String keywords, String requirement, String me
341374 validatePromptLength (prompt );
342375
343376 return gptService .callChatGpt (prompt )
344- .retryWhen (Retry .backoff (3 , Duration .ofMillis (500 ))
345- .doBeforeRetry (retrySignal -> log .warn ("GPT 호출 재시도 {}회" , retrySignal .totalRetriesInARow ()))
377+ .retryWhen (
378+ Retry .backoff (4 , Duration .ofSeconds (1 ))
379+ .maxBackoff (Duration .ofSeconds (5 ))
380+ .jitter (0.5 ) // 최대 50% 랜덤 지연
381+ .filter (this ::isTooManyRequests )
382+ .doBeforeRetry (retry -> log .warn ("GPT 호출 재시도 {}회" , retry .totalRetriesInARow ()))
346383 )
347384 .map (gptResponse -> {
348385 JsonNode choices = gptResponse .get ("choices" );
349386 return choices .get (0 ).get ("message" ).get ("content" ).asText ();
350387 });
351388 }
352389
390+ private boolean isTooManyRequests (Throwable throwable ) {
391+ if (throwable instanceof WebClientResponseException .TooManyRequests ) {
392+ return true ;
393+ }
394+ return false ;
395+ }
396+
397+
353398 /**
354399 * 프롬프트 길이를 토큰 수 기준으로 검증합니다.
355400 */
0 commit comments