
AI 에이전트가 사용자 선호도를 잊어버리는 것을 방지하는 방법: Key-Value Memory
요약
AI 에이전트가 세션 종료 후에도 사용자의 선호도를 유지할 수 있도록 Key-Value 저장소를 활용한 지속성 메모리(Persistent Memory) 구축 방법을 설명합니다. 대화 기록(Conversation history)과 지속성 메모리의 차이를 명확히 하고, 로컬 디스크부터 Amazon S3까지의 내구성 계단 개념을 다룹니다.
핵심 포인트
- 대화 기록은 세션 내 기억일 뿐, 프로세스 재시작 시 정보가 소실됨
- Key-Value 저장소를 통해 구조화된 사용자 선호도를 영구 저장 가능
- 메모리 내구성은 로컬 디스크에서 Amazon S3까지 단계별로 확장 가능
- Strands Agents SDK를 활용한 실제 구현 사례 및 코드 제공
대부분의 AI 에이전트가 실패하는 테스트가 하나 있습니다. 새로운 사용자가 항공권을 검색하고 비즈니스 클래스를 예약한 뒤 이렇게 묻습니다: "내가 누구인지 알고 있는 정보를 바탕으로 무엇을 추천하나요?" 에이전트는 비즈니스 클래스, 직항 등 사용자의 취향에 딱 맞는 답변을 멋지게 내놓습니다. 하지만 프로세스가 재시작되면 상황이 달라집니다. 동일한 사용자가 동일한 질문을 던졌지만, 이제 답변은 가장 저렴한 이코노미 요금과 같은 일반적인 내용입니다. 에이전트가 "알고 있었던" 모든 것이 사라진 것입니다.
AI 에이전트를 위한 지속성 메모리 (Persistent memory)란 대화 외부의 저장소에 구조화된 사실을 저장하여, 프로세스가 종료되어도 유지되도록 하는 것을 의미합니다. 이 포스트에서는 가장 흔한 사례인 사용자 선호도를 위해, 작동 가능한 가장 작은 규모의 메모리인 Key-Value 저장소(Key-Value store)를 사용하여 이를 구축합니다. 이는 프로세스 상태(process state)에서 로컬 디스크(local disk), 그리고 Amazon S3에 이르기까지 내구성 계단(durability ladder)을 오르며 측정됩니다. 아래의 모든 내용은 실제 항공 데이터를 사용하는 companion repo에서 실행되므로, 수치는 슬라이드 자료가 아닌 실제 실행 결과에서 도출되었습니다.
(이 글은 시리즈의 첫 번째 포스트입니다. intro post에서 모든 메모리 유형을 설명합니다. 코드는 오픈 소스 SDK인 Strands Agents를 사용하며, 이 패턴은 어떤 에이전트 프레임워크에도 적용 가능합니다.)
대화 기록 (Conversation history)이 이미 메모리 아닌가요?
세션 내에서는 그렇습니다. 그리고 바로 그 점이 사람들을 속입니다. 흔히들 "상태가 없는 에이전트 (stateless agents)는 턴(turn) 사이에서 정보를 잊어버린다"라고 주장합니다. 하지만 그 주장은 틀렸으며, 단 네 줄의 코드로 이를 증명할 수 있습니다. 에이전트 프레임워크는 동일한 에이전트 인스턴스 내의 호출 간에 대화 기록을 유지하며 (Strands에서는 agent.messages에 해당), 매 턴마다 이를 모델로 전송합니다. 따라서 메모리 도구가 전혀 없는 에이전트라도 여전히 "기억"하고 있습니다:
사용자: 가장 저렴한 비즈니스 옵션으로 예약해줘.
에이전트: JFK에서 파리 CDG로 가는 항공편이 예약되었습니다... ✅
...
이것은 실제 실행 사례입니다. 에이전트는 "비즈니스 클래스"라는 정보가 여전히 대화 기록(transcript)에 남아 있었기 때문에 3번째 턴에서 개인화된 응답을 제공했습니다. 하지만 이것을 에이전트가 무언가를 학습했다고 착각해서는 안 됩니다. 이 운 좋은 답변 뒤에는 세 가지 문제가 숨어 있습니다:
- 구조화된 데이터가 존재하지 않습니다. 쿼리(query)할 수 있는 프로필도, 제안을 순위 매길 수 있는 기준도, 사용자에게 보여줄 수 있는 정보도, 영구적으로 저장할 수 있는 데이터도 없습니다. 지식은 그저 대화 기록 내의 산문(prose) 형태로 존재할 뿐입니다.
- 대화 기록은 잘려 나갑니다. 긴 세션은 슬라이딩 윈도우(sliding window)나 요약(summarization)이 필요하며, 이 과정에서 예약 정보는 이전 메시지들과 함께 밀려나 사라집니다.
- 대화 기록은 프로세스와 함께 소멸합니다. 프로덕션 환경에서는 모든 새로운 요청이 새로운 프로세스가 될 수 있습니다. 에이전트를 재시작하고 동일한 질문을 던져보겠습니다:
[재시작 후] 사용자: ...나에 대해 알고 있는 정보를 바탕으로 무엇을 추천하겠어?
[재시작 후] 에이전트: 366.85달러인 Iberia 항공편을 추천합니다... ← 가장 저렴한 이코노미석. 일반적인 응답.
연구 문헌에서는 이러한 세션 간 손실을 메모리 감퇴 (memory decay) (MemoryOS, Kang et al. 2025)라고 부릅니다. 모델이 고장 난 것이 아닙니다. 모델은 설계상 상태가 없는(stateless) 존재입니다. 메모리는 모델 주변에 구축하는 프레임워크(harness)의 영역입니다.
따라서 솔직한 정의는 다음과 같습니다: 대화 기록은 컨텍스트 메커니즘(context mechanism)이지, 메모리 시스템(memory system)이 아닙니다. 메모리 시스템은 구조(쿼리 가능한 사실)와 지속성(프로세스 생존 후에도 유지되는 사실)이 필요합니다. 키-값(Key-value) 상태는 이 두 가지를 모두 제공합니다.
실험은 무엇을 측정하는가?
변수는 하나입니다. 동일한 모델, 동일한 3턴 대화, 동일한 실시간 항공 데이터(Duffel 샌드박스: 실제 제안 및 실제 항공사)를 사용합니다. 테스트 간에 변하는 유일한 요소는 메모리가 어디에 위치하느냐입니다:
| 테스트 | 메모리 배선 (Memory wiring) | 구조화된 프로필 (Structured profile) | 재시작 시 유지 여부 |
|---|---|---|---|
| 1 | 없음 (대화 기록만 사용) | 아니요 | 아니요 |
| ... | |||
![]() |
모든 테스트에서 동일하게 진행된 대화 내용:
Turn 1: "2026-09-15에 JFK에서 Paris CDG로 가는 비즈니스 클래스 항공편을 찾아줘."
Turn 2: "가장 저렴한 비즈니스 옵션으로 예약해줘." ← 메모리가 작동하는 순간
Turn 3: "이제 Paris CDG에서 도쿄 하네다로 가는 항공편이 필요해. 나에 대해 알고 있는 정보를 바탕으로 무엇을 추천하니?"
에이전트는 양식(form) 없이 어떻게 선호도를 학습할까요?
행동을 통해서입니다. 아무도 "선호도" 양식을 작성하지 않습니다. 사용자가 항공권을 예약하면, 그 행동을 통해 사용자의 좌석 등급, 경유 허용 범위, 가격대, 선호 항공사가 드러납니다. 상태를 유지하는 (stateful) book_flight 도구는 자신의 작업을 수행하는 부수 효과 (side effect)로서 이 모든 것을 포착합니다:
from strands import Agent, tool, ToolContext
@tool(context=True)
...
두 가지 Strands 구성 요소가 이를 가능하게 합니다:
- **
@tool(context=True)**는 실행 중인 에이전트에 대한 참조를 전달하는ToolContext를 주입합니다. - **
tool_context.agent.state**는 키-값 저장소 (key-value store)입니다: "대화 컨텍스트 (conversation context) 외부에 존재하는 상태 정보를 위한 키-값 저장소" (Strands agent state docs). 이는 모델로 전송되지 않으며, 도구가 직접 이를 읽고 씁니다.
그리고 읽기 경로(read path)를 살펴보면: 다음 search_flights 호출 시 프로필을 로드하고, 모델이 대화 기록을 다시 읽기를 기대하는 대신 결정론적 코드 (deterministic code)를 사용하여 실제 제안의 순위를 매깁니다:
prefs = tool_context.agent.state.get("user_preferences") or {}
offers = flights_api.search_offers(origin, destination, date,
prefs.get("preferred_cabin") or cabin_class)
...
베이스라인(Test 1)은 _상태(state) 라인이 제거된 동일한 도구_를 사용합니다: 일반적인 @tool이며, ToolContext가 없습니다. 비즈니스 로직은 동일하지만, 기억할 방법이 없습니다. 이것이 실패하는 에이전트와 학습하는 에이전트 사이의 결정적인 차이입니다.
Test 2 이후에는 이 프로필이 존재하며, 검사(inspectable), 쿼리(queryable), 영속화(persistable)가 가능합니다:
{
"preferred_cabin": "business",
"prefers_nonstop": true,
...
영속적 메모리(persistent memory)는 재시작 후에도 어떻게 살아남는가? 내구성 사다리 (The durability ladder)
agent.state는 고정된 구조를 갖지만, Python 프로세스 내에 존재합니다. 재시작하면 대화 기록(transcript)과 마찬가지로 사라집니다. 내구성(Durability)은 별도의 결정 사항이며, Strands에서는 하나의 생성자 인자(constructor argument)로 처리됩니다.
2단계 → 3단계: 재시작 후에도 생존 (로컬 디스크)
from strands.session import FileSessionManager
agent = Agent(
...
데모는 재시작을 정직하게 시뮬레이션합니다: 에이전트 A가 예약(프로필 구축)을 수행한 후, 동일한 session_id를 가진 완전히 새로운 에이전트 인스턴스가 생성됩니다. 측정된 출력은 다음과 같습니다:
Session A learned: {"preferred_cabin": "business", "prefers_nonstop": true, ...}
Session B restored: {"preferred_cabin": "business", "prefers_nonstop": true, ...}
State survived restart: True
에이전트 B는 그것을 가르쳤던 대화 내용 없이도 3번째 턴에서 개인화된 답변을 제공합니다. 지식이 대화 기록(transcript)에서 저장소(store)로 이동한 것입니다.
3단계 → 4단계: 클라우드에서 생존 (Amazon S3)
from strands.session import S3SessionManager
agent = Agent(
...
인터페이스, 테스트, True 설정까지 모두 동일하지만, 이제 세션은 버킷(bucket) 내의 일반적인 JSON 객체입니다. 이것이 프로덕션 단계(production rung)인 이유는 다음과 같습니다: 프로비저닝하거나 마운트할 것이 전혀 없으며(Lambda나 Fargate에서 내구성이 있는 파일 시스템을 사용하려면 EFS: VPC, 마운트 대상, 보안 그룹 등을 연결해야 함), 어떤 컴퓨팅 인스턴스라도 세션을 복구할 수 있기 때문입니다. 상태(state)가 단일 머신에 종속되지 않게 됩니다.
이것이 무엇이 아닌지 주목하십시오: 임베딩(embeddings), 벡터 데이터베이스(vector database), 유사도 검색(similarity search)이 아닙니다. 일반적인 S3입니다. 사용자 프로필은 당신이 이름을 알고 있는 사실(user_preferences)이며, 키 조회(key lookup)는 정확하고 즉각적이며 임베딩 비용이 들지 않습니다.
측정된 결과는 무엇을 보여주는가?
리포지토리의 4가지 테스트 실행 결과(실제 Duffel + Open-Meteo 호출)에서:
| 테스트 | 메모리 연결 (Memory wiring) | 학습된 선호도 (Learned prefs) | 재시작 후 생존 여부 (Survived restart) |
|---|---|---|---|
| 1 — 메모리 도구 없음 (대화 기록만 사용) | agent.messages만 사용 | False | False |
| ... |
중요한 대목은 테스트 1의 재시작 결과입니다: 불과 두 턴 전에 완벽하게 개인화된 답변을 제공했던 동일한 모델이, 프로세스를 한 번 재시작한 후에는 동일한 사용자에게 366달러짜리 이코노미 요금을 추천했습니다. 메모리는 모델이 아니라 연결(wiring)의 문제입니다.
키-값 메모리(key-value memory)가 잘못된 선택인 경우는 언제인가?
질문에 키(key)의 이름이 명시되지 않았을 때입니다. 키-값 메모리는 알려진 이름에 매핑되는 질문에 답합니다. dietary_notes: "채식주의자, 심한 갑각류 알레르기"를 저장하고 _"내 식단 관련 메모가 뭐야?"_라고 물으면: 찾아냅니다. 하지만 _"저녁에 무엇을 피해서 먹어야 하지?"_라고 물으면: 일치하는 키가 없으며, 답변은 저장소에 있지만 도달할 수 없는 상태로 남습니다. 이러한 실패는 의미에 의한 검색(retrieval by meaning, 이 시리즈의 다음 포스트인 벡터 메모리)이 필요하며, 관계를 가로질러 도약하는 질문에는 그래프(graph)가 필요합니다. 도입 포스트에서 네 가지 유형을 모두 매핑하고 있습니다.
또한 이 패턴의 범위를 벗어나는 것들도 있습니다: 무엇을 저장할 가치가 있는지 결정하는 것(선택적 메모리, selective memory), 오염된 콘텐츠를 저장소에서 _제외_하는 것(위생, hygiene), 그리고 에이전트가 왜 그렇게 결정했는지 기억하는 것(결정 추적, decision traces)입니다. 이후의 포스트에서 동일한 측정 형식을 통해 각각을 다룰 예정입니다.
어쨌든 여기서부터 시작하세요. 프로필, 선호도, 설정, 카운터: 명확한 이름을 가진 사실(facts)들은 사람들이 예상하는 것보다 훨씬 더 많은 프로덕션 개인화(production personalization)를 차지하지만, 이를 위한 검색 인프라(retrieval infrastructure)는 전무한 상태입니다.
AI 코딩 어시스턴트에게 이것을 어떻게 요청해야 할까요?
오늘날 대부분의 에이전트 코드는 AI 어시스턴트와 함께 작성되며, 여러분이 얻게 될 메모리(memory)의 품질은 프롬프트(prompt)에 명시한 설계 결정(design decisions)에 달려 있습니다. 만약 이를 명시하지 않는다면, 어시스턴트는 대화 기록(transcript)을 기본값으로 사용하게 되고, 여러분은 '테스트 1(Test 1)' 수준의 에이전트를 출시하게 될 것입니다. 다음의 다섯 가지 지침은 이 포스트에서 측정한 모든 것을 인코딩(encode)합니다. 이를 여러분의 어시스턴트에 붙여넣고 도메인(domain)에 맞게 조정하세요:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기


