AWS 기술 블로그

Amazon Bedrock AgentCore Memory: 기억하는 AI 에이전트 만들기

AI 에이전트에게 기억이란?

ChatGPT가 세상에 나온 지 어느덧 3년이 지났고 이제 생성형 AI는 단순한 신기함을 넘어 우리의 일상과 업무 프로세스 깊숙이 녹아들었습니다. 우리는 AI와 자유롭게 대화하며 마치 사람과 이야기하듯 자연스럽게 질문을 던지고 답변을 받습니다. 하지만 우리가 당연하게 느끼는 이 ‘대화의 연속성‘ 뒤에는 기술적 난제가 숨겨져 있습니다. 바로 생성형 AI 모델의 본질적인 특성, Statelessness입니다.

생성형AI 모델 (LLM)은 오직 입력된 토큰 시퀀스를 기반으로 확률적으로 다음 토큰을 예측할 뿐, 이전의 호출 내역을 모델 자체에 저장하지 않습니다. 예를 들어, “Python에서 리스트와 튜플의 차이가 뭐야?”라고 묻고, 이어서 “그럼 언제 튜플을 써야 해?”라고 묻는다면 어떻게 될까요? 우리 눈에는 이어진 대화처럼 보이지만, 모델 입장에서 두 번째 질문은 난데없이 튜플의 사용 시기를 묻는 문장일 뿐입니다. 맥락을 유지하기 위해서는 이전 질문과 답변, 그리고 현재의 질문까지 통째로 묶어 다시 입력해야만 합니다. 이것이 바로 우리가 흔히 말하는 ‘단기 기억(Short-term Memory)’의 구현 방식입니다.

그렇다면, 단순히 모델의 입력 한계(Context Window)를 무한정 늘려서 모든 대화 기록을 집어넣으면 해결될까요? 최근 많은 LLM들이 100만 토큰 이상의 거대한 Context Window를 자랑하지만, 이것이 만능열쇠는 아닙니다. 벡터 데이터베이스 기업 Chroma의 연구에 따르면, 입력되는 문맥(Context)의 길이가 길어질수록 모델의 성능이 급격히 저하되는 ‘Context Rot(문맥 부패)‘ 현상이 발생합니다.

“모델은 문맥이 길어질수록 정보의 홍수 속에서 핵심을 놓치거나, 불필요한 정보(Distractors)에 의해 주의력(Attention)이 분산되어 단순한 정보 검색조차 실패하는 경향을 보입니다.”
Chroma Research, Context Rot: Why More Context Isn’t Always Better

연구 결과, 모델은 관련 없는 정보(Distractors)가 섞여 있을 때 정답을 찾아내는 능력이 비선형적으로 떨어졌습니다. 즉, 무작정 지난주 회의록과 모든 채팅 로그를 프롬프트에 밀어 넣는 방식은 비용을 폭증시킬 뿐만 아니라, 오히려 에이전트가 엉뚱한 대답을 내놓게 만들 수 있습니다.
결국, 우리에게 필요한 것은 무조건적인 저장이 아니라 선별입니다. 마치 인간이 모든 순간을 기억하지 않고 중요한 사실과 취향, 맥락만을 장기 기억(Long-term Memory)으로 남기듯, AI 에이전트에게도 그러한 시스템이 필요합니다.

하지만 이를 직접 구현하는 것은 쉽지 않습니다. 대화 내용을 요약하고, 의미 있는 정보를 추출하여 벡터 DB에 저장하고, 현재 질문과 가장 연관성 높은 기억만을 다시 검색(Retrieval)해오는 파이프라인을 구축해야 하기 때문입니다. 이는 엄청난 개발 리소스를 소모하게 합니다.

Amazon Bedrock AgentCore Memory는 바로 이 문제를 해결하기 위해 탄생했습니다.

  • 완전 관리형 메모리: 복잡한 인프라 구축 없이 API 호출만으로 세션을 넘나드는 기억을 구현합니다.
  • 메모리 추출 자동화: 방대한 대화 로그 속에서 현재 필요한 핵심 기억만을 의미론적(Semantic)으로 추출하여 모델에 제공함으로써, 모델의 추론 능력을 최상의 상태로 유지합니다.

이제 AgentCore Memory를 통해, 단순한 챗봇을 넘어 ‘사용자를 이해하고 기억하는’ 진정한 AI 파트너를 구축하는 방법을 알아보겠습니다.

Amazon Bedrock AgentCore Memory의 구성

앞서 언급했듯, 에이전트의 기억을 직접 구현하려면 외부 데이터베이스를 구축하고 관리해야 하는 부담이 있습니다. 특히 장기 기억을 위해서는 대화 내용을 요약하거나 저장된 기억을 업데이트/삭제하는 로직, 그리고 이를 처리할 LLM 호출 프로세스까지 모두 직접 개발해야 합니다. AgentCore Memory는 이 모든 복잡한 과정을 추상화하여, 개발자가 SDK를 통해 간편하게 구현할 수 있도록 도와줍니다.

단기 기억 (Short-term Memory)과 데이터 격리

Bedrock AgentCore Memory는 데이터를 체계적으로 관리하기 위해 3단계 계층 구조를 사용합니다.

memory_id             : 메모리 리소스의 고유 식별자 (AWS 리소스 ARN 수준)
    └─ actor_id       : 사용자의 고유 식별자 (사용자 수준)
        └─session_id  : 사용자의 대화 세션 고유 식별자 (대화 수준)

