662 lines
31 KiB
Markdown
662 lines
31 KiB
Markdown
# 설계안 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 | 초안 작성 |
|