시험 공부를 할 때 책상 위에는 지금 풀고 있는 문제집과 노트만 펼쳐 둡니다. 지난 학기 자료는 서랍에 넣어 두었다가 필요할 때 꺼냅니다. 책상이 좁은데 자료를 전부 올리면 어느 것이 어디 있는지 몰라 오히려 느려집니다.
Agent도 마찬가지입니다. 모델이 한 번에 읽을 수 있는 글의 양인 컨텍스트 창(context window)이 책상이고 호출이 끝나면 책상 위는 치워집니다. 그래서 어제 한 대화를 기억하게 하려면 서랍을 따로 만들어야 합니다. 대화가 길어지면 책상 위 글이 늘어나 정확도가 떨어지기도 합니다. 그림에서 책상을 비우는 방법을 하나씩 바꿔 봅니다.
설명을 위한 예시 값입니다. 쪽지 한 장이 6칸이고 책상은 100칸이라고 가정했습니다. 실제 창은 훨씬 커서 Claude Sonnet 5.5는 100만 토큰입니다. 하지만 창이 커도 글이 길어질수록 정확도는 떨어집니다.
create_agent는 호출이 끝나면 대화를 저장하지 않습니다. 대화를 이어 가려면 저장소인 checkpointer(상태를 저장해 두는 장치)를 붙이고 호출할 때 thread_id를 줍니다. LangChain 문서는 checkpointer를 써서 상태를 저장하면 스레드를 언제든 다시 이어 갈 수 있다고 설명합니다.
thread는 대화 하나를 가리키는 이름표입니다. 같은 이름표로 부르면 이전 메시지가 앞에 붙어서 모델에게 가고 다른 이름표는 새 책상입니다. 스크롤하며 두 책상에 쪽지가 쌓이는 모습을 보세요.
thread "A"와 thread "B"는 서로 다른 책상입니다. 아직 아무 쪽지도 없습니다.
질문 쪽지와 답 쪽지가 A 책상에 쌓입니다. 모델이 받은 메시지는 질문 하나뿐입니다.
이번에는 앞의 쪽지 두 장이 책상에 그대로 있어서 새 질문과 함께 모델에게 갑니다. 모델이 받은 메시지는 세 개입니다. 그래서 "아까 말한 문제 기억나요?"에 답할 수 있습니다.
다른 이름표는 새 책상입니다. A의 쪽지는 건드리지 않고 B 책상에서 질문 하나로 새로 시작합니다. 모델이 받은 메시지는 하나입니다.
from langgraph.checkpoint.memory import InMemorySaver
# 1 대화를 저장할 곳을 붙인다
agent = create_agent(model, tools=[], checkpointer=InMemorySaver())
# 2 같은 thread_id면 앞 대화가 이어진다
cfg = {"configurable": {"thread_id": "A"}}
agent.invoke({"messages": [{"role": "user", "content": "와이파이가 안 돼요"}]}, config=cfg)
agent.invoke({"messages": [{"role": "user", "content": "아까 말한 문제 기억나요?"}]}, config=cfg)
# 3 저장된 상태를 열어 본다
state = agent.get_state(cfg)
print(len(state.values["messages"]))출력은 예시입니다. 질문 두 번과 답 두 번이 저장돼 메시지 4개가 됩니다. 코드는 langchain 1.4와 langgraph 1.2 기준입니다.
InMemorySaver는 프로그램 메모리에 저장해서 프로그램을 끄면 사라집니다. 서비스에서는 Postgres 같은 데이터베이스 기반 checkpointer로 바꿉니다.config의 thread_id가 대화 하나의 이름표입니다. 이 값이 같으면 저장된 메시지를 불러와 이어 쓰고 다르면 새 대화입니다. 이름표를 주지 않으면 checkpointer가 대화를 구분하지 못합니다.get_state가 돌려주는 스냅샷(그 순간의 상태를 찍어 둔 것)의 values["messages"]에 지금까지의 메시지가 들어 있습니다.단기 기억은 한 대화 안에서만 이어집니다. 대화가 끝난 뒤에도 "kim은 macOS를 쓰고 회사 VPN을 쓴다" 같은 정보를 기억하려면 장기 기억이 필요합니다. LangChain 문서는 장기 기억이 단기 기억과 달리 한 스레드에 묶이지 않고 여러 스레드에 걸쳐 남는다고 설명합니다.
장기 기억은 Store(저장소)에 넣습니다. 서랍 이름표인 namespace(이름 묶음)와 항목 이름 key로 값을 넣고 꺼냅니다. 그림에서 사용자를 고르면 같은 코드가 사용자마다 다른 서랍을 엽니다. 없는 서랍을 열면 빈손으로 돌아옵니다.
설명을 위한 예시 데이터입니다. 서랍 이름은 namespace인 ("users", "kim") 같은 묶음 이름이고 그 안에서 key로 항목을 찾습니다.
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
# 4 서랍에 넣는다: 이름 묶음, 항목 이름, 값
store.put(("users", "kim"), "prefs", {"os": "macOS", "vpn": "회사 VPN"})
print(store.get(("users", "kim"), "prefs").value)
# 5 없는 서랍은 None이 돌아온다
print(store.get(("users", "lee"), "prefs"))
# 6 Agent에게 서랍을 연결한다
agent = create_agent(model, tools=tools, store=store, context_schema=UserContext)출력은 예시입니다. InMemoryStore도 개발용이라 서비스에서는 데이터베이스 기반 Store를 씁니다.
put에는 이름 묶음(튜플), 항목 이름, 값을 줍니다. get이 돌려주는 항목의 .value가 넣은 값입니다. 이름 묶음을 사용자별로 나누면 서랍이 사용자마다 갈라집니다.None입니다. 도구 안에서는 None일 때 "저장된 정보 없음" 같은 글을 돌려주면 모델이 이어서 처리합니다.store=를 주면 도구 안에서 runtime.store로 서랍을 열 수 있습니다. 사용자 번호는 모델에게 묻지 않고 runtime.context에서 꺼내 이름 묶음을 만듭니다.대화가 길어지는 대로 두면 모델이 받는 메시지가 한 번 오갈 때마다 두 개씩 늘어납니다. SummarizationMiddleware(루프에 코드를 끼우는 Middleware의 하나)를 달면 메시지가 정한 수를 넘을 때 오래된 부분을 요약 메시지 하나로 바꾸고 최근 메시지만 그대로 남깁니다.
질문 다섯 개를 이어서 보낼 때 모델이 받은 메시지 수입니다. 요약 없이는 1, 3, 5, 7, 9로 늘어납니다. 요약을 켜면 네 번째에서 줄어드는 것을 직접 실행해 확인했습니다.
막대 높이는 그 질문 때 모델이 받은 메시지 수입니다. 요약을 켠 경우는 직접 실행한 값이고 요약이 없는 경우는 한 번 오갈 때마다 두 개씩 늘어난 값입니다. 초록 칸은 요약 메시지가 들어 있다는 표시입니다. 아래 터미널의 2 4 6 4 6은 질문마다 저장된 메시지 수라서 막대(모델이 받은 수)와 숫자가 다릅니다.
# 7 메시지가 6개를 넘으면 요약하고 최근 2개만 남긴다
agent = create_agent(
model, tools=[], checkpointer=InMemorySaver(),
middleware=[SummarizationMiddleware(
model=model, trigger=("messages", 6),
keep=("messages", 2))])
for q in questions:
r = agent.invoke({"messages": [{"role": "user", "content": q}]}, config=cfg)
print(len(r["messages"]))출력은 질문 다섯 개를 보낸 뒤 저장된 메시지 수입니다. 네 번째에서 6개가 4개로 줄었습니다.
trigger는 요약을 시작하는 조건으로 메시지 수나 토큰 수로 줄 수 있습니다. keep은 요약하지 않고 그대로 남길 최근 분량입니다. trigger를 주지 않으면 요약하지 않습니다. 요약은 모델을 한 번 더 부르므로 비용이 듭니다.LangChain의 Context Engineering 글이 나눈 네 가지 전략을 책상과 서랍으로 그린 것입니다.
Andrej Karpathy의 표현을 빌리면 Context Engineering은 모델이 다음 단계에서 쓸 컨텍스트 창을 꼭 맞는 정보로 채우는 일입니다. LangChain은 그 방법을 네 가지로 나눕니다. write는 책상 밖에 적어 두고 select는 필요한 것만 책상으로 가져옵니다. compress는 필요한 토큰만 남기고 isolate는 일을 쪼개 다른 책상에 맡깁니다.
Anthropic도 비슷하게 설명합니다. 모델에게는 주의력 예산(attention budget)이 있고 글이 길어질수록 정확하게 떠올리는 능력이 떨어진다고 봅니다. 그래서 오래 도는 Agent는 메모를 밖에 적어 두고 하위 Agent에게 깨끗한 컨텍스트로 일을 맡깁니다. 이 글에서 본 checkpointer와 Store와 요약이 이 네 가지에 하나씩 대응합니다.
단기 기억은 thread로 잇고 장기 기억은 Store에 넣고 책상은 요약으로 비웁니다.
저장해 두는 일은 어렵지 않습니다. 서랍의 어느 메모를 책상에 올릴지, 오래된 대화를 어떻게 줄일지 정하는 일이 어렵습니다. 요약은 정보를 잃을 수 있으므로 꼭 남겨야 하는 사실은 서랍에 따로 적어 두는 편이 안전합니다. 다음 글에서는 서랍의 메모를 검색해서 꺼내는 Agentic RAG를 알아보겠습니다.