8장 — 기억 관리

지능형 에이전트가 정보를 보존하려면 효과적인 기억 관리가 필수적입니다. 사람과 마찬가지로 에이전트도 효율적으로 작동하려면 여러 유형의 기억이 필요합니다.

에이전틱 시스템에서 기억이란 과거 상호작용과 관찰, 학습 경험에서 얻은 정보를 보존하고 활용하는 에이전트의 능력을 뜻합니다. 이 능력 덕분에 에이전트는 충분한 정보를 바탕으로 의사결정을 내리고, 대화 맥락을 유지하며, 시간이 지남에 따라 성능을 개선할 수 있습니다.

두 가지 기억

단기 기억 (맥락 기억)

작업 기억(working memory) 과 유사하며, 현재 처리 중이거나 최근 접근한 정보를 담습니다. 대규모 언어 모델을 사용하는 에이전트의 경우 단기 기억은 주로 컨텍스트 윈도(Context Window) 안에 존재합니다. 이 윈도에는 최근 메시지, 에이전트 응답, 도구 사용 결과, 현재 상호작용에서 발생한 에이전트 리플렉션이 포함되며, 이 모든 정보가 LLM의 후속 응답과 동작에 영향을 미칩니다.

장기 기억 (영속 기억)

여러 상호작용, 작업 또는 장기간에 걸쳐 에이전트가 보존해야 하는 정보를 담는 저장소 역할을 하며, 장기 지식 베이스와 유사합니다. 데이터는 일반적으로 에이전트가 직접 처리하는 환경 바깥, 즉 데이터베이스·지식 그래프·벡터 데이터베이스 등에 저장됩니다.

벡터 데이터베이스에서는 정보가 수치 벡터로 변환·저장되어, 에이전트가 정확한 키워드 일치가 아닌 의미적 유사성을 기반으로 데이터를 검색할 수 있는데 이 과정을 의미 기반 검색(semantic search) 이라 합니다. 에이전트에게 장기 기억 속 정보가 필요하면 외부 저장소에 질의하여 관련 데이터를 가져온 뒤 단기 컨텍스트에 통합해 바로 사용합니다.

긴 컨텍스트 윈도가 답이 아닌 이유

이 장에서 가장 중요한 대목입니다. 컨텍스트 윈도 용량은 제한되어 있어 에이전트가 직접 접근할 수 있는 최근 정보량에 한계가 있습니다. 단기 기억을 효율적으로 관리하려면 오래된 대화 구간을 요약하거나 핵심 정보를 강조하는 등의 기법으로 이 제한된 공간 안에 가장 관련성 높은 정보를 유지해야 합니다.

그렇다면 “긴 컨텍스트 윈도” 모델을 쓰면 해결될까요? 책의 답은 아니라는 쪽입니다.

최근 등장한 ‘긴 컨텍스트’ 윈도 모델은 이 단기 기억의 크기를 확장해, 단일 상호작용 안에서 더 많은 정보를 담을 수 있게 할 뿐입니다. 그러나 이 컨텍스트는 여전히 일시적이어서 세션이 종료되면 사라지며, 매번 처리하는 비용이 크고 비효율적일 수 있습니다.

따라서 에이전트가 진정한 영속성을 확보하고, 과거 상호작용에서 정보를 회상하며, 오래 유지되는 지식 기반을 구축하려면 별도의 기억 유형이 필요합니다. 컨텍스트 윈도를 키우는 것은 장기 기억의 대체재가 아닙니다.

참고로 이 장의 옮긴이 주는 컨텍스트를 이렇게 정의합니다. 1) 포괄적으로는 인공지능이 특정 작업을 수행할 때 참고하는 모든 정보, 2) 이 장에서는 주로 멀티턴 작업에서 언어 모델의 입력 후보로서 관리되는 상황 정보들.

실제 적용 및 활용 사례

기억 관리는 에이전트가 정보를 추적하고 시간이 지나도 지능적으로 작동하도록 하는 핵심 요소입니다. 이 기능이 있어야 에이전트는 단순한 질의응답 수준을 넘어설 수 있습니다.

