본문 바로가기

AI

LangGraph에서 Router, Agent, ReAct, State, 그리고 Memory 이해하기

💡 LangGraph를 활용하여 LLM을 보다 스마트하게 만들기 위한 핵심 개념들

– Router, Agent, ReAct, State, 그리고 Memory –

에 대해 알아보겠습니다.

1️⃣ Router: LLM이 어떻게 경로를 결정하는가?

LangGraph에서 Router(라우터)입력된 메시지를 보고 적절한 처리 방식을 결정하는 역할을 합니다.


Router의 실행 흐름

  1. Start → Step 1: 첫 번째 단계 실행
  2. Step 1 → LLM: LLM이 입력을 분석
  3. LLM이 판단:
    • Step 2로 이동 → 자연어 응답 반환
    • Step 3로 이동 → Tool 호출 후 결과 반환

즉, Router는 LLM이 단순 응답을 할지, Tool을 활용할지 결정하는 중요한 요소입니다.

Tool을 사용하면 어떻게 동작이 달라질까?

예를 들어, 곱셈을 수행하는 multiply 함수가 있다고 가정해보겠습니다.

def multiply(a: int, b: int) -> int:
    """Multiply a and b.

    Args:
        a: first int
        b: second int
    """
    return a * b

 

이제 LLM이 다음과 같은 질문을 받았을 때, Tool을 사용할지 여부에 따라 결과가 달라집니다.

from langchain_core.messages import HumanMessage
messages = [HumanMessage(content="Hello, what is 2 multiplied by 2? .")]
messages = graph.invoke({"messages": messages})
for m in messages['messages']:
    m.pretty_print()

✅ Tool을 사용한 경우:

================================ Human Message =================================

Hello, what is 2 multiplied by 2? .
================================== Ai Message ==================================
Tool Calls:
  multiply (call_aB8WOVnTjdQTqrxmOkR967VC)
 Call ID: call_aB8WOVnTjdQTqrxmOkR967VC
  Args:
    a: 2
    b: 2
================================= Tool Message =================================
Name: multiply

4

✅ Tool을 사용하지 않은 경우:

from langchain_core.messages import HumanMessage
messages = [HumanMessage(content="Hello, what is 2 plus by 2? .")]
messages = graph.invoke({"messages": messages})
for m in messages['messages']:
    m.pretty_print()
================================ Human Message =================================

Hello, what is 2 plus by 2? .
================================== Ai Message ==================================

The result of 2 plus 2 is 4.

 

💡 LLM이 필요하다고 판단하면 Tool을 사용하지만, 단순한 계산은 바로 응답하기도 합니다.


2️⃣ Agent: ReAct를 적용하여 더 똑똑하게 만들기

✅ 기존 방식 (Router만 사용)

LLM이 입력을 받고 두 가지 방식으로 처리할 수 있어:

  1. 바로 응답하는 경우 → 자연어 응답을 반환하고 끝 (End)
  2. 도구를 호출하는 경우 → 도구 실행 후 결과를 받아서 다시 처리 (Tools → End)

하지만 더 발전된 방식으로 ReAct 아키텍처를 적용할 수 있습니다.

✅ ReAct(Reasoning + Acting)란?

ReAct는 LLM이 도구를 사용한 후, 결과를 다시 활용하여 더 깊이 있는 응답을 생성하는 방식입니다.

ReAct의 3단계 흐름

  1. Act (행동, Acting)
    • 모델이 입력을 분석하고, 도구(tool)를 호출할지 자연어로 답할지 결정함.
    • 만약 도구 호출이 필요하면, ToolMessage(도구 호출 정보)를 생성.
  2. Observe (관찰, Observing)
    • 도구를 호출한 후, 그 결과를 모델에게 다시 전달함.
    • 즉, LLM이 도구의 결과를 보고 추가적인 판단을 내릴 수 있도록 함.
  3. Reason (추론, Reasoning)
    • 모델이 도구의 결과를 분석하여 다음 행동을 결정.
    • 여기서 다시 (1) 새로운 도구를 호출할 수도 있고, (2) 최종적으로 사용자에게 응답할 수도 있음.

여기서 중요한건 tool -> End로 가는게 아니라 LLM을 한번 더 거친다는것!!

 

기존 방식과 ReAct 방식 비교

🔴 기존 방식 (Tool을 한 번 호출하면 끝)

# 기존: tools 실행 후 바로 END
# builder.add_edge("tools", END)  # 이 부분을 삭제하고 아래처럼 수정

# 🔥 루프 추가: tools 실행 후 다시 LLM 실행 가능
# builder.add_edge("tools", "tool_calling_llm")  

🟢 ReAct 방식 (연속적인 계산 가능)