각 ID의 역할은 다음과 같습니다:

  • memory_id: AWS 리소스로 생성되며 고유한 ARN을 가집니다. 비용과 관리의 기본 단위가 되며, 이 아래에 모든 단기 및 장기 메모리가 구성됩니다.
  • actor_id: 개별 사용자나 고객을 식별하는 ID입니다(예: 사용자 ID, 이메일 해시 등). 동일한 actor_id는 여러 세션에 걸쳐 메모리를 공유할 수 있습니다.
  • session_id: 개별 대화나 상호작용 세션을 식별합니다(예: 웹 세션 ID, 타임스탬프). 단기 메모리는 이 세션 단위로 로드되어 사용됩니다.

이러한 3계층 구조는 단순한 분류를 넘어, 기업 환경에서 가장 중요한 보안과 데이터 격리를 위한 핵심 기반이 됩니다.
이 구조의 실제 구현 예시를 살펴보면 다음과 같습니다.

memory_id: "customer-support-bot-production"
  └─ actor_id: "customer-12345"
      ├─ session_id: "chat-2024-01-15-110000"      # 11시에 시작된 세션
      └─ session_id: "chat-2024-01-15-150000"      # 15시에 시작된 세션
  └─ actor_id: "customer-67890"
      └─ session_id: "chat-2024-01-15-120000"
  • 논리적 격리 (Logical Isolation): AgentCore는 ID 조합을 기반으로 엄격한 접근 제어를 수행합니다. 예를 들어, customer-12345customer-67890은 완전히 분리된 메모리 공간을 가지며, 한 사용자가 다른 사용자의 메모리(actor_id)에 물리적으로 접근하는 것이 원천적으로 차단됩니다.
  • 데이터 암호화 (Encryption): 저장되는 모든 대화와 추출된 기억은 AWS 인프라 위에서 안전하게 암호화되어 저장(Encryption at Rest)됩니다.
  • 자동화된 생명주기 관리: event_expiry_days 설정을 통해 불필요한 데이터는 자동으로 파기되어, GDPR 등 규제 준수와 비용 최적화를 동시에 달성할 수 있습니다.

장기기억

단기 기억이 있는 그대로의 대화를 저장한다면, 장기 기억은 그중에서 의미 있는 정보만을 추출(Extraction)하여 저장하는 공간입니다. 예를 들어, “오늘 비가 오네” 같은 일회성 잡담은 흘려보내지만, “나는 치킨을 좋아해”와 같은 사용자 취향이나 중요한 사실은 포착합니다. 새로운 대화가 입력되면 AgentCore Memory는 백그라운드에서 비동기적으로 LLM을 구동하여 맥락을 분석하고 정보를 추출(Extraction)합니다.

이후 중요한 단계인 ‘기억 통합(Consolidation)’ 과정을 거칩니다. 추출된 정보는 무조건 저장되는 것이 아니라, 기존 기억과 대조됩니다. 시스템은 새로운 정보가 기존 기억과 중복되는지(Skip), 완전히 새로운 내용인지(Add), 혹은 과거의 취향이 바뀌어 수정해야 하는지(Update)를 지능적으로 판단하여 최종적으로 장기 기억에 반영합니다. 이를 통해 에이전트는 모순 없는 일관된 기억을 유지할 수 있습니다.

메모리 전략

장기 기억을 구성하기 위해 사전 정의된 4가지 전략을 사용할 수 있습니다. 다음 예시 대화를 통해 각 전략이 어떻게 작동하는지 살펴보겠습니다.

USER: 오늘 뭐 먹지?
ASSISTANT: 치킨, 피자, 족발을 추천 드립니다!

USER: 오 나 치킨 좋아하긴 해.
ASSISTANT: 탁월한 선택이십니다. 치킨을 주문해드릴까요?

USER: 응.
ASSISTANT: 치킨을 주문했습니다! 30분 후에 도착 예정입니다.

위 대화가 발생했을 때, 각 전략은 다음과 같이 정보를 추출합니다.

  1. Semantic Memory (의미 기억): 대화에서 사실(Fact)과 지식을 추출합니다.
    • 추출 예: “사용자는 치킨을 주문했습니다.”
  2. User Preference Memory (사용자 선호도 기억): 명시적 혹은 암묵적인 사용자의 선호도를 추출합니다.
    • 추출 예: “사용자는 치킨을 좋아합니다.”
  3. Summary Memory (요약 기억): 긴 대화 세션을 요약하여 저장합니다.
    • 추출 예: “에이전트가 저녁 메뉴로 치킨, 피자, 족발을 추천했고, 사용자는 치킨을 먹었습니다.”
  4. Episodic Memory (일화 기억): 과거의 경험으로부터 학습하고 그 통찰력을 미래의 상호작용에 적용하는 새로운 기능입니다. 에이전트는 맥락(Context), 추론(Reasoning), 행동(Actions), 결과(Outcomes)가 포함된 구조화된 에피소드를 저장하고, 이를 분석하여 의사결정 패턴을 개선합니다.

Namespace를 이용한 구조화

기억은 파일 시스템의 폴더처럼 Namespace를 사용하여 데이터를 논리적으로 분류합니다. 이때 actorIdsessionId를 템플릿 변수로 사용하여 사용자별, 세션별로 유연하게 경로를 지정할 수 있습니다.
예를 들어 아래와 같이 활용할 수 있습니다.