사례 단기 기억의 역할 장기 기억의 역할
챗봇과 대화형 AI 이전 사용자 입력을 기억해 일관된 응답을 제공한다 사용자 선호도, 과거에 겪은 문제, 이전 대화 내용을 떠올려 개인화되고 연속적인 상호작용을 가능하게 한다
작업 수행 에이전트 이전 단계, 현재 진행 상황, 전체 목표를 추적한다. 작업 컨텍스트나 임시 저장소에 보관된다 현재 컨텍스트에 없는 특정 사용자 관련 데이터에 접근한다
개인화된 경험 사용자 선호도, 과거 행동, 개인 정보를 저장하고 다시 불러와 응답과 제안을 사용자에게 맞게 조정한다
학습과 개선 성공적인 전략, 실수, 새로운 정보를 저장하고 이후 적응에 활용한다. 강화 학습 에이전트가 학습한 전략이나 지식을 이 방식으로 저장한다
정보 검색(RAG) 지식 기반에 접근하며, 흔히 검색 증강 생성 안에서 구현된다
자율 시스템 근거리 주변 환경에 즉각 대응한다 지도, 경로, 물체 위치, 학습된 행동 등 환경 전반에 대한 일반 지식을 담아 둔다

기억이 있어야 에이전트는 이력을 유지하고, 학습하며, 상호작용을 개인화하고, 시간에 따라 변하는 복잡한 문제를 다룰 수 있습니다.

구글 ADK의 기억 관리

ADK는 컨텍스트 관리를 세 가지 핵심 개념으로 구조화합니다.

개념 역할
세션(Session) 특정 상호작용의 메시지와 동작(Event)을 기록하는 개별 대화 스레드이며, 그 대화에 관련된 임시 데이터(상태)도 함께 저장한다
상태(session.state) 세션 내에 저장되는 데이터로, 현재 활성 대화 스레드에 관련된 정보만 담는다
기억(Memory) 과거의 다양한 대화나 외부 소스에서 가져온 정보를 담는 검색 가능한 저장소로, 현재 대화 범위를 넘어서는 데이터 검색에 활용된다

SessionService 는 대화 스레드(Session 객체)의 시작·기록·종료를 관리하고, MemoryService 는 장기 지식(기억) 저장과 검색을 담당합니다.

세션 — 각 대화를 추적하기

Session 객체는 고유 식별자(id, app_name, user_id), Event 객체로 구성된 시간순 기록, 세션별 임시 데이터를 위한 state 저장 영역, 마지막 업데이트 시각(last_update_time)을 캡슐화합니다.

적절한 SessionService 를 선택하는 일은 매우 중요합니다. 어떤 서비스를 택하느냐에 따라 에이전트의 상호작용 이력과 임시 데이터가 어떻게 저장되고 얼마나 지속되는지가 결정되기 때문입니다.

# 예제: InMemorySessionService 사용
# 로컬 개발 및 테스트에 적합하며, 애플리케이션 재시작 시
# 데이터를 유지할 필요가 없는 경우에 사용한다.
from google.adk.sessions import InMemorySessionService
session_service = InMemorySessionService()
# 예제: DatabaseSessionService 사용
# 영속 저장이 필요한 운영 환경 또는 개발 환경에 적합하다.
# 데이터베이스 URL을 설정해야 한다(예: SQLite, PostgreSQL 등).
# 필요 패키지: pip install google-adk[sqlalchemy] 및 데이터베이스
# 드라이버(예: PostgreSQL용 psycopg2)
from google.adk.sessions import DatabaseSessionService
# 로컬 SQLite 파일을 사용하는 예제:
db_url = "sqlite:///./my_agent_data.db"
session_service = DatabaseSessionService(db_url=db_url)
# 예제: VertexAiSessionService 사용
# 구글 클라우드 플랫폼에서 Vertex AI 인프라를 활용한
# 확장 가능한 운영 환경의 세션 관리에 적합하다.
# 필요 패키지: pip install google-adk[vertexai] 및 GCP 설정/인증
from google.adk.sessions import VertexAiSessionService

PROJECT_ID = "your-gcp-project-id"   # 사용할 GCP 프로젝트 ID로 교체
LOCATION = "us-central1"             # 사용할 GCP 리전으로 교체

# 이 서비스에서 사용하는 app_name은 Reasoning Engine의
# ID 또는 이름에 대응해야 한다.
REASONING_ENGINE_APP_NAME = "projects/your-gcp-project-id/locations/us-central1/reasoningEngines/your-engine-id"

