2026년 8월, 멀티 에이전트는 “대화”가 아니라 “오케스트레이션”이다: LangGraph vs AutoGen vs CrewAI 실전 비교와 구현 가이드
들어가며
2026년 기준으로 AI Agent 개발에서 가장 어려운 부분은 “LLM 호출”이 아니라 (1) 여러 단계/여러 역할의 작업을 어떻게 안정적으로 연결할지, (2) 실패했을 때 어디서부터 재개할지, (3) 디버깅/관측 가능성을 어떻게 확보할지입니다. 이 관점에서 LangGraph, AutoGen, CrewAI는 같은 “멀티 에이전트” 카테고리처럼 보이지만, 실제로는 서로 다른 문제를 최적화합니다. (pecollective.com)
- 언제 쓰면 좋은가
- 다단계 워크플로우(리서치→증거검증→작성→리뷰→배포)에서 상태(state), 재시도(retry), 체크포인트(checkpoint), human-in-the-loop가 필요한 경우: LangGraph가 강점. (pecollective.com)
- “에이전트 간 토론/협상” 자체가 제품 기능이고, 자유로운 상호대화가 중요한 경우: AutoGen 계열(다만 MS는 상위 프레임워크로 방향 전환 흐름이 있어 마이그레이션 비용을 고려). (logic.inc)
- 역할 기반(Role/Goal/Task)으로 빠르게 팀(crew)을 구성해 프로토타입→업무 자동화까지 빨리 가고 싶은 경우: CrewAI가 생산성이 좋음. (groovyweb.co)
- 언제 쓰면 안 되는가
- 단순한 1~2단계 자동화는 멀티 에이전트가 오히려 토큰 오버헤드/비결정성/디버깅 비용만 늘립니다(“Router/Workflow 패턴”이 더 낫다는 현장 의견도 많음). (tobias-weiss.org)
- 컴플라이언스/감사 추적이 중요한데, 실행 경로가 대화 흐름에 과도하게 의존하면(특히 AutoGen식 자유 대화) 재현성과 통제가 어려워집니다. (rnwy.com)
🔧 핵심 개념
1) 프레임워크 철학 차이(“멀티 에이전트”를 바라보는 모델)
- LangGraph = stateful directed graph(상태를 가진 그래프/상태 머신)
- 노드(node)=작업 단계(LLM 호출/툴 실행/검증/승인)
- 엣지(edge)=전이(조건 분기, 루프, 실패 시 재시도, 사람 승인 지점)
- 핵심은 “에이전트”가 아니라 오케스트레이션 런타임입니다. 즉, 멀티 에이전트도 결국 그래프 상의 여러 노드로 모델링합니다. (stackaheadai.com)
- CrewAI = role-based crew orchestration(조직도/역할 기반)
- Agent(역할/목표/툴) + Task(업무) + Process(순차/계층)
- “PM이 업무를 쪼개 팀원에게 할당”하는 메타포가 강해서 빠르게 구성 가능.
- 대신 복잡한 분기/중단/재개/정교한 상태 통제는 LangGraph보다 설계 자유도가 떨어질 수 있음(추상화가 높은 만큼 내부 제어를 포기). (groovyweb.co)
- AutoGen = conversational collaboration(대화 기반 협업)
- 여러 에이전트가 대화를 주고받으며 합의를 수렴하는 구조가 강점.
- 단, 2026년 관점에서 “AutoGen을 신규 프로덕션에 채택하면 향후 마이그레이션 계획이 필요”하다는 경고가 반복적으로 보임(MS가 상위 프레임워크로 무게중심 이동). (logic.inc)
- AutoGen 0.4는 2025년 1월 릴리스로 문서/논문 기반 설계가 정리돼 있음. (microsoft.com)
2) 내부 작동 방식: “상태/메모리/재개”가 승패를 가른다
실무에서 멀티 에이전트가 실패하는 지점은 대체로 여기입니다.
- State(불변 로그 + 가변 스냅샷)
- LangGraph는 “Typed State + 체크포인트 + 인터럽트” 같은 장치를 통해 중간 결과를 구조화하고, “실패한 노드부터 재개”를 가능하게 설계합니다. (arxiv.org)
- Observability(추적/리플레이/디버깅)
- 프로덕션에서 중요한 건 “성공률”보다 50번째 실패를 얼마나 빨리 디버깅하느냐입니다(비용은 실행 비용보다 디버깅/재실행에서 터짐). (reddit.com)
- Abstraction tax
- CrewAI처럼 높은 추상화는 “빠른 개발”을 주지만, 복잡한 예외 처리/재개/부분 재실행 요구가 생기면 프레임워크가 허용하는 형태로 문제를 다시 모델링해야 합니다. (groovyweb.co)
💻 실전 코드
현실적인 시나리오: “릴리즈 노트 기반 기술 블로그 초안 생성 파이프라인”
요구사항:
1) URL 목록에서 문서를 수집(툴)
2) 문서별 핵심 변경점 추출(LLM)
3) 상충/근거 부족 시 재검색(루프)
4) 최종 글 작성(LLM)
5) 사람 승인(human-in-the-loop) 후 저장
여기서는 LangGraph로 멀티 에이전트를 ‘그래프’로 구현합니다. (CrewAI/AutoGen은 아래 “판단 기준”에서 대안 제시)
0) 의존성/환경
python -m venv .venv
source .venv/bin/activate
pip install langgraph langchain openai pydantic requests beautifulsoup4
export OPENAI_API_KEY="YOUR_KEY"
1) 그래프 상태 정의 + 툴(문서 수집) + 노드(역할별 에이전트)
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가 강한 이유가 여기(조건 분기/루프/중단/재개를 “대화”가 아니라 “구조”로 고정)입니다. (arxiv.org)
⚡ 실전 팁 & 함정
Best Practice (추천 3가지)
1) State를 “디버깅 가능한 스키마”로 설계
- findings, evidence, tool_result, error를 state에 남겨야 “왜 실패했는지”가 보입니다.
- 특히 멀티 에이전트는 실패 원인이 LLM이 아니라 툴/데이터/파싱/타임아웃인 경우가 많습니다. (nkktech.com)
2) Human-in-the-loop를 ‘마지막’이 아니라 ‘중간’에도 둬라
- 정책/보안/배포 단계는 최종 승인만으로 부족합니다.
- LangGraph는 인터럽트/체크포인트 기반 패턴이 강점으로 반복 언급됩니다. (arxiv.org)
3) “에이전트 수”가 아니라 “실패 시 재개 단위”로 설계
- 역할을 10개로 쪼개는 것보다, 실패 지점을 기준으로 단계화하는 게 비용과 안정성에 직결됩니다.
- 추상화가 높은 프레임워크(CrewAI)일수록 이 관점이 약해지기 쉬움. (groovyweb.co)
흔한 함정/안티패턴
- 대화 로그를 상태로 착각
- AutoGen 스타일에서 “대화가 곧 상태”가 되면 재현성이 떨어집니다. 운영에서 필요한 건 “대화”가 아니라 “결정 근거와 전이 조건”입니다. (rnwy.com)
- 구조화 출력(Structured Output) 없이 파싱 지옥
- 실무에선 JSON schema/typed output 없으면 작은 프롬프트 변경에 파서가 깨집니다.
- 멀티 에이전트를 만능으로 사용
- 단순 업무에선 Router/Workflow가 더 싸고 빠르다는 경험적 주장도 많습니다(“대부분은 오케스트레이션이 핵심”). (tobias-weiss.org)
비용/성능/안정성 트레이드오프
- CrewAI: 빠른 생산성 ↔ 런타임/토큰 오버헤드 및 내부 제어 한계가 문제될 수 있음(특히 복잡한 예외/재개 요구). (groovyweb.co)
- LangGraph: 제어/재개/관측성 ↔ 초기 설계 비용(그래프/상태 설계)이 큼. (pecollective.com)
- AutoGen: 대화형 협업에 강함 ↔ 2026년 기준 생태계/방향성 변화(상위 프레임워크로 이동) 리스크를 고려. (logic.inc)
🚀 마무리
핵심 정리:
- LangGraph는 “멀티 에이전트”라기보다 장기 실행/재개 가능한 상태 머신 런타임에 가깝고, 프로덕션 워크플로우(분기/루프/승인/체크포인트)에 가장 잘 맞습니다. (pecollective.com)
- CrewAI는 역할 기반 팀을 빠르게 세우는 데 최적이며, “조직도 같은 업무 분담”이 명확한 자동화에 강합니다. (groovyweb.co)
- AutoGen은 대화/토론형 멀티 에이전트에 강하지만, 2026년 시점에는 중장기 유지/마이그레이션 전략을 같이 가져가야 합니다. (logic.inc)
도입 판단 기준(실무용): 1) “실패 후 재개”가 핵심이면 → LangGraph 우선 2) “역할 분담이 명확한 업무 자동화”면 → CrewAI 우선 3) “에이전트 간 협상/토론이 제품”이면 → AutoGen(또는 MS 상위 스택) 검토
다음 학습 추천:
- LangGraph의 interrupt/checkpoint/typed state 패턴을 먼저 익히고(장기 실행 프로세스용), (arxiv.org)
- 이후 팀의 조직/업무 형태가 역할 기반이면 CrewAI를 PoC로 붙여 “추상화가 우리 예외 케이스를 수용하는지”를 검증하는 순서를 권합니다. (groovyweb.co)
원하시면, 위 예제를 (A) CrewAI 버전, (B) AutoGen AgentChat 버전으로 같은 시나리오를 구현해 “상태/재시도/재개/관측성” 관점에서 코드 차이를 나란히 비교해 드릴게요.