648 lines
32 KiB
Markdown
648 lines
32 KiB
Markdown
# 설계안 2 LangGraph 전환: Callback Link 에러격리 → LangGraph 조건부 에러 분기
|
||
|
||
> **Celery link/link_error 콜백과 단일 큐 전략을 LangGraph 조건부 엣지 + 에러 서브그래프로 전환한 설계안**
|
||
|
||
---
|
||
|
||
## 목차
|
||
|
||
1. [개요 및 핵심 차이점](#1-개요-및-핵심-차이점)
|
||
2. [아키텍처 설계](#2-아키텍처-설계)
|
||
3. [에러 격리 전략 전환](#3-에러-격리-전략-전환)
|
||
4. [RAG + 에러 격리 통합](#4-rag-에러-격리-통합)
|
||
5. [코드 구현](#5-코드-구현)
|
||
6. [단일 큐 → 단일 그래프 전환](#6-단일-큐--단일-그래프-전환)
|
||
7. [실패 처리 및 복구](#7-실패-처리-및-복구)
|
||
8. [Celery Callback Link 대비 비교](#8-celery-callback-link-대비-비교)
|
||
9. [프롬프트 및 RAG 최적화](#9-프롬프트-및-rag-최적화)
|
||
|
||
---
|
||
|
||
## 1. 개요 및 핵심 차이점
|
||
|
||
### 1.1 설계 철학
|
||
|
||
Celery 설계안 2의 핵심은 **link/link_error 콜백으로 성공/실패 분기를 태스크 외부에서 제어**하는 것입니다.
|
||
LangGraph에서는 **조건부 엣지(conditional_edges)**가 이를 더 유연하게 대체합니다.
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ Celery link/link_error vs LangGraph conditional_edges │
|
||
├──────────────────────────────────────┬──────────────────────────────────────┤
|
||
│ Celery link/link_error │ LangGraph conditional_edges │
|
||
├──────────────────────────────────────┼──────────────────────────────────────┤
|
||
│ lyric.apply_async( │ graph.add_conditional_edges( │
|
||
│ link=song.s(), │ "lyric", │
|
||
│ link_error=lyric_err.s() │ route_lyric_result, │
|
||
│ ) │ {"success": "song", │
|
||
│ │ "api_error": "lyric_api_recovery",│
|
||
│ 이진 분기만 가능 │ "quality_fail": "lyric_retry", │
|
||
│ (성공 or 실패) │ "fatal": "error_handler"} │
|
||
│ │ ) │
|
||
│ │ │
|
||
│ 실패 원인별 분기 불가 │ 실패 원인별 세분화된 분기 가능 │
|
||
│ (lyric_err가 모든 에러 처리) │ (API 에러, 품질 문제 등 구분) │
|
||
└──────────────────────────────────────┴──────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.2 Celery 설계안 2의 3가지 핵심 → LangGraph 대응
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 설계안 2 핵심 → LangGraph 매핑 │
|
||
├─────────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 핵심 1: link/link_error 콜백 │
|
||
│ Celery: link=song.s(), link_error=lyric_err.s() │
|
||
│ LangGraph: conditional_edges로 다중 분기 (성공/실패/재시도/치명적) │
|
||
│ │
|
||
│ 핵심 2: 단일 큐 + 우선순위 │
|
||
│ Celery: pipeline_queue (P=1 lyric, P=5 song, P=9 video) │
|
||
│ LangGraph: 단일 그래프 (노드 실행 순서로 자연스럽게 제어) │
|
||
│ 우선순위 개념 불필요 (그래프가 순서 보장) │
|
||
│ │
|
||
│ 핵심 3: 단계별 독립 에러 핸들러 │
|
||
│ Celery: lyric_error_handler, song_error_handler, video_error_handler │
|
||
│ LangGraph: 각 노드별 에러 서브그래프 + State에 에러 컨텍스트 전달 │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 아키텍처 설계
|
||
|
||
### 2.1 에러 격리 그래프 구조
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ LangGraph 에러 격리 그래프 (Celery 설계안 2 대응) │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
START
|
||
│
|
||
▼
|
||
┌────────────────────────┐
|
||
│ marketing_search │
|
||
│ (30건 외부 검색) │
|
||
└──────────┬─────────────┘
|
||
▼
|
||
┌────────────────────────┐
|
||
│ local_search │
|
||
│ (랜드마크/축제/여행지) │
|
||
└──────────┬─────────────┘
|
||
▼
|
||
┌────────────────────────┐
|
||
│ rag_retrieval │
|
||
└──────────┬─────────────┘
|
||
▼
|
||
┌────────────────────────┐
|
||
│ embedding_store │
|
||
└──────────┬─────────────┘
|
||
▼
|
||
┌────────────────────────┐
|
||
│ generate_lyric │──────────────────────────────────┐
|
||
└──────────┬─────────────┘ │
|
||
▼ │
|
||
┌────────────────────────┐ │
|
||
│ route_lyric_result │ ← 조건부 분기 (link/link_error 대응)│
|
||
└──┬───────┬────────┬────┘ │
|
||
│ │ │ │
|
||
[success] [quality] [api_error] [fatal] │
|
||
│ │ │ │ │
|
||
│ │ ▼ ▼ │
|
||
│ │ ┌──────────────┐ ┌───────────────┐ │
|
||
│ │ │ lyric_api_ │ │ fatal_error │ │
|
||
│ │ │ recovery │ │ _handler │ │
|
||
│ │ │(API키 확인, │ │(DLQ저장,알림) │ │
|
||
│ │ │ 대체 모델) │ └───────┬───────┘ │
|
||
│ │ └──────┬───────┘ ▼ │
|
||
│ │ │ END (실패) │
|
||
│ │ └──→ generate_lyric (재시도) │
|
||
│ │ │
|
||
│ └──→ lyric_quality_retry ──→ generate_lyric │
|
||
│ (프롬프트 피드백 추가) │
|
||
▼ │
|
||
┌────────────────────────┐ │
|
||
│ generate_song │───────────────────────┐ │
|
||
└──────────┬─────────────┘ │ │
|
||
▼ │ │
|
||
┌────────────────────────┐ │ │
|
||
│ route_song_result │ ← 성공/실패 분기 │ │
|
||
└──┬────────────┬────────┘ │ │
|
||
[success] [suno_error] [upload_error] │ │
|
||
│ │ │ │ │
|
||
│ ▼ ▼ │ │
|
||
│ ┌──────────────┐ ┌──────────────┐ │ │
|
||
│ │ suno_recovery│ │upload_retry │ │ │
|
||
│ │(크레딧확인, │ │(Azure재시도) │ │ │
|
||
│ │ 대기후재시도) │ └──────┬───────┘ │ │
|
||
│ └──────┬───────┘ │ │ │
|
||
│ └────────────────┘ │ │
|
||
│ └──→ generate_song │ │
|
||
▼ │ │
|
||
┌────────────────────────┐ │ │
|
||
│ generate_video │───────────────────────┘ │
|
||
└──────────┬─────────────┘ │
|
||
▼ │
|
||
┌────────────────────────┐ │
|
||
│ route_video_result │ │
|
||
└──┬────────────┬────────┘ │
|
||
[success] [render_error] │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌──────────────┐ │
|
||
│ │video_recovery│ │
|
||
│ │(템플릿확인, │ │
|
||
│ │ 렌더링재시도) │ │
|
||
│ └──────┬───────┘ │
|
||
│ └──→ generate_video │
|
||
▼ │
|
||
┌────────────────────────┐ │
|
||
│ save_results │◄──────────────────────────────────┘
|
||
└──────────┬─────────────┘
|
||
▼
|
||
END
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 에러 격리 전략 전환
|
||
|
||
### 3.1 Celery link_error vs LangGraph 조건부 에러 분기
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 에러 격리 전략 비교 │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
[Celery 설계안 2: link_error 핸들러]
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
lyric.apply_async(
|
||
link=song.s(),
|
||
link_error=lyric_error_handler.s() ← 모든 에러가 이 하나의 핸들러로
|
||
)
|
||
|
||
lyric_error_handler(request, exc, traceback):
|
||
# API 키 만료? Rate limit? 네트워크 타임아웃? 프롬프트 오류?
|
||
# 모든 에러가 여기로 → 원인별 분기가 핸들러 내부 if문으로 처리
|
||
|
||
문제:
|
||
- 에러 원인별 분기가 핸들러 내부에서만 가능
|
||
- 에러 복구 후 파이프라인 재개가 복잡
|
||
- link_error 핸들러가 태스크이므로 추가 큐 소비
|
||
|
||
[LangGraph: 조건부 에러 분기]
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
graph.add_conditional_edges(
|
||
"generate_lyric",
|
||
route_lyric_result,
|
||
{
|
||
"success": "generate_song",
|
||
"api_rate_limit": "lyric_rate_limit_wait",
|
||
"api_key_expired": "lyric_api_key_recovery",
|
||
"quality_fail": "lyric_quality_retry",
|
||
"network_error": "lyric_network_retry",
|
||
"fatal": "fatal_error_handler",
|
||
}
|
||
)
|
||
|
||
장점:
|
||
✓ 에러 원인별 전용 복구 노드로 분기
|
||
✓ 복구 후 자연스럽게 원래 노드로 루프백
|
||
✓ State에 에러 컨텍스트 전달 (이전 시도 피드백)
|
||
✓ 그래프 시각화로 에러 흐름 한눈에 파악
|
||
```
|
||
|
||
### 3.2 단계별 에러 라우팅 함수
|
||
|
||
```python
|
||
def route_lyric_result(state: PipelineState) -> str:
|
||
"""
|
||
가사 생성 결과에 따른 분기
|
||
|
||
Celery link_error_handler 내부 if문을 그래프 엣지로 외부화
|
||
"""
|
||
error = state.get("error_message")
|
||
|
||
if not error:
|
||
# 성공
|
||
if state.get("lyric_score", 0) >= 0.7:
|
||
return "success"
|
||
elif state.get("lyric_retry_count", 0) < 3:
|
||
return "quality_fail"
|
||
else:
|
||
return "success" # 3회 시도 후 최선의 결과 사용
|
||
|
||
# 에러 유형별 분기
|
||
if "rate_limit" in error.lower():
|
||
return "api_rate_limit"
|
||
elif "api_key" in error.lower() or "authentication" in error.lower():
|
||
return "api_key_expired"
|
||
elif "timeout" in error.lower() or "connection" in error.lower():
|
||
return "network_error"
|
||
else:
|
||
return "fatal"
|
||
|
||
|
||
def route_song_result(state: PipelineState) -> str:
|
||
"""노래 생성 결과 분기"""
|
||
error = state.get("error_message")
|
||
|
||
if not error and state.get("song_result_url"):
|
||
return "success"
|
||
|
||
if "credit" in str(error).lower() or "quota" in str(error).lower():
|
||
return "suno_credit_error"
|
||
elif "upload" in str(error).lower() or "blob" in str(error).lower():
|
||
return "upload_error"
|
||
elif state.get("song_retry_count", 0) < 3:
|
||
return "suno_retry"
|
||
else:
|
||
return "fatal"
|
||
|
||
|
||
def route_video_result(state: PipelineState) -> str:
|
||
"""비디오 생성 결과 분기"""
|
||
error = state.get("error_message")
|
||
|
||
if not error and state.get("video_result_url"):
|
||
return "success"
|
||
|
||
if "template" in str(error).lower():
|
||
return "template_error"
|
||
elif "render" in str(error).lower():
|
||
return "render_retry"
|
||
else:
|
||
return "fatal"
|
||
```
|
||
|
||
---
|
||
|
||
## 4. RAG + 에러 격리 통합
|
||
|
||
### 4.1 에러 복구 시 RAG 활용
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 에러 복구에서의 RAG 활용 (Celery에서 불가능했던 기능) │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
[시나리오: 가사 품질 검증 실패 → 재생성]
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
||
1차 시도: score=0.45 (지역 특색 부족)
|
||
│
|
||
▼
|
||
lyric_quality_retry 노드:
|
||
- State에서 1차 시도 결과와 검증 피드백 확인
|
||
- 벡터 DB에서 "지역 특색이 잘 반영된 성공 사례" 추가 검색
|
||
- 프롬프트에 피드백 주입:
|
||
"이전 시도에서 지역 특색이 부족했습니다.
|
||
아래 성공 사례를 참고하여 {region}의 특색을 강화하세요:
|
||
{additional_rag_context}"
|
||
│
|
||
▼
|
||
2차 시도: score=0.78 (통과!)
|
||
|
||
Celery link_error에서는:
|
||
- 실패 → 동일 프롬프트로 단순 재시도
|
||
- 1차 시도의 피드백을 2차 시도에 반영할 방법 없음
|
||
- 추가 RAG 검색으로 컨텍스트 보강 불가
|
||
|
||
LangGraph에서는:
|
||
- State에 이전 시도 결과 + 검증 피드백 보존
|
||
- 재시도 시 피드백 기반으로 프롬프트 동적 수정
|
||
- 추가 RAG 검색으로 부족한 컨텍스트 보강
|
||
```
|
||
|
||
### 4.2 에러 복구 노드 구현
|
||
|
||
```python
|
||
async def lyric_quality_retry_node(state: PipelineState) -> dict:
|
||
"""
|
||
가사 품질 실패 시 피드백 기반 재시도
|
||
|
||
Celery link_error와 달리:
|
||
- 이전 시도 결과를 State에서 참조
|
||
- 검증 피드백을 프롬프트에 반영
|
||
- 추가 RAG 검색으로 컨텍스트 보강
|
||
"""
|
||
previous_lyric = state["lyric_result"]
|
||
score = state["lyric_score"]
|
||
retry_count = state["lyric_retry_count"]
|
||
|
||
# 부족한 영역 분석
|
||
feedback = await analyze_quality_gaps(previous_lyric, state)
|
||
|
||
# 부족한 영역에 대해 추가 RAG 검색
|
||
additional_docs = await search_additional_context(
|
||
vectorstore,
|
||
query=f"{state['region']} {feedback['weak_area']}",
|
||
filter={"category": feedback["needed_category"]},
|
||
k=5,
|
||
)
|
||
|
||
return {
|
||
"enriched_context": state.get("enriched_context", "") + "\n\n"
|
||
+ f"[재시도 #{retry_count} 피드백]\n"
|
||
+ f"이전 점수: {score:.2f}\n"
|
||
+ f"개선 필요 영역: {feedback['weak_area']}\n"
|
||
+ f"추가 참조:\n" + format_docs(additional_docs),
|
||
"error_message": None, # 에러 클리어
|
||
"messages": [f"품질 재시도 준비 (#{retry_count})"],
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 코드 구현
|
||
|
||
### 5.1 에러 격리 그래프 빌더
|
||
|
||
```python
|
||
from langgraph.graph import StateGraph, END
|
||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||
|
||
def build_error_isolation_graph() -> StateGraph:
|
||
"""
|
||
Celery 설계안 2 (link/link_error + 단일 큐) → LangGraph 전환
|
||
에러 격리 + RAG 강화 그래프
|
||
"""
|
||
graph = StateGraph(PipelineState)
|
||
|
||
# ─── RAG 노드 ───
|
||
graph.add_node("marketing_search", marketing_search_node)
|
||
graph.add_node("local_search", local_search_node)
|
||
graph.add_node("rag_retrieval", rag_retrieval_node)
|
||
graph.add_node("embedding_store", embedding_store_node)
|
||
|
||
# ─── 메인 파이프라인 노드 ───
|
||
graph.add_node("generate_lyric", lyric_generation_node)
|
||
graph.add_node("validate_lyric", lyric_validation_node)
|
||
graph.add_node("generate_song", song_generation_node)
|
||
graph.add_node("generate_video", video_generation_node)
|
||
graph.add_node("save_results", save_results_node)
|
||
|
||
# ─── 에러 복구 노드 (link_error 핸들러 대응) ───
|
||
graph.add_node("lyric_quality_retry", lyric_quality_retry_node)
|
||
graph.add_node("lyric_api_recovery", lyric_api_recovery_node)
|
||
graph.add_node("song_suno_recovery", song_suno_recovery_node)
|
||
graph.add_node("song_upload_retry", song_upload_retry_node)
|
||
graph.add_node("video_render_recovery", video_render_recovery_node)
|
||
graph.add_node("fatal_error_handler", fatal_error_handler_node)
|
||
|
||
# ─── RAG 엣지 (직선) ───
|
||
graph.set_entry_point("marketing_search")
|
||
graph.add_edge("marketing_search", "local_search")
|
||
graph.add_edge("local_search", "rag_retrieval")
|
||
graph.add_edge("rag_retrieval", "embedding_store")
|
||
graph.add_edge("embedding_store", "generate_lyric")
|
||
graph.add_edge("generate_lyric", "validate_lyric")
|
||
|
||
# ─── 가사 검증 후 분기 (link/link_error 대응) ───
|
||
graph.add_conditional_edges(
|
||
"validate_lyric",
|
||
route_lyric_result,
|
||
{
|
||
"success": "generate_song",
|
||
"quality_fail": "lyric_quality_retry",
|
||
"api_rate_limit": "lyric_api_recovery",
|
||
"api_key_expired": "lyric_api_recovery",
|
||
"network_error": "lyric_api_recovery",
|
||
"fatal": "fatal_error_handler",
|
||
},
|
||
)
|
||
|
||
# 에러 복구 후 재시도 루프백
|
||
graph.add_edge("lyric_quality_retry", "generate_lyric")
|
||
graph.add_edge("lyric_api_recovery", "generate_lyric")
|
||
|
||
# ─── 노래 생성 후 분기 (song_error_handler 대응) ───
|
||
graph.add_conditional_edges(
|
||
"generate_song",
|
||
route_song_result,
|
||
{
|
||
"success": "generate_video",
|
||
"suno_retry": "song_suno_recovery",
|
||
"suno_credit_error": "song_suno_recovery",
|
||
"upload_error": "song_upload_retry",
|
||
"fatal": "fatal_error_handler",
|
||
},
|
||
)
|
||
|
||
graph.add_edge("song_suno_recovery", "generate_song")
|
||
graph.add_edge("song_upload_retry", "generate_song")
|
||
|
||
# ─── 비디오 생성 후 분기 (video_error_handler 대응) ───
|
||
graph.add_conditional_edges(
|
||
"generate_video",
|
||
route_video_result,
|
||
{
|
||
"success": "save_results",
|
||
"template_error": "video_render_recovery",
|
||
"render_retry": "video_render_recovery",
|
||
"fatal": "fatal_error_handler",
|
||
},
|
||
)
|
||
|
||
graph.add_edge("video_render_recovery", "generate_video")
|
||
|
||
# ─── 최종 ───
|
||
graph.add_edge("save_results", END)
|
||
graph.add_edge("fatal_error_handler", END)
|
||
|
||
checkpointer = SqliteSaver.from_conn_string("./checkpoints.db")
|
||
return graph.compile(checkpointer=checkpointer)
|
||
```
|
||
|
||
### 5.2 API 라우터 (콜백 중첩 → 그래프 invoke)
|
||
|
||
```python
|
||
# Celery 설계안 2의 콜백 중첩:
|
||
# lyric.apply_async(
|
||
# link=song.s().set(link=video.s().set(link_error=video_err.s()),
|
||
# link_error=song_err.s()),
|
||
# link_error=lyric_err.s()
|
||
# )
|
||
#
|
||
# LangGraph에서는 단순히:
|
||
# result = await graph.ainvoke(state, config)
|
||
|
||
@router.post("/start")
|
||
async def start_pipeline(request: StartPipelineRequest):
|
||
initial_state = build_initial_state(request)
|
||
config = {"configurable": {"thread_id": request.task_id}}
|
||
|
||
# Celery의 복잡한 콜백 중첩 대신 단일 invoke
|
||
result = await pipeline_graph.ainvoke(initial_state, config)
|
||
|
||
return {
|
||
"task_id": request.task_id,
|
||
"status": result["current_stage"],
|
||
"video_url": result.get("video_result_url"),
|
||
"errors": result.get("error_history", []),
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 단일 큐 → 단일 그래프 전환
|
||
|
||
### 6.1 Celery 단일 큐 장점의 LangGraph 보존
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ Celery 단일 큐 장점 → LangGraph에서의 보존 │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
[Celery 설계안 2: 단일 큐 + 우선순위]
|
||
장점:
|
||
✓ 인프라 단순 (1개 큐)
|
||
✓ 리소스 효율 (유휴 워커 없음)
|
||
✓ 스케일링 단순
|
||
|
||
[LangGraph: 더 단순]
|
||
장점:
|
||
✓ 큐 자체가 불필요 (그래프 내 순차 실행)
|
||
✓ 별도 워커 프로세스 불필요
|
||
✓ Redis 브로커 불필요
|
||
✓ 우선순위 설정 불필요 (그래프가 순서 보장)
|
||
|
||
인프라 비교:
|
||
Celery: Redis + pipeline_queue + Worker × N + Flower
|
||
LangGraph: FastAPI 프로세스 + 벡터 DB + LLM API
|
||
|
||
단, 높은 동시 처리가 필요하면:
|
||
→ LangGraph + asyncio.TaskGroup으로 동시 요청 처리
|
||
→ 또는 LangGraph를 Celery 태스크 내에서 실행 (하이브리드)
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 실패 처리 및 복구
|
||
|
||
### 7.1 에러 핸들러 비교
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 에러 핸들러 비교 │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
[Celery: 단계별 에러 핸들러 태스크]
|
||
lyric_error_handler(request, exc, traceback):
|
||
# request에서 원본 태스크 정보 추출 (복잡)
|
||
task_id = request.kwargs.get('task_id') or \
|
||
request.args[0].get('task_id')
|
||
# Redis에 에러 상태 기록
|
||
redis.hset(f"pipeline:{task_id}:lyric", "status", "failed")
|
||
# DLQ에 저장
|
||
redis.lpush("failed_tasks", json.dumps({...}))
|
||
|
||
문제:
|
||
- request 객체 파싱이 복잡 (args/kwargs 구조 일관성 없음)
|
||
- Redis와 DB 이중 상태 관리
|
||
- 에러 핸들러가 별도 태스크 → 큐 소비
|
||
|
||
[LangGraph: 에러 복구 노드]
|
||
async def lyric_api_recovery_node(state: PipelineState) -> dict:
|
||
# State에서 모든 정보에 바로 접근
|
||
task_id = state["task_id"]
|
||
error = state["error_message"]
|
||
retry_count = state["lyric_retry_count"]
|
||
|
||
# 에러 유형별 복구
|
||
if "rate_limit" in error:
|
||
await asyncio.sleep(60) # 1분 대기
|
||
elif "api_key" in error:
|
||
# 대체 API 키 사용
|
||
state["api_key"] = get_backup_api_key()
|
||
|
||
return {
|
||
"error_message": None, # 에러 클리어
|
||
"lyric_retry_count": retry_count,
|
||
"messages": [f"API 복구 시도: {error}"],
|
||
}
|
||
|
||
장점:
|
||
✓ State에서 모든 정보 직접 접근
|
||
✓ 복구 후 자연스럽게 원래 노드로 루프백
|
||
✓ 별도 큐 소비 없음
|
||
✓ 체크포인트로 복구 이력 보존
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Celery Callback Link 대비 비교
|
||
|
||
```
|
||
┌──────────────────────────┬─────────────────────────┬───────────────────────────┐
|
||
│ 기준 │ Celery link/link_error │ LangGraph conditional_edges│
|
||
├──────────────────────────┼─────────────────────────┼───────────────────────────┤
|
||
│ 분기 유형 │ 이진 (성공/실패) │ 다중 (N개 조건) │
|
||
│ 에러 원인별 분기 │ 핸들러 내부 if문 │ 그래프 엣지로 외부화 │
|
||
│ 에러 복구 후 재시도 │ 수동 (새 콜백 구성) │ 자연스러운 루프백 │
|
||
│ State 접근 │ request 파싱 필요 │ State 직접 접근 │
|
||
│ 피드백 기반 재시도 │ 불가 │ State에 피드백 누적 │
|
||
│ RAG 활용 복구 │ 불가 (메시지 크기 제한) │ State로 RAG 컨텍스트 공유 │
|
||
│ 인프라 │ 단일 큐 + 워커 │ 단일 그래프 (큐/워커 불필요)│
|
||
│ 모니터링 │ Flower │ LangSmith │
|
||
│ 콜백 중첩 가독성 │ 낮음 (깊은 중첩) │ 높음 (그래프 정의) │
|
||
│ 동시 처리 │ 워커 수 × 동시성 │ asyncio (비동기) │
|
||
│ 수평 확장 │ 워커 추가 │ 인스턴스 추가 │
|
||
└──────────────────────────┴─────────────────────────┴───────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 프롬프트 및 RAG 최적화
|
||
|
||
### 9.1 에러 격리 특화 최적화
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 에러 격리 설계 특화 RAG 최적화 │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
1. 실패 피드백 기반 프롬프트 진화
|
||
──────────────────────────────
|
||
매 재시도마다 이전 실패 원인을 프롬프트에 반영:
|
||
"이전 시도 (score: 0.45)에서 지역 특색이 부족했습니다.
|
||
다음 참조 자료를 활용하여 {region}의 특색을 강화하세요."
|
||
→ 동일 실수 반복 방지
|
||
|
||
2. 에러 패턴 학습 및 사전 방지
|
||
──────────────────────────────
|
||
자주 실패하는 조합을 벡터 DB에 "주의 사항"으로 저장:
|
||
metadata: {"type": "caution", "region": "군산", "issue": "지역 특색 부족"}
|
||
→ 동일 지역 요청 시 사전에 주의 사항을 프롬프트에 포함
|
||
|
||
3. 대체 프롬프트 전략 (Fallback Prompts)
|
||
──────────────────────────────────────
|
||
API 에러 복구 후 재시도 시, 간소화된 프롬프트 사용:
|
||
- 1차: 풀 RAG 컨텍스트 + 상세 지침
|
||
- 2차: 핵심 컨텍스트만 + 간략 지침
|
||
- 3차: 최소 프롬프트 (안정성 우선)
|
||
|
||
4. 점진적 컨텍스트 보강
|
||
──────────────────────
|
||
품질 실패 시, 부족한 영역에 대해 추가 RAG 검색:
|
||
- 1차 실패: "지역 특색 부족" → 지역 정보 추가 검색
|
||
- 2차 실패: "마케팅 메시지 약함" → 마케팅 사례 추가 검색
|
||
→ 에러에서 학습하여 컨텍스트 보강
|
||
|
||
5. 멀티 모델 Fallback
|
||
──────────────────────
|
||
GPT-4o 실패 시 Claude, 다시 실패 시 GPT-4o-mini로 대체
|
||
각 모델에 최적화된 프롬프트 템플릿 사용
|
||
→ API 장애 시에도 서비스 지속
|
||
```
|
||
|
||
---
|
||
|
||
## 문서 버전
|
||
|
||
| 버전 | 날짜 | 변경 내용 |
|
||
|------|------|-----------|
|
||
| 1.0 | 2024-XX-XX | 초안 작성 |
|