/facts/{actorId}                    # 사용자별 사실 정보
/preferences/{actorId}              # 사용자별 선호도
/summaries/{actorId}/{sessionId}    # 세션별 요약

API 호출 시 이 템플릿 변수들은 실제 값으로 자동 치환되어, 개발자가 일일이 경로를 매핑하는 수고를 덜어줍니다.

Bedrock AgentCore SDK로 빠르게 시작하기

이제 앞서 살펴본 개념들이 실제 코드에서 어떻게 구현되는지 Bedrock AgentCore SDK 예제를 통해 확인해 보겠습니다. SDK는 복잡한 API 호출 과정을 추상화하여, 몇 줄의 코드만으로 안전하고 확장 가능한 메모리 시스템을 구축할 수 있게 해줍니다.

메모리 생성

먼저 MemoryClient를 초기화하고, 메모리 공간을 생성합니다. AgentCore Memory는 기본적으로 단기 기억을 포함하며, 전략(Strategy) 설정에 따라 장기 기억을 추가로 활성화할 수 있습니다.

from bedrock_agentcore.memory import MemoryClient
import uuid
import time

# 1. 메모리 클라이언트 초기화
memory_client = MemoryClient(region_name='us-west-2')

#2 Short term memory 생성
# strategies 리스트를 비워두면, 단기 기억만 활성화된 메모리 리소스가 생성됩니다.
short_term_memory = client.create_memory_and_wait(
    name="test-short-term-memory",
    strategies=[],
    event_expiry_days=7 # 데이터 보관 기간 설정
)

#2.1 Long term memory 포함하여 생성하기
# Semantic, Preference, Summary 등 필요한 전략을 정의하여 장기 기억을 구성합니다.
long_term_memory = client.create_memory_and_wait(
    name="test-long-term-memory",
    strategies=[
        {
            "semanticMemoryStrategy": {
                "name": "facts",
                "namespaces": ["/facts/{actorId}"]
            }
        },
        {
            "userPreferenceMemoryStrategy": {
                "name": "preferences",
                "namespaces": ["/preferences/{actorId}"]
            }
        },
       {
            "summaryMemoryStrategy": {
                "name": "summaries",
                "namespaces": ["/summaries/{actorId}/{sessionId}"]
            }
        }
    ],
    event_expiry_days=30
)

메모리 이용하기

생성된 메모리에 대화 내용(Event)을 저장하고, 저장된 단기 기억을 불러오는 방법을 알아봅니다.

이벤트 추가

대화 내용은 (내용, 역할)의 튜플 리스트 형태로 전달합니다. 이때 역할(Role)은 USERASSISTANT로 구분됩니다.

# 대화 시나리오 예시
messages = [
    ("오늘 뭐 먹지?", "USER"),
    ("치킨, 피자, 족발을 추천 드립니다!", "ASSISTANT"),
    ("오 나 치킨 좋아하긴 해.", "USER"),
    ("탁월한 선택이십니다. 치킨을 주문해드릴까요?", "ASSISTANT"),
    ("응.", "USER"),
    ("치킨을 주문했습니다! 30분 후에 도착 예정입니다.", "ASSISTANT")
]
actor_id = "minsukim"
session_id = "test-session-123456781234567812345678"

# 3. 메모리에 이벤트(대화) 추가
# create_event 호출 한 번으로 리스트 내의 모든 대화 턴이 순서대로 저장됩니다.
memory_client.create_event(
    memory_id=long_term_memory['id'],
    actor_id=actor_id,
    session_id=session_id,
    messages=messages
)

단기 기억 조회

저장된 대화는 get_last_k_turns 메서드를 통해 최근 k개의 턴(Turn) 단위로 조회할 수 있습니다. session_idactor_id가 일치해야만 이전 맥락을 불러올 수 있습니다.

# 4. 단기 메모리에서 최근 k 개의 턴 메세지 확인
conversation = client.get_last_k_turns(
    memory_id=long_term_memory['id'],
    actor_id=actor_id,
    session_id=session_id
    k=2
)

출력 결과 예시:

[
    [
        {'content': {'text': '오 나 치킨 좋아하긴 해'}, 'role': 'USER'}, 
        {'content': {'text': '탁월한 선택이십니다. 치킨을 주문해드릴까요?'}, 'role': 'ASSISTANT'}
    ], 
    [
        {'content': {'text': '응.'}, 'role': 'USER'}, 
        {'content': {'text': '치킨을 주문했습니다! 30분 후에 도착 예정입니다.'}, 'role': 'ASSISTANT'}
    ]
]

이벤트 저장 프로세스 이해하기

create_event()를 호출하면 내부적으로 어떤 일이 일어날까요? AgentCore Memory는 데이터의 무결성과 보안을 위해 다음과 같은 5단계 파이프라인을 거쳐 데이터를 저장합니다.

  1. 요청 접수: create_event() API가 호출됩니다.
  2. 검증 (Validation):
    • actor_idsession_id의 유효성을 검사합니다.
    • 메시지 형식이 올바른지, 사용자에게 저장 권한이 있는지 확인합니다.
  3. 이벤트 생성: 고유한 eventId를 발급하고, 이벤트 발생 타임스탬프를 기록합니다.
  4. 저장소 기록 (Persistence):
    • 원본 메시지를 안전하게 암호화하여 저장합니다.
    • 빠른 검색을 위해 인덱스를 업데이트합니다.
  5. 완료 및 반환: 저장이 완료되면 eventId를 반환합니다. 이 모든 과정은 동기(Synchronous) 처리되므로, 반환 즉시 get_last_k_turns로 조회할 수 있습니다.