session_service = VertexAiSessionService(project=PROJECT_ID, location=LOCATION)
# 이 서비스를 사용할 때는 서비스 메서드에 REASONING_ENGINE_APP_NAME을 전달한다:
# session_service.create_session(app_name=REASONING_ENGINE_APP_NAME, …)
# session_service.get_session(app_name=REASONING_ENGINE_APP_NAME, …)
# session_service.append_event(session, event, app_name=REASONING_ENGINE_APP_NAME)
# session_service.delete_session(app_name=REASONING_ENGINE_APP_NAME, …)

각 메시지 턴마다 반복되는 처리 절차는 이렇습니다.

메시지 수신
  → Runner 가 SessionService 를 통해 세션을 가져오거나 새로 생성
  → 에이전트가 세션의 컨텍스트(상태와 과거 상호작용)를 활용해 메시지 처리
  → 에이전트가 응답을 생성하며 상태를 갱신할 수도 있음
  → Runner 가 이를 Event 로 캡슐화한 뒤 session_service.append_event 로
    새 이벤트를 기록하고 저장소의 상태를 갱신
  → 세션은 다음 메시지를 대기
  → 상호작용이 종료되면 delete_session 으로 세션을 종료

상태 — 세션의 임시 메모장

session.events 가 전체 대화 이력을 기록하는 반면, session.state 는 활성 대화에 관련된 동적 데이터를 저장·갱신합니다. 기본적으로 딕셔너리로 작동하여 데이터를 키-값 쌍으로 저장합니다. 값은 문자열·숫자·불·리스트, 그리고 이러한 기본 타입으로 구성된 딕셔너리 등 직렬화 가능한 파이썬 타입이어야 합니다.

키 접두어를 사용하면 데이터 범위와 영속성을 정의하여 상태를 체계적으로 구성할 수 있습니다. 접두어가 없는 키는 해당 세션에 한정됩니다.

접두어 범위
user: 데이터를 사용자 ID에 연결하여 모든 세션에서 공유
app: 애플리케이션의 모든 사용자가 공유하는 데이터
temp: 현재 처리 턴에서만 유효하며 영속적으로 저장되지 않는 데이터
(없음) 해당 세션에 한정

상태를 갱신하는 두 가지 방법

상태는 session_service.append_event() 를 통해 세션 이력에 이벤트를 추가할 때 함께 갱신해야 합니다. 그래야 변경 내용이 정확히 추적되고, 영속 서비스에서도 올바르게 저장되며, 상태 변경도 안전하게 처리됩니다.

1. 간편한 방법 — output_key (에이전트 텍스트 응답용)

에이전트의 최종 텍스트 응답을 상태에 직접 저장하려면 이 방법이 가장 간단합니다. LlmAgent 를 설정할 때 output_key 만 지정하면 됩니다. 내부적으로 Runner가 output_key 를 감지하여 append_event 를 호출할 때 state_delta 가 포함된 필요한 동작을 자동으로 생성합니다.

# 구글 ADK에서 필요한 클래스 임포트
from google.adk.agents import LlmAgent
from google.adk.sessions import InMemorySessionService, Session
from google.adk.runners import Runner
from google.genai.types import Content, Part

# output_key를 지정한 LlmAgent 정의
greeting_agent = LlmAgent(
    name="Greeter",
    model="gemini-2.0-flash",
    instruction="Generate a short, friendly greeting.",
    output_key="last_greeting"
)

# --- Runner와 세션 설정 ---
app_name, user_id, session_id = "state_app", "user1", "session1"
session_service = InMemorySessionService()
runner = Runner(
    agent=greeting_agent,
    app_name=app_name,
    session_service=session_service
)
session = session_service.create_session(
    app_name=app_name,
    user_id=user_id,
    session_id=session_id
)
print(f"Initial state: {session.state}")

# --- 에이전트 실행 ---
user_message = Content(parts=[Part(text="Hello")])
print("\n--- Running the agent ---")
for event in runner.run(
    user_id=user_id,
    session_id=session_id,
    new_message=user_message
):
    if event.is_final_response():
        print("Agent responded.")

# --- 갱신된 상태 확인 ---
# Runner가 모든 이벤트 처리를 완료한 *이후에* 상태를 확인해야 한다.
updated_session = session_service.get_session(app_name, user_id, session_id)
print(f"\nState after agent run: {updated_session.state}")

2. 표준적인 방법 — EventActions.state_delta (더 복잡한 갱신용)

