본문 바로가기
최신 AI

LangGraph로 AI 에이전트 구축하기

by 구라100단 2026. 2. 10.

지난 2년간 LangGraph는 내가 AI 분야에서 구축하는 거의 모든 것의 핵심이 되었다. 챗봇, MCP 어시스턴트, 음성봇, 내부 자동화 에이전트 등 추론, 도구 또는 다단계 워크플로우와 관련된 것이라면 아마도 LangGraph로 구축했을 것이다. 이는 내 클라이언트 프로젝트, 개인적인 실험, 심지어 매일 실행되는 프로덕션 시스템에도 계속해서 등장하는 스택이다.

나는 LangGraph에 대한 첫 튜토리얼을 작성했고, 이는 곧 내 글 중 가장 많이 읽힌 글 중 하나가 되었다.. 그 이후 LangGraph는 발전했고, 생태계는 성숙했으며, 나만의 에이전트 구축 방식도 크게 바뀌었다.

새로운 패턴과 더 깔끔해진 2025년 API에 대해 알아보기 전에, LangGraph가 애초에 왜 존재하는지 다시 한번 살펴보자.

LangGraph란 무엇인가?

LangGraph는 LangChain을 기반으로 구축된 라이브러리로, LLM 기반 애플리케이션의 명시적인 제어 흐름에 중점을 둔다. LangGraph는 애플리케이션을 선형 파이프라인으로 구조화하는 대신, 실행이 분기, 반복 및 이전 단계를 다시 방문할 수 있는 그래프로 모델링한다.

전통적인 LangChain 워크플로우는 일반적으로 방향성 비순환 그래프(DAG)이다. 즉, 앞으로 이동하고 종료된다. 이는 많은 사용 사례에 충분하지만, 시스템이 반복하거나, 중간 결과를 재평가하거나, 다음에 무엇을 할지 동적으로 결정하기를 원할 때 제한이 된다. 많은 실제 에이전트는 고정된 순서를 따르기보다는 결과에 따라 반복, 재시도 또는 동작을 조정해야 한다.

LangGraph는 그래프에 주기를 허용하여 이 문제를 해결한다. 노드를 다시 방문할 수 있고, 결정을 반복적으로 내릴 수 있으며, 제어 흐름이 프롬프트 내에 포함되는 대신 명시적으로 처리된다. 이를 통해 계획하고, 행동하고, 결과를 관찰하고, 중지 조건이 충족될 때까지 계속하는 에이전트를 구축할 수 있다.

LangGraph: 노드, 상태, 엣지

개념적 수준에서 LangGraph는 몇 가지 핵심 추상화를 중심으로 구축된다. 에이전트는 모든 단계가 공통 상태를 공유하고 업데이트하는 그래프로 정의된다.

상태, 노드, 엣지를 이해하는 것만으로도 LangGraph가 작동하는 방식을 이해하기에 충분하다.

상태 — 에이전트의 메모리

상태는 그래프를 통해 흐르는 공유 데이터 구조다. 모든 노드는 현재 상태를 수신하고 이에 대한 업데이트를 반환할 수 있다.

상태는 메시지, 결정, 도구 출력, 중간 결과 및 단계 간에 유지되어야 하는 기타 정보와 같은 에이전트의 누적된 컨텍스트로 생각할 수 있다. 상태는 명시적으로 전달되므로 각 노드는 암시적인 프롬프트 기록에 의존하지 않고 지금까지 일어난 모든 일에 접근할 수 있다.

노드 — 집중된 행동 단위

노드는 그래프의 실행 가능한 단위다. 각 노드는 다음과 같은 단일하고 잘 정의된 작업을 나타낸다.

  • LLM 호출
  • 도구 또는 API 호출
  • 데이터 변환 또는 검증
  • 라우팅 결정

실제로 LangGraph는 노드를 작고 집중적으로 유지할 때 가장 잘 작동한다. 한 가지 작업을 수행하는 노드는 에이전트가 발전함에 따라 독립적으로 테스트, 추론, 수정하기가 더 쉽다.

엣지 — 제어 흐름

