메모리 개요 (Memory)

메모리 개요 (Memory)

메모리(Memory)는 이전 상호작용에 대한 정보를 기억해 두는 시스템이에요. AI 에이전트에게 메모리는 특히 중요해요. 이전 대화를 기억하고, 피드백으로부터 학습하며, 사용자 선호에 적응할 수 있게 해 주기 때문이죠. 에이전트가 더 복잡한 작업을 수많은 사용자 상호작용과 함께 처리하게 되면서, 이 능력은 효율성과 사용자 만족도 양쪽에서 필수적이 돼요. 이 개념 가이드는 기억 상기(recall) 범위에 따라 두 가지 유형의 메모리를 다룹니다.

  • 단기 메모리(Short-term memory), 혹은 스레드(thread) 범위 메모리는 세션 안에서 메시지 기록을 유지하며 진행 중인 대화를 추적해요. LangGraph는 단기 메모리를 에이전트의 상태(state)의 일부로 관리합니다. 상태는 체크포인터(checkpointer)를 이용해 데이터베이스에 영속화(persist)되어, 언제든 스레드를 재개할 수 있어요. 단기 메모리는 그래프가 호출되거나 한 단계가 완료될 때 갱신되며, 각 단계가 시작될 때 상태를 읽어 들입니다.
  • 장기 메모리(Long-term memory) 는 세션을 넘어 사용자별 또는 애플리케이션 수준 데이터를 저장하며, 대화 스레드들 전체에 걸쳐 공유돼요. 언제든지 어떤 스레드에서든 상기할 수 있습니다. 메모리는 단일 스레드 ID에 국한되지 않고 임의의 커스텀 네임스페이스(namespace)로 범위가 정해져요. LangGraph는 장기 메모리를 저장하고 상기하기 위한 스토어(stores)를 제공합니다.

Short vs long

단기 메모리 (Short-term memory)

단기 메모리는 애플리케이션이 단일 스레드(thread) 또는 대화 안에서 이전 상호작용을 기억하게 해 줍니다. 스레드는 세션 안의 여러 상호작용을 정리하는데, 마치 이메일이 하나의 대화에 메시지들을 묶어 두는 것과 비슷해요. LangGraph는 단기 메모리를 에이전트 상태의 일부로 관리하며, 스레드 범위의 체크포인트(thread-scoped checkpoint)를 통해 영속화합니다. 이 상태에는 대화 기록뿐 아니라 업로드된 파일, 검색된 문서, 생성된 결과물 같은 다른 상태 데이터도 포함될 수 있어요. 이를 그래프의 상태에 저장함으로써 봇은 특정 대화의 전체 맥락에 접근하면서도, 서로 다른 스레드 간에는 분리를 유지할 수 있게 됩니다.

단기 메모리 관리하기

대화 기록은 가장 흔한 단기 메모리 형태인데, 오래된 대화는 오늘날의 LLM에게 도전 과제가 되기도 해요. 전체 기록이 LLM의 컨텍스트 윈도우에 들어가지 못하면 복구 불가능한 오류가 발생할 수 있습니다. 설령 LLM이 전체 컨텍스트 길이를 지원하더라도, 대부분의 LLM은 긴 컨텍스트에서 여전히 성능이 떨어져요. 오래되거나 주제에서 벗어난 내용에 "산만해지고" 그 와중에 응답 시간은 느려지고 비용은 올라가기 때문이죠. 채팅 모델은 메시지를 이용해 컨텍스트를 받아들이는데, 여기에는 개발자가 제공한 지시(시스템 메시지)와 사용자 입력(휴먼 메시지)이 포함됩니다. 채팅 애플리케이션에서 메시지는 사용자 입력과 모델 응답을 번갈아 쌓여 시간이 지날수록 점점 길어지는 목록을 만듭니다. 컨텍스트 윈도우는 제한적이고 토큰이 많은 메시지 목록은 비용이 들기 때문에, 많은 애플리케이션이 오래된 정보를 수동으로 제거하거나 잊어버리는 기법을 활용하면 이득을 볼 수 있어요.

