o2o-castad-backend/docs/plan/celery/celery-plan_2-callback-link...

648 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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