포스트

멀티 에이전트는 “대화”가 아니라 “오케스트레이션”이다: LangGraph vs AutoGen vs CrewAI 실전 비교와 구현 가이드

2026년 기준으로 AI Agent 개발에서 가장 어려운 부분은 “LLM 호출”이 아니라 (1) 여러 단계/여러 역할의 작업을 어떻게 안정적으로 연결할지, (2) 실패했을 때 어디서부터 재개할지, (3) 디버깅/관측 가능성을 어떻게 확보할지입니다.

멀티 에이전트는 “대화”가 아니라 “오케스트레이션”이다: LangGraph vs AutoGen vs CrewAI 실전 비교와 구현 가이드

들어가며

2026년 기준으로 AI Agent 개발에서 가장 어려운 부분은 “LLM 호출”이 아니라 (1) 여러 단계/여러 역할의 작업을 어떻게 안정적으로 연결할지, (2) 실패했을 때 어디서부터 재개할지, (3) 디버깅/관측 가능성을 어떻게 확보할지입니다. 이 관점에서 LangGraph, AutoGen, CrewAI는 같은 “멀티 에이전트” 카테고리처럼 보이지만, 실제로는 서로 다른 문제를 최적화합니다.1

  • 언제 쓰면 좋은가
    • 다단계 워크플로우(리서치→증거검증→작성→리뷰→배포)에서 상태(state), 재시도(retry), 체크포인트(checkpoint), human-in-the-loop가 필요한 경우: LangGraph가 강점.1
    • “에이전트 간 토론/협상” 자체가 제품 기능이고, 자유로운 상호대화가 중요한 경우: AutoGen 계열(다만 MS는 상위 프레임워크로 방향 전환 흐름이 있어 마이그레이션 비용을 고려).2
    • 역할 기반(Role/Goal/Task)으로 빠르게 팀(crew)을 구성해 프로토타입→업무 자동화까지 빨리 가고 싶은 경우: CrewAI가 생산성이 좋음.3
  • 언제 쓰면 안 되는가
    • 단순한 1~2단계 자동화는 멀티 에이전트가 오히려 토큰 오버헤드/비결정성/디버깅 비용만 늘립니다(“Router/Workflow 패턴”이 더 낫다는 현장 의견도 많음).4
    • 컴플라이언스/감사 추적이 중요한데, 실행 경로가 대화 흐름에 과도하게 의존하면(특히 AutoGen식 자유 대화) 재현성과 통제가 어려워집니다.5

🔧 핵심 개념

1) 프레임워크 철학 차이(“멀티 에이전트”를 바라보는 모델)

  • LangGraph = stateful directed graph(상태를 가진 그래프/상태 머신)
    • 노드(node)=작업 단계(LLM 호출/툴 실행/검증/승인)
    • 엣지(edge)=전이(조건 분기, 루프, 실패 시 재시도, 사람 승인 지점)
    • 핵심은 “에이전트”가 아니라 오케스트레이션 런타임입니다. 즉, 멀티 에이전트도 결국 그래프 상의 여러 노드로 모델링합니다.6
  • CrewAI = role-based crew orchestration(조직도/역할 기반)
    • Agent(역할/목표/툴) + Task(업무) + Process(순차/계층)
    • “PM이 업무를 쪼개 팀원에게 할당”하는 메타포가 강해서 빠르게 구성 가능.
    • 대신 복잡한 분기/중단/재개/정교한 상태 통제는 LangGraph보다 설계 자유도가 떨어질 수 있음(추상화가 높은 만큼 내부 제어를 포기).3
  • AutoGen = conversational collaboration(대화 기반 협업)
    • 여러 에이전트가 대화를 주고받으며 합의를 수렴하는 구조가 강점.
    • 단, 2026년 관점에서 “AutoGen을 신규 프로덕션에 채택하면 향후 마이그레이션 계획이 필요”하다는 경고가 반복적으로 보임(MS가 상위 프레임워크로 무게중심 이동).2
    • AutoGen 0.4는 2025년 1월 릴리스로 문서/논문 기반 설계가 정리돼 있음.7