Filter

메시지 관리 기법에 대한 자세한 내용은 메모리 추가 및 관리 가이드를 참고하세요.

장기 메모리 (Long-term memory)

LangGraph의 장기 메모리는 서로 다른 대화나 세션을 넘어 정보를 유지하게 해 줍니다. 단기 메모리가 스레드 범위인 것과 달리, 장기 메모리는 커스텀 "네임스페이스" 안에 저장됩니다. 장기 메모리는 만능 해법이 없는 복잡한 도전 과제예요. 다만 다음 질문들은 여러분이 여러 기법을 탐색하는 데 도움을 주는 프레임워크를 제공합니다.

  • 메모리의 유형은 무엇인가요? 인간은 사실(facts)을 기억하는 데 의미 기억(semantic memory), 경험을 기억하는 데 일화 기억(episodic memory), 규칙을 기억하는 데 절차 기억(procedural memory)을 사용해요. AI 에이전트도 똑같은 방식으로 메모리를 쓸 수 있습니다. 예를 들어 AI 에이전트는 작업을 완수하기 위해 사용자에 대한 특정 사실을 기억하도록 메모리를 사용할 수 있어요.
  • 메모리는 언제 갱신하고 싶나요? 메모리는 에이전트의 애플리케이션 로직의 일부로 갱신할 수 있어요(예: "핫 패스(hot path)"에서). 이 경우 에이전트는 보통 사용자에게 응답하기 전에 사실을 기억하기로 결정합니다. 또는 메모리를 백그라운드 작업(백그라운드/비동기로 실행되며 메모리를 생성하는 로직)으로 갱신할 수도 있어요. 두 접근 방식의 트레이드오프는 아래 섹션에서 설명합니다.

애플리케이션마다 서로 다른 유형의 메모리가 필요해요. 이 비유가 완벽하진 않지만, 인간 기억 유형을 살펴보면 통찰을 얻을 수 있습니다. 일부 연구(예: CoALA 논문)는 이런 인간 기억 유형을 AI 에이전트에서 쓰이는 것과 매핑하기도 합니다.

메모리 유형 저장 내용 인간 예시 에이전트 예시
의미(Semantic) 기억 사실 학교에서 배운 것들 사용자에 대한 사실
일화(Episodic) 기억 경험 내가 했던 일들 과거 에이전트 행동
절차(Procedural) 기억 지시 본능 또는 운동 능력 에이전트 시스템 프롬프트

의미 기억 (Semantic memory)

의미 기억은 인간과 AI 에이전트 양쪽에서 특정 사실과 개념을 보유하는 것을 말해요. 인간의 경우 학교에서 배운 정보와 개념 및 그 관계에 대한 이해를 포함할 수 있습니다. AI 에이전트의 경우 의미 기억은 과거 상호작용에서 얻은 사실이나 개념을 기억함으로써 애플리케이션을 개인화하는 데 자주 쓰입니다.

의미 기억은 "의미 검색(semantic search)"과는 달라요. 의미 검색은 "의미"를 이용해(보통 임베딩을 통해) 유사한 콘텐츠를 찾는 기법이에요. 의미 기억은 심리학 용어로, 사실과 지식을 저장하는 것을 말하고, 의미 검색은 정확한 일치가 아닌 의미에 기반해 정보를 검색하는 방법이죠.

의미 메모리는 다양한 방식으로 관리할 수 있어요.

프로필 (Profile)

