o2o-castad-backend/docs/plan/celery/celery-plan_3-beat-상태머신-스케줄...

39 KiB
Raw Blame History

설계안 3 LangGraph 전환: Beat 상태머신 → LangGraph 체크포인트 기반 상태 그래프

Celery Beat + DB 폴링 상태 머신을 LangGraph 네이티브 체크포인팅 + 이벤트 소싱 그래프로 전환한 설계안


목차

  1. 개요 및 핵심 차이점
  2. 아키텍처 설계
  3. 상태 머신 전환
  4. RAG 통합 + 이벤트 소싱
  5. 코드 구현
  6. Beat 폴링 제거 → 이벤트 기반 전환
  7. 실패 처리 및 자동 복구
  8. Celery Beat 대비 비교
  9. 프롬프트 및 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 초안 작성