여러 키를 한꺼번에 갱신하거나, 텍스트가 아닌 데이터를 저장하거나, user:app: 처럼 특정 범위를 지정하거나, 에이전트의 최종 텍스트 응답과 무관한 갱신이 필요할 때 씁니다. 상태 변경을 담은 딕셔너리(state_delta)를 직접 만들어 추가할 Event의 EventActions 에 넣습니다.

책은 이를 도구 안에 캡슐화하는 방식을 권장 접근법으로 제시합니다.

import time
from google.adk.tools.tool_context import ToolContext
from google.adk.sessions import InMemorySessionService

# --- 권장되는 도구 기반 접근법 정의 ---
def log_user_login(tool_context: ToolContext) -> dict:
    """
    사용자 로그인 이벤트 발생 시 세션 상태를 갱신한다.
    이 도구는 사용자 로그인과 관련된 모든 상태 변경을 캡슐화한다.

    Args:
        tool_context: ADK가 자동으로 제공하며, 세션 상태에 접근할 수 있게 한다.

    Returns:
        작업 성공을 확인하는 딕셔너리.
    """
    # 제공된 컨텍스트를 통해 상태에 직접 접근한다.
    state = tool_context.state

    # 현재 값 또는 기본값을 가져온 뒤 상태를 갱신한다.
    # 이 방식이 훨씬 깔끔하고 로직을 한곳에 모아 준다.
    login_count = state.get("user:login_count", 0) + 1
    state["user:login_count"] = login_count
    state["task_status"] = "active"
    state["user:last_login_ts"] = time.time()
    state["temp:validation_needed"] = True
    print("State updated from within the `log_user_login` tool.")

    return {
        "status": "success",
        "message": f"User login tracked. Total logins: {login_count}."
    }

# --- 사용 시연 ---
# 실제 애플리케이션에서는 LLM 에이전트가 이 도구의 호출 여부를 결정한다.
# 여기서는 시연 목적으로 직접 호출을 시뮬레이션한다.

# 1. 설정
session_service = InMemorySessionService()
app_name, user_id, session_id = "state_app_tool", "user3", "session3"
session = session_service.create_session(
    app_name=app_name,
    user_id=user_id,
    session_id=session_id,
    state={"user:login_count": 0, "task_status": "idle"}
)
print(f"Initial state: {session.state}")

# 2. 도구 호출 시뮬레이션(실제 앱에서는 ADK Runner가 이 작업을 수행한다.)
# 이 독립 실행 예제를 위해 ToolContext를 수동으로 생성한다.
from google.adk.tools.tool_context import InvocationContext
mock_context = ToolContext(
    invocation_context=InvocationContext(
        app_name=app_name, user_id=user_id, session_id=session_id,
        session=session, session_service=session_service
    )
)

# 3. 도구 실행
log_user_login(mock_context)

# 4. 갱신된 상태 확인
updated_session = session_service.get_session(app_name, user_id, session_id)
print(f"State after tool execution: {updated_session.state}")

session.state 를 직접 수정하면 안 되는 이유

이 절이 이 장에서 가장 실무적인 부분입니다. 세션을 가져온 뒤 session.state 딕셔너리를 직접 수정하는 것은 표준 이벤트 처리 메커니즘을 우회하므로 권장하지 않습니다. 직접 변경하면 이런 문제가 생깁니다.

  • 세션 이벤트 이력에 기록되지 않는다
  • 선택한 SessionService 에서 영속화되지 않을 수 있다
  • 동시성 문제가 발생할 수 있다
  • 타임스탬프 같은 필수 메타데이터도 갱신되지 않는다

권장 방법은 LlmAgentoutput_key 매개변수를 사용하거나, session_service.append_event() 로 이벤트를 추가할 때 EventActions.state_delta 안에 상태 변경 사항을 포함시키는 것입니다. session.state 는 주로 기존 데이터를 읽는 용도로 사용해야 합니다.

정리하면 상태를 설계할 때는 이렇게 합니다.

단순하게 유지하고, 기본 데이터 타입을 사용하며, 키 이름을 명확하게 짓고 접두어를 올바르게 사용하고, 깊은 중첩을 피하며, 항상 append_event 절차를 통해 상태를 갱신합니다.

기억 — MemoryService 로 구현하는 장기 지식