2) 내부 작동 방식: “상태/메모리/재개”가 승패를 가른다

실무에서 멀티 에이전트가 실패하는 지점은 대체로 여기입니다.

  • State(불변 로그 + 가변 스냅샷)
    • LangGraph는 “Typed State + 체크포인트 + 인터럽트” 같은 장치를 통해 중간 결과를 구조화하고, “실패한 노드부터 재개”를 가능하게 설계합니다.8
  • Observability(추적/리플레이/디버깅)
    • 프로덕션에서 중요한 건 “성공률”보다 50번째 실패를 얼마나 빨리 디버깅하느냐입니다(비용은 실행 비용보다 디버깅/재실행에서 터짐).9
  • Abstraction tax
    • CrewAI처럼 높은 추상화는 “빠른 개발”을 주지만, 복잡한 예외 처리/재개/부분 재실행 요구가 생기면 프레임워크가 허용하는 형태로 문제를 다시 모델링해야 합니다.3

💻 실전 코드

현실적인 시나리오: “릴리즈 노트 기반 기술 블로그 초안 생성 파이프라인”
요구사항: 1) URL 목록에서 문서를 수집(툴)
2) 문서별 핵심 변경점 추출(LLM)
3) 상충/근거 부족 시 재검색(루프)
4) 최종 글 작성(LLM)
5) 사람 승인(human-in-the-loop) 후 저장

여기서는 LangGraph로 멀티 에이전트를 ‘그래프’로 구현합니다. (CrewAI/AutoGen은 아래 “판단 기준”에서 대안 제시)

0) 의존성/환경

1
2
3
4
python -m venv .venv
source .venv/bin/activate
pip install langgraph langchain openai pydantic requests beautifulsoup4
export OPENAI_API_KEY="YOUR_KEY"

1) 그래프 상태 정의 + 툴(문서 수집) + 노드(역할별 에이전트)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
from typing import TypedDict, List, Optional, Dict, Any
import requests
from bs4 import BeautifulSoup
from pydantic import BaseModel, Field

from langchain.chat_models import ChatOpenAI
from langchain.schema import SystemMessage, HumanMessage
from langgraph.graph import StateGraph, END

# ---- 1) State ----
class Doc(BaseModel):
    url: str
    text: str

class Finding(BaseModel):
    url: str
    bullets: List[str] = Field(default_factory=list)
    confidence: float = 0.0

class BlogState(TypedDict):
    topic: str
    urls: List[str]
    docs: List[Dict[str, Any]]          # serialized Doc
    findings: List[Dict[str, Any]]      # serialized Finding
    gaps: List[str]                     # "need more evidence about X"
    draft: Optional[str]
    approved: bool

llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0.2)

# ---- 2) Tool: fetch and clean web page text ----
def fetch_url_text(url: str) -> str:
    r = requests.get(url, timeout=20, headers={"User-Agent": "agent-blog-bot/1.0"})
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    for tag in soup(["script", "style", "noscript"]):
        tag.decompose()
    text = " ".join(soup.get_text(" ").split())
    return text[:20000]  # keep bounded

# ---- 3) Nodes ----
def collect_docs(state: BlogState) -> BlogState:
    docs = []
    for url in state["urls"]:
        try:
            docs.append(Doc(url=url, text=fetch_url_text(url)).model_dump())
        except Exception as e:
            # production: record error into state for observability
            docs.append(Doc(url=url, text=f"[FETCH_ERROR] {e}").model_dump())
    return {**state, "docs": docs}