메모리는 사용자, 조직 또는 다른 개체(에이전트 자신 포함)에 대한 잘 범위가 정해진 구체적 정보의 "프로필" 하나로, 지속적으로 갱신될 수 있습니다. 프로필은 일반적으로 여러분이 도메인을 표현하기 위해 고른 다양한 키-값 쌍이 담긴 JSON 문서일 뿐이에요. 프로필을 기억할 때마다 각 갱신마다 프로필을 갱신하고 있는지 확인해야 해요. 그 결과 이전 프로필을 전달하고 모델에 새 프로필 생성을 요청하거나(또는 기존 프로필에 적용할 JSON 패치)하는 식이 됩니다. 프로필이 커질수록 오류가 나기 쉬워지는데, 프로필을 여러 문서로 나누거나 문서 생성 시 엄격한(strict) 디코딩을 사용해 메모리 스키마가 유효하게 유지되도록 하면 도움이 될 수 있어요.

Update profile

컬렉션 (Collection)

또는 메모리를 시간이 지나면서 지속적으로 갱신되고 확장되는 문서 모음으로 관리할 수도 있어요. 각각의 개별 메모리는 범위가 더 좁고 생성이 더 쉬워서, 시간이 지나도 정보를 잃어버릴 가능성이 더 적습니다. LLM이 새 정보를 기존 프로필과 조화시키는 것보다 새로운 개체를 새로 생성하는 것이 더 쉬워요. 그 결과 문서 컬렉션은 다운스트림에서 더 높은 재현율(recall)로 이어지는 경향이 있습니다. 하지만 이는 메모리 갱신 쪽의 복잡성을 가중시켜요. 이제 모델은 목록의 기존 항목을 삭제하거나 갱신해야 하는데, 이는 까다로울 수 있죠. 게다가 일부 모델은 과도하게 삽입하는 쪽으로, 다른 모델은 과도하게 갱신하는 쪽으로 기본 동작할 수 있어요. 이를 관리하는 한 가지 방법은 Trustcall 패키지를 참고하고, 동작을 조정하는 데 LangSmith 같은 도구로 평가(evaluation)를 고려해 보세요. 문서 컬렉션 작업은 목록에 대한 메모리 검색 쪽으로도 복잡성을 옮겨요. 현재 Store의미 검색콘텐츠 필터링을 모두 지원합니다. 마지막으로 메모리 컬렉션을 사용하면 모델에 포괄적인 컨텍스트를 제공하기가 어려워질 수 있어요. 개별 메모리가 특정 스키마를 따를 수는 있지만, 이 구조가 메모리 사이의 전체 맥락이나 관계를 포착하지 못할 수 있기 때문이죠. 그 결과 이러한 메모리로 응답을 생성할 때 모델은 통합 프로필 접근 방식에서 더 쉽게 얻을 수 있었던 중요한 맥락 정보가 부족할 수 있어요.

Update list

어떤 메모리 관리 방식을 쓰든 핵심은 에이전트가 의미 메모리를 사용해 응답을 근거화(ground)한다는 점이며, 이는 종종 더 개인화되고 관련성 높은 상호작용으로 이어집니다.

일화 기억 (Episodic memory)

일화 기억은 인간과 AI 에이전트 양쪽에서 과거 사건이나 행동을 회상하는 것을 말해요. CoALA 논문이 이를 잘 설명합니다. 사실은 의미 기억에 쓸 수 있는 반면, 경험은 일화 기억에 쓸 수 있다는 거죠. AI 에이전트의 경우 일화 기억은 에이전트가 작업을 어떻게 완수하는지 기억하도록 돕는 데 자주 쓰입니다. 실제로 일화 메모리는 종종 퓨샷 예시 프롬프팅(few-shot example prompting)을 통해 구현되는데, 에이전트가 과거 시퀀스에서 학습해 작업을 올바르게 수행하게 하는 방식이에요. 때로는 "말로 설명하는 것(tell)"보다 "보여주는 것(show)"이 더 쉬울 때가 있고, LLM은 예시에서 잘 배웁니다. 퓨샷 학습(few-shot learning)은 의도한 동작을 보여주는 입력-출력 예시로 프롬프트를 갱신해 LLM을 "프로그래밍"하게 해 줍니다. 퓨샷 예시를 생성하는 다양한 모범 사례가 있지만, 흔히 도전 과제는 사용자 입력에 기반해 가장 관련성 높은 예시를 선택하는 데 있습니다. 메모리 스토어는 데이터를 퓨샷 예시로 저장하는 하나의 방법일 뿐이라는 점을 기억하세요. 더 많은 개발자 개입을 원하거나 퓨샷을 평가 하니스에 긴밀하게 연결하고 싶다면, LangSmith Dataset을 사용해 데이터를 저장하고 사용자 입력에 기반해 가장 관련성 높은 예시를 선택하는 자체 검색 로직을 구현할 수도 있어요. 퓨샷 프롬프팅으로 도구 호출 성능을 개선한 블로그 글과 퓨샷 예시로 LLM을 인간 선호에 맞추는 블로그 글을 참고하세요.