세션 구성 요소는 현재 대화 이력(이벤트)과 해당 대화에 한정된 임시 데이터(상태)를 기록·유지합니다. 그러나 에이전트가 여러 차례 상호작용이 이루어지는 동안에도 계속 정보를 보존하거나 외부 데이터에 접근하려면 장기 지식 관리가 필요합니다. 이 역할을 담당하는 것이 MemoryService 입니다.

BaseMemoryService 인터페이스로 정의된 MemoryService 의 주요 기능은 두 가지입니다.

  • 정보 추가 — 세션에서 콘텐츠를 추출해 add_session_to_memory 메서드로 저장
  • 정보 검색 — 에이전트가 저장소에 질의하여 search_memory 메서드로 관련 데이터를 받음
# 예제: InMemoryMemoryService 사용
# 로컬 개발 및 테스트에 적합하며, 애플리케이션 재시작 시
# 데이터를 유지할 필요가 없는 경우에 사용한다.
# 앱이 종료되면 기억 내용이 소실된다.
from google.adk.memory import InMemoryMemoryService
memory_service = InMemoryMemoryService()

운영 환경에서는 일반적으로 VertexAiRagMemoryService 를 사용합니다. 구글 클라우드의 검색 증강 생성 서비스를 활용하여 확장 가능하고 영속적인 의미 기반 검색 기능을 제공합니다.

# 예제: VertexAiRagMemoryService 사용
# 필요 패키지: pip install google-adk[vertexai], GCP 설정/인증,
# 그리고 Vertex AI RAG Corpus
from google.adk.memory import VertexAiRagMemoryService

# Vertex AI RAG Corpus의 리소스 이름
RAG_CORPUS_RESOURCE_NAME = "projects/your-gcp-project-id/locations/us-central1/ragCorpora/your-corpus-id"

# 검색 동작의 선택적 설정
SIMILARITY_TOP_K = 5              # 상위 검색 결과 개수
VECTOR_DISTANCE_THRESHOLD = 0.7   # 벡터 유사도 임계값

memory_service = VertexAiRagMemoryService(
    rag_corpus=RAG_CORPUS_RESOURCE_NAME,
    similarity_top_k=SIMILARITY_TOP_K,
    vector_distance_threshold=VECTOR_DISTANCE_THRESHOLD
)
# 이 서비스를 사용하면 add_session_to_memory나
# search_memory 같은 메서드가 지정된 Vertex AI
# RAG Corpus와 상호작용한다.

LangChain 과 LangGraph 의 기억 관리

LangChain의 기억 기능은 저장된 이력을 참조해 현재 프롬프트를 풍부하게 만들고, 최신 대화를 기록해 향후 활용하는 방식으로 토대를 마련해 줍니다.

구분 특징
단기 기억 스레드 범위(thread-scoped) 로 작동하며, 단일 세션이나 스레드 내 진행 중인 대화를 추적한다. 전체 이력이 쌓이면 LLM의 컨텍스트 윈도에 부담을 주어 오류나 성능 저하로 이어질 수 있다. LangGraph는 이를 에이전트 상태의 일부로 관리하며, 체크포인터(checkpointer) 를 통해 유지하여 스레드를 언제든지 재개할 수 있게 한다
장기 기억 세션 간에 사용자별 또는 애플리케이션 수준의 데이터를 저장하며, 대화 스레드 간에 공유된다. 직접 정의한 “이름공간”에 저장되어 어떤 스레드에서든 언제든 회상할 수 있다. LangGraph는 스토어를 제공하여 에이전트가 지식을 무기한으로 보존할 수 있게 한다

ChatMessageHistory — 수동 기억 관리

정식 체인 외부에서 대화 이력을 직접 간단하게 제어하려면 ChatMessageHistory 클래스가 적합합니다.

from langchain.memory import ChatMessageHistory

# 이력 객체 초기화
history = ChatMessageHistory()

# 사용자 메시지와 AI 메시지 추가
history.add_user_message("I'm heading to New York next week.")
history.add_ai_message("Great! It's a fantastic city.")

# 메시지 리스트 확인
print(history.messages)

ConversationBufferMemory — 체인을 위한 자동 기억

기억을 체인에 직접 통합하려 할 때 흔히 사용됩니다. 두 가지 핵심 매개변수로 동작을 조정합니다.

  • memory_key — 대화 이력이 담길 프롬프트 안 변수명을 지정하는 문자열이며, 기본값은 "history"
  • return_messages — 이력 형식을 결정하는 불 값
    • False(기본값)이면 서식이 적용된 문자열 하나를 반환하며, 일반적인 LLM에 적합
    • True이면 메시지 객체 목록을 반환하며, 채팅 모델에 권장되는 형식
