Agentic AI 구축/LangChain & LangGraph

[LangChain 함수 5] Agent에 Memory 추가하기

gksyb4235 2026. 1. 10. 22:27

왜 Agent에 Memory가 필요한가?


 

Agent를 실제 서비스에 사용하다 보면 금방 한계에 부딪힌다.
가장 대표적인 문제가 바로 “이전 대화를 기억하지 못한다”는 점이다.

 

기본적인 LangChain Agent는 각 호출이 독립적이다.
즉, 이전 질문과 답변은 다음 호출에 자동으로 이어지지 않는다.

이 구조는 단순한 Q&A에는 충분하지만, 다음과 같은 상황에서는 문제가 된다.

  • 여러 턴에 걸친 대화
  • “아까 말한 그 사람”, “방금 조회한 결과” 같은 참조
  • 중단되었다가 다시 이어지는 세션

이런 경우 Agent가 과거 대화 상태를 기억하지 못하면, 대화는 쉽게 붕괴된다.
이를 해결하기 위해 LangChain은 LangGraph를 통해 Memory를 Agent에 통합한다.

 

 

 

LangGraph Runtime과 Context의 역할


LangChain Agent는 내부적으로 LangGraph Runtime 위에서 실행된다.
이 Runtime 객체는 단순 실행 환경이 아니라, Agent가 사용할 수 있는 여러 중요한 정보를 담고 있다.

 

Runtime에는 다음과 같은 요소가 포함된다.

  • Context
    사용자 ID, DB 연결, 설정 정보 등 변하지 않는 정적 정보
  • Store
    장기 메모리나 체크포인트를 저장할 수 있는 기본 저장소
  • Stream Writer
    Custom streaming mode에서 사용했던 스트리밍 인터페이스

이 중 Memory 실습에서 가장 중요한 것은 Context와 Checkpoint(Store)다.
Context는 Tool과 Prompt에 외부 의존성을 주입하는 수단이며,
Checkpoint는 Agent 호출 간 상태를 유지하는 핵심 메커니즘이다.

 

 

 

 

Runtime Context를 이용한 DB 접근


Memory를 설명하기 전에, 먼저 Runtime Context를 사용하는 방법을 살펴보자.
아래 예제에서는 SQLite DB를 Agent에 주입한다.

 

from dataclasses import dataclass
from langchain_community.utilities import SQLDatabase

db = SQLDatabase.from_uri("sqlite:///Chinook.db")

@dataclass
class RuntimeContext:
    db: SQLDatabase

 

이 Context는 Agent 생성 시 context_schema로 등록된다.

from langchain.agents import create_agent

agent = create_agent(
    model="openai:gpt-5",
    tools=[execute_sql],
    system_prompt=SYSTEM_PROMPT,
    context_schema=RuntimeContext,
)

 

Tool 내부에서는 get_runtime()를 통해 Context에 접근할 수 있다.

 

from langgraph.runtime import get_runtime

@tool
def execute_sql(query: str) -> str:
    runtime = get_runtime(RuntimeContext)
    db = runtime.context.db
    return db.run(query)

 

이 방식은 DB뿐 아니라 API 클라이언트, 캐시, 설정 값 등
모든 외부 의존성을 Agent에 안전하게 주입하는 일반적인 패턴이다.

 

 

 

 

Memory의 유무에 따른 Agent의 차이 확인


이제 LangGraph Runtime과 Checkpoint 기반 Short-term Memory가 어떤 역할을 하는지, 그리고 Memory 유무에 따라 Agent의 동작이 어떻게 달라지는지를 살펴보자.

 

 

 

1. Memory가 없는 Agent


# 첫 번째 question을 llm에 넣는다.
question = "This is Frank Harris, What was the total on my last invoice?"
steps = []

for step in agent.stream(
    {"messages": [{"role": "user", "content": question}]},
    stream_mode="values",
    context=RuntimeContext(db=db),
):
    step["messages"][-1].pretty_print()
    steps.append(step)
    
---

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

This is Frank Harris, What was the total on my last invoice?
================================== Ai Message ==================================
Tool Calls:
  execute_sql (call_SPWacdQeKfaiUoanO3BWjy7H)
 Call ID: call_SPWacdQeKfaiUoanO3BWjy7H
  Args:
    query: SELECT name FROM sqlite_master WHERE type='table' ORDER BY name LIMIT 50;
================================= Tool Message =================================
Name: execute_sql