엣지는 실행이 한 노드에서 다음 노드로 이동하는 방법을 정의한다. 에이전트의 제어 흐름을 인코딩하며 다음과 같을 수 있다.

  • 순차적 (항상 다음 노드로 이동)
  • 조건부 (상태에 따라 다음 노드 선택)
  • 주기적 (이전 노드로 돌아가기)

엣지를 명시적으로 정의함으로써 LangGraph는 프롬프트 논리 내에 숨기는 대신 그래프 자체에서 제어 흐름을 볼 수 있도록 한다. 이를 통해 에이전트가 언제 반복하고, 언제 분기하고, 언제 종료되는지 더 쉽게 이해할 수 있다.

이러한 핵심 개념을 바탕으로 이론에서 구체적인 예제로 넘어가 보자. 다음 섹션에서는 상태, 노드, 엣지를 사용하여 실제 훈련 데이터에 따라 자동으로 적응하는 워크플로우를 모델링하면서 Strava 기반 훈련 에이전트를 단계별로 구축해 볼 것이다.


단계별 가이드

자동화하려는 프로세스로 시작하라

개인 Strava 기반 훈련 조교 역할을 하는 AI 에이전트를 구축하고 싶다고 상상해 보자. 명확한 목표(예: 다가오는 경주), 훈련 제약(예: 주 3회 세션)이 있으며, 계획이 실제 수행하는 작업에 따라 자동으로 조정되기를 원한다.

에이전트는 다음을 수행해야 한다.

  1. Strava 계정에 연결하고 최근 활동(달리기, 트레일 달리기 등)을 읽는다.
  2. 목표(경주 거리/날짜, 목표 페이스/완주 목표, 고도 프로필)를 이해한다.
  3. 진행 상한 및 휴식 주간을 존중하여 간결한 1주일 계획을 생성한다.
  4. 계획된 작업과 완료된 작업을 비교하여 누락/추가/힘든 세션 및 과부하 위험을 파악한다.
  5. 위험 플래그가 나타나면 다음 주를 조정하고 변경된 내용과 이유를 설명한다.
  6. SMTP가 설정된 경우 이메일로 계획을 전달하거나, 그렇지 않은 경우 로그/터미널에서 미리 본다.

LangGraph에서 에이전트를 구현하려면 일반적으로 동일한 5단계를 따른다.

1단계: 워크플로우를 개별 단계로 매핑하기

자동화하려는 프로세스를 명확하고 개별적인 단계로 분해하는 것부터 시작한다. 각 단계는 노드가 된다. 즉, 단일하고 잘 정의된 책임을 가진 작은 함수다. 이러한 단계가 식별되면 에이전트의 전체 흐름을 설명하기 위해 연결할 수 있다.

아래 다이어그램은 이러한 노드가 Strava 훈련 에이전트 워크플로우에서 어떻게 어울리는지 보여준다.

2단계: 각 단계가 수행해야 할 작업 식별하기

이제 워크플로우가 있으므로 그래프의 각 노드가 책임지는 것과 작업을 제대로 수행하기 위해 필요한 것이 무엇인지 결정해 보자.

모든 단계에 대해 다음을 결정한다.

  • 어떤 유형의 작업인지
  • 어떤 컨텍스트가 필요한지 (정적 대 동적)
  • 출력으로 무엇을 생성해야 하는지

이 에이전트는 완전히 자동으로 실행되고 매주 이메일만 보내기 때문에 사용자 입력 또는 인간 참여 단계는 포함하지 않는다.

