- Published on
LangGraph 에이전트 워크플로우 실전 가이드: 멀티에이전트 오케스트레이션부터 프로덕션 배포까지
- Authors
- Name
1. LangGraph란
LangGraph는 LangChain 팀이 개발한 상태 기반(Stateful) 에이전트 오케스트레이션 프레임워크다. 기존 LangChain의 Chain/Agent가 선형적이었다면, LangGraph는 그래프 구조로 복잡한 워크플로우를 표현한다.
1.1 왜 LangGraph인가?
| 특성 | LangChain Agent | LangGraph |
|---|---|---|
| 흐름 제어 | 단순 루프 | DAG + 조건부 분기 |
| 상태 관리 | 제한적 | TypedDict 기반 명시적 상태 |
| 멀티에이전트 | 어려움 | 네이티브 지원 |
| 인간 개입 | 커스텀 구현 | interrupt() 내장 |
| 스트리밍 | 기본 | 토큰/이벤트/상태 스트리밍 |
| 디버깅 | 어려움 | LangGraph Studio |
2. 기본 개념
2.1 StateGraph
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
# 1) 상태 정의
class AgentState(TypedDict):
messages: Annotated[list, "add_messages"] # 메시지 누적
next_action: str
result: str
# 2) 노드 함수 정의
def classify_intent(state: AgentState) -> dict:
"""사용자 의도 분류"""
last_msg = state["messages"][-1].content
# LLM으로 의도 분류
intent = llm.invoke(f"Classify intent: {last_msg}")
return {"next_action": intent.content}
def handle_question(state: AgentState) -> dict:
"""질문 처리"""
answer = llm.invoke(state["messages"])
return {"result": answer.content}
def handle_task(state: AgentState) -> dict:
"""작업 실행"""
result = agent_executor.invoke(state["messages"])
return {"result": result}
# 3) 그래프 구축
graph = StateGraph(AgentState)
graph.add_node("classify", classify_intent)
graph.add_node("question", handle_question)
graph.add_node("task", handle_task)
# 4) 엣지 연결
graph.add_edge(START, "classify")
graph.add_conditional_edges(
"classify",
lambda state: state["next_action"],
{
"question": "question",
"task": "task",
}
)
graph.add_edge("question", END)
graph.add_edge("task", END)
# 5) 컴파일 & 실행
app = graph.compile()
result = app.invoke({"messages": [HumanMessage("What is Kubernetes?")]})
2.2 Tool 사용 에이전트
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
@tool
def search_docs(query: str) -> str:
"""Search internal documentation."""
# 벡터 DB 검색
results = vectorstore.similarity_search(query, k=3)
return "\n".join([r.page_content for r in results])
@tool
def run_query(sql: str) -> str:
"""Execute a read-only SQL query."""
return db.execute(sql).fetchall()
@tool
def create_ticket(title: str, description: str) -> str:
"""Create a Jira ticket."""
return jira.create_issue(title=title, description=description)
llm = ChatOpenAI(model="gpt-4o")
# ReAct 에이전트 자동 생성
agent = create_react_agent(
llm,
tools=[search_docs, run_query, create_ticket],
state_modifier="You are a helpful DevOps assistant."
)
# 실행
result = agent.invoke({
"messages": [HumanMessage("Check if order-service has errors and create a ticket")]
})
3. 고급 패턴
3.1 멀티에이전트 오케스트레이션
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import create_react_agent
class SupervisorState(TypedDict):
messages: Annotated[list, "add_messages"]
next_agent: str
final_answer: str
# 전문 에이전트들
researcher = create_react_agent(llm, tools=[search_web, search_docs])
coder = create_react_agent(llm, tools=[run_code, read_file])
reviewer = create_react_agent(llm, tools=[analyze_code, lint_code])
def supervisor(state: SupervisorState) -> dict:
"""어떤 에이전트에게 위임할지 결정"""
response = llm.invoke([
SystemMessage("You are a supervisor. Route to: researcher, coder, reviewer, or FINISH"),
*state["messages"]
])
return {"next_agent": response.content.strip()}
def run_researcher(state):
result = researcher.invoke({"messages": state["messages"]})
return {"messages": result["messages"]}
def run_coder(state):
result = coder.invoke({"messages": state["messages"]})
return {"messages": result["messages"]}
def run_reviewer(state):
result = reviewer.invoke({"messages": state["messages"]})
return {"messages": result["messages"]}
# 그래프 구성
workflow = StateGraph(SupervisorState)
workflow.add_node("supervisor", supervisor)
workflow.add_node("researcher", run_researcher)
workflow.add_node("coder", run_coder)
workflow.add_node("reviewer", run_reviewer)
workflow.add_edge(START, "supervisor")
workflow.add_conditional_edges(
"supervisor",
lambda s: s["next_agent"],
{
"researcher": "researcher",
"coder": "coder",
"reviewer": "reviewer",
"FINISH": END,
}
)
# 각 에이전트 실행 후 supervisor로 복귀
for agent_name in ["researcher", "coder", "reviewer"]:
workflow.add_edge(agent_name, "supervisor")
app = workflow.compile()
3.2 Human-in-the-Loop
from langgraph.types import interrupt, Command
def sensitive_action(state):
"""민감한 작업 전 인간 승인 요청"""
action = state["pending_action"]
# 실행을 중단하고 인간 승인 대기
approval = interrupt({
"question": f"Approve this action?\n{action}",
"options": ["approve", "reject", "modify"]
})
if approval == "approve":
return execute_action(action)
elif approval == "reject":
return {"result": "Action cancelled by user"}
else:
return {"result": f"Action modified: {approval}"}
# 체크포인터와 함께 컴파일
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
app = workflow.compile(checkpointer=checkpointer)
# 실행 (interrupt에서 멈춤)
config = {"configurable": {"thread_id": "user-123"}}
result = app.invoke({"messages": [...]}, config)
# result = {"__interrupt__": {...}}
# 인간이 승인 후 재개
app.invoke(Command(resume="approve"), config)
3.3 병렬 실행
from langgraph.graph import StateGraph
class ParallelState(TypedDict):
query: str
web_results: str
db_results: str
combined: str
def search_web_node(state):
return {"web_results": search_web(state["query"])}
def search_db_node(state):
return {"db_results": search_db(state["query"])}
def combine_results(state):
combined = f"Web: {state['web_results']}\nDB: {state['db_results']}"
return {"combined": combined}
graph = StateGraph(ParallelState)
graph.add_node("web", search_web_node)
graph.add_node("db", search_db_node)
graph.add_node("combine", combine_results)
# 병렬 실행: START → [web, db] → combine
graph.add_edge(START, "web")
graph.add_edge(START, "db")
graph.add_edge("web", "combine")
graph.add_edge("db", "combine")
graph.add_edge("combine", END)
app = graph.compile()
4. 스트리밍
# 토큰 단위 스트리밍
async for event in app.astream_events(
{"messages": [HumanMessage("Explain CQRS")]},
version="v2"
):
if event["event"] == "on_chat_model_stream":
print(event["data"]["chunk"].content, end="", flush=True)
# 노드 단위 스트리밍
for chunk in app.stream({"messages": [HumanMessage("...")]}):
for node_name, output in chunk.items():
print(f"[{node_name}]: {output}")
5. 메모리와 체크포인팅
from langgraph.checkpoint.postgres import PostgresSaver
# PostgreSQL 체크포인터 (프로덕션)
checkpointer = PostgresSaver.from_conn_string(
"postgresql://user:pass@localhost/langgraph"
)
app = workflow.compile(checkpointer=checkpointer)
# 대화 지속 (thread_id로 구분)
config = {"configurable": {"thread_id": "user-456"}}
# 첫 번째 메시지
app.invoke({"messages": [HumanMessage("Hi")]}, config)
# 두 번째 메시지 (이전 대화 컨텍스트 유지)
app.invoke({"messages": [HumanMessage("What did I just say?")]}, config)
6. LangGraph Platform 배포
6.1 langgraph.json
{
"dependencies": ["."],
"graphs": {
"agent": "./agent.py:app"
},
"env": ".env"
}
6.2 배포
# LangGraph CLI 설치
pip install langgraph-cli
# 로컬 테스트
langgraph dev
# Docker 빌드
langgraph build -t my-agent:latest
# LangGraph Cloud 배포
langgraph deploy --app my-agent
7. 퀴즈
Q1. LangGraph의 StateGraph에서 상태(State)의 역할은?
노드 간 공유되는 데이터 컨테이너. TypedDict로 정의하며, 각 노드는 상태를 읽고 업데이트. Annotated reducer로 리스트 누적 등의 병합 전략 정의 가능.
Q2. add_conditional_edges의 용도는?
조건부 분기. 현재 상태에 따라 다음 노드를 동적으로 결정. 라우팅 함수가 상태를 받아 다음 노드 이름을 반환.
Q3. interrupt()의 역할은?
Human-in-the-Loop 구현. 워크플로우 실행을 중단하고, 인간의 입력(승인/거부/수정)을 기다린 후 Command(resume=...)로 재개.
Q4. 멀티에이전트에서 Supervisor 패턴의 장점은?
중앙 Supervisor가 전체 흐름을 제어하여 (1) 각 에이전트의 전문성 활용 (2) 작업 순서 동적 결정 (3) 실행 후 다음 단계 판단 가능.
Q5. 체크포인터(Checkpointer)를 사용하는 이유는?
(1) 대화 지속 — thread_id별 상태 저장 (2) 장애 복구 — 중단점에서 재개 (3) Human-in-the-Loop — interrupt 후 상태 보존.
Q6. LangGraph에서 병렬 실행은 어떻게 구현하는가?
같은 소스 노드(예: START)에서 여러 노드로 엣지를 연결하면 자동으로 병렬 실행. 결과를 합치는 노드에서 모든 선행 노드가 완료될 때까지 대기.
Q7. create_react_agent와 직접 StateGraph 구성의 차이는?
create_react_agent는 ReAct(Reasoning + Acting) 패턴의 프리빌트. 단순 도구 사용 에이전트에 적합. 복잡한 분기/멀티에이전트/커스텀 상태가 필요하면 직접 StateGraph 구성.