messages = [HumanMessage(content="Add 3 and 4. Multiply the output by 2. Divide the output by 5")]

그리고 결과가 항상 tool 을 사용하는것도 아니다 llm 이 판단 할때마다

필요하다고 생각하면 tool 을 사용해서 답변 하고 아니면 그냥 답변한다

💡 하지만 하나 확실한건 ReAct를 적용하면 복잡한 계산도 단계적으로 해결할 수 있다!


3️⃣ LangGraph의 기본 문제: 상태(State)가 휘발됨

LangGraph는 기본적으로 한 번 실행되면 상태(State)가 사라집니다.

✅ 상태가 사라지는 문제 예시

User: "What is 3 + 4?"
LLM: "7"

🚀 이제 사용자가 추가 질문을 하면?

User: "Now multiply that by 2."
LLM: "I don't know what 'that' refers to."

😡 문제: LLM이 이전 응답을 기억하지 못함. LangGraph는 기본적으로 한 번 실행된 후 상태가 사라지기 때문.


4️⃣ 해결책: Checkpointer로 상태 유지하기

LangGraph에서는 Checkpointer를 사용하면 이전 실행 상태를 저장하고 유지할 수 있습니다.

즉, LLM이 "7"을 기억하고 다음 계산을 수행할 수 있도록 하는 기능!

간단한 해결책: MemorySaver 사용하기

🚀 LangGraph는 기본적으로 여러 가지 Checkpointer를 제공하지만, 가장 간단한 방법은 MemorySaver를 사용.

🚀 MemorySaver는 Key-Value Store 방식으로 상태를 저장

from langgraph.checkpoint import MemorySaver

memory = MemorySaver()  # 🔥 메모리 저장소 생성
graph = builder.compile(checkpointer=memory)  # 🔥 체크포인터와 함께 그래프 컴파일

이제 실행될 때마다 상태가 자동으로 저장됨!

  • LangGraph는 각 스텝(step) 실행 후 상태를 저장하고,
  • 다시 실행할 때 저장된 상태에서 이어서 실행할 수 있음.

 체크포인트를 저장하는 thread_id란

💡 thread_id는 LangGraph에서 상태(state)와 체크포인트를 저장하는 고유한 식별자(ID)

  • LangGraph에서 메모리를 유지하려면, 여러 개의 상태를 저장해야 할 수 있음.
  • 각 실행 흐름(Thread)은 여러 개의 상태(Checkpoint)를 저장하고,
  • 나중에 이 실행 흐름을 다시 불러오려면 저장된 thread_id를 사용해야 함.

체크포인터(Checkpoint)를 사용하면 실행 상태를 저장할 수 있음.

✅thread_id를 사용하면 LangGraph가 여러 개의 상태를 저장하고, 특정 세션을 이어서 실행할 수 있음.

✅즉, thread_id는 LangGraph에서 "메모리 기능"을 제공하는 핵심 요소!🚀🔥

 

State란?

LangGraph에서 **State(상태)**는 그래프 내에서 데이터(값)를 저장하고 전달하는 구조

그래프에서 실행되는 모든 노드(Node)와 엣지(Edge)는 State를 입력(Input)으로 받고, 결과(Output)로 새로운 State를 생성.

즉, State는 그래프 전체의 "기억" 역할을 하는 핵심 요소

State의 특징

  1. State에는 "Schema(구조)"가 필요함.
    • State는 어떤 데이터를 저장하는지 정의해야 함.
    • TypedDict 또는 Pydantic 모델을 사용해서 정의 가능.
  2. 모든 노드(Node)는 State를 입력으로 받고, 새로운 State를 출력함.
    • 즉, 노드는 기존 상태를 수정하거나 새로운 데이터를 추가할 수 있음.
  3. State를 업데이트할 때 Reducer Function을 사용.
    • Reducer는 기존 State를 어떻게 업데이트할지 정의하는 함수야.
    • 예를 들어, 새로운 값을 추가할 수도 있고, 기존 값을 덮어쓸 수도 있음.

TypedDict vs dataclass vs Pydantic 비교


정의 방식 class MyState(TypedDict): @dataclass class MyState: class MyState(BaseModel):
속성 접근 state["key"] state.key state.key
유효성 검사 ❌ 없음 ❌ 없음 ✅ 자동 검증
잘못된 값 입력 시 오류 없음 오류 없음 ✅ 오류 발생
LangGraph 호환 ✅ 가능 ✅ 가능 ✅ 가능 (가장 안전)

🚀 마무리

Router → LLM이 도구를 사용할지 판단
Agent + ReAct → LLM이 도구를 반복적으로 활용 가능
Checkpointer + thread_id → 실행 상태를 유지하여 문맥 기억
State 사용 → 데이터 유지 및 업데이트 가능