노드 유형 (LangGraph 튜토리얼)

  • 데이터 단계: 외부 시스템에서 정보를 검색해야 할 때마다 데이터 단계를 사용한다.
    • Strava 활동 동기화: 원시 훈련 데이터를 가져오는 것으로 시작한다.
      • 이 단계가 하는 일: 지난 약 90일간의 Strava 활동을 가져온다.
      • 필요한 컨텍스트: 액세스 토큰 및 클라이언트 ID/비밀을 통한 Strava 인증.
      • 원하는 결과: 나중에 사용할 측정항목(날짜, 기간, 거리, 페이스, 고도, 활동 유형)이 포함된 최근 활동 목록.
  • LLM 단계: 에이전트가 데이터를 분석하고, 트레이드오프에 대해 추론하거나, 사람이 읽을 수 있는 출력을 생성해야 할 때 LLM 단계를 사용한다.
    • 최근 훈련 요약: 다음으로 원시 활동 로그를 의미 있는 신호로 바꾼다.
      • 이 단계가 하는 일: 주간 볼륨, 힘든 세션 수, 장거리 달리기 시간 및 일관성과 같은 상위 수준 훈련 지표로 활동을 집계한다.
      • 필요한 컨텍스트: Strava의 최근 활동, 기본 정의(힘든 세션, 장거리 달리기, 주간 창으로 간주되는 것)
      • 원하는 결과: 추론 및 의사 결정에 사용할 수 있는 최근 훈련의 구조화된 요약.
    • 목표 대비 진행 상황 평가: 이제 상황이 어떻게 진행되고 있는지 평가한다.
      • 이 단계가 하는 일: 최근 훈련 신호를 사용자의 목표 및 제약 조건과 비교하여 전반적인 상태를 결정한다.
      • 필요한 컨텍스트: 훈련 요약, 목표 정의(경주 날짜, 거리, 고도, 목표 노력), 제약 조건(예: 주 3회 세션), 위험 휴리스틱(부하 급증, 강도 누적).
      • 원하는 결과: 훈련 상태(정상/뒤처짐/과부하), 신뢰도 점수, 위험 플래그, 권장 전략(유지, 조정, 휴식)에 대한 구조화된 평가.
    • 다음 주 계획 생성: 이제 실제 계획을 생성한다.
      • 이 단계가 하는 일: 목표와 최근 훈련을 기반으로 세션 횟수와 진행 상한을 존중하는 1주일 훈련 일정(km 단위)을 생성한다.
      • 필요한 컨텍스트: 목표(경주/날짜/거리/고도), 주당 세션 수 및 최근 훈련 요약(볼륨, 강도 신호).
      • 원하는 결과: 다음 주에 대한 구조화된 계획(날짜, 설명, 기간, 강도가 있는 세션)과 비교/조정을 위해 캡처된 원시 생성 계획.
    • 계획 조정 + 경고 추가: 위험이 감지되면 명확하게 표시하고 그에 따라 훈련 계획을 조정해야 한다.
      • 이 단계가 하는 일: 필요한 경우 계획을 수정하고 과부하 또는 불일치가 감지되면 간결한 경고 또는 주의 사항을 생성한다.
      • 필요한 컨텍스트: 위험 플래그, 훈련 계획 초안.
      • 원하는 결과: 최종 계획과 사용자가 알아야 할 짧은 경고 목록.
    • 주간 이메일 작성: 마지막으로 모든 것을 읽을 수 있는 것으로 바꾼다.
      • 이 단계가 하는 일: 훈련 계획, 간략한 근거 및 경고 또는 위험을 포함하는 명확한 주간 이메일을 생성한다.
      • 필요한 컨텍스트: 최종 훈련 계획, 주요 요약 신호, 경고(있는 경우).
      • 원하는 결과: 보낼 준비가 된 주간 이메일(제목 + 본문).
  • 작업 단계: 에이전트가 외부 세계와 상호 작용해야 할 때 작업 단계를 사용한다.
    • 이메일 보내기: 결과를 전달하여 루프를 닫는다.
      • 이 단계가 하는 일: 주간 훈련 이메일을 사용자에게 보낸다.
      • 필요한 컨텍스트: 수신자 이메일 주소, 이메일 제목 및 본문.
      • 원하는 결과: 이메일이 성공적으로 전달되고 기록된다.

이제 단계를 LLM, 데이터, 작업 단계로 분류했으므로, 그래프의 각 노드가 어떤 종류의 작업을 책임지는지 알게 되었다.

다음 질문도 마찬가지로 중요하다. 이러한 노드는 정보를 어떻게 공유하는가?

이에 답하려면 에이전트의 상태, 즉 각 단계가 이전 단계의 작업을 기반으로 구축할 수 있도록 하는 공유 메모리를 설계해야 한다.

3단계: 상태 설계하기

각 단계가 수행하는 작업을 알았으므로 이제 해당 단계가 정보를 공유하는 방법을 결정해야 한다. 상태는 에이전트의 공유 메모리다. 노드가 검색, 계산 또는 결정한 내용을 저장하여 다운스트림 단계가 이를 기반으로 구축할 수 있는 곳이다.