from langchain.memory import ConversationBufferMemory

# 기억 초기화
memory = ConversationBufferMemory()

# 대화 턴 저장
memory.save_context({"input": "What's the weather like?"}, {"output": "It's sunny today."})

# 기억을 문자열로 불러오기
print(memory.load_memory_variables({}))

이 기억을 LLMChain 에 통합하면, 모델이 대화 이력에 접근하여 맥락에 맞는 응답을 제공할 수 있습니다.

from langchain_openai import OpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.memory import ConversationBufferMemory

# 1. LLM과 프롬프트 정의
llm = OpenAI(temperature=0)
template = """You are a helpful travel agent.
Previous conversation:
{history}
New question: {question}
Response:"""
prompt = PromptTemplate.from_template(template)

# 2. 기억 설정
# memory_key "history"는 프롬프트의 변수와 일치해야 한다
memory = ConversationBufferMemory(memory_key="history")

# 3. 체인 구성
conversation = LLMChain(llm=llm, prompt=prompt, memory=memory)

# 4. 대화 실행
response = conversation.predict(question="I want to book a flight.")
print(response)
response = conversation.predict(question="My name is Sam, by the way.")
print(response)
response = conversation.predict(question="What was my name again?")
print(response)

채팅 모델의 효과를 높이려면 return_messages=True 를 설정하여 구조화된 메시지 객체 리스트를 사용하는 것이 좋습니다.

from langchain_openai import ChatOpenAI
from langchain.chains import LLMChain
from langchain.memory import ConversationBufferMemory
from langchain_core.prompts import (
    ChatPromptTemplate,
    MessagesPlaceholder,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)

# 1. 채팅 모델과 프롬프트 정의
llm = ChatOpenAI()
prompt = ChatPromptTemplate(
    messages=[
        SystemMessagePromptTemplate.from_template("You are a friendly assistant."),
        MessagesPlaceholder(variable_name="chat_history"),
        HumanMessagePromptTemplate.from_template("{question}")
    ]
)

# 2. 기억 설정
# 채팅 모델에는 return_messages=True가 필수적이다
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

# 3. 체인 구성
conversation = LLMChain(llm=llm, prompt=prompt, memory=memory)

# 4. 대화 실행
response = conversation.predict(question="Hi, I'm Jane.")
print(response)
response = conversation.predict(question="Do you remember my name?")
print(response)

장기 기억의 세 가지 유형

장기 기억은 인간의 기억에 빗대어 세 가지 유형으로 나눌 수 있습니다.

유형 무엇을 기억하는가 AI 에이전트에서의 구현
의미 기억(Semantic Memory) 사실을 기억하기. 사용자 선호도나 도메인 지식 같은 특정 사실과 개념을 보존 지속적으로 갱신되는 사용자 “프로필”(JSON 문서) 이나 개별 사실 문서의 “컬렉션” 으로 관리
에피소드 기억(Episodic Memory) 경험을 기억하기. 과거의 이벤트나 동작을 회상 주로 작업 수행 방법을 기억하는 데 활용. 실제로는 퓨샷(few-shot) 예시 프롬프팅으로 구현되는 경우가 많다
절차적 기억(Procedural Memory) 규칙을 기억하기. 작업을 수행하는 방법을 담은 기억으로, 에이전트의 핵심 지시와 행동이 포함 흔히 시스템 프롬프트에 들어 있다. 에이전트가 적응하고 개선하기 위해 자기 프롬프트를 수정하는 것은 흔한 패턴

절차적 기억을 갱신하는 효과적인 기법 중 하나가 “리플렉션” 입니다(4장). 에이전트에게 현재 지시와 최근 상호작용을 제시한 뒤 지시를 스스로 개선하도록 요청하는 방식입니다.

# 에이전트의 지시를 갱신하는 노드
def update_instructions(state: State, store: BaseStore):
    namespace = ("instructions",)
    # 저장소에서 현재 지시를 가져온다.
    current_instructions = store.search(namespace)[0]
    # LLM에 대화를 되돌아보고 개선된 새 지시를
    # 생성하도록 요청하는 프롬프트를 만든다.
    prompt = prompt_template.format(
        instructions=current_instructions.value["instructions"],
        conversation=state["messages"]
    )
    # LLM으로부터 새 지시를 받는다.
    output = llm.invoke(prompt)
    new_instructions = output['new_instructions']
    # 갱신된 지시를 저장소에 다시 저장한다.
    store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})