def extract_findings(state: BlogState) -> BlogState:
    findings = []
    gaps = []
    for d in state["docs"]:
        url = d["url"]
        text = d["text"]
        msg = [
            SystemMessage(content=
                "You are a senior engineer. Extract concrete, verifiable release changes / API changes / migration notes. "
                "Return bullets and a confidence 0-1. If text is error or insufficient, set confidence low and describe gap."
            ),
            HumanMessage(content=f"URL: {url}\nCONTENT:\n{text}")
        ]
        out = llm.invoke(msg).content

        # pragmatic parse: in production use structured output / JSON schema
        conf = 0.3
        if "confidence" in out.lower():
            conf = 0.6  # placeholder heuristic

        bullets = [line.strip("- ").strip() for line in out.split("\n") if line.strip().startswith("-")]
        findings.append(Finding(url=url, bullets=bullets[:10], confidence=conf).model_dump())

        if conf < 0.5 or len(bullets) < 3:
            gaps.append(f"{url}: insufficient evidence or parsing failed")
    return {**state, "findings": findings, "gaps": gaps}

def decide_research_loop(state: BlogState) -> str:
    # if gaps exist, loop once to request more URLs (in real system: call search API/tool)
    return "rewrite_urls" if state["gaps"] else "write_draft"

def rewrite_urls(state: BlogState) -> BlogState:
    # In production: replace with actual search tool. Here we simulate: keep existing.
    # The key is the "loop" capability with state.
    return {**state, "gaps": []}

def write_draft(state: BlogState) -> BlogState:
    msg = [
        SystemMessage(content=
            "Write a Korean technical deep-dive blog post draft (2000-3000 chars). "
            "Compare LangGraph, AutoGen, CrewAI for multi-agent implementation. "
            "Must include trade-offs, production tips, and actionable criteria."
        ),
        HumanMessage(content=
            f"TOPIC: {state['topic']}\n"
            f"FINDINGS:\n{state['findings']}\n"
            "Constraints: no greetings, no front matter, no markdown # title."
        )
    ]
    draft = llm.invoke(msg).content
    return {**state, "draft": draft}

def human_approve(state: BlogState) -> BlogState:
    # Human-in-the-loop gate (CLI)
    print("\n=== DRAFT ===\n")
    print(state["draft"][:2000])
    ans = input("\nApprove? (y/n): ").strip().lower()
    return {**state, "approved": (ans == "y")}

def persist(state: BlogState) -> BlogState:
    if not state["approved"]:
        return state
    with open("post_draft.md", "w", encoding="utf-8") as f:
        f.write(state["draft"] or "")
    return state

# ---- 4) Graph ----
g = StateGraph(BlogState)
g.add_node("collect_docs", collect_docs)
g.add_node("extract_findings", extract_findings)
g.add_node("rewrite_urls", rewrite_urls)
g.add_node("write_draft", write_draft)
g.add_node("human_approve", human_approve)
g.add_node("persist", persist)

g.set_entry_point("collect_docs")
g.add_edge("collect_docs", "extract_findings")
g.add_conditional_edges("extract_findings", decide_research_loop, {
    "rewrite_urls": "rewrite_urls",
    "write_draft": "write_draft",
})
g.add_edge("rewrite_urls", "collect_docs")
g.add_edge("write_draft", "human_approve")
g.add_edge("human_approve", "persist")
g.add_edge("persist", END)

app = g.compile()

if __name__ == "__main__":
    init: BlogState = {
        "topic": "2026년 8월 AI Agent 개발 방법: LangGraph vs AutoGen vs CrewAI",
        "urls": [
            # 실전에서는 릴리즈 노트/공식 문서/레포 README를 넣으세요
            "https://github.com/langchain-ai/langgraph",
            "https://github.com/crewAIInc/crewAI",
            "https://github.com/microsoft/autogen",
        ],
        "docs": [],
        "findings": [],
        "gaps": [],
        "draft": None,
        "approved": False
    }
    final_state = app.invoke(init)
    print("\nDONE. approved=", final_state["approved"])