[('Album',), ('Artist',), ('Customer',), ('Employee',), ('Genre',), ('Invoice',), ('InvoiceLine',), ('MediaType',), ('Playlist',), ('PlaylistTrack',), ('Track',)]
================================== Ai Message ==================================
Tool Calls:
  execute_sql (call_i8pE95id3zcxIC872mUpq6Ug)
 Call ID: call_i8pE95id3zcxIC872mUpq6Ug
  Args:
    query: SELECT CustomerId, FirstName, LastName, Company, Email FROM Customer WHERE FirstName = 'Frank' AND LastName = 'Harris' LIMIT 5;
================================= Tool Message =================================
Name: execute_sql

[(16, 'Frank', 'Harris', 'Google Inc.', 'fharris@google.com')]
================================== Ai Message ==================================
Tool Calls:
  execute_sql (call_tp0ulQe2L3SK8uCKEeVlaryL)
 Call ID: call_tp0ulQe2L3SK8uCKEeVlaryL
  Args:
    query: SELECT InvoiceId, InvoiceDate, Total FROM Invoice WHERE CustomerId = 16 ORDER BY InvoiceDate DESC LIMIT 1;
================================= Tool Message =================================
Name: execute_sql

[(374, '2013-07-04 00:00:00', 5.94)]
================================== Ai Message ==================================

Your last invoice total was $5.94. (Invoice 374 dated 2013-07-04)

 

첫 번째 질문에서 Agent는 DB를 조회해 사용자의 마지막 인보이스 금액을 알아낸다.

이때 Agent는 SQL Tool을 여러 번 호출해 답을 도출한다.
하지만 Memory를 활성화하지 않은 상태에서, 이어지는 후속 질문을 던지면 문제가 발생한다.

 

# 후속 질문으로 제목이 뭐였는지 물어본다

question = "What were the titles?"

for step in agent.stream(
    {"messages": [{"role": "user", "content": question}]},
    context=RuntimeContext(db=db),
    stream_mode="values",
):
    step["messages"][-1].pretty_print()
    
---

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

What were the titles?
================================== Ai Message ==================================
Tool Calls:
  execute_sql (call_cmhGWD1P4Ta4WqeHaPGmq5ga)
 Call ID: call_cmhGWD1P4Ta4WqeHaPGmq5ga
  Args:
    query: SELECT name FROM sqlite_master WHERE type='table' ORDER BY name LIMIT 5;
================================= Tool Message =================================
Name: execute_sql

[('Album',), ('Artist',), ('Customer',), ('Employee',), ('Genre',)]
================================== Ai Message ==================================

Could you clarify which titles you mean—album titles, track titles, or something else? If you want album titles, I can list the first 5.

 

Agent는 이 질문이 무엇을 가리키는지 이해하지 못한다.
이유는 단순하다.

  • 이전 질문과 답변이 현재 호출에 포함되지 않기 때문이다
  • “Frank Harris”와 “last invoice”라는 맥락이 사라졌기 때문이다

이것이 Memory가 없는 Agent의 전형적인 한계다.

 

 

 

2. Memory가 있는 Agent


이제 Memory를 추가해보자.
LangGraph에서는 Checkpoint를 통해 Short-term Memory를 제공한다.

from langgraph.checkpoint.memory import InMemorySaver # InMemorySaver import
from langchain.agents import create_agent
from langchain_core.messages import SystemMessage

agent = create_agent(
    model="openai:gpt-5",
    tools=[execute_sql],
    system_prompt=SYSTEM_PROMPT,
    context_schema=RuntimeContext,
    checkpointer=InMemorySaver(), # checkpointer 정의
)

 

그리고 Agent를 호출할 때 thread_id를 지정한다.

 

# 다시 첫 번째 질문
question = "This is Frank Harris, What was the total on my last invoice?"
steps = []

for step in agent.stream(
    {"messages": [{"role": "user", "content": question}]},
    # 이제 Agent를 호출할 때, 이 설정 config parameter인 thread id를 전달한다.
    # 이는 특정 thread의 상태를 추적할 수 있도록 보장한다.
    {"configurable": {"thread_id": "1"}}, 
    context=RuntimeContext(db=db),
    stream_mode="values",
):
    step["messages"][-1].pretty_print()
    steps.append(step)
    
---

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

This is Frank Harris, What was the total on my last invoice?
================================== Ai Message ==================================
Tool Calls:
  execute_sql (call_6UxxtVjiPsxrp3dBYtn8eDXB)
 Call ID: call_6UxxtVjiPsxrp3dBYtn8eDXB
  Args:
    query: SELECT name, sql FROM sqlite_master WHERE type='table' AND (lower(name) LIKE '%invoice%' OR lower(name) LIKE '%customer%' OR lower(name) LIKE '%client%') ORDER BY name LIMIT 5;
