AI 메모리는 프롬프트 트릭이 아니라 제품 상태(Product State)여야 합니다
요약
AI 제품 구축 시 메모리를 단순한 프롬프트 주입 방식이 아닌, 사용자가 소유하고 제어할 수 있는 '제품 상태(Product State)'로 설계해야 함을 강조합니다. 단기 컨텍스트와 저장된 메모리를 분리하여 아키텍처 관점에서 접근할 것을 제안합니다.
핵심 포인트
- 메모리를 단순 프롬프트 트릭이 아닌 시스템 아키텍처의 핵심 요소로 다뤄야 함
- 단기 컨텍스트, 요약, 명시적 사실 등 메모리의 다양한 유형을 구분하여 관리 필요
- 사용자가 메모리 데이터를 소유하고 제어할 수 있는 권한(삭제, 수정 등) 보장 필수
- 저장된 메모리와 프롬프트 시점의 메모리를 분리하여 설계할 것을 권장
성찰적인 (reflective) AI 제품을 구축하는 과정에서 메모리 문제에 직면했습니다.
AI 메모리의 쉬운 버전은 유혹적입니다:
사용자에 대한 유용한 사실을 저장한다.
그것들을 다음 프롬프트 (prompt)에 다시 넣는다.
그것을 메모리라고 부른다.
이 방식은 위험도가 낮은 개인화 (personalization)에는 효과적일 수 있습니다.
어시스턴트가 특정 리포지토리 (repo)가 pnpm을 사용한다는 점을 기억하거나, 사용자가 짧은 답변을 선호한다는 점, 또는 팀이 스테이징 브랜치 (staging branch)를 preview라고 부른다는 점을 기억하는 정도라면 아마 괜찮을 것입니다.
하지만 사용자가 개인적인 자료를 제품으로 가져오기 시작하면 문제는 달라집니다.
제 경우, 이 제품은 사람들이 꿈, 기분, 관계 패턴, 반복되는 상징, 그리고 사적인 성찰을 탐색할 수 있게 해줍니다. 저는 의미 있는 세션들이 가공되지 않은 채팅 기록 (raw chat history) 속으로 사라지는 것을 원하지 않았습니다. 하지만 사용자가 입력한 모든 문장이 조용히 영구적인 메모리가 되는 것도 원하지 않았습니다.
그 점이 저의 아키텍처 (architecture)를 바꾸어 놓았습니다.
질문은 더 이상 다음과 같지 않았습니다:
모델이 무엇을 기억할 수 있는가?
대신 다음과 같이 바뀌었습니다:
사용자가 소유하는 것은 무엇인가?
사용자가 무엇을 승인했는가?
언제 저장되는가?
...
이러한 구분은 제품 디자인 (product design)처럼 들리지만, 빠르게 시스템 디자인 (system design)의 영역으로 넘어갑니다.
흔한 실수: 메모리를 하나의 바구니로 취급하는 것
많은 AI 제품들은 마치 메모리가 단 하나인 것처럼 설명합니다:
메모리: 켜기/끄기
이것은 단순하지만, 너무 많은 것을 숨깁니다.
실제로 "메모리"는 보통 여러 가지 서로 다른 작업들을 혼합합니다:
- 단기 대화 컨텍스트 (short-term conversation context)
- 지난 세션의 요약 (a summary of the last session)
- 사용자가 명시적으로 기억되기를 원하는 사실들
- 모델이 추론한 패턴 (model-inferred patterns)
- 사용자가 작성한 배경 컨텍스트 (user-authored background context)
- 현재 턴 (turn)을 위해 선택된 검색 결과 (retrieval results)
- 내보내기(export) 또는 삭제가 가능해야 하는 계정 데이터
이 모든 것이 하나의 보이지 않는 바구니가 된다면, 개발자는 더 단순한 구현을 얻게 되지만 사용자는 더 불투명한 제품을 얻게 됩니다.
또한 다음과 같은 기본적인 질문에 답할 수 있는 능력도 상실하게 됩니다:
- 어시스턴트가 왜 이 내용을 언급했나요?
- 그것이 자동으로 저장되었나요?
- 이것이 향후 프롬프트 (prompts)에 사용되나요?
- 모든 것을 삭제하지 않고 이 항목 하나만 삭제할 수 있나요?
- 메모리를 일시 중지하면 어떻게 되나요?
- 구독이 종료되면 어떻게 되나요?
이것들은 단순한 UX (User Experience) 질문이 아닙니다. 이것들은 아키텍처 (architecture) 질문입니다.
저장된 메모리 (Stored memory)와 프롬프트 메모리 (prompt memory)는 다릅니다
저에게 가장 큰 아키텍처적 변화는 저장된 메모리 (stored memory)를 프롬프트 시점의 메모리 (prompt-time memory)와 분리하는 것이었습니다.
사용자가 저장된 메모리 자산을 소유하고 있을 수 있지만, 그렇다고 해서 모델이 항상 그것들을 사용할 수 있어야 한다는 의미는 아닙니다.
결국 저는 메모리를 두 가지 서로 다른 상태 (states)로 생각하게 되었습니다:
이것은 사용자에게 속해 있습니다.
이것은 현재 프롬프트에 들어가는 것이 허용됩니다.
이 두 가지는 동일한 약속이 아닙니다.
사용자는 어시스턴트가 향후 모든 세션에서 메모리를 자동으로 사용하는 것과 별개로, 저장된 메모리를 조회, 내보내기(export) 또는 삭제할 수 있어야 합니다.
이러한 분리는 작지만 중요한 액세스 계층 (access layer)으로 이어졌습니다:
type MemoryAccessState = {
userMemoryEnabled: boolean;
planKey: string;
...
중요한 필드는 userMemoryEnabled가 아닙니다.
바로 canUsePromptMemory입니다.
이 필드는 실제 런타임 (runtime) 질문에 답합니다: 이번 턴 (turn)의 프롬프트에 메모리를 포함해야 하는가?
이렇게 함으로써 혼란스러운 제품 약속을 방지할 수 있습니다.
예를 들어:
- 무료 사용자는 메모리 자산을 저장하고 있을 수 있지만, 향후 프롬프트에는 활성화되지 않을 수 있습니다.
- 유료 사용자는 세션에 장기 메모리 (long-term memory)를 허용할 수 있습니다.
- 사용자는 자산을 삭제하지 않고 메모리를 일시 중지할 수 있습니다.
- 구독이 중단된 경우, 모델이 메모리를 호출하게 하지 않으면서도 정의된 기간 동안 메모리를 유지할 수 있습니다.
- 프롬프트 메모리가 비활성화된 상태에서도 삭제 및 내보내기 기능은 여전히 작동할 수 있습니다.
이러한 분리가 없다면, 메모리는 숨겨진 권한 상태 (privilege state)가 됩니다. 데이터는 존재하고, 사용자는 그중 일부를 보며, 모델은 그것을 사용할 수도 있고 사용하지 않을 수도 있는데, 아무도 그 경계를 명확하게 설명할 수 없게 됩니다.
패턴: 라이프사이클 (lifecycle)에 따라 메모리를 분리하기
저는 메모리를 하나의 기능으로 취급하는 것을 멈추고, 일련의 제품 상태 (product states)로 취급하기 시작했습니다.
단순화된 형태는 다음과 같습니다:
conversation (대화)
-> session note (세션 노트)
-> user-approved memory item (사용자 승인 메모리 항목)
...
각 계층은 서로 다른 라이프사이클 (lifecycle)을 가집니다.
conversation은 가공되지 않은 교환 데이터입니다. 동일 세션 내의 연속성을 유지하는 데는 유용하지만, 그 자체만으로는 노이즈가 너무 많아 장기 메모리 (long-term memory)가 되기에는 부적합합니다.
session note는 세션이 종료된 후 생성되는 구조화된 산출물 (artifact)입니다. 주제, 상징, 갈등 및 유용한 연속성 포인트를 요약할 수 있습니다.
memory item은 더 작고 명시적입니다. 사용자가 직접 검토하고 승인할 수 있는 종류의 항목입니다.
room context는 사용자가 작성한 배경 정보입니다. 사용자가 직접 작성했기 때문에, 추론된 메모리 (inferred memory)보다 더 높은 신뢰도를 가진 것으로 취급합니다.
inner map은 반복되는 주제에 대한 버전 관리된 장기 스냅샷 (long-term snapshot)입니다. 이 계층은 실제보다 더 권위 있게 들릴 위험이 있으므로 주의가 필요합니다.
retrieval evidence는 시스템이 현재 턴 (turn)을 위해 선택한 데이터입니다.
prompt context는 모델이 보게 되는 최종적으로 컴파일된 메모리입니다.
이 방식은 작업량을 늘리지만, 시스템을 추론하기 더 쉽게 만듭니다.
사용자는 모든 자산 (asset)이 프롬프트에 활성화되어 있지 않더라도 자산을 소유할 수 있습니다. 어시스턴트는 저장된 모든 이력이 똑같이 중요하다는 척하지 않고도 관련 메모리를 사용할 수 있습니다. 제품은 매번 새로운 예외 사항을 만들어낼 필요 없이, 각 계층을 일시 중지, 유지, 내보내기 또는 삭제할 수 있습니다.
검색 (Retrieval)은 느낌(vibes)이 아니라 근거(evidence)를 반환해야 합니다
메모리가 계층화되면, 검색 (retrieval) 또한 더 명시적으로 변해야 합니다.
메모리 결과는 단순히 프롬프트에 붙여넣어진 텍스트여서는 안 됩니다. 왜 그것이 선택되었는지 설명할 수 있는 충분한 메타데이터 (metadata)를 포함해야 합니다.
단순화된 버전은 다음과 같습니다:
type MemoryEvidence = {
source: "session-note" | "memory-item";
id: string;
...
reason 필드는 화려하지는 않지만 유용합니다.
다음과 같은 내용을 담을 수 있습니다:
semantic long-memory match (의미론적 장기 메모리 일치)
strong theme/symbol overlap (강한 주제/상징 중첩)
partial approved-memory overlap (부분적 승인 메모리 중첩)
...
이는 디버깅을 도울 뿐만 아니라, 제품이 정직함을 유지하도록 돕습니다.
만약 어시스턴트(Assistant)가 현재 턴(turn)에 특정 메모리를 가져온다면, 시스템은 왜 그 메모리가 선택되었는지 설명할 수 있어야 합니다.
첫 번째 버전에서는 검색 (Retrieval)이 마법 같을 필요는 없습니다. 저는 보수적인 하이브리드 접근 방식 (Hybrid approach)을 선호합니다:
사용자가 작성한 컨텍스트 (user-authored context)
+ 최신 장기 스냅샷 (latest long-term snapshot)
+ 관련 세션 노트 (relevant session notes)
...
벡터 검색 (Vector search)은 유용하지만, 그것이 유일한 규칙이 되어서는 안 됩니다.
개인용 또는 성찰적 제품의 경우, 최신성 (Recency), 명시적 승인 (Explicit approval), 사용자가 작성한 컨텍스트 (User-authored context), 그리고 명확한 폴백 동작 (Fallback behavior)은 유사도 점수 (Similarity score)만큼이나 중요합니다.
작은 실패 사례: 소름 끼치는 회상 (The creepy callback)
이것은 제가 피하고 싶었던 종류의 동작입니다.
사용자가 언젠가 다음과 같이 썼다고 가정해 봅시다:
잠겨 있는 정원 문에 대한 꿈을 계속 꿔요.
2주 후 사용자가 다음과 같이 씁니다:
오늘 도움을 요청하고 싶었을 때 수치심을 느꼈어요.
잘못된 메모리 동작:
이것은 당신의 잠겨 있는 정원 문 꿈과 연결됩니다.
그럴 수도 있고, 아닐 수도 있습니다.
이러한 회상 (Callback)은 영리해 보일 수 있지만, 만약 사용자가 그 연결을 요청하지 않았고 시스템이 왜 그 메모리를 불러왔는지 설명할 수 없다면, 이는 소름 끼치게 느껴질 수 있습니다.
더 나은 동작:
여기에는 어떤 주제적 공통점이 있을 수 있지만, 제가 억지로 연결 짓지는 않겠습니다.
만약 연결되는 것처럼 느껴진다면, 오늘의 수치심과 이전의 문 이미지를 비교해 볼 수 있습니다.
그렇지 않다면, 오늘 일어난 일에만 집중하겠습니다.
말투는 다르지만, 진짜 차이는 아키텍처 (Architecture)에 있습니다.
시스템은 다음 사항들을 알아야 합니다:
- 이전 노트가 사용자에 의해 승인된 것인가, 아니면 모델이 추론한 것인가?
- 의미적 유사성 (Semantic similarity), 주제 중첩 (Theme overlap), 또는 단순한 최신성 (Recency) 때문에 선택되었는가?
- 사용자가 이를 검사할 수 있는가?
- 사용자가 이를 삭제할 수 있는가?
- 다음 턴이 시작되기 전에 메모리를 일시 중지할 수 있는가?
만약 이러한 답변들이 오직 모델의 응답 내부에만 존재한다면, 그 제품은 잘못된 곳에서 너무 유연한(soft) 것입니다.
메모리 경계 (Memory boundary)는 프롬프트 (Prompt) 외부에 존재해야 합니다.
보존 (Retention) 또한 메모리 설계의 일부입니다
또 다른 실수는 보존 (Retention)을 단지 법적/개인정보 보호 페이지의 문제로만 취급하는 것입니다.
메모리 집약적인 제품에서, 보존 (Retention)은 제품 동작의 일부입니다.
저는 규칙을 지루하고 명시적으로 만드는 것을 선호합니다:
활성 구독 (Active subscription):
메모리 축적 가능
활성화 시 메모리가 프롬프트에 포함될 수 있음
...
중요한 부분은 정확한 일수가 아닙니다. 중요한 점은 제품이 소유권 (Ownership), 접근 (Access), 그리고 사용 (Usage)의 경계를 모호하게 만들지 않는 것입니다.
사용자는 저장된 메모리가 현재 활성화되어 있는지 추측해야만 해서는 안 됩니다.
어시스턴트가 메모리를 사용하는 것을 중단시키기 위해 모든 것을 삭제해야 하는 상황도 발생해서는 안 됩니다.
제가 유지할 가드레일 (Guardrails)
민감하거나 성찰적인 AI 제품의 경우, 저는 다음과 같은 가드레일로 시작할 것입니다:
- 원문 전사 데이터 (Raw transcript)를 장기 메모리 계층 (Long-term memory layer)으로 취급하지 마십시오.
- 더 작은 메모리 항목을 생성하기 전에 구조화된 세션 노트 (Structured session note)를 생성하십시오.
- 강력한 장기 메모리 항목에 대해서는 명시적인 승인을 요구하십시오.
- 저장된 메모리 (Stored memory)와 프롬프트 시점의 메모리 (Prompt-time memory)를 분리하십시오.
- 계정 또는 세션 UI에 메모리 상태를 표시하십시오.
- 사용자가 저장된 자산을 삭제하지 않고도 메모리를 일시 중지할 수 있게 하십시오.
- 사용자가 개별 메모리 항목을 삭제할 수 있게 하십시오.
- 내보내기 (Export) 및 삭제 경로를 지루할 정도로 단순하고 신뢰할 수 있게 유지하십시오.
- 검색된 메모리가 왜 프롬프트에 포함되었는지 기록하십시오.
- 결제 또는 계정 상태가 변경될 때 보존 (Retention) 규칙을 명시하십시오.
이 중 어느 것도 복잡한 에이전트 프레임워크 (Agent framework)를 필요로 하지 않습니다.
이것은 메모리를 프롬프트 장식 (Prompt decoration)이 아닌 제품 상태 (Product state)로 취급할 것을 요구합니다.
이것이 과잉 대응 (Overkill)이 될 수 있는 경우
이 패턴은 일부 제품에는 너무 무겁습니다.
저는 다음과 같은 경우에는 이 모든 것을 구축하지 않을 것입니다:
- 일회성 데모 (Throwaway demo)
- 상태가 없는 어시스턴트 (Stateless assistant)
- 해롭지 않은 UI 선호도만 기억하는 도구
- 사용자와 개발자가 동일 인물인 개인용 로컬 프로토타입 (Private local prototype)
반면, 다음과 같은 경우에는 가치가 있습니다:
- 사용자가 민감하거나 개인적인 자료를 가져오는 경우
- 메모리가 향후 모델의 동작을 변화시키는 경우
- 제품에 계정, 결제, 내보내기 또는 삭제 약속이 있는 경우
- 사용자가 AI가 왜 무언가를 기억하는지 합리적으로 물을 수 있는 경우
- 연속성 (Continuity)이 유료 가치의 일부인 경우
마지막 포인트가 중요합니다.
만약 메모리가 사람들이 비용을 지불하는 요소의 일부라면, 그것은 단순한 프롬프트 트릭 (Prompt trick)이 되어서는 안 됩니다.
제가 이것을 적용하는 방법
저는 꿈, 기분, 상징, 그리고 반복되는 패턴을 위한 비임상적 AI 자기 탐색 공간인 Jung Room을 작업하면서 이 패턴을 구축했습니다.
해당 제품 특화 버전은 다음과 같이 구성됩니다:
세션 노트 (session notes)
+ 저장된 메모리 항목 (saved memory items)
+ 사용자가 작성한 룸 컨텍스트 (user-authored room context)
...
이 제품을 통해 얻은 일반적인 교훈은 이 제품보다 더 광범위합니다:
메모리는 강력해지기 전에 검사 가능(inspectable)해야 합니다.
빌더 체크리스트 (Builder checklist)
AI 제품에 메모리를 추가하고 있다면, 저는 초기에 다음과 같은 질문들을 던질 것입니다:
- 정확히 무엇이 저장되고 있는가?
- 어떤 부분이 사용자에 의해 작성되었는가?
- 어떤 부분이 모델에 의해 추론(inferred)되었는가?
- 어떤 부분에 명시적인 승인(explicit approval)이 필요한가?
- 어떤 부분이 향후 프롬프트(prompts)에 포함될 수 있는가?
- 사용자가 프롬프트 시점의 메모리(prompt-time memory)를 일시 중지할 수 있는가?
- 사용자가 계정을 삭제하지 않고도 하나의 메모리만 삭제할 수 있는가?
- 결제 상태(billing state)가 변경되면 어떤 일이 발생하는가?
- 검색(retrieval)에 실패했을 때의 폴백(fallback)은 무엇인가?
- 왜 특정 메모리가 선택되었는지 설명할 수 있는가?
만약 이러한 질문들에 대한 답이 불분명하다면, 메모리 기능이 기술적으로는 작동할지 몰라도 사용자에게는 여전히 신뢰할 수 없게 느껴질 수 있습니다.
열린 질문 (Open question)
여러분은 본인의 AI 앱에서 메모리를 어떻게 처리하고 계신가요?
메모리를 개인적인 프롬프트 컨텍스트 (private prompt context), 사용자가 소유한 제품 상태 (user-owned product state), 검색 증거 (retrieval evidence), 또는 완전히 다른 무언가로 취급하고 계신가요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기