상태에 속하는 것을 결정할 때 우리는 두 가지 간단한 규칙을 따른다. 정보 조각이 여러 단계에 걸쳐 지속되어야 하는 경우 상태에 저장해야 한다. 동일한 정보를 기존 데이터에서 다시 파생할 수 있는 경우 저장을 피하고 대신 필요할 때 계산한다.

Strava 훈련 에이전트의 경우 다음을 추적해야 한다.

  • 원시 Strava 활동: 나중에 재구성할 수 없으며 여러 단계에서 재사용된다.
  • 훈련 요약: 주간 볼륨, 힘든 세션 수, 장거리 달리기 시간과 같은 집계된 신호.
  • 목표 및 제약 조건: 경주 날짜, 거리, 주당 세션 수와 같은 정적 입력.
  • 평가 결과: 다운스트림 결정을 주도하는 진행 상황, 신뢰도, 위험 플래그에 대한 에이전트의 평가.
  • 생성된 훈련 계획: 사용자에게 전송될 다음 주의 계획.
  • 실행 메타데이터: 디버깅 및 복구를 위한 타임스탬프 및 식별자.

상태를 정의해 보자.

from typing import TypedDict, Literal, List, Dict

class TrainingEvaluation(TypedDict):
    status: Literal["on_track", "behind", "overloaded"]
    confidence: float
    risk_flags: List[str]
    recommendation: Literal["keep", "adjust", "deload"]

class TrainingSession(TypedDict):
    day: str
    description: str
    duration_min: int
    intensity: Literal["easy", "moderate", "hard"]

class StravaTrainingAgentState(TypedDict):
    # Raw Strava data
    activities: List[Dict] | None
    # Aggregated training signals
    training_summary: Dict | None
    # Goal and constraints
    goal: Dict
    sessions_per_week: int
    # Evaluation output
    evaluation: TrainingEvaluation | None
    # Generated plan
    next_week_plan: List[TrainingSession] | None
    # Generated communication
    weekly_email: str | None
    # Execution metadata
    run_id: str
    last_sync_timestamp: str

상태에는 원시 또는 구조화된 데이터만 포함된다. 각 노드는 이 공유 상태에서 읽고, 하나의 특정 작업을 수행하고, 결과를 구조화된 형식으로 다시 쓴다.

4단계: 노드 구축하기

워크플로우와 상태가 준비되었으므로 각 단계를 코드로 전환할 수 있다. LangGraph에서 노드는 Python 함수에 지나지 않는다. 현재 상태를 입력으로 사용하고 업데이트하려는 필드를 반환한다. 이러한 단순성은 의도적이다. 프레임워크별 세부 정보가 아닌 시스템을 통해 데이터가 흐르는 방식에 중점을 둔다.

모든 노드의 전체 구현은 GitHub 리포지토리에서 사용할 수 있다. 이 글에서는 가장 중요한 generate_next_week_plan에 중점을 둘 것이다.

예제를 쉽게 따라갈 수 있도록 ChatOpenAI 주위에 최소한의 래퍼를 사용한다. 이 래퍼는 단순히 LLM 호출을 중앙 집중화하여 노드 논리를 깨끗하게 유지한다.

from dataclasses import dataclass
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage

@dataclass
class LLMService:
    model: str = "gpt-4o-mini"
    temperature: float = 0.1

    def __post_init__(self) -> None:
        self.client = ChatOpenAI(model=self.model, temperature=self.temperature)

    def structured_completion(self, system: str, user: str) -> str:
        return self.client.invoke(
            [SystemMessage(content=system), HumanMessage(content=user)]
        ).content

generate_next_week_plan 노드는 에이전트의 실제 "지능"이 있는 곳이다. 그래프의 이전 모든 것은 입력(Strava 동기화, 훈련 요약, 진행 상황 평가)을 준비한다. 그 이후의 모든 것은 결과를 구체화하거나 전달한다. 이 노드의 책임은 간단하다. 현재 상태가 주어지면 선택한 전략과 제약 조건을 존중하는 1주일 훈련 계획을 생성하는 것이다.