# 지시를 사용해 응답을 생성하는 노드
def call_model(state: State, store: BaseStore):
    namespace = ("agent_instructions",)
    # 저장소에서 최신 지시를 가져온다.
    instructions = store.get(namespace, key="agent_a")[0]
    # 가져온 지시를 사용해 프롬프트를 구성한다.
    prompt = prompt_template.format(instructions=instructions.value["instructions"])
    # … 애플리케이션 로직 계속

LangGraph 스토어

LangGraph는 장기 기억을 JSON 문서 형태로 저장소에 저장합니다. 각 기억은 직접 정의한 이름공간(폴더와 유사)고유 키(파일명과 유사) 아래에 구성되어, 정보를 체계적으로 구성하고 쉽게 검색할 수 있습니다.

from langgraph.store.memory import InMemoryStore

# 실제 임베딩 함수를 대체하는 플레이스홀더
def embed(texts: list[str]) -> list[list[float]]:
    # 실제 애플리케이션에서는 적절한 임베딩 모델을 사용해야 한다
    return [[1.0, 2.0] for _ in texts]

# 인메모리 저장소 초기화. 운영 환경에서는 데이터베이스 기반 저장소를 사용한다.
store = InMemoryStore(index={"embed": embed, "dims": 2})

# 특정 사용자와 애플리케이션 컨텍스트에 대한 이름공간 정의
user_id = "my-user"
application_context = "chitchat"
namespace = (user_id, application_context)

# 1. 저장소에 기억 저장
store.put(
    namespace,
    "a-memory",   # 이 기억의 키
    {
        "rules": [
            "User likes short, direct language",
            "User only speaks English & python",
        ],
        "my-key": "my-value",
    },
)

# 2. 이름공간과 키로 기억 조회
item = store.get(namespace, "a-memory")
print("Retrieved Item:", item)

# 3. 이름공간 내에서 기억을 검색하되, 내용으로 필터링하고
# 질의 벡터와의 유사도 순으로 정렬한다.
items = store.search(
    namespace,
    filter={"my-key": "my-value"},
    query="language preferences"
)
print("Search Results:", items)

Vertex AI Memory Bank

Vertex AI Agent Engine의 관리형 서비스인 Memory Bank 는 에이전트에게 영속적인 장기 기억을 제공합니다. 제미나이 모델을 사용해 대화 이력을 비동기적으로 분석하여 핵심 사실과 사용자 선호도를 추출합니다.

추출된 정보는 사용자 ID 같은 정의된 범위별로 구성되어 영속적으로 저장되며, 새로운 데이터를 통합하고 모순을 해소하는 방식으로 지능적으로 갱신됩니다. 새 세션이 시작되면 에이전트는 전체 데이터 회상이나 임베딩 기반 유사도 검색을 통해 관련 기억을 가져옵니다.

from google.adk.memory import VertexAiMemoryBankService

agent_engine_id = agent_engine.api_resource.name.split("/")[-1]
memory_service = VertexAiMemoryBankService(
    project="PROJECT_ID",
    location="LOCATION",
    agent_engine_id=agent_engine_id
)

session = await session_service.get_session(
    app_name=app_name,
    user_id="USER_ID",
    session_id=session.id
)

await memory_service.add_session_to_memory(session)

각 기억에는 고유한 USER_IDAPP_NAME 태그가 붙어 향후 정확하게 검색할 수 있습니다. Memory Bank는 구글 ADK와 매끄럽게 통합되어 별도 설정 없이 바로 사용할 수 있으며, LangGraph나 CrewAI 같은 다른 에이전트 프레임워크 사용자에게도 직접 API 호출을 통한 지원을 제공합니다.

정리

기억 관리란?

에이전틱 시스템이 복잡한 작업을 수행하고 일관된 경험을 제공하려면 과거 상호작용에서 얻은 정보를 기억해야 합니다. 기억 메커니즘이 없으면 에이전트는 무상태(stateless) 존재가 되어, 대화 맥락을 유지하거나 경험에서 학습하거나 사용자에게 맞춤형 응답을 제공할 수 없습니다. 이 때문에 에이전트는 단순한 일회성 상호작용에 머물게 되며, 다단계 절차나 변화하는 사용자 요구를 처리하지 못합니다. 핵심 문제는 단일 대화에서 당장 쓰이는 일시적 정보와 오랜 시간에 걸쳐 축적되는 방대한 영속 지식을 어떻게 효과적으로 관리할 것인가입니다.