예상 출력(요지)

  • DRAFT가 콘솔에 출력되고 Approve? (y/n)에서 승인 시 post_draft.md 저장
  • 핵심은 “extract_findings → gaps 있으면 루프 → write_draft → 승인 게이트”가 코드로 명시된다는 점입니다. LangGraph가 강한 이유가 여기(조건 분기/루프/중단/재개를 “대화”가 아니라 “구조”로 고정)입니다.8

⚡ 실전 팁 & 함정

Best Practice (추천 3가지)

1) State를 “디버깅 가능한 스키마”로 설계

  • findings, evidence, tool_result, error를 state에 남겨야 “왜 실패했는지”가 보입니다.
  • 특히 멀티 에이전트는 실패 원인이 LLM이 아니라 툴/데이터/파싱/타임아웃인 경우가 많습니다.10

2) Human-in-the-loop를 ‘마지막’이 아니라 ‘중간’에도 둬라

  • 정책/보안/배포 단계는 최종 승인만으로 부족합니다.
  • LangGraph는 인터럽트/체크포인트 기반 패턴이 강점으로 반복 언급됩니다.8

3) “에이전트 수”가 아니라 “실패 시 재개 단위”로 설계

  • 역할을 10개로 쪼개는 것보다, 실패 지점을 기준으로 단계화하는 게 비용과 안정성에 직결됩니다.
  • 추상화가 높은 프레임워크(CrewAI)일수록 이 관점이 약해지기 쉬움.3

흔한 함정/안티패턴

  • 대화 로그를 상태로 착각
    • AutoGen 스타일에서 “대화가 곧 상태”가 되면 재현성이 떨어집니다. 운영에서 필요한 건 “대화”가 아니라 “결정 근거와 전이 조건”입니다.5
  • 구조화 출력(Structured Output) 없이 파싱 지옥
    • 실무에선 JSON schema/typed output 없으면 작은 프롬프트 변경에 파서가 깨집니다.
  • 멀티 에이전트를 만능으로 사용
    • 단순 업무에선 Router/Workflow가 더 싸고 빠르다는 경험적 주장도 많습니다(“대부분은 오케스트레이션이 핵심”).4

비용/성능/안정성 트레이드오프

  • CrewAI: 빠른 생산성 ↔ 런타임/토큰 오버헤드 및 내부 제어 한계가 문제될 수 있음(특히 복잡한 예외/재개 요구).3
  • LangGraph: 제어/재개/관측성 ↔ 초기 설계 비용(그래프/상태 설계)이 큼.1
  • AutoGen: 대화형 협업에 강함 ↔ 2026년 기준 생태계/방향성 변화(상위 프레임워크로 이동) 리스크를 고려.2

🚀 마무리

핵심 정리:

  • LangGraph는 “멀티 에이전트”라기보다 장기 실행/재개 가능한 상태 머신 런타임에 가깝고, 프로덕션 워크플로우(분기/루프/승인/체크포인트)에 가장 잘 맞습니다.1
  • CrewAI는 역할 기반 팀을 빠르게 세우는 데 최적이며, “조직도 같은 업무 분담”이 명확한 자동화에 강합니다.3
  • AutoGen은 대화/토론형 멀티 에이전트에 강하지만, 2026년 시점에는 중장기 유지/마이그레이션 전략을 같이 가져가야 합니다.2

도입 판단 기준(실무용): 1) “실패 후 재개”가 핵심이면 → LangGraph 우선 2) “역할 분담이 명확한 업무 자동화”면 → CrewAI 우선 3) “에이전트 간 협상/토론이 제품”이면 → AutoGen(또는 MS 상위 스택) 검토

다음 학습 추천:

  • LangGraph의 interrupt/checkpoint/typed state 패턴을 먼저 익히고(장기 실행 프로세스용),8
  • 이후 팀의 조직/업무 형태가 역할 기반이면 CrewAI를 PoC로 붙여 “추상화가 우리 예외 케이스를 수용하는지”를 검증하는 순서를 권합니다.3
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.