이 노드는 전적으로 상태 기반이다. 선수의 목표, 최근 훈련에 대한 간략한 요약, 현재 권장 사항(유지, 조정 또는 휴식) 및 주당 세션 수를 읽는다. 추가 컨텍스트는 필요하지 않다.

시스템 프롬프트는 모델을 달리기 코치로 고정하고 가이드라인을 인코딩한다. 구조화된 JSON만 반환하고, 요청된 세션 수를 존중하고, 휴식 주간이 아닌 한 진행을 제한하고, 설명을 간결하게 유지한다. 사용자 프롬프트는 상태에서 실제 컨텍스트를 주입한다.

def generate_next_week_plan(state: StravaTrainingAgentState) -> dict:
    """LangGraph node: generate next week's training plan."""
    llm: LLMService = _get_service("llm")
    goal = state.get("goal", {})
    summary = state.get("training_summary", {})
    sessions = state.get("sessions_per_week", 3)
    recommendation = (state.get("evaluation") or {}).get("recommendation", "adjust")

    system_prompt = (
        "You are a running coach. Return ONLY valid JSON: "
        "a list of sessions with fields: day (Mon-Sun), description, "
        "duration_min, intensity (easy|moderate|hard). "
        "Respect sessions_per_week exactly. Cap progression at +10% unless deloading (20–30% down)."
    )

    user_prompt = (
        f"Goal: {json.dumps(goal)}\n"
        f"Recent summary: {json.dumps(summary)}\n"
        f"Recommendation: {recommendation}\n"
        f"Sessions per week: {sessions}"
    )

    plan = llm.structured_completion(system_prompt, user_prompt)
    return {"next_week_plan": plan}

5단계: 함께 연결하기

모든 노드가 구현되었으므로 마지막 단계는 이를 작동하는 LangGraph 워크플로우에 연결하는 것이다. 이것이 에이전트의 제어 흐름이 명시적이 되는 곳이다. 다음에 실행할 노드, 그래프가 분기되는 위치, 중지 시기를 정의한다.

상태 스키마로 StateGraph를 만들고 각 노드를 등록하는 것으로 시작한다.

# Initiat graph
graph = StateGraph(StravaTrainingAgentState)

# Add nodes
graph.add_node("sync_strava_activities", sync_strava_activities)
graph.add_node("summarize_recent_training", summarize_recent_training)
graph.add_node("evaluate_progress_vs_goal", evaluate_progress_vs_goal)
graph.add_node("generate_next_week_plan", generate_next_week_plan)
graph.add_node("adjust_plan_add_warnings", adjust_plan_add_warnings)
graph.add_node("compose_weekly_email", compose_weekly_email)
graph.add_node("send_email", send_email)

다음으로 실행이 그래프를 통해 이동하는 방식을 정의하는 엣지를 추가한다. 대부분의 엣지는 단순하고 순차적이다. 한 노드는 항상 다른 노드를 따른다.

핵심 부분은 generate_next_week_plan 이후의 조건부 엣지다. 이 시점에서 에이전트는 이미 계획을 생성했지만 항상 동일하게 처리하고 싶지는 않다. 평가에서 정상 궤도에 있다고 하면(유지), 즉시 게시할 수 있다. 권장 사항이 조정 또는 휴식인 경우, 계획을 완화하고 경고를 첨부할 수 있는 추가 노드를 통해 라우팅한다.

LangGraph에서 add_conditional_edges는 다음을 사용한다.

  • 분기할 노드,
  • 키를 반환하는 라우팅 함수,
  • 해당 키에서 다음 노드로의 매핑.

여기서 라우팅 함수는 단순히 상태에서 권장 사항을 읽는다.

# Add edges
graph.add_edge(START, "sync_strava_activities")
graph.add_edge("sync_strava_activities", "summarize_recent_training")
graph.add_edge("summarize_recent_training", "evaluate_progress_vs_goal")
graph.add_edge("evaluate_progress_vs_goal", "generate_next_week_plan")

graph.add_conditional_edges(
    "generate_next_week_plan",
    lambda state: state.get("evaluation", {}).get("recommendation", "adjust"),
    {
        "keep": "compose_weekly_email",
        "adjust": "adjust_plan_add_warnings",
        "deload": "adjust_plan_add_warnings",
    },
)

