ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Chain이 아니라 Graph — LangGraph로 AI 에이전트를 만드는 이유
    IT 2026. 4. 16. 21:00
    Chain이 아니라 Graph — LangGraph로 AI 에이전트를 만드는 이유

    LangChain이 있는데 왜 LangGraph가 따로 필요할까? LLM 에이전트의 핵심 흐름이 일직선(Chain)이 아니라 순환하는 그래프(Graph)이기 때문입니다. 실제 코드를 따라가며, 이 그래프가 어떤 모양이고 왜 이런 구조가 되는지 풀어봅니다.

    Chain의 한계 — 직선은 판단을 못 한다

    LangChain의 기본 모델은 이름 그대로 Chain입니다. A → B → C 순서로 단계를 밟는 파이프라인이죠.

    # Chain 방식: 검색 → LLM → 출력 (순서 고정)
    chain = retriever | prompt | llm | output_parser

    이 구조는 "검색해서 답변해" 같은 단순 RAG에는 충분합니다. 하지만 이런 상황에서 막힙니다:

    • 검색 결과가 불충분하면? → 다시 검색해야 하는데, 체인은 뒤로 못 돌아갑니다
    • 웹 검색이 필요한지, RAG가 필요한지? → LLM이 판단해야 하는데, 체인은 분기를 못 합니다
    • 도구를 3번 쓸지 1번 쓸지? → 실행 중에 결정해야 하는데, 체인은 고정된 순서만 따릅니다

    에이전트에게 필요한 건 "다음에 뭘 할지 스스로 판단하고, 필요하면 되돌아가는" 구조입니다. 이것은 직선이 아니라 순환 그래프입니다.

    ReAct Agent의 그래프 구조

    LangGraph의 create_react_agent가 만드는 그래프는 아래와 같은 형태입니다. 핵심은 LLM 노드와 Tool 노드 사이의 순환입니다.

    diagram

    그래프의 흐름을 정리하면:

    1. START → LLM: 사용자 메시지 + 시스템 프롬프트 + 메모리 컨텍스트를 받는다
    2. LLM → 조건 분기: LLM의 응답에 tool_calls가 있는지 검사한다
    3. YES → Tools: 지정된 도구를 실행하고, 결과를 메시지에 추가한다
    4. Tools → LLM: 도구 결과를 포함해서 다시 LLM에게 돌아간다 (이것이 핵심 루프)
    5. NO → END: tool_calls가 없으면 최종 텍스트 응답이므로 출력한다

    Chain이었다면 3번에서 끝나야 합니다. 하지만 Graph이기 때문에 4번이 가능합니다. LLM이 도구 결과를 보고 "아직 부족하다, 한 번 더 검색하자"고 판단할 수 있다는 뜻입니다.

    코드로 보는 그래프 생성

    LangGraph의 create_react_agent는 위 그래프를 단 몇 줄로 만들어 줍니다:

    # agent/graph.py
    from langgraph.prebuilt import create_react_agent
    from langchain_core.messages import SystemMessage
    
    graph = create_react_agent(
        model=llm,           # ChatOpenAI (vLLM 엔드포인트)
        tools=tools,         # @tool 함수 리스트
        prompt=system_prompt, # 시스템 프롬프트
    )

    내부적으로 이 함수는 StateGraph를 생성하고, 두 개의 노드(agent, tools)와 조건부 엣지를 연결합니다. 직접 만든다면 이런 형태입니다:

    # create_react_agent의 내부 동작 (개념)
    from langgraph.graph import StateGraph, END
    
    graph = StateGraph(AgentState)
    graph.add_node("agent", call_llm)      # LLM 호출
    graph.add_node("tools", execute_tools) # 도구 실행
    
    graph.set_entry_point("agent")
    graph.add_conditional_edges(
        "agent",
        should_continue,  # tool_calls 있으면 "tools", 없으면 END
        {"tools": "tools", "end": END},
    )
    graph.add_edge("tools", "agent")       # ← 이것이 순환 엣지

    마지막 줄 add_edge("tools", "agent")가 Chain과 Graph의 결정적 차이입니다. 도구 실행 후 다시 LLM으로 돌아가는 이 엣지가 에이전트의 자율적 반복을 가능하게 합니다.

    도구(Tool) 등록 — @tool 데코레이터

    도구 정의에는 LangChain@tool 데코레이터를 사용합니다. LangGraph 자체에 별도의 도구 시스템이 있는 것이 아니라, LangChain이 제공하는 도구 추상화를 그대로 가져다 씁니다. @tool은 Python 함수의 docstring을 LLM에게 전달할 도구 설명으로, 타입 힌트를 파라미터 스키마로 자동 변환합니다.

    from langchain_core.tools import tool
    
    @tool
    def search_knowledge_vault(
        query: str,
        top_k: int = 3,
        category: Optional[str] = None,
    ) -> str:
        """개인 지식금고(Obsidian vault)에서 관련 정보를 검색합니다.
        업무 기술 노트, 생활 기록, 경영학 수업 노트 등을 포함합니다."""
        
        results = search(query=query, top_k=top_k, ...)
        return formatted_results

    이 함수 하나를 등록하면, LLM은 자동으로 아래와 같은 JSON 스키마를 전달받습니다:

    {
      "name": "search_knowledge_vault",
      "description": "개인 지식금고에서 관련 정보를 검색합니다...",
      "parameters": {
        "query": {"type": "string"},
        "top_k": {"type": "integer", "default": 3},
        "category": {"type": "string", "nullable": true}
      }
    }

    LLM은 이 스키마를 보고 언제, 어떤 파라미터로 호출할지를 스스로 결정합니다. 사람이 "RAG 성능 이슈 정리해줘"라고 하면, LLM이 search_knowledge_vault("RAG 성능 이슈")를 호출하겠다고 응답하는 식입니다.

    도구 실패 방어 — _safe_tool 래퍼

    에이전트 루프에서 도구 하나가 예외를 던지면 전체 그래프가 멈춥니다. 이를 막기 위해 모든 도구를 래퍼로 감쌉니다:

    def _safe_tool(tool_func):
        original_func = tool_func.func
        def wrapper(*args, **kwargs):
            try:
                return original_func(*args, **kwargs)
            except Exception as e:
                return f"[도구 오류] {tool_func.name}: {e}"
        tool_func.func = wrapper
        return tool_func
    
    # 그래프 생성 시 모든 도구에 적용
    tools = [_safe_tool(t) for t in CORE_TOOLS]

    예외가 에러 텍스트로 변환되어 LLM에게 돌아갑니다. LLM은 이 에러 메시지를 보고 다른 도구를 시도하거나, 사용자에게 상황을 설명할 수 있습니다. 그래프의 순환 구조 덕분에 가능한 복구 패턴입니다.

    실행 예시 — 그래프가 실제로 도는 모습

    사용자가 "지난달 논의한 RAG 성능 이슈 정리해줘"라고 하면, 그래프는 아래처럼 순환합니다:

    diagram

    Chain이었다면 Iteration 1에서 검색 결과를 받고 바로 답변해야 합니다. 하지만 Graph 구조이기 때문에 LLM이 "결과가 부족하다"고 판단하면 추가 검색을 할 수 있습니다. 이 자율적 판단이 에이전트의 핵심 가치입니다.

    스트리밍 — astream_events로 그래프 내부 엿보기

    LangGraph는 그래프 실행 중 각 노드의 이벤트를 스트리밍으로 내보냅니다. 이를 활용하면 사용자에게 에이전트가 지금 무엇을 하고 있는지 실시간으로 보여줄 수 있습니다:

    async for event in graph.astream_events(
        {"messages": langchain_messages}, version="v2"
    ):
        if event["event"] == "on_tool_start":
            # "검색 중..." 상태 표시
            send_status(tool_name=event["name"], status="running")
        
        elif event["event"] == "on_chat_model_stream":
            # LLM 토큰을 실시간 스트리밍
            send_token(event["data"]["chunk"])

    on_tool_start, on_tool_end, on_chat_model_stream 같은 이벤트가 그래프의 각 노드 전환 시점에 발생합니다. 그래프의 구조가 명확하기 때문에, 이벤트 타이밍도 예측 가능합니다.

    왜 Graph인가 — 정리

    LangGraph를 쓰는 이유를 한 문장으로 요약하면: 에이전트의 흐름에는 루프가 있기 때문입니다.

      Chain (LangChain) Graph (LangGraph)
    흐름 A → B → C (직선) A → B → A → B → C (순환)
    도구 호출 고정된 순서, 1회 LLM이 판단, 0~N회
    분기 사전 정의된 조건 LLM 출력 기반 동적 분기
    상태 관리 입력 → 출력 파이프 메시지 리스트에 누적
    적합한 용도 단순 RAG, 변환 파이프라인 에이전트, 멀티턴 도구 사용

    Chain은 "정해진 절차를 따라 처리"하는 데 강합니다. Graph는 "상황을 보고 다음 행동을 결정"하는 데 강합니다. AI 에이전트는 후자입니다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.