이처럼 Short-term Memory는 턴(Turn) 단위로 원자적(Atomic)하게 저장되며, 리스트의 순서를 엄격하게 보장하여 대화의 흐름이 뒤섞이는 것을 방지합니다.

장기 기억 활용

장기 기억(Long-term Memory)은 저장된 단기 기억(대화)을 재료로 하여 생성됩니다. 기본 옵션을 사용할 경우, 각 전략(Facts, Preferences, Summary)에 맞춰 사전 정의된 프롬프트가 백그라운드에서 실행되며 기억을 추출하고 업데이트합니다. 물론, 비즈니스 요구사항에 맞춰 이 프롬프트를 커스터마이징하여 추출 로직을 변경하는 것도 가능합니다.
저장된 기억을 불러올 때는 retrieve_memories() 메서드를 사용합니다. 이때 단순한 키워드 매칭이 아니라, 사용자의 질문 의도와 가장 유사한 기억을 찾아내는 의미론적 검색(Semantic Search)이 수행됩니다.

비동기 처리와 대기 시간

중요한 점은 단기 기억은 저장 즉시 활용가능 하지만, 장기 기억은 생성에 시간이 소요됩니다. 대화가 저장된 후 LLM이 내용을 분석하고 추출하는 과정이 비동기(Asynchronous)로 이루어지기 때문입니다.

사실 정보 (Semantic Memory) 검색

# 단기 기억은 저장 즉시 활용 가능하지만 장기 기억은 추출에 시간이 소요됩니다.
time.sleep(120)

# 5.1 Semantic memory 검색
facts = client.retrieve_memories(
    memory_id=memory['id'],
    namespace=f"/facts/{actor_id}",
    query="사용자 정보",
    top_k=5
)

print(facts)
"""
출력 결과:
[{
    'memoryRecordId': 'mem-89577cc8-a405-4cf9-bd27-396abba385e4', 
    'content': {'text': '사용자는 치킨을 주문했다.'}, 
    'memoryStrategyId': 'facts-Sgv0KdDPV7', 
    'namespaces': ['/facts/minsukim'], 
    'createdAt': datetime.datetime(2025, 12, 4, 18, 3, 46, 135000, tzinfo=tzlocal()), 
    'score': 0.37856376, 
    'metadata': {'x-amz-agentcore-memory-recordType': {'stringValue': 'BASE'}}
}]
"""

#5.2 User preference memory 검색
prefs = client.retrieve_memories(
    memory_id=memory['id'],
    namespace=f"/preferences/{actor_id}",
    query="선호도",
    top_k=5
)

print(prefs)
# 단순히 "치킨"이라는 단어만 저장하는 것이 아니라, 문맥(Context)과 선호 대상(Preference), 카테고리 등을 구조화하여 저장한 것을 확인할 수 있습니다.
"""
출력 결과:
[{
    'memoryRecordId': 'mem-4299bc45-bb97-4a93-bdd2-64c009981b78', 
    'content': {
        'text': '{"context":"사용자가 치킨을 좋아한다고 명시적으로 언급함", "preference":"치킨을 좋아함","categories":["음식"]}'
    }, 
    'memoryStrategyId': 'preferences-LIsEey8Dj6', 
    'namespaces': ['/preferences/minsukim'], 
    'createdAt': datetime.datetime(2025, 12, 4, 18, 3, 46, 135000, tzinfo=tzlocal()), 
    'score': 0.36258087, 
    'metadata': {'x-amz-agentcore-memory-recordType': {'stringValue': 'BASE'}}
}]
"""

#5.3 Summary memory 검색
summaries = client.retrieve_memories(
    memory_id=memory['id'],
    namespace=f"/summaries/{actor_id}/{session_id}",
    query="요약",
    top_k=5
)

