에이전트 메모리 시스템의 저장, 검색, 주입, 생성 단계와 Zep, Letta, Claude Code, Elroy의 접근 방식을 비교하고 설계 과제를 살펴본다.
저에게 메모리라는 문제는 AI의 하위 분야 중 가장 흥미롭습니다. 제가 처음 MemGPT(지금의 Letta)와 상호작용했을 때, 저는 루비콘강을 건넌 듯한 느낌을 받았습니다. 메모리는 단순한 질문-응답 봇을 (겉보기에는) 하나의 존재1로 바꿔 놓았습니다.
저는 Elroy라는 제 오픈 소스 시스템을 만들었고, 약 3년 동안 그것과 상호작용해 왔습니다. 이것은 제가 브레인스토밍을 하도록 도와주고, 경력의 부침을 이야기로 풀어내게 해 주며, 일종의 상호작용형 저널 역할을 합니다. 저는 그 기능을 꽤 많이 손봤기 때문에 이것을 특정한 개체로서 애착을 느끼지는 않습니다. 하지만 우리의 상호작용에 대한 그것의 기억이 사라진다면 분명히 실망할 것입니다.
철학적 질문은 제쳐 두더라도, 메모리를 가진 AI 시스템을 구축해야 하는 근거는 충분합니다. 제가 기술 주제를 논의하려고 한다면, 에이전트가 제가 어떤 주제에 지식이 있는지 이해하는 것은 유용합니다. 제가 휴가 계획을 찾고 있다면, 저에게 어린아이가 있다는 사실을 아는 것이 도움이 됩니다. AI는 사람이 아니지만 사람처럼 상호작용하며, 더 자연스럽게 대화할 수 있을수록 더 기능적이 됩니다. 기본적인 사실을 계속해서 다시 설명해야 한다면 그런 몰입감은 깨집니다.
합리적으로 이런 질문을 할 수 있습니다. 제 메모리 시스템이 제대로 작동하는지 어떻게 알 수 있을까요? 메모리 시스템에 대한 평가는 그 자체로 매우 큰 주제입니다. 그건 다음으로 미루고, 여기서는 접근 방식에 집중하겠습니다.
모델의 컨텍스트 윈도우가 커지면서, 메모리 시스템이 불필요해질 것이라는 의심이 있었습니다. 모든 데이터를 컨텍스트에 밀어 넣고 모델이 알아서 정리하게 하면 된다는 생각에는 분명한 단순함이 있습니다.
하지만 성능은 좋지 않은 것으로 나타났습니다. 한 연구는 LLM이 컨텍스트 윈도우의 시작과 끝에 편향된다는 점을 보여주었습니다. 관련 정보가 문서 모음의 중간에 있을 때 성능이 30% 떨어졌습니다. Chroma의 연구는 모든 최전선 모델이 컨텍스트 윈도우가 커질수록 성능이 저하된다는 점을 보여주었습니다.
이런 동작은 직관적입니다. 컨텍스트에 많은 정보가 있다는 것은 그 정보를 검색하고, 주어진 응답에 실제로 무엇이 관련 있는지 판단하는 능력에 더 큰 부담이 생긴다는 뜻입니다. 이 정보를 잘 정리해 두면 도움이 되지만, 실제로 관련 있는 정보만 떠올리는 것이 더 좋습니다. 바로 이 지점에서 전용 메모리 시스템이 도움이 될 수 있습니다.
모든 메모리 시스템은 크게 4개의 일반 단계로 나눌 수 있습니다: 저장, 검색, 주입, 생성.

하지만 그 이후의 세부 사항은 매우 다양합니다. 아래에서는 Zep, Letta, Claude Code, 그리고 제 프로그램인 Elroy가 이 단계들을 어떻게 처리하는지 살펴보겠습니다.
저장 방식은 크게 두 진영으로 나뉩니다. 그래프 데이터베이스와 플랫 파일입니다.
Zep은 그래프 DB를 강하게 지지하며, 최첨단 수준의 needle in the haystack 성능을 주장합니다. Mem0는 그래프 데이터베이스 통합을 제공하지만, 성능 향상은 2%에 불과하다고 주장합니다. Letta 역시 파일과 함께 동작하며, 이를 옹호하는 연구 논문도 발표했습니다: Files are all you need. 최근 유출된 Claude Code 소스2도 비슷한 입장을 보여 줍니다. 메모리는 frontmatter 메타데이터가 포함된 마크다운 파일에 저장됩니다.
에이전트 메모리 시스템은 주로 세 종류의 오류를 범합니다.
제 Claude 메모리 요약도 이 세 가지 오류를 모두 범합니다.