================================= Tool Message =================================
Name: execute_sql

[('Customer', 'CREATE TABLE [Customer]\n(\n    [CustomerId] INTEGER  NOT NULL,\n    [FirstName] NVARCHAR(40)  NOT NULL,\n    [LastName] NVARCHAR(20)  NOT NULL,\n    [Company] NVARCHAR(80),\n    [Address] NVARCHAR(70),\n    [City] NVARCHAR(40),\n    [State] NVARCHAR(40),\n    [Country] NVARCHAR(40),\n    [PostalCode]...'), ('Invoice', 'CREATE TABLE [Invoice]\n(\n    [InvoiceId] INTEGER  NOT NULL,\n    [CustomerId] INTEGER  NOT NULL,\n    [InvoiceDate] DATETIME  NOT NULL,\n    [BillingAddress] NVARCHAR(70),\n    [BillingCity] NVARCHAR(40),\n    [BillingState] NVARCHAR(40),\n    [BillingCountry] NVARCHAR(40),\n    [BillingPostalCode]...'), ('InvoiceLine', 'CREATE TABLE [InvoiceLine]\n(\n    [InvoiceLineId] INTEGER  NOT NULL,\n    [InvoiceId] INTEGER  NOT NULL,\n    [TrackId] INTEGER  NOT NULL,\n    [UnitPrice] NUMERIC(10,2)  NOT NULL,\n    [Quantity] INTEGER  NOT NULL,\n    CONSTRAINT [PK_InvoiceLine] PRIMARY KEY  ([InvoiceLineId]),\n    FOREIGN KEY...')]
================================== Ai Message ==================================
Tool Calls:
  execute_sql (call_Hw7yZCByHPvz4BNoufseRCW7)
 Call ID: call_Hw7yZCByHPvz4BNoufseRCW7
  Args:
    query: SELECT i.InvoiceId, i.InvoiceDate, i.Total
FROM Invoice AS i
JOIN Customer AS c ON c.CustomerId = i.CustomerId
WHERE c.FirstName = 'Frank' AND c.LastName = 'Harris'
ORDER BY i.InvoiceDate DESC
LIMIT 1;
================================= Tool Message =================================
Name: execute_sql

[(374, '2013-07-04 00:00:00', 5.94)]
================================== Ai Message ==================================

Your most recent invoice total is 5.94 (Invoice ID 374, dated 2013-07-04).

 

첫 번째 질문은 마찬가지로 잘 수행한다.

여기서 thread_id는 다음을 의미한다.

  • 동일한 thread_id는 동일한 대화 흐름
  • Message와 Agent State가 해당 thread에 저장됨
  • 후속 호출 시 자동으로 이전 상태가 복원됨

이제 같은 thread_id로 두 번째 질문을 하면,

 

# 이제 동일한 thread id로 후속 질문을 수행한다.
question = "What were the titles?"
steps = []

for step in agent.stream(
    {"messages": [{"role": "user", "content": question}]},
    {"configurable": {"thread_id": "1"}},
    context=RuntimeContext(db=db),
    stream_mode="values",
):
    step["messages"][-1].pretty_print()
    steps.append(step)
    
---

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

What were the titles?
================================== Ai Message ==================================
Tool Calls:
  execute_sql (call_NVIzXxF2vjAD1uAi2kI3CXl4)
 Call ID: call_NVIzXxF2vjAD1uAi2kI3CXl4
  Args:
    query: SELECT il.InvoiceLineId, t.Name AS Title
FROM InvoiceLine AS il
JOIN Track AS t ON t.TrackId = il.TrackId
WHERE il.InvoiceId = 374
ORDER BY il.InvoiceLineId
LIMIT 5;
================================= Tool Message =================================
Name: execute_sql

[(2021, 'Holier Than Thou'), (2022, 'Through The Never'), (2023, 'My Friend Of Misery'), (2024, 'The Wait'), (2025, 'Blitzkrieg')]
================================== Ai Message ==================================

The titles on your last invoice (ID 374) include:
- Holier Than Thou
- Through The Never
- My Friend Of Misery
- The Wait
- Blitzkrieg

Would you like the full list with quantities and prices?

 

Agent는 이전 대화를 정확히 기억하고,
Frank Harris의 인보이스에 포함된 트랙 제목들을 자연스럽게 이어서 답변한다.