39 KiB
39 KiB
설계안 3 LangGraph 전환: Beat 상태머신 → LangGraph 체크포인트 기반 상태 그래프
Celery Beat + DB 폴링 상태 머신을 LangGraph 네이티브 체크포인팅 + 이벤트 소싱 그래프로 전환한 설계안
목차
- 개요 및 핵심 차이점
- 아키텍처 설계
- 상태 머신 전환
- RAG 통합 + 이벤트 소싱
- 코드 구현
- Beat 폴링 제거 → 이벤트 기반 전환
- 실패 처리 및 자동 복구
- Celery Beat 대비 비교
- 프롬프트 및 RAG 최적화
1. 개요 및 핵심 차이점
1.1 설계 철학
Celery 설계안 3은 Beat 스케줄러가 DB를 폴링하여 다음 단계를 디스패치하는 이벤트 소싱 패턴입니다. LangGraph는 체크포인터가 네이티브로 상태를 관리하므로, Beat 폴링이 불필요해집니다.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Celery Beat 상태머신 vs LangGraph 체크포인트 상태 그래프 │
├──────────────────────────────────────┬──────────────────────────────────────┤
│ Celery Beat + 상태머신 │ LangGraph + Checkpointer │
├──────────────────────────────────────┼──────────────────────────────────────┤
│ DB에 Pipeline 테이블 추가 필요 │ 체크포인터가 상태를 자동 저장 │
│ Beat가 10초마다 DB 폴링 │ 폴링 불필요 (이벤트 기반) │
│ Beat가 "다음 단계" 결정 │ 그래프 엣지가 "다음 단계" 결정 │
│ 워커가 DB 상태만 변경하고 종료 │ 노드가 State 변경하고 다음으로 진행 │
│ 10초 지연 (폴링 간격) │ 지연 없음 (즉시 실행) │
│ DB가 진실의 원천 (ACID) │ 체크포인터 + DB 이중 보장 │
│ Beat 단일 장애점 │ 단일 장애점 없음 (stateless 실행) │
│ Pipeline 모델 마이그레이션 필요 │ 추가 테이블 불필요 │
│ SQL 쿼리로 모니터링 │ LangSmith + 체크포인트 이력 │
│ stuck 감지 (15분 타임아웃) │ 예외 즉시 감지 (동기 실행) │
└──────────────────────────────────────┴──────────────────────────────────────┘
1.2 핵심 개념 매핑
┌─────────────────────────────────────────────────────────────────────────────┐
│ Celery Beat 개념 → LangGraph 매핑 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Pipeline 테이블 → LangGraph State + Checkpointer │
│ PipelineStatus enum → State의 current_stage 필드 │
│ PipelineStage enum → 그래프 노드 이름 │
│ Beat 스케줄러 → 그래프 엔진 (자동 노드 전이) │
│ dispatch_pipelines() → 그래프 엣지 (자동 다음 노드 실행) │
│ next_stage_created 플래그 → 불필요 (그래프가 자동 전이) │
│ stuck 감지 → 타임아웃 데코레이터 + 예외 처리 │
│ DB 폴링 사이클 → 불필요 (이벤트 기반 실행) │
│ retry_count / max_retries → State에서 관리 + 조건부 엣지 │
│ config_json → State 객체 (구조화된 타입) │
│ DLQ (Dead Letter Queue) → fatal_error_handler 노드 + DB 기록 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
1.3 Beat의 핵심 이점을 LangGraph에서 보존
┌─────────────────────────────────────────────────────────────────────────────┐
│ Beat 설계안의 강점 보존 방법 │
└─────────────────────────────────────────────────────────────────────────────┘
[강점 1: 완전한 태스크 독립성]
Beat: 각 태스크가 "다음 단계가 있다"는 것조차 모름
LangGraph: 각 노드는 State만 변경, 다음 노드를 모름
→ 동일하게 보존됨
[강점 2: DB가 진실의 원천]
Beat: Pipeline 테이블이 모든 상태 관리 (ACID)
LangGraph: 체크포인터(SQLite/Postgres) + 비즈니스 DB 이중 기록
→ 체크포인터로 상태 관리 + save_to_db 노드로 비즈니스 DB 저장
[강점 3: 이벤트 소싱 (상태 이력)]
Beat: Pipeline 테이블에 상태 변경 이력 축적
LangGraph: 체크포인터에 모든 State 스냅샷 저장
→ 더 풍부한 이력 (State 전체를 매 노드마다 저장)
[강점 4: 자동 복구 (stuck 감지)]
Beat: 15분 폴링으로 stuck 감지 → 재디스패치
LangGraph: 노드에 타임아웃 설정 → 예외 → 조건부 복구 노드
→ 더 즉각적인 감지 (10초 폴링 대기 없음)
[강점 5: 런타임 제어]
Beat: DB 수정으로 재시도 횟수/상태 변경
LangGraph: 체크포인트에서 State 수정 후 재개
→ API를 통한 런타임 제어 (update_state)
2. 아키텍처 설계
2.1 전체 아키텍처 비교
┌─────────────────────────────────────────────────────────────────────────────┐
│ Beat 아키텍처 → LangGraph 아키텍처 전환 │
└─────────────────────────────────────────────────────────────────────────────┘
[Celery Beat 아키텍처] [LangGraph 아키텍처]
━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━
Client Client
│ │
▼ ▼
FastAPI FastAPI
│ │
▼ ▼
DB (Pipeline 생성) LangGraph Engine
↑ │
│ 10초마다 폴링 ├── marketing_search
│ ├── local_search
Celery Beat ──→ 큐에 디스패치 ├── rag_retrieval
│ ├── embedding_store
▼ ├── generate_lyric
Workers (lyric/song/video) ├── validate_lyric
│ ├── generate_song
▼ ├── generate_video
DB (상태 변경) └── save_results
│
▼
Checkpointer (SQLite/Postgres)
+
비즈니스 DB (PostgreSQL)
제거된 컴포넌트:
✗ Celery Beat 프로세스
✗ Pipeline 테이블 (별도 마이그레이션)
✗ Redis 브로커
✗ 3종류 Worker 프로세스
✗ scheduler_queue
✗ DB 폴링 로직
2.2 LangGraph 그래프 구조
START
│
▼
┌────────────────────────┐
│ marketing_search │ ← RAG (신규)
│ 30건 외부 검색 │
└──────────┬─────────────┘
│ ◄── Checkpoint #1 저장
▼
┌────────────────────────┐
│ local_search │ ← 지역 정보 (신규)
│ 각 10건 → 3건 선택 │
└──────────┬─────────────┘
│ ◄── Checkpoint #2 저장
▼
┌────────────────────────┐
│ rag_retrieval │ ← 벡터 DB 검색
└──────────┬─────────────┘
│ ◄── Checkpoint #3 저장
▼
┌────────────────────────┐
│ embedding_store │ ← 임베딩 저장
└──────────┬─────────────┘
│ ◄── Checkpoint #4 저장
▼
┌────────────────────────┐
│ generate_lyric │ ← Beat의 lyric Worker 대응
│ (RAG 강화 ChatGPT) │
└──────────┬─────────────┘
│ ◄── Checkpoint #5 저장
▼
┌────────────────────────┐
│ validate_lyric │
└──┬──────────┬──────────┘
[pass] [retry] [fail]
│ │ │
│ └──→ generate_lyric (루프)
│ │
│ fatal_handler → END
│ ◄── Checkpoint #6 저장
▼
┌────────────────────────┐
│ generate_song │ ← Beat의 song Worker 대응
│ (Suno API + 폴링) │
└──────────┬─────────────┘
│ ◄── Checkpoint #7 저장
▼
┌────────────────────────┐
│ generate_video │ ← Beat의 video Worker 대응
│ (Creatomate 렌더링) │
└──────────┬─────────────┘
│ ◄── Checkpoint #8 저장
▼
┌────────────────────────┐
│ save_results │ ← 비즈니스 DB 저장
└──────────┬─────────────┘
▼
END
매 노드 실행 후 Checkpoint 자동 저장
→ Beat의 DB 상태 기록과 동일하지만, 폴링 불필요
3. 상태 머신 전환
3.1 Pipeline 상태 머신 → LangGraph State
┌─────────────────────────────────────────────────────────────────────────────┐
│ Pipeline 상태 머신 → LangGraph State 전환 │
└─────────────────────────────────────────────────────────────────────────────┘
[Celery Beat: Pipeline 모델]
━━━━━━━━━━━━━━━━━━━━━━━━━━
class PipelineStage(Enum):
LYRIC = "lyric"
SONG = "song"
VIDEO = "video"
class PipelineStatus(Enum):
PENDING = "pending"
DISPATCHED = "dispatched"
PROCESSING = "processing"
STAGE_COMPLETED = "stage_completed"
PIPELINE_COMPLETED = "pipeline_completed"
FAILED = "failed"
DEAD = "dead"
→ DB 테이블 + Beat 폴링 + 4단계 처리 사이클
[LangGraph: State + 그래프 노드]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class PipelineState(TypedDict):
current_stage: str # 현재 노드 이름
# ... 나머지 필드들
→ 그래프 엔진이 자동으로 상태 전이
→ PENDING, DISPATCHED 상태 불필요 (그래프가 즉시 실행)
→ stuck 감지 불필요 (동기 실행이므로 타임아웃으로 처리)
상태 전이 매핑:
PENDING → (제거: 그래프가 즉시 실행)
DISPATCHED → (제거: 큐 발행 불필요)
PROCESSING → 노드 실행 중 (자동)
STAGE_COMPLETED → 다음 노드로 자동 전이
FAILED → 조건부 엣지로 에러 핸들링 노드
DEAD → fatal_error_handler → END
PIPELINE_COMPLETED → END 노드 도달
3.2 Beat의 4단계 폴링 사이클 제거
┌─────────────────────────────────────────────────────────────────────────────┐
│ Beat 폴링 4단계 → LangGraph에서의 제거 │
└─────────────────────────────────────────────────────────────────────────────┘
[Beat의 매 10초 사이클]
━━━━━━━━━━━━━━━━━━━━━
Step 1: pending 레코드 → 큐에 디스패치
LangGraph: 제거! 그래프가 즉시 다음 노드 실행
→ 10초 지연 → 0초 지연
Step 2: stage_completed → 다음 stage 생성
LangGraph: 제거! 그래프 엣지가 자동으로 다음 노드로 전이
→ Pipeline 레코드 생성 불필요
Step 3: failed → 재시도 판단
LangGraph: 조건부 엣지가 즉시 에러 복구 노드로 분기
→ 폴링 대기 없이 즉각 복구
Step 4: stuck 감지 → 재디스패치
LangGraph: 노드에 타임아웃 설정
→ soft_time_limit 대신 asyncio.wait_for(coro, timeout=300)
→ 타임아웃 발생 시 즉시 에러 처리
결과: Beat의 4단계 폴링이 모두 불필요해짐
- 지연: 10초 → 0초
- DB 부하: 중간 → 없음 (폴링 쿼리 제거)
- 복잡도: Pipeline 모델 + 디스패처 → 그래프 정의만
4. RAG 통합 + 이벤트 소싱
4.1 이벤트 소싱: 체크포인트 기반
┌─────────────────────────────────────────────────────────────────────────────┐
│ 이벤트 소싱: Pipeline 테이블 → Checkpointer │
└─────────────────────────────────────────────────────────────────────────────┘
[Celery Beat: Pipeline 테이블 이벤트 시퀀스]
id │ stage │ status │ created_at
────┼───────┼─────────────────┼───────────
1 │ lyric │ pending │ 12:00:00
1 │ lyric │ dispatched │ 12:00:10 ← 10초 대기
1 │ lyric │ processing │ 12:00:11
1 │ lyric │ stage_completed │ 12:00:16
2 │ song │ pending │ 12:00:20 ← 4초 대기 (다음 폴링)
...
[LangGraph: Checkpointer 이벤트 시퀀스]
step │ node_name │ state_snapshot │ created_at
──────┼───────────────────┼──────────────────────────┼───────────
1 │ marketing_search │ {marketing_docs: [...]} │ 12:00:00
2 │ local_search │ {local_*: [...]} │ 12:00:02
3 │ rag_retrieval │ {rag_similar: [...]} │ 12:00:03
4 │ embedding_store │ {messages: [...]} │ 12:00:04
5 │ generate_lyric │ {lyric_result: "..."} │ 12:00:09
6 │ validate_lyric │ {lyric_score: 0.78} │ 12:00:10
7 │ generate_song │ {song_url: "..."} │ 12:01:15 ← 즉시 실행!
8 │ generate_video │ {video_url: "..."} │ 12:03:30
9 │ save_results │ {current_stage: "done"} │ 12:03:31
장점:
✓ 각 단계의 전체 State 스냅샷 보존 (Pipeline 테이블보다 풍부)
✓ 폴링 대기 없이 즉시 다음 단계 실행
✓ RAG 검색 결과까지 이력에 포함
✓ 어떤 지점에서든 State를 복원하여 재실행 가능
4.2 RAG 결과의 이벤트 소싱 통합
┌─────────────────────────────────────────────────────────────────────────────┐
│ RAG 결과 + 이벤트 소싱 통합 │
└─────────────────────────────────────────────────────────────────────────────┘
Beat에서 불가능했던 것:
- Pipeline 테이블에 RAG 검색 결과를 저장하려면 추가 컬럼/테이블 필요
- config_json에 모든 것을 JSON으로 직렬화 → 쿼리 불편
LangGraph Checkpointer에서 가능한 것:
- State에 RAG 결과가 자연스럽게 포함
- 매 노드마다 전체 State 스냅샷 저장
- 특정 시점의 RAG 컨텍스트를 조회 가능
예: "이 가사가 참조한 마케팅 문서는 무엇이었나?"
→ step 5 (generate_lyric) 시점의 State에서
marketing_docs, local_landmarks 등 조회 가능
5. 코드 구현
5.1 그래프 빌더 (Beat 디스패처 대응)
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.postgres import PostgresSaver
def build_state_machine_graph() -> StateGraph:
"""
Celery Beat + 상태머신 → LangGraph 전환
Beat의 dispatch_pipelines() 함수가 담당하던 역할을
그래프 엣지 정의로 대체합니다.
Beat의 4단계 사이클:
Step 1 (pending → dispatch): 엣지로 자동 전이
Step 2 (completed → next stage): 엣지로 자동 전이
Step 3 (failed → retry): 조건부 엣지로 복구
Step 4 (stuck → reset): 타임아웃 + 에러 처리
"""
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)
# ─── 메인 노드 (Beat의 Worker 대응) ───
graph.add_node("generate_lyric", lyric_node_with_timeout)
graph.add_node("validate_lyric", lyric_validation_node)
graph.add_node("generate_song", song_node_with_timeout)
graph.add_node("generate_video", video_node_with_timeout)
graph.add_node("save_results", save_results_node)
# ─── 에러 복구 노드 ───
graph.add_node("lyric_recovery", lyric_recovery_node)
graph.add_node("song_recovery", song_recovery_node)
graph.add_node("video_recovery", video_recovery_node)
graph.add_node("fatal_handler", fatal_handler_node)
# ─── 엣지 (Beat의 NEXT_STAGE_MAP 대응) ───
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")
# ─── 조건부 분기 (Beat의 재시도 판단 대응) ───
graph.add_conditional_edges(
"validate_lyric",
route_after_lyric_validation,
{
"pass": "generate_song",
"retry": "lyric_recovery",
"fail": "fatal_handler",
},
)
graph.add_edge("lyric_recovery", "generate_lyric")
# Song 에러 분기
graph.add_conditional_edges(
"generate_song",
route_after_song,
{
"success": "generate_video",
"retry": "song_recovery",
"fail": "fatal_handler",
},
)
graph.add_edge("song_recovery", "generate_song")
# Video 에러 분기
graph.add_conditional_edges(
"generate_video",
route_after_video,
{
"success": "save_results",
"retry": "video_recovery",
"fail": "fatal_handler",
},
)
graph.add_edge("video_recovery", "generate_video")
graph.add_edge("save_results", END)
graph.add_edge("fatal_handler", END)
# ─── 체크포인터 (Beat의 Pipeline 테이블 대응) ───
# PostgreSQL 체크포인터로 ACID 보장 (Beat의 DB 기반 장점 보존)
checkpointer = PostgresSaver.from_conn_string(
"postgresql://user:pass@localhost/castad"
)
return graph.compile(checkpointer=checkpointer)
5.2 타임아웃 노드 래퍼 (Beat의 stuck 감지 대응)
import asyncio
from functools import wraps
def with_timeout(timeout_seconds: int):
"""
Beat의 stuck 감지를 대체하는 타임아웃 데코레이터
Beat: dispatched_at < NOW() - 15분 → 재디스패치
LangGraph: 노드 실행이 timeout 초과 → 예외 → 조건부 복구
"""
def decorator(node_func):
@wraps(node_func)
async def wrapper(state: PipelineState) -> dict:
try:
result = await asyncio.wait_for(
node_func(state),
timeout=timeout_seconds,
)
return result
except asyncio.TimeoutError:
return {
"error_message": f"Timeout after {timeout_seconds}s",
"current_stage": f"{state.get('current_stage', 'unknown')}_timeout",
"messages": [f"타임아웃: {timeout_seconds}초 초과"],
}
return wrapper
return decorator
# Beat에서 soft_time_limit=540이었던 Song 태스크:
@with_timeout(540)
async def song_node_with_timeout(state: PipelineState) -> dict:
"""Suno API 호출 (최대 9분)"""
# ... Suno API 호출 로직
pass
# Beat에서 time_limit=900이었던 Video 태스크:
@with_timeout(900)
async def video_node_with_timeout(state: PipelineState) -> dict:
"""Creatomate 렌더링 (최대 15분)"""
# ... Creatomate 호출 로직
pass
5.3 API (Beat의 "DB 레코드 생성만" 대응)
@router.post("/start")
async def start_pipeline(request: StartPipelineRequest):
"""
Beat 방식: Pipeline 레코드만 생성, Beat가 나중에 감지
LangGraph 방식: 그래프 직접 실행 (즉시 시작)
Beat: "파이프라인이 생성되었습니다. Beat가 곧 처리를 시작합니다."
LangGraph: "파이프라인이 즉시 시작됩니다."
"""
initial_state = build_initial_state(request)
config = {"configurable": {"thread_id": request.task_id}}
# Beat에서는 DB INSERT만 하고 반환했지만,
# LangGraph에서는 즉시 실행 (또는 백그라운드 실행)
# 옵션 1: 동기 실행 (완료까지 대기)
# result = await pipeline_graph.ainvoke(initial_state, config)
# 옵션 2: 백그라운드 실행 (Beat 방식과 유사한 비동기)
import asyncio
asyncio.create_task(
pipeline_graph.ainvoke(initial_state, config)
)
return {
"task_id": request.task_id,
"message": "파이프라인이 즉시 시작됩니다.",
# Beat: "Beat가 곧 처리를 시작합니다." (10초 후)
}
@router.get("/status/{task_id}")
async def get_status(task_id: str):
"""
Beat: Pipeline 테이블 SELECT 쿼리
LangGraph: 체크포인터에서 최신 State 조회
"""
config = {"configurable": {"thread_id": task_id}}
state = await pipeline_graph.aget_state(config)
if not state or not state.values:
raise HTTPException(404, "Pipeline not found")
values = state.values
return {
"task_id": task_id,
"current_stage": values.get("current_stage", "unknown"),
"lyric_score": values.get("lyric_score"),
"song_url": values.get("song_result_url"),
"video_url": values.get("video_result_url"),
"error": values.get("error_message"),
"messages": values.get("messages", []),
# Beat에서의 stages 정보도 제공
"checkpoint_history": [
{"step": s.step, "node": s.metadata.get("source")}
for s in pipeline_graph.get_state_history(config)
],
}
@router.post("/retry/{task_id}")
async def retry_pipeline(task_id: str):
"""
Beat: failed Pipeline의 status를 pending으로 변경, Beat가 재디스패치
LangGraph: 체크포인트에서 State 수정 후 재개
Beat 방식:
UPDATE pipelines SET status='pending', dispatched_at=NULL WHERE ...
→ Beat가 다음 사이클에서 감지 (10초 후)
LangGraph 방식:
State의 에러 클리어 → 즉시 재개
"""
config = {"configurable": {"thread_id": task_id}}
# State 수정: 에러 클리어
await pipeline_graph.aupdate_state(
config,
{"error_message": None},
)
# 즉시 재개 (Beat처럼 10초 대기 없음)
result = await pipeline_graph.ainvoke(None, config)
return {
"task_id": task_id,
"resumed": True,
"status": result.get("current_stage"),
}
5.4 가사 생성 노드 (Beat의 Worker 대응)
async def lyric_node_with_timeout(state: PipelineState) -> dict:
"""
Beat 방식의 lyric Worker → LangGraph 노드
Beat Worker 특징:
- pipeline_id를 받아 DB에서 모든 데이터 조회
- 결과를 반환하지 않음 (DB 상태만 변경)
- "다음 단계가 있다"는 것조차 모름
LangGraph 노드 특징:
- State에서 모든 데이터 접근 (DB 조회 최소화)
- State 변경으로 결과 반환
- 다음 노드를 모름 (그래프 엣지가 결정)
- RAG 컨텍스트 접근 가능
"""
# Beat에서는 pipeline_id로 DB 조회했지만,
# LangGraph에서는 State에서 직접 접근
customer_name = state["customer_name"]
region = state["region"]
# ─── Beat에서 불가능했던 RAG 컨텍스트 활용 ───
marketing_ctx = format_docs(state.get("marketing_docs", [])[:5])
landmark_ctx = format_docs(state.get("local_landmarks", []))
festival_ctx = format_docs(state.get("local_festivals", []))
travel_ctx = format_docs(state.get("local_travel", []))
rag_ctx = format_docs(state.get("rag_similar_docs", [])[:3])
# 재시도 시 피드백 포함
feedback = state.get("enriched_context", "")
# Structured Output
llm = ChatOpenAI(model="gpt-4o", temperature=0.8)
structured_llm = llm.with_structured_output(LyricOutput)
result = await structured_llm.ainvoke(
prompt.format(
customer_name=customer_name,
region=region,
detail_region_info=state["detail_region_info"],
language=state["language"],
marketing_context=marketing_ctx,
landmark_context=landmark_ctx,
festival_context=festival_ctx,
travel_context=travel_ctx,
rag_similar_context=rag_ctx,
feedback=feedback,
output_schema=LyricOutput.model_json_schema(),
)
)
lyric_text = "\n".join(line.text for line in result.lines)
# Beat Worker: DB 상태만 변경하고 return 없음
# LangGraph 노드: State 변경 dict 반환
return {
"lyric_result": lyric_text,
"lyric_structured": result.model_dump(),
"lyric_retry_count": state.get("lyric_retry_count", 0) + 1,
"current_stage": "lyric_completed",
"error_message": None,
"messages": [f"가사 생성 완료 (시도 #{state.get('lyric_retry_count', 0) + 1})"],
}
6. Beat 폴링 제거 → 이벤트 기반 전환
6.1 폴링 vs 이벤트 기반 비교
┌─────────────────────────────────────────────────────────────────────────────┐
│ Beat 폴링 → LangGraph 이벤트 기반 비교 │
└─────────────────────────────────────────────────────────────────────────────┘
[Beat: 폴링 기반]
━━━━━━━━━━━━━━━
T+0.0 API: Pipeline(status=pending) INSERT
T+0.0~10.0 대기 (Beat 다음 폴링까지)
T+10.0 Beat: SELECT WHERE status=pending → 발견!
T+10.0 Beat: status=dispatched, lyric_queue에 디스패치
T+10.1 Worker: 태스크 수신, 처리 시작
T+15.0 Worker: 처리 완료, status=stage_completed
T+15.0~25.0 대기 (Beat 다음 폴링까지)
T+25.0 Beat: stage_completed 발견! → song pending 생성
...
총 파이프라인 지연: 단계당 ~10초 × 3단계 = ~30초 추가 지연
[LangGraph: 이벤트 기반]
━━━━━━━━━━━━━━━━━━━━━━
T+0.0 API: graph.ainvoke(state)
T+0.0 marketing_search 즉시 실행
T+2.0 local_search 즉시 실행
T+3.0 rag_retrieval 즉시 실행
T+4.0 embedding_store 즉시 실행
T+9.0 generate_lyric 즉시 실행
T+10.0 validate_lyric 즉시 실행
T+10.0 generate_song 즉시 실행 (대기 없음!)
...
총 파이프라인 지연: 0초 (폴링 없음)
7. 실패 처리 및 자동 복구
7.1 Beat의 재시도 vs LangGraph 재시도
┌─────────────────────────────────────────────────────────────────────────────┐
│ 재시도 메커니즘 비교 │
└─────────────────────────────────────────────────────────────────────────────┘
[Beat: DB 기반 재시도]
━━━━━━━━━━━━━━━━━━━━
1. Worker 실패 → Pipeline.status = 'failed', retry_count += 1
2. Beat 폴링: WHERE status='failed' AND retry_count < max_retries
3. 재시도 간격 확인: last_failed_at < NOW() - retry_delay
4. pending으로 변경 → 다음 폴링에서 디스패치
장점: DB 수정으로 런타임 제어 가능
단점: 재시도까지 최대 20초+ 지연 (폴링 + 재시도 간격)
[LangGraph: 그래프 기반 재시도]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 노드 실행 결과 → 조건부 엣지에서 판단
2. retry 라우트 → recovery 노드 → 원래 노드 루프백
3. recovery 노드에서 대기 시간, 설정 변경 등 처리
장점: 즉각 복구, 피드백 기반 재시도
단점: 런타임 제어는 API로 State 수정 필요
[런타임 제어 비교]
Beat:
UPDATE pipelines SET max_retries=5 WHERE task_id='xxx';
UPDATE pipelines SET status='pending' WHERE task_id='xxx';
LangGraph:
await graph.aupdate_state(config, {"lyric_retry_count": 0})
await graph.ainvoke(None, config) # 재개
8. Celery Beat 대비 비교
┌──────────────────────────┬──────────────────────────┬───────────────────────────┐
│ 기준 │ Celery Beat + 상태머신 │ LangGraph + Checkpointer │
├──────────────────────────┼──────────────────────────┼───────────────────────────┤
│ 상태 저장소 │ Pipeline 테이블 (MySQL) │ Checkpointer (Postgres) │
│ 다음 단계 결정 │ Beat 폴링 (10초마다) │ 그래프 엣지 (즉시) │
│ 지연 │ ~10초/단계 │ 0초 │
│ DB 부하 │ 중간 (폴링 쿼리) │ 낮음 (체크포인트 쓰기만) │
│ 추가 인프라 │ Beat + scheduler_queue │ 없음 │
│ 마이그레이션 │ Pipeline 테이블 필요 │ 체크포인터 테이블 자동 │
│ 태스크 독립성 │ 최고 (다음 단계 모름) │ 최고 (다음 노드 모름) │
│ 이벤트 소싱 │ Pipeline 테이블 │ 체크포인트 히스토리 │
│ 상태 이력 풍부도 │ 상태 + 시간만 │ 전체 State 스냅샷 │
│ 자동 복구 │ stuck 감지 (15분) │ 타임아웃 + 즉시 복구 │
│ 런타임 제어 │ DB UPDATE │ aupdate_state API │
│ 단일 장애점 │ Beat 프로세스 │ 없음 │
│ RAG 통합 │ config_json에 직렬화 │ State로 자연스럽게 통합 │
│ 모니터링 │ SQL 쿼리 │ LangSmith + 체크포인트 │
│ 동시 처리 │ Worker 수 × 동시성 │ asyncio 기반 │
│ 적합한 상황 │ 복잡한 워크플로 │ 품질 중심 + RAG 강화 │
└──────────────────────────┴──────────────────────────┴───────────────────────────┘
9. 프롬프트 및 RAG 최적화
9.1 Beat 설계안 특화 최적화 (이벤트 소싱 활용)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 이벤트 소싱 + RAG 최적화 전략 │
└─────────────────────────────────────────────────────────────────────────────┘
1. 체크포인트 히스토리 기반 학습
─────────────────────────────
과거 파이프라인 실행의 State 히스토리를 분석:
- 높은 lyric_score를 받은 실행의 marketing_docs 패턴
- 재시도 없이 통과한 실행의 RAG 컨텍스트 특성
→ 성공 패턴을 학습하여 검색 쿼리 최적화
2. State 스냅샷 기반 디버깅
─────────────────────────
실패한 파이프라인의 정확한 State 복원:
- 어떤 검색 결과가 프롬프트에 포함되었는지
- 어떤 RAG 문서가 참조되었는지
- 프롬프트 전문 확인
→ 실패 원인 분석 후 프롬프트/RAG 개선
3. A/B 테스트 (체크포인트 비교)
────────────────────────────
동일 입력에 대해 다른 RAG 설정으로 실행:
Thread A: chunk_size=500, reranker=cross-encoder
Thread B: chunk_size=300, reranker=cohere
→ 체크포인트의 lyric_score 비교로 최적 설정 도출
4. 누적 지식 그래프
────────────────
매 파이프라인 실행 시:
- 검색 결과 → 벡터 DB 저장 (임베딩)
- 성공 가사 → "성공 사례" 컬렉션 저장
- 실패 패턴 → "주의 사항" 컬렉션 저장
→ 실행할수록 RAG 품질 향상
5. 동적 폴링 간격 (하이브리드)
────────────────────────────
즉시 실행이 부담스러운 경우 (대량 요청):
- LangGraph를 Celery 태스크 내에서 실행
- Beat 폴링은 유지하되 LangGraph로 내부 처리
→ 분산 실행 + RAG 강화의 장점 결합
6. 컨텍스트 캐싱 전략
──────────────────
동일 region 반복 요청 시:
- 체크포인터에서 이전 실행의 local_search 결과 확인
- 7일 이내면 외부 검색 스킵 → State에서 복사
→ API 호출 비용 90% 절감 (동일 지역)
문서 버전
| 버전 | 날짜 | 변경 내용 |
|---|---|---|
| 1.0 | 2024-XX-XX | 초안 작성 |