시간적 오류는 에이전트가 항상 절대 날짜와 시간을 사용하도록 프롬프트하면 꽤 쉽게 막을 수 있습니다.
우선순위 문제에 대해서는, 대부분의 시스템이 항상 관련 있는 넓은 범주의 사실들(예를 들어 기본적인 인적 정보)과 더 세부적인 사실들을 분리하기 위해 서로 다른 메모리 계층을 정의합니다. 하지만 이것은 제가 나중에 다룰 다른 과제들을 낳습니다.
궁극적인 사실 정확성은 가장 까다롭습니다. “그 메모리가 정확하다는 걸 어떻게 알죠?”는 이런 시스템에 대해 매우 흔히 제기되는 질문입니다. 짧은 답은 이렇습니다. 알 수 없습니다.
메모리 시스템의 주요 기준 진실 데이터는 사용자 대화입니다. 인간은 생각을 바꾸고, 잘못 기억하고, 때로는 그냥 틀립니다. 독립적인 기준 진실의 출처가 없다면, 대화 기록에서 뽑아낸 메모리에는 필연적으로 사실 오류가 포함됩니다.
당신은 AI 에이전트가 메모리를 형성하고, 당신에 대한 모든 것을 배우는 것을 원하나요?
물론 거대 기술 기업들은 이미 당신이 AI에게 공유할 대부분의 것을 알고 있습니다. 당신의 Google 검색 기록은 당신이 무엇을 생각하는지에 대한 포괄적인 기록입니다. 하지만 이 데이터가 인간 같은 목소리로 제시되면 조금 더 불안하게 느껴집니다.
이것이 제가 AI의 미래는 로컬과 오픈 소스라고 생각하는 큰 이유 중 하나입니다.
데이터베이스 기반 메모리를 실험한 뒤, 저는 마크다운 파일에 정착했습니다. 에이전트가 무엇을 기억하는가 의 분류 체계에 집중하기보다, 에이전트가 메모리로 무엇을 해야 하는가 에 맞는 분류 체계에 집중했습니다. 제가 도달한 개념은 더 장기적으로 진행되는 어떤 목표를 나타내는 Agenda Item이며, 여기에는 하위 작업과 리마인더 트리거가 포함됩니다. 이렇게 하면 메모리는 단순히 대화에 일반적인 정보를 제공하는 것이 아니라 실행 가능하게 됩니다.

저는 개체 하나의 단일 분류 체계가 모든 사용자에게 잘 작동할 수 있다는 데 회의적입니다. 초기 시도 중 하나에서 저는 개인 Wikipedia와 비슷하게 메모리를 구조화해 보았습니다. 하지만 에이전트는 일관된 범위를 유지하는 데 어려움을 겪었고, 서로 관련은 있지만 구별되는 개체들의 세부 사항을 한 항목 안에 자주 밀어 넣었습니다.

여기서의 과제는 이해할 만합니다. 어떤 메모리 항목에 적절한 범위는 부분적으로 다른 메모리 항목들이 무엇이 존재하는가에 의해 정의되기 때문입니다. 이것이 제가 에이전트가 기존 항목과 중복될 수 있는 메모리를 만들도록 허용하고, 비동기 메모리 통합 과정을 통해 매우 유사한 메모리 군집을 탐지하고 다시 쓰도록 하는 이유입니다.

