LLM 에이전트의 컨텍스트 윈도우(Context Window)를 무작정 채우지 마세요: Mem0를 활용한 구조화된 메모리 카테고리
요약
LLM 에이전트의 컨텍스트 윈도우 효율성을 높이기 위해 Mem0를 활용한 구조화된 메모리 관리 방법을 제안합니다. 모든 정보를 동일하게 저장하는 대신, 정보의 성격에 따라 메모리 타입을 분류하여 검색 정확도를 높이고 불필요한 정보 유입을 방지합니다.
핵심 포인트
- 단순한 메모리 저장은 컨텍스트 윈도우를 잡다한 정보로 채우는 문제를 야기함
- 사용자 사실, 피드백, 프로젝트 상태, 참조 등 메모리를 유형별로 구분해야 함
- Mem0의 메타데이터 필터링을 활용해 타입화된 메모리 스키마 구축 가능
- 정보의 유효 기간과 중요도에 따른 차별화된 검색 전략이 필요함
Stop Stuffing Your LLM Agent's Context Window: Structured Memory Categories with Mem0
LLM 에이전트에게 "메모리 (Memory)"를 부여하는 대부분의 튜토리얼은 동일한 세 줄의 코드를 보여줍니다:
m = Memory()
m.add("User likes dark mode", user_id="alice")
m.search("What does the user prefer?", user_id="alice")
이 방식은 데모에서는 작동합니다. 하지만 몇 주 동안 실행되는 실제 에이전트에서는 무너집니다. 왜냐하면 모든 사실을 동일하게 중요하고 동일하게 영구적인 것으로 취급하기 때문입니다. 실제로 에이전트는 감쇠(decay)되고, 검색(retrieval)되며, 완전히 다른 방식으로 무효화되는 최소 네 가지의 _서로 다른 종류_의 메모리를 축적합니다. 만약 이들을 모두 같은 방식으로 저장한다면, 두 가지 실패 모드 중 하나를 겪게 됩니다. 에이전트가 오래된 프로젝트 상태를 여전히 유효한 것처럼 다시 읽거나, search()를 호출할 때마다 가치 낮은 잡다한 정보(trivia)로 컨텍스트 윈도우 (Context Window)를 가득 채워버리는 것입니다.
이 글에서는 작동하는 코드와 함께, 이 두 가지 문제를 모두 해결하는 Mem0 기반의 타입화된 메모리 스키마 (typed memory schema)를 살펴봅니다.
"그냥 모두 저장하기"가 실패하는 이유
당신의 에이전트가 동일한 저장소(repo)에서 세션에 걸쳐 작업하는 코딩 어시스턴트라고 가정해 봅시다. 몇 주가 지나면 에이전트는 다음과 같은 것들을 학습하게 됩니다:
- "사용자는 백엔드 엔지니어이며, React는 처음이다."
- "통합 테스트에서 데이터베이스를 모킹(mock)하지 마세요. 지난 분기에 문제가 있었습니다."
- "인증(auth) 재작성은 법적 검토로 인해 차단되었으며, 예상 완료 시간(ETA)은 다음 주 목요일입니다."
- "버그 보고서는 Linear의
INGEST프로젝트에서 추적됩니다."
이것들은 텍스트로 볼 때는 비슷해 보이지만, 동작 방식은 완전히 다릅니다:
- **사용자 사실 (User facts)**은 안정적입니다. 거의 변하지 않으며 거의 항상 검색되어야 합니다.
- **피드백/수정 사항 (Feedback/corrections)**은 행동 규칙입니다. 잡다한 정보로 드러나는 것이 아니라 조용히 적용되어야 합니다.
- **프로젝트 상태 (Project state)**는 빠르게 감쇠합니다. "다음 주 목요일 ETA"는 일주일만 지나도 거짓이 되며, 정보가 오래된 후 검색되면 적극적으로 해가 됩니다.
- **참조 (References)**는 외부 시스템을 가리키는 포인터입니다. 그것들이 언제 유효한지에 대한 컨텍스트 없이는 무용지물입니다.
단순한 memory.add(text) 호출로는 이를 표현할 방법이 없습니다. 나중에 search()를 호출하면, Mem0의 관련성 순위 지정(relevance ranking)은 3주 전의 "다음 주 목요일 도착 예정(ETA)"이라는 메모와 영구적인 사용자 선호도를 나란히 노출할 것입니다. 두 메모 모두 사용자의 쿼리에 대한 의미론적 유사성(semantic similarity) 점수가 비슷하기 때문입니다.
Mem0의 메타데이터를 사용한 타입화된 스키마 (Typed Schema)
Mem0의 add()는 임의의 metadata를 허용하며, search()/get_all()은 이를 기반으로 한 필터링(filtering)을 지원합니다. 이는 Mem0의 내부 구조를 건드리지 않고도 가벼운 타입 시스템(type system)을 구축하기에 충분합니다.
from mem0 import Memory
m = Memory()
...
이제 검색은 하나의 맹목적인 의미론적 검색(semantic search) 대신 두 단계의 프로세스가 됩니다. 먼저 종류(kind)별로 관련 메모리를 가져온 다음, LLM이 각 타입을 어떻게 사용할지 결정하게 합니다.
def load_context(user_id, query):
facts = m.search(query, user_id=user_id, filters={"kind": "user"})
rules = m.get_all(user_id=user_id, filters={"kind": "feedback"})
...
이제 여러분은 구분되지 않은 하나의 메모리 덤프(memory dump) 대신, 세 개의 뚜렷한 섹션으로 시스템 프롬프트(system prompt)를 구성합니다. 즉, "사용자가 누구인지", "위반해서는 안 될 고정 규칙은 무엇인지", "현재 진행 중인 사항은 무엇인지"를 나누는 것입니다. 특히 feedback 종류의 메모리는 의미론적으로 검색하기보다는 시스템 프롬프트 상단에 무조건적으로(unconditionally) 주입되어야 합니다. "pre-commit hook을 건너뛰지 마세요"와 같은 수정 사항은 현재 쿼리가 "hook"과 어휘적 중복(lexical overlap)이 없더라도 적용되어야 하기 때문입니다.
감쇠(Decay) 처리: 대부분의 통합 과정에서 생략되는 부분
프로젝트 상태(Project-state) 메모리는 정보가 오래되었을 때 실제 버그를 유발하는 주범입니다. Mem0는 메모리를 자동으로 만료시키지 않으므로, 쓰기 경로(write path)뿐만 아니라 읽기 경로(read path)에도 만료 로직을 구축해야 합니다.
from datetime import date
def load_project_state(user_id, query):
...
(별도의 cron job 대신) 읽기 시점에 가지치기(Pruning)를 수행하면 추가 인프라 없이도 저장소를 스스로 정화(self-cleaning) 상태로 유지할 수 있으며, 프롬프트의 "프로젝트 상태" 섹션을 위한 토큰 예산이 무제한으로 늘어나지 않음을 의미합니다.
가장 많은 토큰을 낭비하게 만드는 검색 실수
제가 Mem0 통합 과정에서 목격한 가장 큰 비효율성은, 모든 메모리 유형(memory kind)에 대해 매 턴마다 사용자의 원문 메시지를 쿼리로 하여 top_k 기본값으로 search()를 한 번씩 호출하는 것이었습니다. 이는 매 턴마다 3~4번의 벡터 검색(vector search)과 수 KB의 검색된 텍스트를 발생시키며, 그중 대부분은 "그게 작동했나요?"와 같은 짧은 후속 질문과는 무관한 내용입니다.
두 가지 저렴한 해결책:
feedback유형의 메모리를 세션 동안 프로세스 내에서 캐싱(Cache in-process) 하세요. 매 턴 Mem0에 다시 쿼리하는 대신, 이들은 변경되는 일이 드물기 때문에 세션 시작 시 한 번만 로드하면 됩니다.- 진행 중인 작업과 관련 없는 쿼리가 들어오는 턴에서는
project유형의 검색을 완전히 건너뛰세요. (단순한 키워드 게이트 — "상태(status)", "여전히(still)", "차단됨(blocked)", "언제(when)" — 만으로도 재현율(recall)을 해치지 않으면서 검색량의 3분의 1을 줄일 수 있습니다.)
결과
단일 평면 저장소(flat store)를 사용하는 대신 메모리를 유형별로 분리한 것은 제가 관리하는 장기 실행 코딩 에이전트(coding agent)에서 세 가지 구체적인 효과를 가져왔습니다. 첫째, 오래된 "진행 중(in progress)" 메모리가 현재의 사실로 다시 읽히는 것을 방지했습니다. 둘째, 행동 교정(behavioral corrections)이 의미론적 운(semantic luck)에 의존하는 대신 일관되게 적용되도록 했습니다. 셋째, 만료(expiry)와 캐싱을 가능하게 함으로써 평균 턴당 검색된 메모리 토큰을 약 40% 절감했습니다. 이 중 어느 것도 mem0ai가 이미 제공하는 기능 이상의 것을 요구하지 않습니다. 모든 사실을 동일하게 취급하는 기본 메모리 저장소 위에 실제 타입 시스템(type system)을 구축하기에는 metadata와 filters만으로도 충분합니다.
만약 단일 세션 이상 실행되도록 설계된 에이전트에 Mem0를 통합하고 있다면, 스키마(schema)를 신중하게 설계할 가치가 있습니다. SDK 호출 자체는 쉬운 부분입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기