graph.add_edge("adjust_plan_add_warnings", "compose_weekly_email")
graph.add_edge("compose_weekly_email", "send_email")
graph.add_edge("send_email", END)

# Compile graph
app = graph.compile()

마지막으로 나머지 노드를 연결하고 그래프를 컴파일한다.

6단계: 에이전트 테스트하기

전체 에이전트를 로컬에서 테스트하려면 GitHub 리포지토리를 복제하고 README의 설정 지침을 따른다. 가상 환경을 만들거나 활성화하고, 종속성을 설치하고, 필요한 Strava 및 OpenAI 자격 증명으로 .env 파일을 구성한 후 단일 명령으로 에이전트를 실행할 수 있다.

python strava_training_agent.py

각 실행 시 에이전트는 최근 Strava 활동을 가져오고, 훈련을 요약하고, 다음 주 계획을 생성하고, 주간 이메일을 작성한다.

SMTP 구성은 선택 사항이다. 이메일 관련 환경 변수가 설정되지 않은 경우, 에이전트는 이메일을 보내는 대신 이메일 미리보기를 인쇄하고 기록한다. 이렇게 하면 이메일 인프라를 미리 설정하지 않고도 전체 워크플로우를 처음부터 끝까지 쉽게 테스트할 수 있다.

자동 실행

에이전트가 로컬에서 작동하면 다음 질문은 자동으로 실행하는 방법이다. 이 워크플로우는 일주일에 한 번만 실행하면 되므로 항상 켜져 있는 인프라가 필요하지 않다.

개인 프로젝트 및 튜토리얼의 경우 GitHub Actions가 가장 간단한 옵션인 경우가 많다. 이 리포지토리에는 cron 트리거로 에이전트를 예약하고 로컬에서 사용하는 것과 동일한 스크립트를 실행하는 weekly-agent.yml 워크플로우가 포함되어 있다. Strava 및 OpenAI 자격 증명(및 선택적으로 SMTP 자격 증명)은 암호화된 GitHub Secrets로 저장되며 GitHub는 실행 및 로깅을 처리한다. 설정은 노력이 적고 안정적이며 다른 사람들이 복제하기 쉽다.

최종 생각

이 튜토리얼의 목표는 가능한 가장 정교한 훈련 시스템을 구축하는 것이 아니라, LangGraph가 에이전트 워크플로우를 명시적이고 테스트 가능하며 추론하기 쉬운 방식으로 구조화하는 데 어떻게 도움이 되는지 보여주는 것이었다.

문제를 그래프(공유 상태, 작은 집중 노드, 명확하게 정의된 제어 흐름 포함)로 모델링하면, 변화하는 입력에 적응하더라도 동작이 보이고 예측 가능한 에이전트를 얻게 된다. 동일한 패턴은 이 Strava 예제를 훨씬 넘어서도 잘 적용된다. 고객 지원 에이전트, 데이터 분석 보조원, 내부 도구 또는 추론, 행동, 반복이 필요한 모든 시스템에 말이다.

에이전트 자체를 개선할 수 있는 몇 가지 명백한 방향도 있다. 이 튜토리얼에서는 아키텍처에 집중하기 위해 의도적으로 프롬프트를 단순하게 유지했다. 프롬프트 디자인에 더 많은 시간을 투자하여(더 풍부한 코칭 컨텍스트, 더 명확한 진행 규칙, 회복 휴리스틱 또는 스포츠별 지식 추가) 그래프를 전혀 변경하지 않고도 눈에 띄게 더 나은 훈련 계획을 얻을 수 있다. 프롬프트는 워크플로우가 제자리에 있으면 종종 가장 높은 레버리지 개선 사항이다.

거기에서 사용자 피드백 루프, 훈련 블록 전체에 걸친 장기 기억, 수면 또는 심박수 변동성과 같은 추가 데이터 소스, 또는 위험 신호가 누적될 때 진행을 중지하는 더 엄격한 안전 검사로 에이전트를 확장할 수 있다.

핵심은 이러한 개선이 점진적이라는 것이다. 문제를 노드로 분해하고, 상태를 신중하게 정의하고, 그래프를 의도적으로 연결할 수 있다면, 이미 LangGraph로 강력하고 적응 가능한 에이전트를 구축하기 위한 견고한 기반을 갖추고 있는 것이다.