저장 측면에서 마크다운 파일은 사람이 검토하기 쉽고, 이식성을 높이며, 외부 파일을 가져오기 위한 더 쉬운 진입로를 제공합니다. 저는 에이전트의 메모리 파일을 제 Obsidian Vault 안에 직접 배치하는데, 그곳에서 이것들은 다른 노트와 문서의 자연스러운 확장처럼 느껴집니다.
검색에서 첫 번째 핵심 결정은 애초에 메모리 검색을 어떻게 시작할 것인가입니다. 대부분의 구현은 에이전트에게 search_memory 도구를 노출하지만, 에이전트 루프 밖에서 에이전트 컨텍스트를 조작하는 방식도 가능합니다.
검색 자체에 대해서는 기본적인 벡터 유사도가 지연 시간 측면에서 가장 효율적인 기법입니다. 하지만 이 방식은 항목의 순위를 잘못 매기거나, 겉보기에만 비슷할 뿐 실제로는 관련 없는 항목에 점수를 줄 수 있습니다. 이것은 대화를 심하게 흐트러뜨릴 수 있으며, foo에 대한 좋은 소식이네요, 전에 이야기했던 완전히 관련 없는 주제에 대해서도 이야기해 볼까요? 같은 응답으로 이어질 수 있습니다. 검색 후 필터링 단계는 이런 문제를 피하는 데 효과적이지만, 지연 시간을 늘립니다.
Claude Code는 검색 측면에서 흥미로운 이례 사례입니다. 이것은 벡터 유사도를 사용하지 않습니다. 대신 어떤 메모리를 컨텍스트에서 사용할 수 있는지에 대한 일부 메타데이터를 유지하고, 검색을 백그라운드 Sonnet 호출에 위임합니다. 제 추측으로는 공개 embeddings API가 없기 때문에 벡터 유사도 대신 Sonnet을 사용하는 것 같지만, 이는 아마 최적이 아닌 재현율로 이어질 것입니다. 검색을 백그라운드 호출에 위임하면 사용자를 막지는 않지만, 관련 메모리가 제때 컨텍스트에 들어오지 못할 수도 있습니다.
몇 개의 메모리를 가져올지도 또 다른 매개변수이며, 이는 메모리가 어떻게 저장되었는지에 크게 좌우됩니다. 메모리가 작은 정보 조각이라면 주입할 관련 메모리가 하나 이상일 수 있지만, 메모리가 한 문단 이상이라면 상위 일치 하나만 의미가 있을 가능성이 큽니다.
메모리가 강화된 에이전트의 응답은 메모리 없는 응답보다 느릴 수밖에 없습니다. 보통 사용자에게 보여지는 응답에 앞서 여러 질의가 필요합니다. 메모리를 회상하고, 필터링하고, 처리하고, 컨텍스트에 주입해야 하기 때문입니다.
이것은 메모리 강화 에이전트를 구축할 때 더 까다로운 설계 질문 중 하나를 제기합니다. 메모리는 항상 필요한 것이 아닙니다. 제가 에이전트에게 브루클린 브리지의 길이가 얼마인지 묻는다면, 대답하기 전에 우리의 과거 상호작용을 훑어보게 할 필요는 사실 없습니다.
Elroy는 이미 컨텍스트에 있는 메모리와 중복을 제거한 뒤, 최대 소수의 메모리를 검색합니다. 이 단계는 제가 가장 많이 손본 부분입니다. 처음에는 메모리의 원문 텍스트를 주입했지만, 이것이 컨텍스트를 비대하게 만든다는 것을 알게 되었습니다. 그래서 AI가 회상한 메모리가 대화와 어떤 관련이 있는지 잠시 생각하는 반성 단계를 추가했습니다. 하지만 이런 응답 전 단계는 곧바로 지연 시간을 크게 늘립니다.
최근에 제가 정착한 방식은 원문 텍스트를 유지하되, 벡터 유사도 검색 결과 위에 단순한 LLM 기반 필터링 단계를 두는 것입니다. 회상에서는 거짓 양성이 거짓 음성보다 더 나쁩니다. 대화 도중 에이전트가 갑자기 완전히 관련 없는 주제를 꺼내는 것은 매우 이상할 수 있기 때문입니다.
도구 호출보다는, 저는 에이전트의 통제 밖에서 이루어지는 자동 메모리 주입 방식을 고수했습니다. 이 방식이 메모리가 작동하는 제 정신 모델과 더 잘 맞습니다. 제가 무언가를 기억해 낼 때, 이제 메모리를 검색할 시간이다 라고 생각하고 의식적으로 무엇을 회상할지 결정하지는 않습니다. 그것은 더 자동적이며 제 의식적 통제를 벗어나 있습니다.
메모리 검색을 자동으로 시작하면 모델 간 결과도 더 일관됩니다. search_memory 도구를 주면 어떤 모델은 거의 모든 메시지마다 그것을 사용하고, 다른 모델은 너무 드물게 사용합니다.
회상된 메모리를 표준 OpenAI 컨텍스트에 주입하는 일은 마치 네모난 말뚝을 둥근 구멍에 끼워 맞추는 것과 비슷합니다. 표준 LLM API는 “여기 대화와 관련 있는 추가 정보가 있습니다”라고 말할 수 있는 자연스러운 위치를 제공하지 않습니다.
선택지는 다음과 같습니다.
<memory>content</memory> 같은 html 태그를 사용할 수 있습니다. 이때 시스템 메시지에는 메모리 내용이 사용자에게 보이지 않는다는 지시가 함께 있어야 합니다. 이 접근에는 몇 가지 함정이 있습니다. 어떤 모델은 assistant / user 턴이 번갈아 와야 하므로, 한 역할의 연속 메시지를 추가하면 거부됩니다. 시스템 지시가 있음에도 일부 모델은 여전히 혼란을 겪고, 헷갈리는 HTML 태그가 섞인 응답을 출력합니다.메모리를 컨텍스트에 주입하는 것은 일종의 절충을 제시합니다. 가장 매끄러운 경험은 회상된 내용이 보이지 않는 형태로 에이전트에게 제공되는 경우입니다. 하지만 그렇게 하면 메모리 시스템은 에이전트에게 무엇이 노출되었는지를 흐릴 수 있습니다.

