SK플래닛 ai활용 데이터엔지니어 과정 2기/ML & DL

LangGraph 3 - 단기기억

dev-lee 2026. 6. 2. 12:59

1. 이번 단계에서 달라지는 것

한 번 질의하면 잊어버리던 에이전트에, 대화를 이어 기억하는 단기기억을 추가함.

②편의 에이전트는 매 질의가 "새 채팅창"이었음. 이전 대화를 기억하지 못해 같은 맥락을 반복 설명해야 했음. 이번에는 Checkpointer를 붙여 대화 상태를 저장하고, thread_id로 사용자별 기억을 관리함.

  • MemorySaver — 상태를 저장하는 단기기억 저장소. 현재는 RAM 기반(프로그램 종료 시 삭제)임.
  • checkpointer 옵션 — compile 시 기억 공간을 그래프에 연결함.
  • thread_id — 대화 세션을 구분하는 식별자. 같은 id면 기억이 누적됨.

그래프 구조 자체는 ②편과 동일하다. 바뀐 건 "상태를 저장하고 다음 호출에 다시 불러온다"는 점뿐이다.


2. 메모리 생성

`MemorySaver` 객체 하나를 만들어 두면 됨.
from langgraph.checkpoint.memory import MemorySaver  # 단기기억용. 종료 시 삭제

# 메모리 생성 -> 현재는 RAM 저장. 실제 운영은 외부 벡터/영속 DB로 대체
memory = MemorySaver()
인메모리라 재시작하면 사라짐. 실서비스에서는 물리적 DB(예: Redis, Postgres 등)로 교체하는 게 일반적임.

3. 모델·도구·노드 (②편과 동일)

기억 추가만이 목적이므로 LLM·도구·판단 노드는 그대로 가져감.
from langgraph.graph import StateGraph, END, MessagesState, START
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from langchain_aws import ChatBedrockConverse
from langgraph.prebuilt import ToolNode, tools_condition
from dotenv import load_dotenv
import os, boto3

load_dotenv()

llm = ChatBedrockConverse(
    model=os.getenv('MODEL_ID'),
    client=boto3.client('bedrock-runtime', region_name=os.getenv('AWS_REGION'))
)

@tool
def multiply(a: int, b: int) -> int:
    '''두 수를 곱한 후 반환'''
    print(f'       [TOOL 실행] {a}x{b} 계산중..')
    return a * b

tools = [multiply]
llm_with_tools = llm.bind_tools(tools)

def chatbot_node(state: MessagesState):
    res = llm_with_tools.invoke(state['messages'])
    return {"messages": [res]}
핵심 로직은 변하지 않았음. 기억은 그래프 외부(체크포인터)에서 관리되기 때문임.

4. 그래프 구성 — 컴파일에 checkpointer 연결

`compile(checkpointer=memory)` 한 줄이 단기기억을 켜는 스위치임.
workflow = StateGraph(MessagesState)
workflow.add_node('chatbot', chatbot_node)     # 판단 노드
workflow.add_node('tools', ToolNode(tools))    # 행동 노드
workflow.add_edge(START, 'chatbot')

workflow.add_conditional_edges(
    'chatbot',          # 텍스트 응답 -> END
    tools_condition     # 도구 필요 -> tools 노드
)
workflow.add_edge('tools', 'chatbot')

# 컴파일 옵션으로 단기기억 공간 제공
app = workflow.compile(checkpointer=memory)
`checkpointer`가 붙으면, 그래프는 매 실행 후 상태를 저장하고 다음 호출 때 같은 `thread_id`의 상태를 자동으로 복원함.

5. 실행 — thread_id로 세션 고정

`config`에 `thread_id`를 담아 넘기면 대화가 누적됨.
if __name__ == '__main__':
    # 사용자별 기억 관리. 여기서는 "user-1"로 고정
    config = {"configurable": {"thread_id": "user-1"}}
    while True:
        user_input = input('\n유저: ').lower()
        if user_input == 'q':
            break
        prompt = {"messages": [HumanMessage(content=user_input)]}
        # 실행 시 config를 함께 전달해야 기억이 이어짐
        for evt in app.stream(prompt, stream_mode='values', config=config):
            msg = evt['messages'][-1]
            print("Agent", msg.content)
`config` 없이 호출하면 체크포인터가 어떤 세션인지 알 수 없어 기억이 이어지지 않음.

6. 동작 확인

이전 질의를 기억하는지 3턴에 걸쳐 확인함.
  • 1차 — 100 곱하기 2는 → 200
  • 2차 — 100 곱하기 3는 → 300
  • 3차 — 그간 물어봤던 질문들을 간단하게 정리해줘

3차 응답에서 에이전트가 앞선 두 질문을 스스로 정리해 냈음.

Agent 지금까지 물어보신 질문들을 정리하면 다음과 같습니다:

1. 100 곱하기 2는? → 답: 200
2. 100 곱하기 3은? → 답: 300

두 번 모두 곱셈 계산을 요청하셨습니다.

질문을 거듭할수록 이전 대화가 상태에 누적되어 프롬프트로 함께 전달된다. 그래서 "정리해줘" 같은 맥락 의존 질의가 가능해진다.


요약

그래프 로직은 그대로 둔 채 체크포인터만 추가해, 맥락을 기억하는 멀티턴 에이전트로 발전시켰음.

 

항목  LangGraph 2 LangGraph 3
대화 기억 없음(매번 초기화) 있음(누적)
추가 요소 MemorySaver + thread_id
변경 지점 compile(checkpointer=...), config 전달
저장 위치 RAM(향후 외부 DB로 확장)

이제 에이전트는 "기억"한다. 다음 단계에서는 LLM이 모르는 외부·사내 데이터를 RAG로 끌어와 할루시네이션을 막는다.