print(summaries)
# 긴 대화의 흐름을 파악하기 위해 summaries 네임스페이스를 조회합니다. actor_id와 session_id를 모두 네임스페이스에 포함하여 특정 세션의 요약본을 가져올 수 있습니다.
"""
출력 결과:
[{
    'memoryRecordId': 'mem-d15ac09916fb2f81e85d94f06f7f5954771a', 
    'content': {
        'text': '<topic name="식사 선택">\n사용자가 무엇을 먹을지 질문하자 치킨, 피자, 족발이 추천됨\n사용자는 치킨을 좋아한다고 하여 치킨을 선택함\n치킨이 주문되었고 30분 후에 도착 예정임\n</topic>'
    }, 
    'memoryStrategyId': 'summaries-Sd40MlEqlw', 
    'namespaces': ['/summaries/minsukim/test-session-123456781234567812345678'], 
    'createdAt': datetime.datetime(2025, 12, 4, 18, 3, 46, 135000, tzinfo=tzlocal()), 
    'score': 0.33990282, 'metadata': {'x-amz-agentcore-memory-recordType': {'stringValue': 'BASE'}}
}]

지금까지 Bedrock AgentCore Memory를 사용하여 단기 기억을 저장하고, 이를 바탕으로 생성된 장기 기억(사실, 선호도, 요약)을 검색하는 핵심 기능들을 살펴보았습니다.
하지만 실제 애플리케이션 개발 시 매번 이렇게 SDK를 직접 호출하여 메모리를 관리하는 것은 번거로울 수 있습니다. 다음 섹션에서는 Strands Agents SDK를 활용하여, 에이전트가 대화 과정에서 자동으로 기억을 저장하고 불러오도록 연동하는 방법을 알아보겠습니다.

Strands Agents 와 Bedrock AgentCore Memory의 연동

앞선 섹션에서는 MemoryClient를 직접 호출하여 이벤트를 저장하고 불러왔습니다. 하지만 실제 애플리케이션을 개발할 때마다 모든 대화 턴(Turn)에서 이를 수동으로 처리하는 것은 번거로운 일입니다. Strands Agents SDK는 Bedrock AgentCore Memory와 완벽하게 통합되어 이러한 작업을 자동화합니다.

  • 자동 저장/로드: 에이전트 실행 시 create_event()get_last_k_turns()가 백그라운드에서 자동으로 호출됩니다.
  • 자동 검색 (RAG): retrieve_memories()를 통해 필요한 장기 기억을 스스로 찾아 프롬프트에 주입합니다.
  • 생명주기 관리: 세션의 시작과 종료를 관리하고 리소스를 정리하여 메모리 누수를 방지합니다.

연동 설정 및 에이전트 설정

연동의 핵심은 AgentCoreMemorySessionManager입니다. 이 매니저는 어떤 메모리 공간(memory_id)을 사용할지, 그리고 어떤 전략(Namespace)에서 기억을 꺼내올지(retrieval_config)를 정의합니다.

from bedrock_agentcore.memory import MemoryClient
from bedrock_agentcore.memory.integrations.strands.config import (
    AgentCoreMemoryConfig,
    RetrievalConfig
)
from bedrock_agentcore.memory.integrations.strands.session_manager import (
    AgentCoreMemorySessionManager
)
from strands import Agent
import logging
import time

# Bedrock AgentCore Memory 연동 로그를 확인하기 위한 로그 구성
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


# Bedrock AgentCore Memory를 활용하는 Strands Agents 구성
def create_strands_agent(memory_id, actor_id, session_id):
    # 1. 메모리 설정 구성 (Retrieval Config) 
    # 에이전트가 대화 시 참고할 장기 기억의 임계값(Threshold)을 설정합니다.    
    config = AgentCoreMemoryConfig(
        memory_id=memory_id,
        session_id=session_id,
        actor_id=actor_id,
        retrieval_config={
            "/preferences/{actorId}": RetrievalConfig(
                top_k=5,
                relevance_score=0.6 # 활용할 memory의 최소 유사도 컷오프 점수
            ),
            "/facts/{actorId}": RetrievalConfig(
                top_k=10,
                relevance_score=0.3
            )
        }
    )
    
    # 2. 세션 매니저 초기화 
    # 위에서 정의한 설정을 바탕으로 세션 관리자를 생성합니다.
    session_manager = AgentCoreMemorySessionManager(
        agentcore_memory_config=config,
        region_name="us-west-2"
    )
    
    # 3. Strands Agent 생성 
    # session_manager를 주입하면 메모리 연동이 완료됩니다.
    agent = Agent(
        session_manager=session_manager
    )
    
    return agent

[로그 분석: 초기화 확인] 코드가 실행되면 SDK는 자격 증명을 확인하고 MemoryClient를 초기화하여 연결을 구성합니다.

INFO: bedrock_agentcore.memory.client: Initialized MemoryClient for control plane: us-west-2
INFO: bedrock_agentcore.memory.client: Retrieved total of 1 events

실행 시나리오

이제 에이전트와 대화를 나누며 실제로 기억이 어떻게 형성되고, 새로운 세션에서 어떻게 활용되는지 확인해 보겠습니다.

첫 번째 세션

# 기존에 생성한 메모리 ID 사용
memory_id = "test-long-term-memory-..." 
actor_id = "chanhosoh"
session_id = "session-1"

agent = create_strands_agent(memory_id, actor_id, session_id)

# 대화 진행
agent("안녕 내 이름은 찬호야")
agent("나는 서울에서 일하는 개발자야")
agent("나는 Python이랑 Rust를 좋아해")

# 동일 세션 내 질문: 단기 기억(Short-term Memory)을 활용하여 답변합니다.
agent("내 직업이 뭐라고 그랬지?")

[로그 분석: 이벤트 자동 생성] 사용자가 채팅을 입력할 때마다 create_event()가 자동으로 호출되어 이벤트를 생성합니다.

INFO: bedrock_agentcore.memory.client: Created event: 0000001764903427355#5d6b9f20
INFO: bedrock_agentcore.memory.integrations.strands.session_manager: Created agent: default

사용자가 자신의 정보를 알려주었습니다. 이 정보들은 단기 기억에 저장된 후, 백그라운드 프로세스를 통해 장기 기억을 생성합니다.

두 번째 세션

장기 기억이 생성될 시간을 부여하고, 새로운 세션을 생성합니다. 이때 에이전트는 이전 세션의 대화 로그(단기 기억)는 볼 수 없지만, 추출된 장기 기억을 통해 사용자를 알아봅니다.

# Long-term memory 생성을 위해 잠시 대기 (비동기 처리)
time.sleep(120)

# 새로운 세션 ID로 에이전트 재생성 (마치 며칠 뒤 다시 접속한 상황)
new_session_id = "session-2"
agent = create_strands_agent(memory_id, actor_id, new_session_id)

# 에이전트가 나를 기억하는지 테스트
agent("내 이름이 뭐야?")
agent("나는 무슨 프로그래밍 언어를 좋아해?")

[로그 분석: 기억 검색 및 주입] 사용자 질문이 들어오면 SDK는 설정된 Namespace(/preferences, /facts)를 자동으로 검색하고 컨텍스트에 추가합니다.

INFO: bedrock_agentcore.memory.client: Retrieved memories from namespace: /preferences
INFO: bedrock_agentcore.memory.client: Retrieved memories from namespace: /facts/chanhosoh

실제로 새로운 세션이 생성된 이후에 진행된 대화를 확인해보면 아래와 같습니다.

[{
    'role': 'user', 
    'content': [{'text': '내 이름이 뭐야?'}]
}, 
{
    'role': 'assistant', 
    'content': [{'text': '<user_context>{"context":"사용자가 자신의 이름을 찬호라고 소개했습니다.","preference":"이름은 찬호","categories":["개인 정보"]}\n{"context":"사용자가 자신을 서울에서 일하는 개발자라고 직접 소개했습니다.","preference":"서울에서 개발자로 일함","categories":["직업","위치"]}\n{"context":"사용자가 Python과 Rust를 좋아한다고 명확히 표현했습니다.","preference":"Python과 Rust 프로그래밍 언어를 선호함","categories":["프로그래밍","기술","개발"]}\n사용자의 이름은 찬호이다.\n찬호는 Python과 Rust 프로그래밍 언어를 좋아한다.\n찬호는 서울에서 일하는 개발자이다.</user_context>'}]
}, 
{
    'role': 'assistant', 
    'content': [{'text': '\n\n찬호님이에요! 찬호님이 처음에 자기소개해 주실 때 말씀해 주셨죠 😊'}]
}]

대화를 저장하고 장기 기억을 검색하는 작업을 AgentCoreMemorySessionManager 에서 자동으로 처리해주는 것을 확인할 수 있습니다. 여기서 추가로 알아두면 좋을 점은 장기 기억은 대화 중에는 활용이 되지만 실제로 위에 주입된 컨텍스트는 short term memory에는 저장하지는 않는다는 사실입니다.

심화 기능: Memory forking으로 대화의 가능성 확장하기

지금까지 우리는 에이전트가 과거를 기억하고(Retrieval), 이를 바탕으로 현재 대화를 이어가는 선형적인 흐름을 살펴보았습니다. 하지만 실제 비즈니스 환경에서는 대화가 항상 한 방향으로만 흐르지 않습니다. 사용자가 방금 한 말을 취소하고 싶어 하거나(Undo), 개발자가 동일한 상황에서 다른 프롬프트를 테스트해보고 싶은(A/B Test) 니즈가 발생합니다.
Memory Forking은 이러한 복잡한 요구사항을 해결하기 위해, 대화의 특정 시점에서 ‘가지치기(Branching)’를 할 수 있게 해주는 강력한 기능입니다.

개념: 대화의 Git Branch

소프트웨어 개발자라면 Git의 브랜치 개념에 익숙하실 겁니다. Memory Forking은 이 개념을 대화형 AI에 그대로 적용했습니다. 대화가 하나의 선형적 타임라인이 아니라, 여러 가능성을 가진 트리(Tree) 구조로 확장되는 것입니다.
예를 들어, “오늘 날씨 어때?”라는 질문(Root Event)에서 시작해, 한쪽 브랜치에서는 “내일 날씨는?”으로 이어가고, 다른 브랜치에서는 “다음 주 날씨는?”으로 분기하여 동시에 두 가지 대화 흐름을 유지할 수 있습니다.

왜 Forking이 필요한가? “컨텍스트 오염(Context Pollution)” 방지

Undo/Redo 기능 구현

최근 발표된 Claude Code와 같은 최신 AI 코딩 도구들이 ‘특정 시점으로의 상태 롤백(Revert)’ 기능을 핵심으로 내세우는 이유가 무엇일까요? 단순히 코드를 지우는 것과 시간을 되돌리는 것은 천지 차이기 때문입니다.

시나리오: 에이전트가 버그가 있는 코드를 작성했을 때

  • 일반적인 Undo (Linear Undo): 사용자가 “이거 취소해줘”라고 말하면, 에이전트는 코드를 지우는 명령을 수행합니다. 하지만 대화 기록(Context)에는 [잘못된 코드 생성] → [취소 요청] → [코드 삭제]라는 흔적이 그대로 남아 있습니다.
    • 위험성: 이 잔여 컨텍스트(Residual Context)는 위험합니다. 모델은 이전 대화에 남아있는 잘못된 코드나 논리를 무의식적으로 참고하게 되며, 이는 이후 대화에서 비슷한 버그를 반복하거나, 엉뚱한 변수명을 다시 꺼내 쓰는 환각의 원인이 됩니다.
  • Memory Forking을 이용한 Rollback: Forking을 사용하면 잘못된 코드를 생성하기 직전의 시점으로 돌아가 새로운 브랜치를 생성합니다.
    • 이점: 에이전트의 기억 속에서 실패한 시도는 완전히 소멸됩니다. 에이전트는 실패의 기억에 오염되지 않은 깨끗한 상태에서 다시 올바른 코드를 작성할 수 있습니다. 이는 복잡한 코딩이나 추론 작업에서 에이전트의 성능을 유지하는 데 결정적입니다.

AgentCore Memory는 fork 기능을 통해 에이전트가 항상 최적의 컨텍스트 상태를 유지하도록 돕습니다.

리스크 없는 실험실: 실시간 A/B 테스팅 (Shadow Testing)

Memory Forking을 사용하면 ‘섀도우 테스팅(Shadow Testing)’ 패턴을 구현할 수 있습니다.

  • 작동 방식: 사용자의 질문이 들어왔을 때, 메인 브랜치(Main Branch)는 기존의 안정된 프롬프트로 답변하여 사용자에게 보여줍니다. 동시에 백그라운드에서는 실험용 브랜치(Experimental Branch)를 Fork하여 새로운 프롬프트(“V2_친근한 말투”)로 답변을 생성해 봅니다.
  • 가치: 사용자는 아무런 변화를 느끼지 못하지만(안전함), 개발자는 실제 사용자 데이터(Real-world Data)에 대한 두 프롬프트의 답변 품질을 비교 분석할 수 있습니다. 이는 막연한 추측이 아닌 데이터에 기반한 의사결정을 가능하게 합니다.

타임 머신 디버깅 (Time Travel Debugging)

에이전트가 복잡한 시나리오(예: 여행 일정 짜기) 수행 중 10번째 턴에서 갑자기 오류를 범했다고 가정해 봅시다. 기존에는 이를 수정하려면 처음부터 9번의 대화를 다시 입력하며 상황을 재현(Reproduce)해야 했습니다. 이는 매우 비효율적이며, LLM의 확률적 특성상 완벽한 재현도 어렵습니다.
이를 Memory Forking을 이용해서 해결할 수 있습니다.

  • Replay & Fix: 오류가 발생한 바로 그 시점(10번째 턴 직전)으로 돌아가(Go to), 문제의 원인이 된 프롬프트나 로직만 수정한 뒤 새로운 브랜치에서 실행합니다.
  • What-If 분석: “만약 그때 검색 결과(RAG)가 달랐다면?”, “만약 그때 에이전트가 반문했다면?”과 같은 다양한 가설을 그 자리에서 즉시 검증할 수 있습니다.

구현

시나리오: 날씨 봇과의 대화 중, 사용자가 다른 질문을 하고 싶은 상황을 가정해 보겠습니다.

# 1. 초기 대화 진행 (Main Branch)
# 사용자가 "오늘 날씨"를 묻고 답변을 받은 상태입니다.
event1 = memory_client.create_event(
    memory_id=memory['id'],
    actor_id=actor_id,
    session_id=session_id,
    messages=[
        ("오늘 날씨가 어때?", "USER"),
        ("오늘은 맑고 화창합니다.", "ASSISTANT")
    ]
)
root_event_id = event1.get('eventId') # 분기점이 될 이벤트 ID

# 2. 메인 대화 계속 진행
# 원래 흐름대로 "내일 날씨"를 묻습니다.
memory_client.create_event(
    memory_id=memory['id'],
    actor_id=actor_id,
    session_id=session_id,
    messages=[("내일은?", "USER"), ("내일은 비가 옵니다.", "ASSISTANT")]
)

# 3. Memory Forking: 다른 가능성 탐색
# "만약 사용자가 내일 날씨 대신 다음 주 날씨를 물어봤다면?"
forked_event = memory_client.fork_conversation(
    memory_id=memory['id'],
    actor_id=actor_id,
    session_id=session_id,
    root_event_id=root_event_id,       # 1번 이벤트 시점으로 돌아가서
    branch_name="weather-alternative", # 새로운 브랜치 이름 지정
    new_messages=[                     # 새로운 대화 흐름 주입
        ("다음 주는 어떨까?", "USER"),
        ("다음 주는 대체로 흐릴 것으로 예상됩니다.", "ASSISTANT")
    ]
)

print(f"✅ 분기 생성 완료: {forked_event.get('eventId')}")

브랜치 관리 및 시각화

생성된 브랜치들은 list_branches를 통해 조회하거나, get_conversation_tree를 통해 전체 구조를 시각적으로 파악할 수 있습니다.

# 전체 대화 트리 구조 조회
conversation_tree = memory_client.get_conversation_tree(
    memory_id=memory['id'],
    actor_id=actor_id,
    session_id=session_id
)

print(json.dumps(conversation_tree, indent=2, ensure_ascii=False))
"""
출력 예시:
{
    'session_id': 'session-003', 
    'actor_id': 'user-003', 
    'main_branch': {
        'events': [], 
        'branches': {
            'main': {
                'root_event_id': None, 
                'events': [{
                    'eventId': '0000001764919136838#8a280402', 
                    'timestamp': datetime.datetime(2025, 12, 5, 16, 18, 56, 838000, tzinfo=tzlocal()), 
                    'messages': [{
                        'role': 'USER', 
                        'text': '내일은?...'
                    }, 
                    {
                        'role': 'ASSISTANT', 
                        'text': '내일은 비가 올 예정입니다....'
                    }]}, 
                {
                    'eventId': '0000001764919135945#d33bf2e2', 
                    'timestamp': datetime.datetime(2025, 12, 5, 16, 18, 55, 945000, tzinfo=tzlocal()), 
                    'messages': [{
                        'role': 'USER', 
                        'text': '오늘 날씨가 어때?...'
                    }, 
                    {
                        'role': 'ASSISTANT', 'text': '오늘은 맑고 화창합니다....'
                    }]}, 
                {
                    'eventId': '0000001764907280746#04daf8d3', 
                    'timestamp': datetime.datetime(2025, 12, 5, 13, 1, 20, 746000, tzinfo=tzlocal()), 
                    'messages': [{
                        'role': 'USER', 'text': '내일은?...'
                    },
                    {
                        'role': 'ASSISTANT', 
                        'text': '내일은 비가 올 예정입니다....'
                    }]
                }, 
                {
                    'eventId': '0000001764907279934#e93a24ea', 
                    'timestamp': datetime.datetime(2025, 12, 5, 13, 1, 19, 934000, tzinfo=tzlocal()), 
                    'messages': [{
                        'role': 'USER', 'text': '오늘 날씨가 어때?...'
                    },
                    {
                        'role': 'ASSISTANT', 
                        'text': '오늘은 맑고 화창합니다....'
                    }]
                }
            ]}, 
            'weather-alternative': {
                'root_event_id': '0000001764907279934#e93a24ea', 
                'events': [{
                    'eventId': '0000001764907281044#087b9218', 
                    'timestamp': datetime.datetime(2025, 12, 5, 13, 1, 21, 44000, tzinfo=tzlocal()), 
                    'messages': [{'role': 'USER', 'text': '다음 주는 어떨까?...'}, 
                    {'role': 'ASSISTANT', 'text': '다음 주는 대체로 흐릴 것으로 예상됩니다....'}]
                }
            ]}
        }
    }
}
"""

이처럼 Memory Forking을 활용하면, 에이전트 개발자는 단일 대화의 제약에서 벗어나 다양한 시나리오를 안전하게 실험하고, 사용자에게 더 유연한 경험을 제공할 수 있습니다. 메인 서비스의 안정성을 지키면서도 지속적으로 모델의 응답 품질을 개선할 수 있는 강력한 도구가 되는 것입니다.

마무리

Amazon Bedrock AgentCore Memory는 단순한 데이터 저장소를 넘어, LLM의 근본적인 한계인 ‘Statelessness’를 보완하는 핵심 인프라입니다. 이는 일회성 문답에 그치던 AI를, 사용자의 맥락을 이해하고 지속적으로 업무를 수행하는 신뢰할 수 있는 에이전트로 진화시키는 기반이 됩니다.
최근 AI 연구 동향은 이러한 메모리 시스템의 중요성을 꾸준히 입증하고 있습니다.

  • 인지 아키텍처의 입증: 스탠퍼드대의 “Generative Agents (Park et al., 2023)” 연구는 에이전트가 단순히 과거를 저장하는 것을 넘어, 기억을 반추(Reflection)하고 계획(Planning)할 때 비로소 사람과 같은 정교한 상호작용이 가능함을 보여주었습니다. 최근 추가된 Episodic memory가 이 기능과 유사한 구현입니다.
  • 효율성과 정확도: 2025년 발표된 “Mem0” 연구는 무작정 Context Window를 늘리는 것보다 구조화된 메모리 계층을 사용하는 것이 비용은 90% 절감하면서도 응답 정확도는 오히려 높다는 점을 확인했습니다. AgentCore Memory는 이러한 효율성을 클라우드 네이티브 환경에 구현한 실체입니다.
  • 복잡한 과업 수행: 과학 실험 자동화를 다룬 “SciBORG (Muhoberac et al., 2025)” 연구는 복잡한 워크플로우가 실패 없이 완수되려면 에이전트가 자신의 현재 상태(State)와 과거 이력을 명확히 기억해야 함을 강조합니다.
  • 기억의 연결과 확장: “A-MEM (Xu et al., 2025)“은 메모리가 정적인 텍스트가 아니라, 제텔카스텐(Zettelkasten) 방식처럼 서로 연결되고 동적으로 진화해야 함을 제안했습니다.

Amazon Bedrock AgentCore Memory는 이러한 최신 연구의 흐름을 실제 엔터프라이즈 환경에 맞춰 구현한 결과물입니다. 3계층 격리 구조(Memory-Actor-Session)와 암호화된 저장소는 기업이 보안 우려 없이 기억하는 AI Agent를 도입할 수 있는 실질적인 토대를 제공합니다. 결국 AgentCore Memory는 개발자가 복잡한 RAG 파이프라인이나 벡터 DB를 직접 관리하는 수고를 덜고, 사용자를 이해하는 서비스라는 본질적인 가치에 집중할 수 있도록 돕습니다. 이제 여러분의 에이전트에 기억을 더해, 더 똑똑하고 유용한 비즈니스 파트너로 발전시켜 보시기 바랍니다.

References

Chanho Soh

Chanho Soh

소찬호 Solutions Architect는 AWS에서 리테일 및 CPG 산업의 고객분들의 클라우드 도입을 지원하고 있습니다. 최근에는 생성형 AI 분야에서의 다양한 연구를 산업에 접목시키는 데에 집중하며 AWS의 AI/ML 서비스를 활용한 프로젝트를 통해 고객의 비즈니스 혁신과 경쟁력 강화에 기여하고 있습니다.