Joyuno's Dev Blog

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

AI Agent

들어가며

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 버전으로 같은 시나리오를 구현해 “상태/재시도/재개/관측성” 관점에서 코드 차이를 나란히 비교해 드릴게요.