LangGraph vs CrewAI: 에이전트가 3개를 넘어서면 상태 머신 오케스트레이션으로 대응하라
싱글 에이전트는 간단하다. 프롬프트를 넣고, 결과를 받는다. 두 번째 에이전트를 붙여도 여전히 관리 가능하다. 그러나 에이전트가 세 개를 넘어서는 순간, 전혀 다른 문제들이 등장한다. 상태는 핸드오프마다 유실되고, 나이브한 구현에서 토큰 비용은 에이전트 수에 따라 O(n²)로 불어나며, 첫 에이전트의 오류 하나가 파이프라인 전체를 오염시킨다. 2025년은 업계가 "에이전트 운영화(operationalization)"를 본격적으로 마주한 해였다 — arXiv 논문(2601.13671)에 따르면 400개 이상의 기업이 멀티에이전트 시스템을 프로덕션에서 운영하고 있다.
LangGraph의 방향 그래프 기반 상태 머신과 CrewAI의 역할 기반 팀 모델은 근본적으로 다른 철학에서 출발하며, 어느 쪽을 선택하느냐가 시스템의 복잡도, 유지보수성, 비용을 결정한다. 각 프레임워크의 작동 원리를 코드 예시와 함께 이해하고, 어떤 상황에서 무엇을 골라야 하는지 판단 기준을 제시한다.
LangGraph vs CrewAI 핵심 차이 30초 요약
LangGraph CrewAI 비유 상태 다이어그램 팀 조직도 설계 단위 노드(함수) + 엣지(조건) 에이전트(역할) + 태스크 상태 관리 TypedDict + reducer로 명시적 context 파라미터로 선언적 분기/루프 조건부 엣지로 자유롭게 Process.hierarchical로 제한적 Human-in-the-loop interrupt_before/after 네이티브 지원 네이티브 지원 없음 강점 복잡한 조건 분기, 사람 승인, 루프 역할 명확한 팀 협업 구조 학습 곡선 중상 (그래프 개념 이해 필요) 낮음 (직관적 역할 추상화)
핵심 개념
멀티에이전트 오케스트레이션이란
멀티에이전트 오케스트레이션은 여러 AI 에이전트가 협력하여 복잡한 태스크를 처리하도록 조율하는 아키텍처 패턴이다. 단일 에이전트가 처리하기 어려운 태스크(복잡한 추론, 전문 도메인 분리, 병렬 처리)를 여러 에이전트로 분산한다.
조율 방식은 크게 세 가지로 분류된다:
| 방식 | 설명 | 적합한 상황 |
|---|---|---|
| Supervisor | 중앙 에이전트가 하위 에이전트에 위임 | 역할이 명확히 분리된 경우 |
| Peer-to-Peer | 에이전트가 서로 직접 통신 | 동적 협업이 필요한 경우 |
| 이벤트 기반 | 메시지 버스(Kafka 등)를 통한 느슨한 결합 | 대규모 분산 시스템 |
에이전트 3개 초과가 만들어내는 네 가지 문제
단순해 보이는 에이전트 추가가 왜 갑자기 복잡해지는가. 에이전트 수 n이 3을 넘어서는 시점에 네 가지 문제가 동시에 터진다.
1. 상태 전파 문제 — 에이전트 A가 수집한 컨텍스트를 B, C, D에 어떻게 넘길 것인가. 전체 대화 이력을 그대로 넘기면 토큰이 폭발하고, 필요한 청크만 주입하면 정보 유실이 생긴다.
2. 토큰 비용 증가 — 각 에이전트에 전체 이력을 그대로 전달하는 나이브한 구현에서는 비용이 O(n²)로 증가한다. 각 에이전트에 요약본만 넘기면 O(n)으로 줄일 수 있지만, 의도적인 설계 없이는 대부분 최악의 경우로 흘러간다.
3. 연쇄 실패(Cascade Failure) — 업스트림 에이전트의 오류나 환각이 검증 없이 다운스트림으로 전파된다. 팩트체크 에이전트 없이 리서치 결과를 바로 작성 에이전트에 넘기면, 잘못된 정보가 최종 출력에 그대로 포함된다.
4. 조율 복잡도 — 에이전트가 늘수록 "누가 언제 실행되는가"를 명확히 정의해야 한다. 암묵적 순서에 의존하면 병렬 실행 시 레이스 컨디션이 발생한다.
Cascade Failure: 하나의 컴포넌트 실패가 연쇄적으로 다운스트림 컴포넌트를 실패시키는 현상. 마이크로서비스 아키텍처에서도 동일한 패턴이 나타난다.
LangGraph: 방향 그래프 기반 상태 머신
LangGraph는 워크플로우를 수학적 방향 그래프(DAG 또는 Cyclic Graph)로 모델링한다. DAG(Directed Acyclic Graph)는 루프 없는 방향 그래프, Cyclic Graph는 루프가 있는 그래프다 — 재시도나 사람 승인 후 재실행 같은 워크플로우에서는 사이클이 필수다.
핵심 개념 세 가지만 이해하면 된다:
- 노드(Node): 에이전트 또는 로직 함수
- 엣지(Edge): 상태 전이 조건 (
"approve"→END,"reject"→"code_review") - 상태(State):
TypedDict로 스키마를 정의하고,Annotatedreducer로 불변 관리
TypedDict는 Python 딕셔너리에 타입 힌트를 붙이는 표준 방법이다. Annotated[list[str], operator.add]는 "이 필드가 업데이트될 때 덮어쓰지 말고 리스트를 이어붙여라"는 reducer를 선언하는 문법이다 — 핸드오프 시 이전 에이전트의 결과가 사라지지 않도록 보장한다.
상태가 모든 에이전트를 관통하는 단일 진실의 원천(Single Source of Truth)이 된다는 점이 핵심이다. 각 노드는 상태의 일부를 읽고, 변경분(diff)만 반환한다.
LangGraph의 중요한 차별점 중 하나가 Human-in-the-loop 지원이다. interrupt_before 또는 interrupt_after 파라미터로 특정 노드 실행 전후에 워크플로우를 일시정지하고 사람의 검토를 받은 뒤 재개할 수 있다. 보안 스캔 결과를 사람이 직접 확인한 뒤 배포를 진행하는 워크플로우가 대표적인 예다. CrewAI에는 이에 해당하는 네이티브 기능이 없다.
Reducer: 현재 상태와 새로운 값을 받아 다음 상태를 반환하는 순수 함수.
operator.add를 reducer로 지정하면 리스트가 덮어쓰기 대신 누적된다.
CrewAI: 역할 기반 팀 오케스트레이션
CrewAI는 다른 철학에서 출발한다. 에이전트를 "디지털 팀원"으로 모델링한다. 각 에이전트는 세 가지 속성을 갖는다:
- role: 이 에이전트의 직함 ("기술 리서처", "팩트 체커")
- goal: 이 에이전트가 달성해야 할 목표
- backstory: 에이전트의 전문성과 행동 방식을 정의하는 배경 스토리
태스크 간 핸드오프는 context=[이전_태스크] 파라미터로 선언적으로 처리된다. allow_delegation=False는 중요한 옵션이다 — 이를 켜두면 에이전트가 작업을 다른 에이전트에게 재위임하면서 예상치 못한 태스크 체인이 생기고, 어느 에이전트가 실제로 작업을 수행했는지 추적하기 어려워진다.
실행 순서는 Process.sequential (순차) 또는 Process.hierarchical (Manager LLM이 위임) 중 선택한다.
실전 적용
예시 1: LangGraph로 코드 리뷰 파이프라인 구축
코드 리뷰 → 테스트 분석 → 보안 스캔 → 조건부 승인/재검토 루프를 구현한다. 보안 이슈가 감지되면 코드 리뷰 단계로 되돌아가는 사이클이 핵심이다.
주의: 아래 코드는 LangGraph의 상태 흐름을 보여주기 위한 예시다. LLM 호출은 생략하고 하드코딩된 반환값을 사용한다. 프로덕션 코드에서는
from langchain_openai import ChatOpenAI를 import하고 각 노드에서llm.invoke(...)호출로 대체한다.
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
import operator
# 1. 공유 상태 스키마 — 모든 에이전트가 이 타입을 공유한다
class ReviewState(TypedDict):
code: str
review_result: str
test_result: str
security_result: str
errors: Annotated[list[str], operator.add] # reducer: 덮어쓰기 대신 누적
approved: bool
iteration: int
# 2. 에이전트 노드 — 순수 함수, 상태 diff만 반환
def code_review_agent(state: ReviewState) -> dict:
# 실제 구현: llm.invoke(f"다음 코드를 리뷰하세요: {state['code']}")
return {
"review_result": "구조 개선 필요: 의존성 주입 패턴 미적용",
"iteration": state["iteration"] + 1
}
def test_agent(state: ReviewState) -> dict:
return {"test_result": "커버리지 78% — 경계값 테스트 누락"}
def security_scan_agent(state: ReviewState) -> dict:
return {"security_result": "SQL Injection 취약점 감지 (line 42)"}
# 3. 조건부 엣지 함수 — 다음 노드를 결정
def approval_gate(
state: ReviewState,
) -> Literal["approve", "reject", "reject_final"]:
has_vulnerability = "취약점" in state["security_result"]
iteration_limit_reached = state["iteration"] >= 3
if has_vulnerability and iteration_limit_reached:
# 최대 반복 도달 시 취약점 있는 코드를 승인하지 않고 실패 종료
# 프로덕션에서는 알림 발송 + 수동 검토 요청으로 연결
return "reject_final"
if has_vulnerability:
return "reject"
return "approve"
# 4. 그래프 조립
builder = StateGraph(ReviewState)
builder.add_node("code_review", code_review_agent)
builder.add_node("test", test_agent)
builder.add_node("security_scan", security_scan_agent)
builder.set_entry_point("code_review")
builder.add_edge("code_review", "test")
builder.add_edge("test", "security_scan")
builder.add_conditional_edges(
"security_scan",
approval_gate,
{
"approve": END,
"reject": "code_review", # 실패 시 첫 단계로 루프백
"reject_final": END # 반복 초과 시 실패 종료 (취약점 있는 상태로 승인 금지)
}
)
# 5. 체크포인트 — 중간 실패 시 해당 노드에서 재시작 가능
# Human-in-the-loop 활성화 예시:
# graph = builder.compile(checkpointer=checkpointer, interrupt_before=["security_scan"])
# → 보안 스캔 전에 실행이 멈추고, 사람이 직접 검토 후 graph.invoke()로 재개 가능
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# 실행
config = {"configurable": {"thread_id": "review-001"}}
result = graph.invoke(
{"code": "...", "errors": [], "iteration": 0, "approved": False},
config=config
)| 코드 포인트 | 역할 |
|---|---|
Annotated[list[str], operator.add] |
errors 리스트를 덮어쓰지 않고 누적 — 핸드오프 시 정보 유실 방지 |
add_conditional_edges |
보안 스캔 결과에 따라 다음 노드를 동적으로 결정 |
MemorySaver |
각 스텝의 상태를 저장 — 중간 실패 시 해당 체크포인트부터 재시작 |
reject_final 종료 노드 |
최대 반복 도달 시 취약점 있는 코드를 승인하지 않고 실패 종료 |
interrupt_before (주석) |
Human-in-the-loop: 특정 노드 전에 실행을 멈추고 사람 검토 후 재개 |
예시 2: CrewAI로 콘텐츠 생성 파이프라인 구축
리서처 → 팩트체커 → 작성자 → 편집자의 4단계 파이프라인이다. 각 에이전트가 자신의 역할에만 집중하고, context로 이전 결과를 자동으로 주입받는다.
from crewai import Agent, Task, Crew, Process
# 1. 역할 기반 에이전트 정의
researcher = Agent(
role="기술 리서처",
goal="멀티에이전트 오케스트레이션 최신 트렌드와 실제 사례를 수집한다",
backstory=(
"10년 경력의 기술 저널리스트. arXiv, GitHub, 공식 문서만 신뢰하며, "
"마케팅 블로그 내용을 그대로 인용하지 않는다."
),
allow_delegation=False, # 켜두면 에이전트가 작업을 재위임해 예상치 못한 태스크 체인이 생긴다
verbose=True
)
fact_checker = Agent(
role="팩트 체커",
goal="리서치 결과의 정확성을 검증하고 수치·인용 오류를 제거한다",
backstory="전직 학술 연구원. 출처 없는 주장은 모두 의심하고 검증을 요구한다.",
allow_delegation=False
)
writer = Agent(
role="기술 작가",
goal="검증된 정보를 개발자 친화적인 마크다운 블로그 글로 변환한다",
backstory="오픈소스 커뮤니티에서 활동하는 5년 경력의 기술 블로거.",
allow_delegation=False
)
editor = Agent(
role="편집자",
goal="초안의 가독성, 논리 흐름, 문체를 개선해 최종 출판 품질로 다듬는다",
backstory="기술 미디어 10년 경력의 시니어 에디터. 독자 친화성을 최우선으로 본다.",
allow_delegation=False
)
# 2. 태스크 정의 — context가 핸드오프를 처리한다
research_task = Task(
description="LangGraph와 CrewAI의 멀티에이전트 오케스트레이션 패턴 조사",
expected_output="핵심 개념, 실제 사례, 장단점, 참고 자료 목록",
agent=researcher
)
fact_check_task = Task(
description="리서치 결과의 수치와 사실 관계 검증. 오류 발견 시 수정사항 명시",
expected_output="검증된 사실 목록과 수정된 오류 내역",
agent=fact_checker,
context=[research_task] # 리서처 결과를 컨텍스트로 자동 주입
)
write_task = Task(
description="검증된 정보로 2000자 이상의 기술 블로그 초안 작성 (마크다운)",
expected_output="제목, 도입, 핵심 개념, 실전 예시, 결론을 포함한 마크다운 글",
agent=writer,
context=[research_task, fact_check_task] # 두 태스크 결과 모두 주입
)
edit_task = Task(
description="작성된 초안을 검토하여 가독성, 논리 흐름, 표현을 최종 수준으로 개선",
expected_output="편집 완료된 최종 마크다운 글",
agent=editor,
context=[write_task]
)
# 3. Crew 조립 및 실행
crew = Crew(
agents=[researcher, fact_checker, writer, editor],
tasks=[research_task, fact_check_task, write_task, edit_task],
process=Process.sequential, # 순차 실행
verbose=True
)
result = crew.kickoff()
print(result.raw)예시 3: Supervisor 패턴 — LangGraph와 CrewAI 비교
트리아지 에이전트가 의도를 분류하고 전문 에이전트에 위임하는 패턴을 두 프레임워크로 각각 구현한다.
LangGraph — 조건부 엣지로 분기:
def triage_agent(state: dict) -> dict:
intent = classify_intent(state["query"]) # "payment" | "shipping" | "general"
return {"intent": intent}
def route_by_intent(state: dict) -> str:
return state["intent"] # 반환값이 다음 노드 이름
builder.add_node("triage", triage_agent)
builder.add_node("payment_agent", payment_agent)
builder.add_node("shipping_agent", shipping_agent)
builder.add_node("general_agent", general_agent)
builder.set_entry_point("triage")
builder.add_conditional_edges(
"triage",
route_by_intent,
{
"payment": "payment_agent",
"shipping": "shipping_agent",
"general": "general_agent"
}
)CrewAI — Process.hierarchical로 Manager 위임:
from crewai import Agent, Task, Crew, Process
payment_agent = Agent(
role="결제 전문가",
goal="결제 관련 고객 문의를 처리한다",
backstory="결제 시스템 5년 경력. 환불, 결제 오류, 카드 문제를 전문으로 한다.",
allow_delegation=False
)
shipping_agent = Agent(
role="배송 전문가",
goal="배송 추적과 배송 관련 문의를 처리한다",
backstory="물류 시스템 전문가. 배송 지연, 분실, 주소 변경을 담당한다.",
allow_delegation=False
)
manager = Agent(
role="고객 서비스 매니저",
goal="고객 문의를 분석하고 적절한 전문가에게 위임한다",
backstory="고객 서비스 총괄. 모든 문의를 접수하고 전문팀에 배분한다.",
)
handle_query_task = Task(
description="고객 문의를 처리하고 완전한 답변을 제공한다",
expected_output="고객 문의에 대한 완전하고 정확한 답변",
agent=manager
)
crew = Crew(
agents=[payment_agent, shipping_agent],
tasks=[handle_query_task],
process=Process.hierarchical, # Manager LLM이 자동으로 적절한 에이전트에 위임
manager_agent=manager,
verbose=True
)LangGraph는 분기 조건을 코드로 명시하므로 예측 가능하고 테스트하기 쉽다. CrewAI의 Process.hierarchical은 Manager LLM이 자연어로 판단하므로 설정이 간단하지만 위임 결정이 LLM에 의존한다는 트레이드오프가 있다.
장단점 분석
장점
| 항목 | LangGraph | CrewAI |
|---|---|---|
| 상태 관리 | TypedDict + reducer로 타입 안전한 불변 상태 | context 파라미터로 선언적 핸드오프 |
| 복잡한 분기 | 조건부 엣지로 임의의 분기·루프 표현 | Process.hierarchical로 Manager 위임 |
| 재시작/복구 | 체크포인트 기반 중간 재시작, 타임트래블 디버깅 | 태스크 단위 재시도 |
| Human-in-the-loop | interrupt_before/after로 네이티브 지원 |
네이티브 지원 없음 |
| 병렬 실행 | Send() API로 팬아웃 지원 |
Crew 레벨 병렬 처리 |
| 학습 곡선 | 그래프 개념 이해 필요 | 역할 기반 추상화로 직관적 |
| 프로덕션 사례 | LinkedIn, Uber 등 400개+ 기업 (arXiv 2601.13671) | 역할 기반 파이프라인으로 광범위하게 활용 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| LangGraph 학습 곡선 | 그래프 설계와 상태 reducer 개념 이해 선행 필요 | 공식 튜토리얼의 simple graph → 복잡 예시 순서로 학습 |
| CrewAI 직접 통신 불가 | Manager↔Worker 계층 외 에이전트 간 직접 통신 없음 | 중간 결과를 공유 스토리지(파일/DB)에 저장하는 우회 |
| 토큰 비용 폭발 | 나이브한 구현에서 핸드오프마다 컨텍스트 누적으로 O(n²) 증가 | 각 에이전트에 필요한 청크만 주입, 요약 에이전트 삽입으로 O(n)으로 감소 |
| 연쇄 실패 | 업스트림 오류가 다운스트림 전체에 전파 | retry 로직, fallback 노드, 팩트체크 에이전트 삽입 |
| 관찰가능성 부재 | 어느 에이전트에서 무슨 일이 일어났는지 추적 불가 | LangSmith 또는 LangFuse로 모든 에이전트 호출 추적 |
| JS/TS 지원 | LangGraph.js 존재 / CrewAI는 Python 전용 | JS/TS 스택에서는 LangGraph.js 또는 AutoGen 고려 |
관찰가능성(Observability): 시스템 내부 상태를 외부에서 관측할 수 있는 능력. 멀티에이전트에서는 각 에이전트의 입력·출력·지연시간·토큰 사용량을 추적하는 것을 의미한다.
타임트래블 디버깅: LangGraph 체크포인터 기능으로, 과거의 특정 상태로 되돌아가 다른 경로를 실행해볼 수 있다. 에이전트 행동 디버깅에 강력한 도구다.
실무에서 가장 흔한 실수
- 상태 스키마 설계 후순위화 — 에이전트를 먼저 만들고 나중에 상태를 연결하려 하면, 핸드오프마다 컨텍스트 유실이 발생한다. 공유 상태 스키마를 가장 먼저 설계하라.
- 무한 루프 방지 누락 — LangGraph에서 조건부 엣지로 루프를 만들 때 iteration 카운터나 최대 재시도 수 없이 설계하면, 특정 조건에서 파이프라인이 영원히 순환한다. 항상 탈출 조건과 실패 종료 노드를 명시하라.
- 관찰가능성 없이 프로덕션 배포 — 로컬에서는 verbose 출력으로 디버깅이 가능하지만, 프로덕션에서는 LangSmith·LangFuse 없이 어느 에이전트가 실패했는지, 얼마나 비용이 들었는지 알 수 없다. 관찰가능성은 기능 개발보다 먼저 갖춰야 한다.
마치며
에이전트 오케스트레이션에서 프레임워크 선택보다 중요한 것은 세 가지 원칙이다: 첫째, 상태 설계 — 에이전트 간 공유 상태 스키마를 먼저 정의하라. 둘째, 실패 격리 — 개별 에이전트 실패가 전체 파이프라인을 죽이지 않도록 retry/fallback 노드를 설계하라. 셋째, 관찰가능성 — 모든 에이전트 호출을 추적할 수 있는 체계를 갖춘 뒤 프로덕션으로 넘어가라. LangGraph는 복잡한 조건 분기, 루프, 사람 승인이 필요한 워크플로우에 강하고, CrewAI는 역할이 명확하게 분리된 팀 협업 구조를 직관적으로 모델링할 때 빛난다.
지금 바로 시작할 수 있는 3단계:
- LangGraph 기초 실습:
pip install langgraph후 아래 최소 예시를 그대로 실행하는 것이 출발점이다. 2-노드 그래프로 상태 흐름을 직접 확인하자: from langgraph.graph import StateGraph, END from typing import TypedDict class State(TypedDict): value: str def node_a(state): return {"value": "A 처리됨"} def node_b(state): return {"value": state["value"] + " → B 처리됨"} g = StateGraph(State) g.add_node("a", node_a) g.add_node("b", node_b) g.set_entry_point("a") g.add_edge("a", "b") g.add_edge("b", END) print(g.compile().invoke({"value": ""})) # 출력: {'value': 'A 처리됨 → B 처리됨'}- CrewAI 역할 설계 연습:
pip install crewai후 현재 팀이 수동으로 처리하는 3단계 이상의 프로세스를 찾아researcher → reviewer → writer패턴으로 Agent와 Task를 정의해crew.kickoff()를 실행한다. - 관찰가능성 연결: 어느 프레임워크를 쓰든 LangSmith를 먼저 붙이고 에이전트 호출 트레이스를 확인한 뒤 프로덕션으로 넘어간다. 두 환경변수를 함께 설정해야 트레이싱이 활성화된다:
export LANGCHAIN_API_KEY="your-api-key" export LANGCHAIN_TRACING_V2="true"
다음 글: 에이전트 파이프라인의 토큰 비용을 절반으로 줄이는 컨텍스트 압축 전략 — 요약 에이전트, 청크 주입, 프롬프트 캐싱의 실전 비교.
참고 자료
- LangGraph vs CrewAI vs AutoGen: Complete Guide 2026 | DEV Community
- CrewAI vs LangGraph vs AutoGen | DataCamp
- Mastering LangGraph State Management in 2025 | Sparkco
- LangGraph Review: Agentic State Machine 2025 | Sider AI
- CrewAI Process Types | DeepWiki
- Choosing the right orchestration pattern for multi-agent systems | Kore.ai
- The Orchestration of Multi-Agent Systems: Architectures, Protocols, and Enterprise Adoption | arXiv
- Multi-Agent Coordination Patterns | Claude Blog
- AI Agent Orchestration Patterns | Azure Architecture Center
- Multi-Agent Collaboration via Evolving Orchestration | arXiv
- Production Multi-Agent System with LangGraph | Markaicode
- Four Design Patterns for Event-Driven Multi-Agent Systems | Confluent
- Unlocking exponential value with AI agent orchestration | Deloitte