왜 사용하는가?

표준적인 해결책은 단기 저장과 장기 저장을 구분하는 두 가지 구성 요소로 이루어진 기억 시스템을 구현하는 것입니다. 단기 기억은 LLM의 컨텍스트 윈도 안에 최근 상호작용 데이터를 담아 대화 흐름을 유지합니다. 영속적으로 보존해야 하는 정보라면 외부 데이터베이스(주로 벡터 저장소)를 사용하는 장기 기억 솔루션을 적용하는데, 이는 의미 기반(semantic) 검색을 효율적으로 수행하기 위해서입니다.

언제 사용해야 하는가?

에이전트가 단일 질문에 답하는 것 이상의 역할을 해야 할 때 이 패턴을 사용합니다. 대화 전반에 걸쳐 맥락을 유지하거나, 다단계 작업의 진행 상황을 추적하거나, 사용자 선호도와 이력을 기억해 상호작용을 개인화해야 하는 에이전트에게 필수적입니다. 에이전트가 과거의 성공, 실패, 또는 새로 획득한 정보를 바탕으로 학습하거나 적응해야 할 때마다 기억 관리를 구현해야 합니다.

기억 관리 패턴 개요도

  사용자 ──► 프롬프트 ──► 에이전트 ⇄ 메모리
     ▲                        │
     │                        ▼
     └────────────────────  출력

그림 8.1 — 기억 관리 디자인 패턴

핵심 정리

  • 기억은 에이전트가 정보를 추적하고, 학습하며, 상호작용을 개인화하는 데 매우 중요합니다.
  • 대화형 AI는 단일 대화 내 당장 필요한 컨텍스트를 위한 단기 기억여러 세션 전반에서 유지되는 지식을 위한 장기 기억 모두에 의존합니다.
  • 단기 기억은 일시적이며, LLM의 컨텍스트 윈도나 프레임워크의 컨텍스트 전달 방식에 따라 제한되는 경우가 많습니다.
  • 장기 기억은 벡터 데이터베이스 같은 외부 저장소에 저장해 대화가 바뀌어도 정보를 유지하며, 검색을 통해 접근합니다.
  • ADK 같은 프레임워크에는 세션(대화 스레드), 상태(임시 대화 데이터), MemoryService(검색 가능한 장기 지식) 같은 기억 관리 전용 구성 요소가 있습니다.
  • ADK의 session.state 는 임시 대화 데이터를 위한 딕셔너리이며, 접두어(user:, app:, temp:)를 보면 데이터 소속과 지속 여부를 알 수 있습니다.
  • ADK에서 상태를 갱신할 때는 상태 딕셔너리를 직접 변경하지 말고, 이벤트를 추가할 때 EventActions.state_deltaoutput_key 를 사용해야 합니다.
  • LangChain은 ConversationBufferMemory 같은 실용적 도구를 제공하여 단일 대화 이력을 프롬프트에 자동으로 주입합니다.
  • LangGraph는 스토어를 사용해 의미 기억, 에피소드 기억, 갱신 가능한 절차적 규칙까지 여러 사용자 세션 전반에서 저장 및 검색할 수 있어, 고급 장기 기억을 지원합니다.
  • Memory Bank는 사용자별 정보를 자동으로 추출·저장·회상하여 에이전트에게 영속적인 장기 기억을 제공하는 관리형 서비스입니다.

결론

이 장에서는 에이전틱 시스템에서 기억 관리가 맡는 중요한 역할을 깊이 살펴보며, 일시적인 컨텍스트와 오랫동안 유지되는 지식의 차이를 설명했습니다. 이러한 기억 유형이 어떻게 구성되며, 더 지능적이고 기억력을 갖춘 에이전트를 구축할 때 이들이 어디에서 활용되는지를 다루었습니다. 구글 ADK가 세션, 상태, MemoryService 같은 구체적 구성 요소를 통해 이를 처리하는 방식도 상세히 살펴보았습니다.

이제 에이전트의 단기 기억과 장기 기억을 모두 다루었으므로, 에이전트가 어떻게 학습하고 적응하는지로 넘어갈 수 있습니다.

참고 문헌