절차 기억 (Procedural memory)

절차 기억은 인간과 AI 에이전트 양쪽에서 작업을 수행하는 데 쓰이는 규칙을 기억하는 것을 말해요. 인간에게 절차 기억은 자전거 타기처럼 기본 운동 능력과 균형을 통한 작업 수행 방법에 대한 내면화된 지식과 같습니다. 반면 일화 기억은 보조 바퀴 없이 처음으로 자전거를 성공적으로 탔을 때나 경치 좋은 길의 기억에 남는 자전거 타기 같은 구체적 경험을 회상하는 것이죠. AI 에이전트의 경우 절차 기억은 에이전트의 기능을 함께 결정하는 모델 가중치, 에이전트 코드, 에이전트의 프롬프트의 조합입니다. 실제로 에이전트가 자신의 모델 가중치를 수정하거나 코드를 다시 쓰는 경우는 드물어요. 하지만 자신의 프롬프트를 수정하는 것은 더 흔합니다. 에이전트의 지시를 다듬는 효과적인 접근 방식 중 하나는 "Reflection" 또는 메타 프롬프팅(meta-prompting)입니다. 이는 에이전트에게 현재 지시(예: 시스템 프롬프트)와 최근 대화 또는 명시적인 사용자 피드백을 함께 프롬프트로 제공하는 것을 말해요. 그러면 에이전트는 이 입력을 바탕으로 자신의 지시를 다듬습니다. 이 방식은 지시를 처음부터 명시하기 어려운 작업에 특히 유용한데, 에이전트가 상호작용으로부터 학습하고 적응할 수 있게 해 주기 때문이에요. 예를 들어 저희는 외부 피드백과 프롬프트 재작성을 사용해 트위터용 고품질 논문 요약을 만드는 Tweet 생성기를 만들었습니다. 이 경우 특정 요약 프롬프트를 사전에 명시하기 어려웠지만, 사용자가 생성된 트윗을 비판하고 요약 과정을 어떻게 개선할지 피드백을 주는 것은 꽤 쉬웠어요. 아래 의사 코드는 LangGraph 메모리 스토어로 이를 구현하는 방법을 보여줍니다. 스토어로 프롬프트를 저장하고, update_instructions 노드로 현재 프롬프트(및 사용자 피드백)를 가져온 뒤, call_model 노드로 그 지시를 사용하는 식이에요.

# 지시를 *사용*하는 노드
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"])
    ...

# 지시를 갱신하는 노드
def update_instructions(state: State, store: BaseStore):
    namespace = ("instructions",)
    instructions = store.search(namespace)[0]
    # 메모리 로직
    prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"])
    output = llm.invoke(prompt)
    new_instructions = output['new_instructions']
    store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
    ...

Update instructions

메모리 쓰기 (Writing memories)

에이전트가 메모리를 쓰는 방법에는 "핫 패스(hot path)"와 "백그라운드(background)" 두 가지가 있어요.

Hot path vs background

핫 패스에서 (In the hot path)

