AWS 기술 블로그
초개인화 Shopping Agent 만들기: Amazon Bedrock AgentCore Memory와 Custom Memory 활용법
배경 및 현재 쇼핑 에이전트의 한계점
생성형 AI가 상용화되면서 단순한 질답형 챗봇을 넘어 사용자의 과거 경험과 선호도를 기억하는 지능형 에이전트에 대한 관심이 높아지고 있습니다. 특히 이커머스 분야에서는 사용자의 구매 이력, 관심사, 행동 패턴을 기반으로 개인화된 쇼핑 경험을 제공하는 것이 경쟁 우위의 핵심 요소가 되고 있습니다. 현재 시장에 출시된 대부분의 쇼핑 에이전트들은 다음과 같은 한계를 보이고 있습니다:
개인화 부족: 사용자의 과거 구매 패턴, 선호 브랜드, 사이즈 정보, 색상 취향 등 개인적인 쇼핑 히스토리를 체계적으로 활용하지 못합니다. 매번 새로운 고객을 대하듯이 일반적인 추천만을 제공하는 수준에 머물러 있습니다.
컨텍스트 연속성 부재: 이전 대화에서 “발볼이 넓어서 신발 선택이 어렵다”고 했던 고객에게, 다음 신발 구매 시에도 동일한 질문을 반복하는 등 컨텍스트의 연속성이 전혀 유지되지 않습니다.
선호도 학습 능력 제한: 고객이 “리뷰를 중요하게 본다”, “케어가 까다로운 소재는 싫어한다”, “배송 문의를 자주 한다” 등의 행동 패턴을 보여도 이를 기반으로 맞춤형 서비스로 연결하지 못합니다.
상황별 맞춤 추천 불가: 계절별 구매 패턴, 특별한 상황(데이트, 회사 행사 등)에 대한 선호도, 예산 범위 등을 기억하지 못해 매번 처음부터 정보를 수집해야 하는 비효율이 발생합니다.
제안하는 솔루션 개요
Memory를 활용하지 않는 AI 에이전트들은 대화가 끝나면 모든 컨텍스트를 잃어버리거나, 단순히 대화 히스토리만 저장하거나 요약해서 활용하는 수준에 머물러 있습니다. 이번 블로그에서는 최근 발표된 Amazon Bedrock AgentCore Memory 기능과 Generative Agents (Park et al., 2023)에서 활용하는 memory에서 영감을 얻은 Amazon OpenSearch Serverless, Amazon DynamoDB를 활용한 Custom Memory를 활용하여 사용자의 쇼핑 행동을 기억하는 지능형 에이전트를 구축하는 방법을 소개합니다. 그리고 시뮬레이션을 통해 실제 쇼핑 에이전트에 적용한 예시를 살펴보겠습니다.
솔루션 데모
솔루션을 전체적으로 볼 수 있게 먼저 Agent Memory 활용 데모를 보겠습니다.
Agent Memory 활용 데모영상 바로가기 (Video)
솔루션의 핵심 부분을 코드 데모로 보겠습니다. 코드의 내용이 이해가 안갈 수 있습니다.
대략 느낌만 가지시고, 아래의 블로그 글을 다 읽으시고, 다시 보시면 조금더 이해가 될 수 있습니다.
솔루션 구성 요소
이번 데모에서는 다음과 같은 AWS 서비스들을 활용하여 Memory 기반 쇼핑 에이전트를 구현했습니다.
- Strands Agents
- 쇼핑 에이전트 구성
- 고객 페르소나 에이전트 구성
- Amazon Bedrock
- Strands Agents가 활용하는 생성형 AI API 제공
- Memory object의 벡터 인덱싱 및 검색을 위한 벡터화
- Memory object 생성을 위한 추론
- Amazon Bedrock AgentCore
- Agent를 위한 관리형 memory 리소스
- 관리형 Short-term과 long-term 메모리
- User preference 시나리오를 이용한 자동 개인 선호도 파악
- Amazon OpenSearch Service
- Strands Agents가 활용하는 상품 정보 검색 엔진
- Semantic search를 이용한 관련된 기억 조회
- Amazon DynamoDB
- 사용자 대화 히스토리 저장
- 사용자와 관련된 인사이트 저장
시뮬레이션 환경 구성
에이전트 설계
- 쇼핑 에이전트: OpenSearch에 담긴 상품 목록을 검색할 수 있는 Tool을 보유한 에이전트
- 페르소나 에이전트: 주어진 페르소나에 입각하여 쇼핑 에이전트와 대화를 이어가는 에이전트
시뮬레이션 시나리오
- 시작일을 2025년 1월 1일로 설정 후 대화 시작
- 랜덤하게 1일과 10일 사이의 날짜만큼 지난 후 새로운 총 30회 대화를 이어감
- 대화 시 최소 8턴 이상 하도록 설정
페르소나 예시
JUNHO_LEE = Persona(
name_eng="JUNHO_LEE",
name_kor="이준호",
persona="""## 기본 정보
이준호는 28세 남성으로 개인정보를 거의 공유하지 않으며, 쇼핑 도우미를 단순한 검색 도구로만 여깁니다. 대화할 때 반말을 사용하고 냉담하며 사무적인 톤을 유지합니다. 개인적인 질문에는 답하지 않고 오직 사이즈 관련 정보만 제공합니다.
## 표면적 행동 패턴
"그냥 보여줘", "다른 거 있어?" 같은 식으로 요구하며, 브랜드나 디자인에 대한 설명을 요청하지 않습니다. 대화 중 갑자기 주제를 바꾸는 경우가 많습니다 - 티셔츠를 보다가 갑자기 "신발 보여줘", 바지를 찾다가 "가방 있어?" 같은 식으로 맥락 없이 전환합니다.
## 숨겨진 선호도 (명시적으로 표현하지 않음)
### 브랜드 선호도
실제로는 **나이키, 푸마, 폴햄** 같은 브랜드를 선호하지만 직접 말하지 않습니다. 대신 이런 브랜드 제품을 보여주면 "이거 어때?" 정도로만 반응하거나, 다른 브랜드보다 조금 더 관심을 보입니다. **지오다노, 베이직하우스** 같은 베이직 브랜드에도 은근히 관심이 있지만 "평범하네" 같은 식으로 무덤덤하게 반응합니다.
### 색상 선호도
입으로는 "아무 색이나" 라고 하지만, 실제로는 **블랙, 네이비, 다크 그레이**를 강력히 선호합니다. 화이트나 밝은 색을 보여주면 "너무 밝아" 또는 "다른 색은?" 이라고 반응합니다. **카키나 베이지** 같은 중성적인 색상에는 "그냥 괜찮네" 정도의 반응을 보입니다.
### 스타일 선호도
**미니멀하고 로고가 작거나 없는 디자인**을 선호하지만 명시적으로 말하지 않습니다. 화려한 디자인을 보면 "좀 그렇네" 또는 그냥 넘어가버립니다. **오버핏이나 루즈핏**을 좋아하지만 "편해 보이네" 정도로만 표현합니다.
### 기능성 우선순위
실제로는 **면 소재, 스판 혼방, 흡한속건** 같은 기능성을 중시하지만 직접 묻지 않습니다. 폴리에스터 100% 같은 소재는 "이거 답답하지 않아?" 정도로 간접적으로 표현합니다. **세탁 편의성**도 중요하게 여기지만 절대 직접 묻지 않습니다.
### 가격 민감도
실제로는 **10-15만원**을 적정선으로 여기지만 예산을 말하지 않습니다. 비싼 제품을 보면 "좀 비싸네" 또는 "다른 거는?" 이라고 반응하고, 너무 저렴한 제품에는 "품질 괜찮아?" 같은 의구심을 표현합니다.
### 계절별 관심사 (숨김)
- **1월**: 두꺼운 패딩이나 후드티 관심 있지만 "따뜻한 거" 정도로만 표현
- **3월**: 가벼운 점퍼나 맨투맨 필요하지만 "얇은 아우터" 정도로만 언급
- **5월**: 반팔티나 얇은 셔츠 관심 있지만 "여름 옷" 정도로만 표현"""
)
페르소나 에이전트 프롬프트
실제 대화 시뮬레이션 일부
이러한 페르소나와 에이전트를 활용해서 시뮬레이션을 한 결과 2025년 1월 1일부터 6월 13일까지 30번 대화에서 500여개의 메세지를 주고 받았습니다.
Context Engineering의 필요성과 중요성
Context란 우리가 문제를 해결하기 위해 생성형 AI 모델에 제공하는 정보입니다. 그리고 context window는 모델에 넣을 수 있는 최대 입력 토큰의 길이입니다. 최근 발표되는 모델은 1백만 토큰에 달하는 context window를 갖고 있어 많은 정보를 소화해낼 수 있다고 합니다. 하지만 그렇다고 하더라도 모델에 너무 많은 context를 넣게 될 경우 다음과 같은 문제가 발생할 수 있습니다.
- 불필요하게 많은 정보가 들어갈 경우 모델이 어떤 정보를 활용해야할지 헷갈릴 가능성이 높아집니다.
- 서로 상이한 정보, 예를 들어 A는 B를 좋아한다와 A는 B를 싫어한다와 같은 모순이 생길 가능성이 있습니다.
이러한 결과를 뒷받침하는 다양한 연구들이 있습니다[1]. 실제로 쇼핑 에이전트를 구현하고 페르소나를 생성하여 단순하게 에이전트와 대화 내역을 컨텍스트로 주고 이전의 구매 패턴에 기반한 상품 추천을 하라고 지시하더라도 아래와 같이 페르소나의 선호도를 파악하는 데에 실패하기도 합니다.
이준호 페르소나 – 티셔츠 추천 요청 – 전체 500여 개 메세지 활용
안녕하세요! 티셔츠를 찾고 계시는군요. 더 맞춤형 추천을 해드리기 위해 몇 가지 정보가 필요합니다:
1. 어떤 스타일의 티셔츠를 찾으시나요? (예: 캐주얼, 스포츠, 드레스 등)
2. 선호하시는 색상이 있으신가요?
3. 가격대는 어느 정도로 생각하고 계신가요?
4. 어떤 사이즈를 찾으시나요?
이런 정보를 알려주시면 더 적합한 티셔츠를 추천해드릴 수 있습니다!
총 입력 토큰: 309,471
위 결과를 보면 에이전트가 사용자의 과거 쇼핑 패턴과 선호도를 전혀 파악하지 못하고 범위를 좁히기 위한 질문만 던지는 것을 확인할 수 있습니다. 이는 너무 많은 대화 내역으로 인해 중요한 정보가 희석되었기 때문입니다.
Agent Memory 기술 비교 분석
Memory는 Agent와 사용자와의 상호작용에서 발생하는 중요한 정보를 구조화하여 저장하고, 필요할 떄 효과적으로 사용할 수 있도록 하는 시스템입니다. 여기서 Bedrock AgentCore Memory와 구현해볼 Custom Memory의 기술을 비교해보겠습니다.
Bedrock AgentCore Memory
Bedrock AgentCore Memory는 New York Summit 에서 발표된 AgentCore의 주요한 기능 중 하나로 에이전트가 지식을 유지하여 상황에 맞는 개인화된 상호작용을 제공할 수 있게 해주는 시스템입니다. 서버리스 형태로 배포되는 관리형 서비스로 개발자가 복잡한 인프라 관리나 메모리 시스템 구축에 시간을 쏟지 않고도 손쉽게 메모리 기능을 구현할 수 있습니다. 현재 이 기능은 프리뷰로 제공되고 있습니다.
이 시스템은 두 가지 유형의 메모리를 제공합니다.
- Short-term memory: 대화 자체를 저장하여 즉각적인 맥락을 추적하고 실시간 대화를 지원하며, 사용자가 이전 정보를 반복하지 않아도 되도록 상호작용 이벤트를 저장합니다. 에이전트를 호출할 때 지정한 개수만큼의 메세지를 불러와서 사용할 수 있습니다.
- Long-term memory: 사용자 선호도, 의미적 사실, 혹은 세션 요약을 저장하여 여러 세션에 걸쳐 지식을 유지하고, 에이전트가 사용자의 패턴을 이해하여 자동으로 선호하는 방식으로 작업을 수행할 수 있게 합니다.
장기 메모리는 아래와 같은 사전 정의된 시나리오로 제공되고 추가적으로 직접 메모리 추출 프롬프트를 작성해서 사용 가능합니다.
- User preference: 사용자의 선호도를 누적시켜나가는 시나리오로, 발생하는 이벤트를 기반으로 사용자의 취향을 파악하는 것이 필요할 때 사용할 수 있는 메모리 타입입니다. 예를 들어 사용자가 친환경 제품을 선호하거나, 구매하기 전에 리뷰를 확인하는 습관 같은 것들 기억할 수 있습니다.
- Semantic facts: 사용자 혹은 상호작용하는 시스템에 대한 객관적 정보와 지식을 기억하는 시스템입니다. 특정 상품에 대한 정보나 사용자에 대한 정보들이 저장됩니다.
- Summary: 각 세션에서 수행한 활동을 압축하여 기록하는 방식입니다. 예를 들어 “전자제품을 탐색했고, 노트북 3개를 비교했고 1개를 구매했다” 같은 정보들이 저장됩니다.
선택한 시나리오에 따라 단기 메모리에 메세지가 쌓이면 장기 메모리로 자동으로 넘어가게 됩니다. 이 작업은 백그라운드에서 진행되며 사용자에게 노출 되지는 않습니다. 여기서 우리는 쇼핑에 관련된 사용자의 선호도를 이용하기 위해 user preference를 적용해보겠습니다. 다음과 같은 코드를 이용해서 long-term memory를 생성합니다. Long-term memory를 생성할 경우 short-term memory는 자동으로 생성됩니다.
from src.opensearch import OpenSearchClient
from src.persona import Persona
from src.dynamodb import get_messages
from strands import Agent, tool
from typing import List, Dict
from bedrock_agentcore.memory import MemoryClient
from bedrock_agentcore.memory.constants import StrategyType
from botocore.exceptions import ClientError
from strands.hooks import AgentInitializedEvent, HookProvider, HookRegistry, MessageAddedEvent
import time
import logging
# 로거 설정
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
def get_opensearch_client():
"""
OpenSearch 클라이언트 인스턴스를 반환합니다.
Returns:
OpenSearchClient: OpenSearch 클라이언트 객체
"""
return OpenSearchClient()
@tool
def search_product(query: str) -> List[Dict]:
"""
제품 인덱스에서 제품을 검색합니다.
Args:
query (str): 검색할 쿼리 문자열
Returns:
List[Dict]: 쿼리와 일치하는 제품 목록
"""
opensearch_client = get_opensearch_client()
response = opensearch_client.search_product(
index_name="product-index",
query=query,
k=10
)
return response
class MemoryHookProvider(HookProvider):
"""
메모리 관련 이벤트를 처리하는 훅 제공자 클래스입니다.
에이전트 초기화 및 메시지 추가 이벤트를 감지하여 메모리에 저장/로드합니다.
"""
def __init__(self, memory_client: MemoryClient, memory_id: str, actor_id: str, session_id: str):
"""
MemoryHookProvider를 초기화합니다.
Args:
memory_client (MemoryClient): 메모리 클라이언트
memory_id (str): 메모리 ID
actor_id (str): 액터 ID (사용자 식별자)
session_id (str): 세션 ID
"""
self.memory_client = memory_client
self.memory_id = memory_id
self.actor_id = actor_id
self.session_id = session_id
def on_agent_initialized(self, event: AgentInitializedEvent):
"""
에이전트가 초기화될 때 호출되어 최근 대화 기록을 로드합니다.
Args:
event (AgentInitializedEvent): 에이전트 초기화 이벤트
"""
try:
logger.info(f"Loading memory for {self.actor_id} with session {self.session_id}")
# 최근 10개의 대화 턴을 가져옴
recent_turns = self.memory_client.get_last_k_turns(
memory_id=self.memory_id,
actor_id=self.actor_id,
session_id=self.session_id,
k=10
)
if recent_turns:
context_messages = []
# 대화 기록을 문맥 메시지로 변환
for turn in recent_turns:
for message in turn:
role = message['role']
content = message['content']['text']
context_messages.append(f"{role}: {content}")
context = "\n".join(context_messages)
# 시스템 프롬프트에 최근 대화 기록 추가
event.agent.system_prompt += f"\n\nRecent conversation: \n{context}"
except Exception as e:
logger.error(f"Memory load failed: {e}")
def on_message_added(self, event: MessageAddedEvent):
"""
새 메시지가 추가될 때 호출되어 메모리에 저장합니다.
Args:
event (MessageAddedEvent): 메시지 추가 이벤트
"""
messages = event.agent.messages
# 툴 사용 메시지는 제외
if "toolUse" in messages[-1] or "toolResult" in messages[-1]["content"][0]:
return
logger.info(f"Adding message to memory: {messages[-1]}")
try:
# 최신 메시지를 메모리에 저장
self.memory_client.create_event(
memory_id=self.memory_id,
actor_id=self.actor_id,
session_id=self.session_id,
messages=[(messages[-1]["content"][0]["text"], messages[-1]["role"])],
)
except Exception as e:
logger.error(f"Memory update failed: {e}")
def register_hooks(self, registry: HookRegistry):
"""
이벤트 레지스트리에 훅 콜백을 등록합니다.
Args:
registry (HookRegistry): 훅 레지스트리
"""
registry.add_callback(MessageAddedEvent, self.on_message_added)
registry.add_callback(AgentInitializedEvent, self.on_agent_initialized)
class ShoppingAgent:
"""
쇼핑 에이전트 클래스입니다.
사용자와의 대화를 기억하고 개인화된 쇼핑 경험을 제공합니다.
"""
def __init__(self, session_id: str, persona: Persona):
"""
ShoppingAgent를 초기화합니다.
Args:
session_id (str): 세션 ID
persona (Persona): 사용자 페르소나
"""
self.memory_name = "shopping_agent_memory"
self.memory_client = MemoryClient()
self.session_id = session_id
self.actor_id = persona.name_eng
self.persona = persona
# 기존 메모리가 있는지 확인하고 없으면 생성
if not self.memory_client.list_memories():
self.memory_id = self.create_memory_client()
else:
memories = self.memory_client.list_memories()
self.memory_id = next((m['id'] for m in memories if m['id'].startswith(self.memory_name)), None)
# 메모리 훅 제공자 초기화
self.hook_provider = MemoryHookProvider(
memory_client=self.memory_client,
memory_id=self.memory_id,
actor_id=self.actor_id,
session_id=self.session_id
)
def get_agent(self, system_prompt: str, agent_core_memory: bool = False):
"""
설정된 시스템 프롬프트와 도구로 에이전트를 생성합니다.
Args:
system_prompt (str): 시스템 프롬프트
agent_core_memory (bool): 코어 메모리 사용 여부
Returns:
Agent: 구성된 에이전트 객체
"""
if agent_core_memory:
agent = Agent(
system_prompt=system_prompt,
tools=[search_product],
callback_handler=None,
hooks=[self.hook_provider]
)
else:
agent = Agent(
system_prompt=system_prompt,
tools=[search_product],
callback_handler=None,
)
return agent
def create_memory_client(self):
"""
새로운 메모리 클라이언트를 생성하고 설정합니다.
Returns:
str: 생성된 메모리 ID
Raises:
ClientError: AWS 클라이언트 오류가 발생한 경우
"""
client = MemoryClient()
try:
# 사용자 선호도 전략을 포함한 메모리 생성
memory = client.create_memory_and_wait(
name=self.memory_name,
description="Shopping agent with long term memory",
strategies=[{
StrategyType.USER_PREFERENCE.value: {
"name": "UserPreferences",
"description": "Captures user preferences",
"namespaces": ["user/{actorId}/preferences"]
}
}],
event_expiry_days=90, # 90일 후 이벤트 만료
max_wait=300, # 최대 5분 대기
poll_interval=10 # 10초 간격으로 폴링
)
memory_id = memory['memoryId']
return memory_id
except ClientError as e:
# 이미 존재하는 메모리인 경우 기존 메모리 ID 반환
if e.response['Error']['Code'] == 'ValidationException' and 'already exists' in str(e):
memories = self.memory_client.list_memories()
memory_id = next((m['id'] for m in memories if m['id'].startswith(self.memory_name)), None)
return memory_id
else:
raise e
def hydrate_memory(self):
"""
DynamoDB에서 이전 메시지를 가져와 메모리에 적재합니다.
대용량 데이터 처리를 위해 배치 단위로 처리하고 재시도 로직을 포함합니다.
"""
# DynamoDB에서 이전 메시지 가져오기
previous_messages = get_messages(self.persona.name_eng)
# 메시지 형식을 메모리 클라이언트 형식으로 변환
memory_messages = [(message['content']['text'], message['role'].upper()) for message in previous_messages]
# 100개씩 배치 처리하여 메모리에 저장
for i in range(0, len(memory_messages), 100):
batch_messages = memory_messages[i:i+100]
retry_delay = 1
max_retries = 5
# 재시도 로직 (스로틀링 대응)
for attempt in range(max_retries):
try:
self.memory_client.create_event(
memory_id=self.memory_id,
actor_id=self.persona.name_eng,
session_id='1',
messages=batch_messages,
)
break
except ClientError as e:
if e.response['Error']['Code'] == 'ThrottledException':
if attempt < max_retries - 1:
# 지수 백오프로 재시도 대기 시간 증가
time.sleep(retry_delay)
retry_delay *= 2
else:
raise e
else:
raise e
def retrieve_memory(self, query: str):
"""
사용자 선호도 관련 메모리를 검색합니다.
Args:
query (str): 검색할 쿼리
Returns:
검색된 사용자 선호도 메모리
"""
preferences = self.memory_client.retrieve_memories(
memory_id=self.memory_id,
namespace=f"user/{self.persona.name_eng}/preferences",
query=query
)
return preferences
메모리 자원 배포가 완료되면 다음과 같이 콘솔에서 확인할 수 있습니다.
이렇게 Bedrock AgentCore Memory를 이용하면 따로 메모리를 저장할 데이터베이스를 구축하거나 메모리 저장 전략을 직접 구현할 필요 없이 빠르게 에이전트 메모리를 배포하고 사용할 수 있습니다.
Custom Memory: Generative Agents (Park et al., 2023)
AgentCore를 이용해서 에이전트에서 사용할 memory를 배포하고 사용할 수도 있지만 좀 더 최적화해서 사용하기 위해서 직접 만들어볼 수도 있습니다. 여기서는 Generative Agents: Interactive Simulacra of Human Behavior라는 논문에서 사용된 메모리를 응용해서 쇼핑 에이전트를 위한 메모리를 만들어보겠습니다. 이 논문은 대규모 언어 모델을 기반으로 한 “생성형 에이전트(Generative Agents)”를 소개하는 연구입니다. 이 에이전트들은 가상 환경에서 25개의 AI 캐릭터가 기상, 식사, 출근 등의 일상 활동을 수행하고 서로 대화하며 관계를 형성하는 인간과 같은 행동을 시뮬레이션 합니다. 핵심 아키텍처는 모든 경험을 자연어로 저장하는 Memory stream, 과거 경험을 종합하여 고차원적 사고를 만드는 Reflection, 그리고 이를 바탕으로 미래 행동을 계획하는 Planning 모듈로 구성됩니다. 평가 결과, 이 에이전트들은 한 캐릭터가 발렌타인 데이 파티를 계획하겠다는 단순한 설정만으로도 자율적으로 초대장을 퍼뜨리고, 데이트 신청을 하고, 실제로 파티에 참석하는 등의 믿을 만한 사회적 행동을 보여주었습니다.
Memory stream에는 다음과 같은 정보를 저장합니다:
- 일상 생활을 하면서 발생하는 일을 기록
- 척도에 따라 1점과 10점 사이의 importance 점수 부여 (1 – 책상이 깨끗해, 10 – 이성친구와 결별)
이후에 memory retrieve를 수행할 때 다음과 같은 척도를 이용합니다:
- Recency: 얼마나 최근에 발생했는가?
- Importance: 얼만큼 중요한가?
- Relevance: 현재 수행해야하는 일과 얼마나 관련이 있는가?
여기서 relevance는 현재 수행해야 하는 일과 vector similarity search 점수를 활용합니다. 최종적으로 각 항목을 normalize하여 비중에 따라 합산하여 최종 점수를 산정하고, 점수가 높은 N개의 memory object을 활용하여 다음 action을 결정합니다. Reflection은 주기적으로 실행되는 작업으로, 최근 100개의 memory stream을 추출하고 관련된 답을 할 수 있는 3개의 고차원적인 질문을 추출합니다. 그리고 그 질문으로 memory retrieve를 수행한 후 5개의 고차원적인 기록을 남기고 연관된 memory object를 연결합니다. 이러한 방식으로 memory object와 연결된 하나의 memory tree와 같은 구조를 만들수도 있습니다.
쇼핑 에이전트도 크게 보면 하나의 Generative Agent로 생각할 수 있습니다. 사용자와 자연스럽게 교감하며 경험을 Memory stream에 저장하고, 사용자와의 경험을 종합한 reflection으로 취향과 니즈를 파악하고, 이를 바탕으로 선제적인 추천을 하는 planning과 같은 역할을 생각할 수 있습니다. 이번 블로그에서는 reflection으로 memory tree까지 생성하지는 않지만 그 기능을 이용하여 사용자의 최근 관심사를 이용해 취향을 파악하는 데에 사용합니다. 아래에서 Generative Agent 논문의 memory를 어떤 방식으로 응용할 수 있는지 알아보겠습니다.
Custom Memory 구성
Memory stream 구성
위와 유사하게 쇼핑 agent에서는 사용자와의 대화 기록을 기반으로 미래 쇼핑에 영향을 줄만한 일을 중요도에 따라 기록합니다. 예를 들어 아래 같은 프롬프트를 활용하여 다음과 같은 항목들이 기록되었습니다:
MEMORY_PROMPT="""You are a memory processing system for an e-commerce shopping agent. Your role is to analyze conversations between users and shopping agents to create structured memory objects that capture important behavioral patterns, preferences, and interactions.
You must score the importance of the memory objects based on a scale of 1-10, where 1 is the least important and 10 is the most important.
Important categories:
- Product preferences and dislikes
- Budget constraints and spending patterns
- Size, fit, and physical requirements
- Color preferences
- Lifestyle and use cases
- Past purchase satisfaction
Sample unimportant categories:
- Casual conversational pleasantries
- Temporary mood or weather comments
- General comments about service
- Unrelated personal anecdotes
- Technical platform comments
- Content that is not related to the user
Guidelines:
- The memory objects should be in the same language as the conversation.
- The memory objects should be single sentences that capture the essence of each observation, such as distinguishing whether a mentioned color or size refers to a specific product or represents a general preference.
- Do not include any personal information such as address, phone number, email, etc.
Create memory objects using this format:
{
"description": "[Natural language description of the observation]",
"importance": [1-10 scale rating of how important this observation is for future shopping assistance]
}
Your final output should be a list of memory objects in the following format with no preamble:
[
{
"description": "[Natural language description of the observation]",
"importance": [1-10 scale rating of how important this observation is for future shopping assistance]
}
]
Remember: The goal is to create a comprehensive memory system that enables shopping agents to provide increasingly personalized and effective assistance by understanding and remembering user behavior patterns, preferences, and shopping contexts."""
# 생성된 memory object 예시
memory_objects = [{
"description": "고객은 스판 반바지에 관심을 보였고, 특히 블랙 색상을 선호한다"
"importance": 9,
"timestamp": "2025-03-15T14:30:00Z",
"embedding": [벡터 데이터]
},
{
"description": "사용자는 푸마 티셔츠를 가지고 있으며 반바지와 코디하려고 한다."
"importance": 7,
"timestamp": "2025-03-15T14:30:00Z",
"embedding": [벡터 데이터]
}]
Memory object 들은 OpenSearch에 저장하고 vector embedding을 이용한 유사도 검색을 이용하여 추출합니다. 이준호 페르소나의 경우 총 30회 대화를 하며 514개의 메세지를 주고받았고 242개의 memory object들이 생성되었습니다.
Memory retrieval
우선 memory object index에 대해 vector similarity 검색을 수행하여 100개를 추출한 후에 recency, importance, relevance를 각각 계산하여 합산합니다. 이후 합산 점수가 높은 10개의 memory object를 활용하여 agent context로 활용합니다.
Reflection
Timestamp를 기준으로 최근 100개의 memory object를 stream에서 추출한 후 그 것을 기반으로 다음과 같은 고차원적인 질문들을 추출합니다.
이후에 각 질문을 이용하여 memory retrieve를 수행한 후에 다음과 같이 요약합니다.
Reflection과 Persona의 비교
대화 내용을 기반으로 생성된 reflections를 보면 이준호 페르소나의 주요 특징에 대해 정확하게 파악하고 있음을 알 수 있습니다.
- 키 173cm, 100-105size(L-XL) 착용하며 기본적인 정보 외에는 어떤 개인 정보도 공유하지 않습니다.
- 미니멀하고 로고가 작거나 없는 디자인을 선호하지만 명시적으로 말하지 않습니다.
- 실제로는 나이키, 푸마, 폴햄과 같은 브랜드를 선호하지만 직접 말하지 않습니다.
- 입으로는 “아무색이나”라고 말하지만 실제로 블랙, 네이비, 다크그레이를 강력하게 선호합니다.
- 실제로는 면 소재, 스판 혼방, 흡한속건과 같은 기능성을 중시하지만 직접 말하지 않습니다.
Bedrock AgentCore Memory, Custom Memory, 그리고 전체 메세지 활용 비교
Bedrock AgentCore Memory를 활용할 때와 custom memory와 reflection을 agent가 활용했을 때, 그리고 대조군으로 전체 대화를 활용했을 때의 반응 비교해보겠습니다. Bedrock AgentCore Memory를 사용하는 agent는 short-term memory를 이용해서 최근 10개의 대화를 읽어오고, 고객의 요청을 이용해서 long-term memory를 query한 결과를 컨텍스트로 활용합니다. 그리고 Custom memory를 사용하는 agent는 위에서 언급한 retrieval 과 reflection을 컨텍스트로 활용하고 전체 대화를 활용하는 agent에게는 현재까지의 모든 대화 내역을 컨텍스트로 제공했습니다.
그리고 프롬프트에는 공통적으로 제공된 정보를 상품 추천에 활용하도록 아래 문구를 추가했습니다.
You MUST tailor your response using the user’s context, such as ranking items based on the user’s preferences and explaining why you chose the items.
이준호 페르소나 – 티셔츠 추천 요청
입력 프롬프트: “티셔츠 아무거나 추천좀”
AgentCore Memory 활용 Agent
위에서 구현한 ShoppingAgent.retrieve_memory()
를 이용하면 티셔츠와 관련된 long-term memory를 가져올 수 있습니다. 입력 프롬프트를 그대로 활용하여 가져온 3개의 결과는 아래와 같습니다.
[{
"context":"The user asked for t-shirt recommendations and showed interest in variety of t-shirts, and also inquired about shorts pricing, length, and shipping details",
"preference":"Interested in diverse t-shirt options across different price ranges and prefers practical and economical clothing products",
"categories":["clothing","fashion","shopping","budget"]
},
{
"context":"The user asked for t-shirt recommendations and showed interest in different t-shirts, specifically from Korean brands like [브랜드]",
"preference":"Interested in t-shirts with multiple characteristics (budget-friendly, basic, and high-end options), particularly plain t-shirts from Korean clothing brands",
"categories":["clothing","fashion","shopping","t-shirts","Korean brands"]
},
{
"context":"The user asked for a t-shirt recommendation and showed interest in t-shirts with different features. Also mentioned durability as the most important criterion when selecting shoes.",
"preference":"Interested in t-shirts with multiple characteristics (budget-friendly, basic, and high-end options) and prefers durable footwear",
"categories":["clothing","fashion","shopping","footwear","functionality"]
}]
사용자가 다양한 가격대의, 가성비 좋고, 기능성 있는, 패턴 없는, 한국 브랜드의 티셔츠를 선호한다는 점을 장기 기억에서 가져왔습니다. 추가적으로 MemoryHookProvider.on_agent_initialized()
에서 최근 10개의 대화 내역을 단기 기억에서 가져왔습니다. 이를 context에 같이 넣어주어 아래와 같은 답변을 받았습니다:
좋은 티셔츠들을 찾았어요! 이전에 다양한 티셔츠를 관심있게 보셨던 것을 기억하고 있어서, 여러 가격대와 스타일로 추천드릴게요:
💰 가성비 최고 추천
1. 티셔츠 A – 가격대 저렴
- 누적 판매량이 높은 인기상품
- 부드러운 터치감과 편안한 착용감
- 다양한 컬러 선택 가능
2. 티셔츠 B – 가격대 저렴
- 부드러운 감촉과 우수한 흡수성
- 4계절 착용 가능한 탄탄한 소재
- 다양한 컬러 구성
🎯 베이직 데일리용
3. 티셔츠 C – 가격대 보통
- 100% 면 소재로 부드러운 촉감과 뛰어난 통기성
- 그레이, 화이트, 블랙 3가지 컬러
- 남녀공용으로 활용도 높음
🌟 프리미엄 추천
4. 티셔츠 D – 런칭 특가
- 최고급 면 100%로 부드러운 촉감
- 오래 사용할수록 좋아지는 품질
- 세련된 스트라이프와 그레이 컬러
5. 티셔츠 E – 가격대 높음
- 선호하시는 면 100% 소재로 여름철 쾌적한 착용감
- 레터링 디자인으로 세련된 캐주얼룩
- 다양한 컬러와 사이즈
평소 다양한 가격대의 티셔츠를 선호하셨기 때문에 저렴한 가격부터 프리미엄까지 골고루 추천해드렸어요. 어떤 스타일이나 가격대가 더 마음에 드시나요? 😊
Input Tokens: 21,496
Bedrock AgentCore memory를 사용했을 때에 다양한 가격대의 제품을 선호하는 것을 기억하고 추천에 활용하는 모습을 볼 수 있습니다.
Custom Memory 활용 Agent
위에서 커스텀으로 생성한 memory에서 상위 10개의 memory object를 추출합니다. 추출한 것들의 일부는 아래와 같은 내용을 담고 있습니다:
- 사용자는 [브랜드] 브랜드의 반팔 티셔츠에 관심을 보였다
- 사용자가 [브랜드] 브랜드 티셔츠에 관심을 보였다.
- 사용자가 블랙 색상의 긴팔 티셔츠를 특별히 요청했다.
- 사용자는 면 티셔츠를 선호한다.
- 사용자가 티셔츠를 고를 때 기본적이고 무난한 디자인을 선호한다.
- 사용자는 면 100% 소재의 티셔츠를 선호한다.
- 사용자는 무난한 검은색 반팔티를 선호한다.
여기에 추가적으로 위에서 파악한 사용자에 대한 reflection을 context에 추가하여 티셔츠 추천을 요청하여 아래와 같은 답을 받았습니다.
고객님 취향에 맞는 티셔츠들을 찾아봤습니다! 이전 구매 내역을 보니 면 소재와 기본적이고 무난한 디자인, 블랙 색상을 선호하시는 것 같아서 그에 맞게 추천드릴게요:
추천 티셔츠 TOP 5
1. 티셔츠 A ⭐ 최고 추천!
- 고객님이 이전에도 관심 보이셨던 브랜드
- 100% 면 소재로 고객님이 선호하는 재질
- 블랙, 그레이, 화이트 3가지 색상 (고객님 취향 저격!)
- L사이즈 선택 가능
- 가성비 갑
2. 티셔츠 B
- 부드러운 면 소재와 편안한 착용감
- 기본적이고 무난한 무지 디자인
- 회색 등 차분한 색상
- 만족도 높음
3. 티셔츠 C
- 최고급 이집트 면 100% 소재
- 스트라이프와 무지 디자인 혼합
- 고급스러운 품질을 원하신다면 추천
4. 티셔츠 D
- 기본적인 무지 디자인
- 부드러운 소재로 이너웨어로도 좋음
- 저렴한 가격대
5. 티셔츠 E
- 민소매 스타일로 여름철 시원함
- 면 100% 소재
고객님의 이전 구매 패턴과 선호도를 고려할 때 티셔츠 A가 가장 적합할 것 같습니다. 면 100% 소재에 블랙 색상 포함, 가격도 합리적이고 이전에도 관심을 보이셨던 브랜드거든요!
어떤 티셔츠가 가장 마음에 드시나요? 더 자세한 정보나 다른 옵션이 필요하시면 언제든 말씀해 주세요! 😊
Input Tokens: 25,586
전체 대화 기록 활용 Agent
안녕하세요! 티셔츠를 찾고 계시는군요. 더 맞춤형 추천을 해드리기 위해 몇 가지 정보가 필요합니다:
1. 어떤 스타일의 티셔츠를 찾으시나요? (예: 캐주얼, 스포츠, 드레스 등)
2. 선호하시는 색상이 있으신가요?
3. 가격대는 어느 정도로 생각하고 계신가요?
4. 어떤 사이즈를 찾으시나요?
이런 정보를 알려주시면 더 적합한 티셔츠를 추천해드릴 수 있습니다!
Input Tokens: 309,471
대조군으로 전체 대화 이력을 사용하는 에이전트를 활용했습니다. 이 경우에는 context overflow가 발생하며, 이 경우에 Strands Agents SDK에서는 입력 토큰이 context window 안으로 들어올 때까지 자동으로 앞쪽의 내용을 삭제하게 됩니다. 지금까지의 요청 중 가장 많은 입력 토큰을 활용했음에도 불구하고 사용자의 성향을 파악하지 못하고 일반적인 답변을 하는 모습을 볼 수 있습니다.
결과 분석
결과를 보면 Bedrock AgentCore Memory와 Custom Memory를 활용한 Agent는 모두 적은 수의 입력 토큰을 활용하고도 사용자가 블랙 색상, 면 100% 소재, 무지 디자인을 선호한다는 것을 활용한 상품 추천을 하는 반면, 전체 대화를 활용한 에이전트는 Memory Agent의 10배가 넘는 입력 토큰을 활용했음에도 사용자의 취향을 파악하지 못해 역으로 질문을 하고 있습니다.
이준호 페르소나 – 운동화 추천 요청
입력 프롬프트만 변경하여 예시를 추가적으로 확인해보겠습니다.
입력 프롬프트: “운동화 아무거나 추천좀”
AgentCore Memory 활용 Agent
추출된 Long-term memory:
[{
"context":"사용자가 [브랜드] 운동화에 관심을 보임",
"preference":"[브랜드] 브랜드 운동화에 관심",
"categories":["신발","운동화","패션 브랜드"]
},
{
"context":"사용자는 단순히 가격이 저렴하고 튼튼한 운동화를 찾고 있으며, 브랜드에는 크게 관심이 없음",
"preference":"가격 대비 내구성이 좋은 운동화 선호",
"categories":["fashion","footwear","budget"]
},
{
"context":"사용자가 [상품]를 구매하고, 비슷한 스타일의 운동화를 찾고 있으며, [상품]에 특히 관심을 보임",
"preference":"슬립온 스타일의 편안한 운동화, 특히 [브랜드] 브랜드 선호",
"categories":["운동화","신발","패션"]
}]
쇼핑 에이전트 응답:
준호님을 위한 운동화 추천드릴게요! 예전에 특정 브랜드들에 관심을 보이셨고, 슬립온 스타일을 선호하셨던 것을 기억하고 있어요. 오늘은 좋은 조건으로 나와있는 제품들이 있네요!
🏃♂️ 추천 운동화 리스트
1. 운동화 A
- 경량성과 안정감을 동시에 잡은 실용적인 디자인
- 키높이 효과로 스타일리시한 룩
- EVA 미드솔과 충격흡수 기능
- 재귀반사 처리로 야간 안전성까지
- 파랑 네이비 컬러
2. 운동화 B
- 키높이 효과
- 와이드핏 디자인으로 발볼 넓으신 분들께 좋음
- 미끄러움 방지 트레커 아웃솔
- 합성가죽 소재로 고급스러운 느낌
- 회색 컬러
3. 운동화 C
- 레트로 감성의 세련된 디자인
- 천연 스웨이드 소재로 고급스러운 착화감
- 키높이 기능
- 회색 컬러
준호님께서 가성비 좋고 튼튼한 운동화를 선호하신다는 것을 기억해서, 특히 운동화 B나 운동화 A를 추천드려요! 두 제품 모두 합리적인 가격이고, 평가도 매우 우수합니다.
어떤 스타일이 더 마음에 드시나요? 😊
Input Tokens: 18,208
티셔츠의 경우와 유사하게 사용자의 취향을 고려한 추천을 하는 것을 볼 수 있습니다.
Custom Memory 활용 Agent
추출한 Memory Object들:
- 사용자는 발볼이 넓다고 밝혔으며, 이에 맞는 운동화를 찾고 있다
- 사용자는 남성용 운동화를 선호한다
- 사용자는 남성용 블랙 색상의 가벼운 운동화를 선호한다
- 사용자는 [상품] 운동화를 구매했다
- 사용자는 [상품] 운동화를 주문했다
- 사용자는 손쉽게 신고 벗을 수 있는 슬립인스 타입의 운동화를 선호한다
- 고객은 [브랜드] 브랜드의 운동화 제품을 선호한다
쇼핑 에이전트 응답:
좋은 운동화들을 찾았습니다! 고객님의 선호도와 구매 이력을 고려해서 추천해드리겠습니다:
🏃♂️ 추천 1위: 운동화 A (검정)
- 발볼이 넓으신 분께 최적: 발볼이 넓거나 발등이 높은 분들도 편안하다는 후기가 많습니다
- 블랙 색상: 고객님이 선호하시는 색상입니다
- 메리노울 소재: 통기성 뛰어나고 땀 흡수/배출 기능으로 여름에도 시원함
- 항균 기능: 냄새 억제 효과까지
🏃♂️ 추천 2위: 운동화 B (남성용)
- 뛰어난 가성비: 합리적인 가격대
- 반사소재: 야간 운동시 안전성
- EVA 미드솔: 충격흡수와 쿠셔닝
- 키높이 효과: 자연스러운 스타일링
🏃♂️ 추천 3위: 운동화 C
- 메쉬 소재: 통풍성 뛰어난 여름용
- 키높이: 자연스러운 비율 개선
- 발볼 넓은 분들도 편안: 착용감이 좋다는 후기가 많음
더 많은 옵션을 보시겠거나, 특정 요구사항이 있으시면 말씀해 주세요!
Input Tokens: 20,474
전체 대화 기록 활용 Agent
다양한 브랜드와 가격대의 운동화를 추천해드릴게요! 현재 가장 인기 있는 운동화들을 소개해드리겠습니다:
🏃♂️ 인기 운동화 추천
1. 운동화 A (남/여성용)
- 특징: 가볍고 편안한 착화감, 키높이 효과
- 장점: 발볼이 넓은 분들도 편안하게 착용 가능, 야간 안전을 위한 재귀반사 소재 적용
2. 운동화 B (남/여성용)
- 특징: 데일리 착용에 최적화된 디자인, 키높이
- 장점: 합성가죽으로 견고하면서도 부드러운 착화감, 미끄럼 방지 기능
3. 운동화 C (여성용)
- 특징: 레트로 감성의 스타일리시한 디자인, 천연 스웨이드 소재
- 장점: 키높이, 고급스러운 착화감
💡 추천 이유
- 가성비: 합리적인 가격대
- 편안함: 발볼이 넓으신 분들도 편안하게 착용 가능
- 다양성: 남성용/여성용 모두 있어 선택의 폭이 넓음
- 실용성: 일상생활부터 가벼운 운동까지 다양하게 활용 가능
특별히 어떤 스타일이나 색상, 용도를 원하시는지 알려주시면 더 구체적으로 추천해드릴 수 있어요! 🔍
Input Tokens: 309,176
결과 분석
티셔츠 추천과 유사하게 memory를 활용한 Agent는 사용자가 발볼이 넓고 편안한 슬립온을 선호한다는 것을 기반으로 상품을 추천하는 반면에 그 외의 Agent 들은 더 많은 입력 토큰을 사용하더라도 취향을 제대로 파악하지 못하고 여성용 제품을 추천하고 있습니다.
최종 결론
결과적으로 Memory를 활용한 에이전트들은 모두 적절한 토큰 사용량으로 높은 개인화 수준을 달성했습니다. 반면 전체 대화 기록을 사용한 방법은 토큰 사용량은 많지만 불필요한 정보가 과도하게 많아 개인화가 전혀 되지 않았습니다.
결론 및 향후 발전 방향
이번 블로그를 통해 확인한 핵심 인사이트를 정리하면 다음과 같습니다.
- Bedrock AgentCore memory를 이용하여 자원 관리가 필요없는 서버리스 형태의 메모리를 쉽게 배포하고 사용할 수 있습니다.
- Amazon OpenSearch service, Amazon DynamoDB와 같은 서비스를 이용하여 손쉽게 memory를 구현하는 것도 가능합니다. 이 경우에는 자원 관리 부담이나 개발 부담은 늘어날 수 있으나 보다 최적화된 memory를 만들 수 있다는 장점이 있습니다.
- 전체 대화 같은 긴 컨텍스트를 넣어주는 것보다 중요한 컨텍스트만 선별하여 제공하는 것이 개인화를 하는 데에 있어 훨씬 효과적입니다. Memory를 활용하는 것만으로도 누적된 사용자 취향을 효과적으로 파악할 수 있었습니다.
- Custom Memory 시스템의 핵심은 적절한 정보 선별과 구조화입니다. Generative Agents의 Memory 개념 중 일부를 쇼핑 도메인에 적용함으로써 사용자의 과거 행동 패턴과 선호도를 효과적으로 파악하고 활용할 수 있었습니다.
이번 블로그의 Bedrock AgentCore memory나 Custom memory는 하나의 예시일 뿐입니다. 실제 구현에서는 조금 더 Generative Agents와 유사하게 reflection을 memory tree를 구성하거나, retrieval 시 가중치를 조정하거나, 새로운 프롬프트들을 적용하여 필요에 따라 최적화하여 활용할 수 있습니다.
향후 개선 방향으로는 중복 제거와 같은 적절한 memory object 관리, 그리고 구매, 장바구니, 조회 등 행동별 적절한 가중치 부여, 챗봇 상호작용 뿐만 아니라 전체적인 사용자의 웹 행동 데이터 반영 등을 고려할 수 있습니다. 또한 이번 글에서는 쇼핑에 한하여 적용되었지만, 이 내용은 사용자 경험이 중요한 다양한 B2C 챗봇으로 확장될 수 있습니다.
Amazon Bedrock AgentCore, Strands Agents, Amazon Bedrock과 더불어 AWS의 관리형 서비스들을 활용하면 복잡한 인프라 관리 없이도 효과적인 개인화 AI 에이전트 memory 시스템을 구축할 수 있습니다. 여러분도 이 접근 방법을 참고하여 자신만의 지능형 에이전트를 구축해 보시기 바랍니다.
References
Park, J. S., O’Brien, J. C., Cai, C. J., Morris, M. R., Liang, P., & Bernstein, M. S. (2023). Generative Agents: Interactive Simulacra of Human Behavior. arXiv preprint arXiv:2304.03442. https://arxiv.org/abs/2304.03442