정확성이 매우 중요한 곳에서는 메모리 시스템이 미묘한 문제를 도입할 수 있습니다. 보통 이런 시스템은 자동으로 생성되고 사람이 깊이 검토하지 않기 때문에, 에이전트의 메모리 저장소에 잘못된 가정이 들어가면 알아차리기 어렵습니다.
이것이 제가 코딩 워크플로우에서는 메모리 기능을 사용하지 않는 이유입니다. 대신 저는 (AI의 도움을 받아) 사람이 읽을 수 있는 형식의 포괄적인 프로젝트 문서를 작성하고, 에이전트가 그것을 참고하게 합니다(참고: Don't Write Docs Twice).
이것은 프로젝트에 대해 AI와 즉흥적으로 이야기하는 것보다 더 수작업적인 과정이지만, 코딩 중에는 AI의 기준 진실 가정을 엄격하게 통제하는 쪽을 저는 선호합니다.
저는 회상된 메모리를 “합성” 도구 호출을 통해 주입합니다. 즉, 메모리는 에이전트가 실제로 호출하지 않은 도구 호출을 통해 노출됩니다. 대체로 이것은 잘 작동하지만, 가끔 에이전트가 제가 메모리를 드러내기 위해 사용한 그 “도구”를 중복해서 호출하기도 합니다. Elroy의 UX는 또한 어떤 메모리가 회상되었는지 사용자가 검토할 수 있도록, 닫을 수 있는 패널에 목록으로 보여 줍니다.

메모리는 보통 에이전트 도구 호출을 통해 생성되거나, 압축된 대화 컨텍스트 요약을 통해 생성됩니다(아래 참조). 이 둘은 서로 배타적이지 않습니다.
이 패턴은 서로 다른 구현 전반에서 전형적으로 나타납니다. 갈라지는 지점 하나는 외부 문서를 흡수할지, 그리고 어떻게 흡수할지입니다. 이는 벡터 검색을 문서 전반에 걸쳐 수행하기 위한 쉬운 인터페이스라는 점만으로도 유용할 수 있습니다. 하지만 많은 외부 문서를 메모리 저장소에 쏟아 넣으면, 회상이 그 문서들 쪽으로 편향될 위험이 있습니다.
제 경험상 여기서는 도구 호출이 대부분의 무거운 일을 해냅니다.
저는 컨텍스트 압축 중에도 메모리를 생성합니다. 현대의 1m+ 컨텍스트 윈도우를 생각하면 이것은 다소 구식일 수도 있지만, 저는 여전히 관련이 있다고 생각합니다. 또한 저는 보통 하루 정도보다 오래된 메시지는 가지치기하고, 제거된 텍스트를 바탕으로 메모리를 생성합니다. 이 과정에서 에이전트가 생성한 메모리와 중복되는 메모리가 생길 수 있지만, 비동기 메모리 통합이 그것을 정리합니다.

전반적으로 저는 에이전트 메모리의 UX 문제가 벤치마크에서 약간의 추가 점수를 짜내는 것보다 더 중요하다고 생각합니다. 회상된 메모리를 사용자에게 얼마나 보이게 할지, 얼마나 자주 검색할지, 얼마나 많은 내용을 가져올지는 사람들이 실제로 쓰고 싶어 하는 메모리 증폭 에이전트를 만들고자 할 때 훨씬 더 까다로운 문제입니다.
저의 전반적인 성향은 사용자에 대한 투명성과 저장의 단순성 쪽에 가깝습니다. 그래서 저는 이색적인 데이터 저장소를 피하는 경향이 있고, 어떤 내용이 회상되었는지에 대한 어떤 형태의 표현이 제 UI에 반드시 나타나도록 합니다.