런타임 중에 메모리를 생성하는 것은 장점과 도전 과제를 모두 갖습니다. 긍정적인 면으로, 이 접근 방식은 실시간 갱신을 가능하게 해 새 메모리가 이후 상호작용에서 즉시 사용 가능해져요. 또한 메모리가 생성되고 저장될 때 사용자에게 알릴 수 있어 투명성을 제공합니다. 하지만 이 방식에는 도전 과제도 있어요. 무엇을 메모리에 남길지 결정하려면 에이전트가 새 도구를 필요로 하므로 복잡성이 늘어날 수 있어요. 게다가 무엇을 저장할지 추론하는 과정이 에이전트 지연 시간에 영향을 줄 수 있습니다. 마지막으로 에이전트는 메모리 생성과 다른 책임 사이에서 멀티태스킹을 해야 해서, 생성되는 메모리의 양과 질에 영향을 줄 수 있어요. 예를 들어 ChatGPT는 save_memories 도구를 사용해 메모리를 콘텐츠 문자열로 upsert하며, 각 사용자 메시지마다 이 도구를 쓸지, 어떻게 쓸지 결정합니다. 참고 구현으로 memory-agent 템플릿을 확인해 보세요.

백그라운드에서 (In the background)

메모리를 별도의 백그라운드 작업으로 생성하는 것은 여러 장점을 제공해요. 기본 애플리케이션의 지연 시간을 없애고, 애플리케이션 로직을 메모리 관리와 분리하며, 에이전트가 더 집중된 작업 완수에 집중할 수 있게 해 줍니다. 이 접근 방식은 중복 작업을 피하도록 메모리 생성 시점을 유연하게 정할 수도 있어요. 하지만 이 방식에도 도전 과제가 있습니다. 메모리 쓰기 빈도를 정하는 것이 중요해지는데, 갱신이 드물면 다른 스레드에 새 맥락이 제공되지 않을 수 있기 때문이에요. 메모리 형성을 언제 트리거할지 정하는 것도 중요해요. 흔한 전략으로는 일정 시간 경과 후 스케줄링(새 이벤트가 발생하면 재스케줄링), cron 스케줄 사용, 사용자나 애플리케이션 로직에 의한 수동 트리거가 있습니다. 참고 구현으로 memory-service 템플릿을 확인해 보세요.

메모리 저장 (Memory storage)

LangGraph는 장기 메모리를 스토어의 JSON 문서로 저장합니다. 각 메모리는 커스텀 namespace(여러분이 특정)와 구별되는 key(파일 이름처럼) 아래로 정리돼요. 네임스페이스에는 사용자나 조직 ID, 또는 정보 정리를 쉽게 하는 다른 라벨이 자주 포함됩니다. 이 구조는 메모리의 계층적 구성을 가능하게 해요. 네임스페이스 간 검색은 콘텐츠 필터를 통해 지원됩니다.

from langgraph.store.memory import InMemoryStore


def embed(texts: list[str]) -> list[list[float]]:
    # 실제 임베딩 함수 또는 LangChain 임베딩 객체로 교체하세요
    return [[1.0, 2.0] * len(texts)]


# InMemoryStore는 인메모리 딕셔너리에 데이터를 저장합니다. 운영 환경에서는 DB 기반 스토어를 사용하세요.
store = InMemoryStore(index={"embed": embed, "dims": 2})
user_id = "my-user"
application_context = "chitchat"
namespace = (user_id, application_context)
store.put(
    namespace,
    "a-memory",
    {
        "rules": [
            "User likes short, direct language",
            "User only speaks English & python",
        ],
        "my-key": "my-value",
    },
)
# ID로 "memory" 가져오기
item = store.get(namespace, "a-memory")
# 이 네임스페이스 안에서 "memories" 검색, 콘텐츠 동일성 필터링, 벡터 유사도로 정렬
items = store.search(
    namespace, filter={"my-key": "my-value"}, query="language preferences"
)

메모리 스토어에 대한 자세한 내용은 영속성(Persistence) 가이드를 참고하세요.

더 알아보기 (Learn more)