bugfix for suno error msg

get_video
Dohyun Lim 2026-02-06 10:59:02 +09:00
parent c207b8a48f
commit f6ce81e14e
9 changed files with 11035 additions and 1 deletions

View File

@ -163,7 +163,8 @@ class SunoService:
if data.get("code") != 200:
error_msg = data.get("msg", "Unknown error")
raise SunoResponseError(f"Suno API error: {error_msg}", original_response=data)
logger.error(f"[Suno] API error: {error_msg} | response: {data}")
raise SunoResponseError("api 에러입니다.", original_response=data)
response_data = data.get("data")
if response_data is None:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,661 @@
# 설계안 1 LangGraph 전환: Chain Primitive → LangGraph 선언적 그래프
> **Celery Chain의 선언적 파이프라인을 LangGraph StateGraph로 전환한 설계안**
---
## 목차
1. [개요 및 핵심 차이점](#1-개요-및-핵심-차이점)
2. [아키텍처 설계](#2-아키텍처-설계)
3. [데이터 흐름 상세](#3-데이터-흐름-상세)
4. [RAG 파이프라인 통합](#4-rag-파이프라인-통합)
5. [코드 구현](#5-코드-구현)
6. [상태 관리 및 체크포인팅](#6-상태-관리-및-체크포인팅)
7. [실패 처리 전략](#7-실패-처리-전략)
8. [Celery Chain 대비 비교](#8-celery-chain-대비-비교)
9. [프롬프트 및 RAG 최적화](#9-프롬프트-및-rag-최적화)
---
## 1. 개요 및 핵심 차이점
### 1.1 설계 철학
Celery Chain은 `chain(A.s() | B.s() | C.s())`으로 선언적 파이프라인을 정의합니다.
LangGraph는 `StateGraph`**더 유연한 선언적 그래프**를 정의하면서,
**조건부 분기, 루프, 상태 체크포인팅**을 네이티브로 지원합니다.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Celery Chain vs LangGraph StateGraph │
├──────────────────────────────────┬──────────────────────────────────────────┤
│ Celery Chain │ LangGraph StateGraph │
├──────────────────────────────────┼──────────────────────────────────────────┤
│ chain(A.s() | B.s() | C.s()) │ graph.add_edge("A", "B") │
│ │ graph.add_edge("B", "C") │
│ │ │
│ 직선형 파이프라인만 가능 │ 분기, 루프, 병렬 모두 가능 │
│ 이전 태스크 반환값 → 다음 입력 │ State 객체로 전체 상태 공유 │
│ 실패 시 chain 전체 중단 │ 체크포인트에서 재시작 가능 │
│ Celery 엔진이 연결 관리 │ 그래프 정의로 연결 관리 │
│ 부분 재시작: 새 chain 생성 필요 │ thread_id로 정확한 지점에서 재개 │
│ 결과 전파: 메시지 크기 제한 │ State 객체: 크기 제한 없음 │
└──────────────────────────────────┴──────────────────────────────────────────┘
```
### 1.2 핵심 이점: Chain의 약점 해결
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Celery Chain의 약점 → LangGraph 해결 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 약점 1: 직선형만 가능 │
│ Chain: lyric → song → video (분기 불가) │
│ Graph: lyric → [검증 통과?] → song / [재생성] → lyric (루프) │
│ │
│ 약점 2: 부분 재시작 어려움 │
│ Chain: song 실패 시 → 새 chain(song.s(), video.s()) 생성 필요 │
│ Graph: checkpointer에서 song 노드부터 자동 재개 │
│ │
│ 약점 3: 메시지 크기 제한 │
│ Chain: 반환값 < 1KB (Redis )
│ Graph: State 객체에 제한 없음 (RAG 컨텍스트 등 대량 데이터 가능) │
│ │
│ 약점 4: RAG 통합 불가 │
│ Chain: 각 태스크가 독립적이라 RAG 컨텍스트 공유 어려움 │
│ Graph: State에 RAG 컨텍스트를 포함하여 모든 노드에서 접근 가능 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 2. 아키텍처 설계
### 2.1 LangGraph 그래프 구조
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Celery Chain → LangGraph 선언적 그래프 전환 │
└─────────────────────────────────────────────────────────────────────────────┘
[Celery Chain 원본]
chain(
generate_lyric.s(data).set(queue='lyric_queue'),
generate_song.s().set(queue='song_queue'),
generate_video.s().set(queue='video_queue'),
)
↓↓↓ LangGraph 전환 ↓↓↓
[LangGraph StateGraph]
START
┌───────────────────┐
│ marketing_search │ ← RAG 강화 (신규)
│ (30건 외부 검색) │
└────────┬──────────┘
┌───────────────────┐
│ local_search │ ← 지역 정보 (신규)
│ (랜드마크/축제) │
└────────┬──────────┘
┌───────────────────┐
│ rag_retrieval │ ← 벡터 DB 검색 (신규)
│ (유사 문서 검색) │
└────────┬──────────┘
┌───────────────────┐
│ embedding_store │ ← 임베딩 저장 (신규)
└────────┬──────────┘
┌───────────────────┐
│ generate_lyric │ ← Celery lyric_task 대응
│ (RAG 강화 프롬프트)│ + Pydantic 정형화
└────────┬──────────┘
┌───────────────────┐
│ validate_lyric │ ← 조건부 분기 (신규)
└────┬─────────┬────┘
[pass] [retry: count < 3]
│ │
│ └──→ generate_lyric (루프백)
┌───────────────────┐
│ generate_song │ ← Celery song_task 대응
│ (Suno API) │
└────────┬──────────┘
┌───────────────────┐
│ generate_video │ ← Celery video_task 대응
│ (Creatomate) │
└────────┬──────────┘
┌───────────────────┐
│ save_results │
└────────┬──────────┘
END
```
### 2.2 Celery Chain과의 매핑
```
┌────────────────────────────────────────────────────────────────────┐
│ Celery Chain 요소 → LangGraph 매핑 │
├───────────────────────┬────────────────────────────────────────────┤
│ Celery Chain 요소 │ LangGraph 대응 │
├───────────────────────┼────────────────────────────────────────────┤
│ chain() │ StateGraph().compile() │
│ .s() (signature) │ graph.add_node("name", func) │
│ | (pipe 연산자) │ graph.add_edge("A", "B") │
│ apply_async() │ graph.ainvoke(state, config) │
│ 반환값 전파 │ State 객체 공유 │
│ link_error │ 조건부 엣지 + 에러 핸들링 노드 │
│ .set(queue=...) │ (LangGraph는 큐 개념 없음 - 단일 프로세스) │
│ AsyncResult.get() │ checkpointer.get_tuple(config) │
│ chain_id │ thread_id (configurable) │
│ result.parent │ state history (체크포인트 이력) │
└───────────────────────┴────────────────────────────────────────────┘
```
---
## 3. 데이터 흐름 상세
### 3.1 Chain의 결과 전파 vs State 공유
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 데이터 전달 방식 비교 │
└─────────────────────────────────────────────────────────────────────────────┘
[Celery Chain: 반환값 전파]
━━━━━━━━━━━━━━━━━━━━━━━━━
lyric_task(data) → {"task_id":"xxx", "lyric_result":"..."} → song_task(prev)
─────────────────────────────────────
이 반환값이 메시지로 전달 (크기 제한)
문제:
- lyric_result가 크면 메시지 크기 초과
- RAG 컨텍스트 같은 대량 데이터 전달 불가
- 이전 단계의 중간 결과 접근 불가
[LangGraph: State 공유]
━━━━━━━━━━━━━━━━━━━━━━
State = {
task_id: "xxx",
marketing_docs: [...30건...], ← 모든 노드에서 접근 가능
local_landmarks: [...3건...],
rag_similar_docs: [...10건...],
lyric_result: "가사 전문...",
lyric_structured: {title, lines, keywords...},
song_result_url: "...",
video_result_url: "...",
}
모든 노드가 State 전체를 읽고 수정 가능
→ RAG 컨텍스트, 검색 결과, 중간 결과 모두 공유
→ 크기 제한 없음
```
### 3.2 시퀀스 다이어그램
```mermaid
sequenceDiagram
participant C as Client
participant API as FastAPI
participant G as LangGraph
participant Search as Tavily Search
participant VDB as Vector DB
participant LLM as ChatGPT
participant Suno as Suno API
participant CM as Creatomate
participant DB as PostgreSQL
C->>API: POST /pipeline/start
API->>G: graph.ainvoke(initial_state)
Note over G: marketing_search_node
G->>Search: 5개 쿼리 × 6건 = 30건 검색
Search-->>G: 검색 결과 → State.marketing_docs
Note over G: local_search_node
G->>Search: 랜드마크/축제/여행지 각 10건
Search-->>G: 결과 → State.local_*
Note over G: rag_retrieval_node
G->>VDB: 유사 문서 검색 (벡터 + BM25)
VDB-->>G: 유사 문서 → State.rag_similar_docs
Note over G: embedding_store_node
G->>VDB: 새 검색 결과 임베딩 저장
Note over G: lyric_generation_node
G->>LLM: RAG 강화 프롬프트 + Pydantic 스키마
LLM-->>G: 정형화된 가사 → State.lyric_structured
Note over G: lyric_validation_node
G->>G: 규칙 + LLM 점수 계산
alt score >= 0.7
Note over G: song_generation_node
G->>Suno: 가사 → 음악 생성
Suno-->>G: song_url → State.song_result_url
Note over G: video_generation_node
G->>CM: 렌더링 요청
CM-->>G: video_url → State.video_result_url
else score < 0.7 && retry < 3
G->>G: lyric_generation_node로 루프백
end
Note over G: save_results_node
G->>DB: 최종 결과 저장
G-->>API: 최종 State 반환
API-->>C: 결과 응답
```
---
## 4. RAG 파이프라인 통합
### 4.1 Chain에서 불가능했던 RAG 통합
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Celery Chain에서 RAG가 어려웠던 이유 → LangGraph 해결 │
└─────────────────────────────────────────────────────────────────────────────┘
[Celery Chain의 한계]
━━━━━━━━━━━━━━━━━━━
chain(
lyric_task.s(data), # ← data에 RAG 컨텍스트를 넣으려면?
song_task.s(), # 메시지 크기 1KB 제한에 걸림
video_task.s(),
)
방법 1: data에 RAG 결과 포함 → 메시지 너무 커짐
방법 2: 각 태스크 내에서 개별 RAG → 중복 검색, 일관성 없음
방법 3: Redis에 RAG 결과 저장 → 추가 인프라, 동기화 문제
[LangGraph의 해결]
━━━━━━━━━━━━━━━━━
State에 RAG 결과를 자연스럽게 포함:
graph.add_node("marketing_search", search_30_docs)
graph.add_node("local_search", search_local_info)
graph.add_node("rag_retrieval", vector_db_search)
graph.add_node("lyric_gen", use_all_rag_context) # State에서 모두 접근
→ 검색은 한 번만, 결과는 모든 노드에서 공유
→ State 크기 제한 없음
→ 체크포인트로 검색 결과까지 저장/복구
```
### 4.2 RAG 노드 상세
Chain 원본의 각 태스크 앞에 RAG 노드를 삽입:
```
[Celery Chain 원본]
lyric_task → song_task → video_task
[LangGraph 전환]
search(30건) → local(30건) → rag_retrieve → embed_store
→ lyric_gen(RAG강화) → validate → song_gen → video_gen
추가된 4개 노드:
1. marketing_search: 외부 검색 30건 수집, 리랭크, 필터링
2. local_search: 지역 정보 3카테고리 × 10건, 상위 3건 선택
3. rag_retrieval: 벡터 DB에서 기존 유사 문서 검색
4. embedding_store: 새 검색 결과를 벡터 DB에 저장
```
---
## 5. 코드 구현
### 5.1 그래프 정의 (Chain 선언 대응)
```python
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver
def build_chain_style_graph() -> StateGraph:
"""
Celery chain(A|B|C)에 대응하는 LangGraph 그래프
+ RAG 강화 노드 추가
"""
graph = StateGraph(PipelineState)
# ─── Celery Chain에 없던 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)
# ─── Celery Chain의 기존 태스크 대응 ───
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)
# ─── 엣지 연결 (Chain의 | 연산자 대응) ───
graph.set_entry_point("marketing_search")
# chain과 동일한 직선 연결
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")
# ─── Chain에서 불가능했던 조건부 분기 (신규) ───
graph.add_conditional_edges(
"validate_lyric",
lambda state: (
"pass" if state["lyric_score"] >= 0.7
else "retry" if state["lyric_retry_count"] < 3
else "fail"
),
{
"pass": "generate_song",
"retry": "generate_lyric", # 루프백!
"fail": "save_results",
},
)
graph.add_edge("generate_song", "generate_video")
graph.add_edge("generate_video", "save_results")
graph.add_edge("save_results", END)
# ─── 체크포인터 (Chain의 부분 재시작 한계 해결) ───
checkpointer = SqliteSaver.from_conn_string("./checkpoints.db")
return graph.compile(checkpointer=checkpointer)
```
### 5.2 가사 노드 (Chain의 generate_lyric 대응)
```python
async def lyric_generation_node(state: PipelineState) -> dict:
"""
Celery chain의 generate_lyric 태스크를 LangGraph 노드로 전환
차이점:
- Celery: data dict를 입력받아 처리 후 반환값으로 전달
- LangGraph: State에서 RAG 컨텍스트 포함 전체 데이터 접근
"""
# ─── Chain에서는 불가능했던 RAG 컨텍스트 활용 ───
marketing_ctx = format_docs(state["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])
# Structured Output (Pydantic 정형화)
llm = ChatOpenAI(model="gpt-4o", temperature=0.8)
structured_llm = llm.with_structured_output(LyricOutput)
prompt = ChatPromptTemplate.from_template(LYRIC_GENERATION_TEMPLATE)
result: LyricOutput = await structured_llm.ainvoke(
prompt.format(
customer_name=state["customer_name"],
region=state["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,
output_schema=LyricOutput.model_json_schema(),
)
)
lyric_text = "\n".join(line.text for line in result.lines)
# ─── Chain에서는 경량 dict만 반환했지만 ───
# ─── LangGraph에서는 State에 직접 저장 ───
return {
"lyric_result": lyric_text,
"lyric_structured": result.model_dump(),
"lyric_retry_count": state.get("lyric_retry_count", 0) + 1,
"current_stage": "lyric_completed",
"messages": [f"가사 생성 완료 (시도 #{state.get('lyric_retry_count', 0) + 1})"],
}
```
### 5.3 FastAPI 통합 (Chain의 pipeline.py 대응)
```python
# app/api/routers/v1/pipeline.py
"""
Celery chain().apply_async() 대신 graph.ainvoke() 사용
비교:
Celery: pipeline = chain(A.s(data) | B.s() | C.s())
result = pipeline.apply_async()
LangGraph: result = await graph.ainvoke(state, config)
"""
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(prefix="/pipeline", tags=["Pipeline"])
pipeline_graph = build_chain_style_graph()
@router.post("/start")
async def start_pipeline(request: StartPipelineRequest):
initial_state = {
"task_id": request.task_id,
"customer_name": request.customer_name,
"region": request.region,
"detail_region_info": request.detail_region_info,
"language": request.language,
"orientation": request.orientation,
"genre": request.genre,
# RAG 관련 초기값
"marketing_docs": [],
"local_landmarks": [],
"local_festivals": [],
"local_travel": [],
"rag_similar_docs": [],
"enriched_context": "",
# 생성 결과 초기값
"lyric_result": None,
"lyric_structured": None,
"lyric_score": 0.0,
"lyric_retry_count": 0,
"song_result_url": None,
"song_duration": None,
"video_result_url": None,
# 메타
"current_stage": "started",
"error_message": None,
"messages": [],
}
# Chain의 apply_async() 대응
# thread_id = task_id로 체크포인트 관리
config = {"configurable": {"thread_id": request.task_id}}
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"),
"lyric_score": result.get("lyric_score"),
}
@router.post("/resume/{task_id}")
async def resume_pipeline(task_id: str):
"""
Chain에서는 새 chain을 만들어야 했지만,
LangGraph에서는 체크포인트에서 자동 재개
"""
config = {"configurable": {"thread_id": task_id}}
# 마지막 체크포인트에서 자동 재개
result = await pipeline_graph.ainvoke(None, config)
return {
"task_id": task_id,
"resumed": True,
"status": result["current_stage"],
}
```
---
## 6. 상태 관리 및 체크포인팅
### 6.1 Chain의 parent 추적 vs 체크포인트 히스토리
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 상태 추적 비교 │
└─────────────────────────────────────────────────────────────────────────────┘
[Celery Chain]
result.id → video 태스크 ID
result.parent.id → song 태스크 ID
result.parent.parent.id → lyric 태스크 ID
※ chain_id로는 마지막 태스크만 조회 가능
[LangGraph]
checkpointer.list(config)로 전체 이력 조회:
Step 1: marketing_search → State snapshot #1
Step 2: local_search → State snapshot #2
Step 3: rag_retrieval → State snapshot #3
Step 4: embedding_store → State snapshot #4
Step 5: generate_lyric → State snapshot #5
Step 6: validate_lyric → State snapshot #6
Step 7: generate_lyric → State snapshot #7 (재생성)
Step 8: validate_lyric → State snapshot #8 (통과)
Step 9: generate_song → State snapshot #9
Step 10: generate_video → State snapshot #10
Step 11: save_results → State snapshot #11
→ 모든 단계의 전체 State를 타임라인으로 조회 가능
→ 어떤 단계에서든 정확한 재시작 가능
```
---
## 7. 실패 처리 전략
### 7.1 Chain의 실패 vs LangGraph의 실패
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 실패 처리 비교 │
└─────────────────────────────────────────────────────────────────────────────┘
[Celery Chain 실패 시나리오]
━━━━━━━━━━━━━━━━━━━━━━━━
chain(lyric.s() | song.s() | video.s())
song 실패 시:
1. song 태스크 자체 재시도 (max_retries=3)
2. 모든 재시도 실패 → chain 전체 FAILURE
3. 부분 재시작: 새 chain(song.s(prev), video.s()) 수동 생성 필요
4. lyric 결과를 DB에서 다시 조회해야 함
[LangGraph 실패 시나리오]
━━━━━━━━━━━━━━━━━━━━━━━
generate_song 노드 실패 시:
1. 예외 발생 → 마지막 체크포인트 저장됨
2. State에 lyric_result, RAG 컨텍스트 등 모든 데이터 보존
3. resume: graph.ainvoke(None, config)
→ generate_song 노드부터 정확히 재시작
4. DB 재조회 불필요 (State에 모두 있음)
추가 기능:
- 실패 노드에서 에러 메시지를 State에 기록
- 조건부 엣지로 대체 경로 실행 가능
- Human-in-the-loop: 사람이 확인 후 재개 가능
```
---
## 8. Celery Chain 대비 비교
```
┌──────────────────────────┬──────────────────────┬──────────────────────────┐
│ 기준 │ Celery Chain │ LangGraph StateGraph │
├──────────────────────────┼──────────────────────┼──────────────────────────┤
│ 파이프라인 정의 │ chain(A|B|C) │ add_node + add_edge │
│ 데이터 전달 │ 반환값 전파 (<1KB) State ()
│ 조건부 분기 │ 불가능 │ conditional_edges │
│ 루프 │ 불가능 │ 자연스러운 루프백 │
│ RAG 통합 │ 메시지 크기 제한 │ State로 자유롭게 통합 │
│ 부분 재시작 │ 새 chain 수동 생성 │ 체크포인트 자동 재개 │
│ 상태 이력 │ parent 체인 제한적 │ 전체 State 타임라인 │
│ 선언적 수준 │ 높음 (1줄 선언) │ 높음 (그래프 빌더) │
│ 태스크 독립성 │ 높음 (반환값만) │ 높음 (State 읽기/쓰기) │
│ 분산 실행 │ 다중 워커 수평 확장 │ 단일 프로세스 │
│ 에러 핸들링 │ link_error │ try/except + 조건부 분기 │
│ 모니터링 │ Flower │ LangSmith │
│ 실시간 추적 │ chain_id polling │ 스트리밍 이벤트 │
└──────────────────────────┴──────────────────────┴──────────────────────────┘
```
---
## 9. 프롬프트 및 RAG 최적화
### 9.1 Chain 방식에서 할 수 없었던 최적화
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ LangGraph에서만 가능한 최적화 │
└─────────────────────────────────────────────────────────────────────────────┘
1. 반복 개선 루프 (Iterative Refinement)
─────────────────────────────────────
generate_lyric → validate → [점수 낮으면] → 피드백 포함 재생성
Chain에서는 불가능했지만, LangGraph에서는 자연스러운 루프
2. 적응형 RAG (Adaptive RAG)
─────────────────────────
첫 검색 결과가 부족하면 → 쿼리 재구성 → 재검색
State에 검색 이력 저장 → 점진적 품질 향상
3. 멀티 에이전트 협업
─────────────────────
마케팅 분석 에이전트 → 가사 작성 에이전트 → 품질 평가 에이전트
각 에이전트가 State를 통해 협업
4. 동적 프롬프트 구성
─────────────────────
검색된 문서의 양과 품질에 따라 프롬프트 구조를 동적으로 변경
지역 정보가 풍부하면 지역 참조 강화, 부족하면 일반 마케팅 강화
5. 피드백 기반 임베딩 갱신
─────────────────────────
사용자가 높이 평가한 가사 → "성공 사례"로 벡터 DB에 저장
이후 유사 요청 시 우선 참조 → 시간이 갈수록 품질 향상
```
---
## 문서 버전
| 버전 | 날짜 | 변경 내용 |
|------|------|-----------|
| 1.0 | 2024-XX-XX | 초안 작성 |

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,647 @@
# 설계안 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 | 초안 작성 |

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,837 @@
# 설계안 3 LangGraph 전환: Beat 상태머신 → LangGraph 체크포인트 기반 상태 그래프
> **Celery Beat + DB 폴링 상태 머신을 LangGraph 네이티브 체크포인팅 + 이벤트 소싱 그래프로 전환한 설계안**
---
## 목차
1. [개요 및 핵심 차이점](#1-개요-및-핵심-차이점)
2. [아키텍처 설계](#2-아키텍처-설계)
3. [상태 머신 전환](#3-상태-머신-전환)
4. [RAG 통합 + 이벤트 소싱](#4-rag-통합-이벤트-소싱)
5. [코드 구현](#5-코드-구현)
6. [Beat 폴링 제거 → 이벤트 기반 전환](#6-beat-폴링-제거--이벤트-기반-전환)
7. [실패 처리 및 자동 복구](#7-실패-처리-및-자동-복구)
8. [Celery Beat 대비 비교](#8-celery-beat-대비-비교)
9. [프롬프트 및 RAG 최적화](#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 디스패처 대응)
```python
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 감지 대응)
```python
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 레코드 생성만" 대응)
```python
@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 대응)
```python
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 | 초안 작성 |

File diff suppressed because it is too large Load Diff