# 설계안 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 | 초안 작성 |