LLM 앱 개발 실전 #9 대화 메모리와 컨텍스트 관리
2편에서 봤듯이 API는 상태를 저장하지 않으므로, 대화를 이어가려면 히스토리를 매번 통째로 보냅니다. 그런데 대화가 길어지면 이 히스토리가 끝없이 쌓입니다. 두 가지 문제가 생깁니다. 매번 보내는 토큰이 늘어 비용이 오르고, 결국 모델이 한 번에 받을 수 있는 컨텍스트 한계에 부딪힙니다. 이번 글에서는 이 히스토리를 다루는 방법을 정리합니다.
쌓이는 히스토리의 문제 #
대화의 매 turn마다 messages에 사용자 메시지와 답변이 더해집니다. 10번 주고받으면 20개, 100번이면 200개가 됩니다. 매 호출은 이 전체를 다시 보내므로, 100번째 질문 하나를 보내려고 앞선 99번의 대화를 전부 함께 보내는 셈입니다.
여기서 두 가지가 문제입니다.
- 토큰 비용 — 입력 토큰은 히스토리가 길수록 비례해 늘어납니다. 긴 대화일수록 한 번 답하는 비용이 점점 커집니다.
- 컨텍스트 한계 — 모델마다 한 번에 받을 수 있는 토큰의 상한이 있습니다. 히스토리가 그 한계를 넘으면 더는 보낼 수 없습니다.
그래서 길어지는 대화에서는 히스토리를 그대로 두지 않고 어떤 식으로든 줄여야 합니다. 방법은 크게 잘라내기와 요약하기입니다.
슬라이딩 윈도우 — 최근 것만 남기기 #
가장 단순한 방법은 최근 메시지 몇 개만 남기고 오래된 것은 버리는 것입니다. 창문을 미끄러뜨리듯 항상 최근 구간만 본다고 해서 슬라이딩 윈도우라고 합니다.
MAX_TURNS = 10 # 최근 10턴(20개 메시지)만 유지
def trim(messages):
# system 은 별도 파라미터이므로 messages 에는 user/assistant 만 있다
if len(messages) > MAX_TURNS * 2:
return messages[-MAX_TURNS * 2:]
return messages구현이 쉽고 비용도 안정적입니다. 단점은 오래된 대화를 통째로 잊는다는 점입니다. 앞에서 “내 이름은 민수야"라고 했어도 창문 밖으로 밀려나면 그 사실이 사라집니다. 그래서 최근 맥락만 중요한 가벼운 챗봇에 잘 맞습니다.
요약으로 압축하기 #
오래된 대화를 버리기 아깝다면, 버리는 대신 요약합니다. 앞부분 대화를 Claude에게 짧게 요약시켜 한 덩어리로 만들고, 그 요약을 히스토리 앞에 두는 방식입니다.
def summarize(old_messages) -> str:
text = "\n".join(f"{m['role']}: {m['content']}" for m in old_messages)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{
"role": "user",
"content": f"다음 대화를 이후 맥락 유지에 필요한 핵심만 짧게 요약해줘:\n\n{text}",
}],
)
return next(b.text for b in response.content if b.type == "text")
# 오래된 앞부분은 요약하고, 최근 대화는 그대로 둔다
summary = summarize(messages[:-10])
messages = [
{"role": "user", "content": f"[이전 대화 요약] {summary}"},
] + messages[-10:]이렇게 하면 “민수"라는 이름처럼 오래전 정보도 요약에 담겨 살아남습니다. 토큰도 줄어듭니다. 대신 요약하느라 호출이 한 번 더 들고, 요약 과정에서 세부가 일부 사라질 수 있습니다. 긴 상담이나 작업을 이어가는 앱에 잘 맞습니다.
서버가 알아서 압축하게 하기 #
이 압축을 직접 짜는 대신, Claude API가 서버에서 자동으로 처리하게 할 수도 있습니다. 컨텍스트가 한계에 가까워지면 이전 내용을 알아서 요약해 주는 컴팩션(compaction) 기능입니다. 현재 베타로 제공됩니다.
response.content 전체를 히스토리에 더해야 합니다. 텍스트만 저장하면 요약 상태를 잃어버립니다.직접 관리할지, 서버에 맡길지는 앱에 따라 정합니다. 동작을 세밀하게 제어하고 싶으면 슬라이딩 윈도우나 요약을 직접 짜고, 긴 대화를 간편하게 이어가고 싶으면 컴팩션을 씁니다.
RAG와는 무엇이 다른가 #
8편의 RAG와 헷갈릴 수 있어 짚어 두겠습니다. RAG는 외부 문서에서 관련 내용을 찾아 넣는 것이고, 여기서 다루는 메모리는 지금까지의 대화를 관리하는 것입니다. 둘은 함께 쓰입니다. 사내 문서를 RAG로 가져오면서, 길어지는 대화 히스토리는 요약으로 압축하는 식입니다.
어떤 전략을 언제 쓰나 #
두 전략은 상황에 따라 고릅니다.
- 슬라이딩 윈도우 — 구현이 쉽고 비용이 일정합니다. 최근 맥락만 중요한 가벼운 챗봇에 맞습니다. 단점은 오래된 정보를 잊는 것입니다.
- 요약 — 오래된 정보를 살리지만, 요약 호출이 더 들고 세부가 일부 사라집니다. 긴 상담이나 작업을 이어가는 앱에 맞습니다.
실제로는 둘을 섞어 쓰기도 합니다. 최근 몇 턴은 원문 그대로 두어 정확도를 지키고, 그보다 오래된 부분은 요약 한 덩어리로 압축하는 방식입니다. 최근 대화의 정확함과 오래된 맥락의 보존을 함께 얻습니다.
어느 쪽이든 기준이 되는 것은 토큰입니다. 다음 #12에서 다룰 토큰 계산으로 히스토리가 몇 토큰인지 재고, 정해 둔 상한을 넘으면 잘라내거나 요약하도록 만들면, 비용과 컨텍스트 한계를 함께 관리할 수 있습니다.
흔히 걸려 넘어지는 곳 #
- 히스토리를 무한정 쌓는다 — 줄이는 장치 없이 계속 쌓으면 비용이 점점 오르다 어느 순간 컨텍스트 한계로 호출이 실패합니다. 길어질 수 있는 대화라면 처음부터 줄이는 전략을 둡니다.
- system을 잘라낸다 —
system은 별도 파라미터라messages를 자를 때 영향받지 않습니다. 역할 지침을messages안에 넣어 뒀다면 잘려 나갈 수 있으니, 지침은system에 둡니다. - 컴팩션에서 텍스트만 저장한다 — 컴팩션을 쓸 때
response.content전체가 아니라 텍스트만 히스토리에 넣으면 요약 상태가 사라집니다.
마무리 #
이번 글에서는 길어지는 대화의 히스토리를 다루는 방법을 정리했습니다.
- 히스토리는 토큰 비용과 컨텍스트 한계 때문에 계속 쌓아 둘 수 없습니다.
- 슬라이딩 윈도우는 최근 것만 남겨 단순하고, 요약은 오래된 정보를 압축해 살립니다.
- 직접 관리하는 대신 서버의 컴팩션 기능에 맡길 수도 있습니다.
여기까지 단일 도구 호출, 검색, 메모리를 다뤘습니다. 다음 글인 “LLM 앱 개발 실전 #10 AI 에이전트 만들기"에서는 이 조각들을 묶어, Claude가 스스로 도구를 골라 여러 단계를 밟아 가며 일을 처리하는 에이전트를 만들겠습니다.