OpenClaw 에이전트의 메모리 두 층을 분리했더니 달라진 것들
멀티턴 에이전트를 처음 만들 때 저도 그랬지만, 대부분 히스토리를 그냥 프롬프트 맨 앞에 통째로 붙입니다. 처음 몇 턴은 잘 돌아가요. 그런데 30턴, 50턴을 넘어가면 컨텍스트 윈도우가 넘치거나, 에이전트가 앞에서 한 말을 잊어버리거나, 매 요청마다 토큰 비용이 폭등합니다. 결국 "어디서부터 잘라야 하지?"를 고민하다가 땜질식 트리밍을 반복하게 됩니다.
이 문제의 본질은 컨텍스트 윈도우를 저장소처럼 쓰고 있다는 것입니다. Mem0 팀이 정리한 표현대로, 컨텍스트 윈도우는 RAM이지 디스크가 아닙니다. RAM은 빠르지만 용량이 작고 휘발성이에요. 이걸 오래 보관하는 디스크처럼 쓰면 당연히 무너집니다.
해법은 두 계층을 명확히 나누는 겁니다. 단기 버퍼는 현재 세션의 워킹 메모리로, 장기 벡터 스토어는 세션 경계를 넘는 영구 기억으로 역할을 분리하고, 에이전트 루프가 매 턴마다 두 계층을 순서에 맞게 읽고 쓰도록 설계하는 것이죠. 이 글은 그 분리를 OpenClaw 에이전트 루프 안에서 실제로 구현하는 과정을 풀어냅니다.
두 계층의 역할이 다른 이유
단기 메모리는 지금 이 작업만 알면 됩니다
단기 메모리는 현재 세션에서 주고받은 대화, 진행 중인 작업 상태, 시스템 프롬프트를 담습니다. 서브밀리초 접근이 가능하지만 세션이 끝나면 사라집니다. OpenClaw의 State 객체나 Python 딕셔너리로 충분합니다. Redis나 DragonflyDB를 붙이면 여러 에이전트 인스턴스 사이에서 공유하거나 크래시 복구도 됩니다.
장기 메모리는 과거 세션까지 알아야 하는 것을 다룹니다
장기 메모리는 임베딩으로 인덱싱된 과거 사실, 요약, 절차 지식입니다. Qdrant, Pinecone, Chroma 같은 벡터 DB에 저장되고, 의미적 유사도 검색으로 런타임에 꺼내옵니다. 세션이 몇 달 지나도 "저번에 이 사용자가 선호한다고 했던 거 뭐였지?"를 물을 수 있습니다.
그래서 루프가 중요합니다
단순히 두 저장소를 쓰는 것과, 에이전트 루프가 두 계층을 순서대로 조율하는 것은 다릅니다. 루프 내 분리 설계의 핵심은 매 턴마다 아래 순서를 명시적으로 타는 것입니다.
이 파이프라인이 없으면 두 저장소는 그냥 독립된 DB일 뿐, 메모리 레이어가 아닙니다. 뒤에서 볼 코드 예시는 이 순서를 그대로 따라갑니다. "무엇을 어느 계층에 저장할지"를 판단하는 로직 자체는 뒤 섹션에서 별도로 다룹니다.
세 계층은 언제 도입하나
실무에서 조금 더 들어가면 장기 메모리 안에서도 성격이 다른 것들이 섞입니다. 학계와 산업계에서 2계층을 넘어 3계층 모델로 발전하는 흐름이 있는데, 에피소드·시맨틱·절차적 계층으로 나누는 방식입니다.
| 계층 | 저장 내용 | 인덱스 방식 | 예시 |
|---|---|---|---|
| 에피소드 | 언제 무슨 일이 있었는가 | 시계열 인덱스 | 도구 호출 이력, 대화 요약 |
| 시맨틱 | 사실·개념 관계 | 벡터 + 그래프 | 사용자 선호, 도메인 지식 |
| 절차적 | 효과가 검증된 실행 패턴 | 벡터 유사도 | 성공한 도구 시퀀스 |
도구 사용 에이전트 연구(arXiv 2512.07287)에서 에피소드와 절차적 메모리를 분리 저장하고, 새 태스크 수행 시 절차적 메모리를 검색해 실행 계획에 반영하는 하이브리드 아키텍처가 구체화됐습니다.
다만 3계층 도입은 공짜가 아닙니다. 저장 스키마, 검색 파이프라인, 성공/실패 라벨링 로직이 각각 늘어납니다. 저는 아래 기준을 만난 뒤에 3계층으로 넘어가는 편을 권합니다.
- 에피소드 분리: 세션이 며칠에 걸쳐 이어지고, "언제 무슨 일이 있었나"를 시간 축으로 조회할 요구가 실제로 생겼을 때.
- 절차적 분리: 도구 호출 시퀀스가 반복되고, 같은 실패 패턴이 눈에 띄기 시작할 때. 성공 사례를 별도 스토어에 넣고 다음 계획 단계에 주입합니다.
그전까지는 2계층으로 충분합니다. 이 글의 코드 예시도 2계층에 집중합니다.
OpenClaw 루프에 실제로 붙이기
아래 코드는 Mem0와 Anthropic SDK를 조합한 개념적 예시입니다. OpenClaw 에이전트 구조에 맞게 어댑테이션이 필요하며, OpenClaw 고유 API 부분은 개념적 예시로 표기했습니다.
메모리 클라이언트 초기화
from mem0 import MemoryClient
from anthropic import Anthropic
# 장기 메모리 — Mem0 (ADD/UPDATE/DELETE 시맨틱 지원)
mem0_client = MemoryClient(api_key="your-mem0-key")
# 단기 버퍼 — 인-프로세스 (세션 범위)
# 개념적 예시: OpenClaw의 State 객체로 관리
class ShortTermBuffer:
# max_turns 기본값 안내:
# 모델 컨텍스트 크기, 평균 메시지 길이, 턴당 토큰 비용을 함께 놓고
# (평균 메시지 토큰 × 2 × max_turns) + 시스템 프롬프트 + 장기 검색 결과가
# 목표 입력 토큰 예산에 들어오는 값으로 튜닝. 아래 값은 예시일 뿐.
def __init__(self, max_turns: int = 20):
self.turns = []
self.max_turns = max_turns
def append(self, role: str, content: str):
self.turns.append({"role": role, "content": content})
if len(self.turns) > self.max_turns:
self.turns = self.turns[-self.max_turns:]
def to_messages(self) -> list:
return self.turns루프 내 컨텍스트 조립
매 턴에서 두 계층을 읽어 컨텍스트를 조립하는 핵심 로직입니다. 장기 메모리 검색 결과를 호출자에게도 함께 돌려주는 이유는 아래 관찰성 섹션에서 이어집니다.
def build_context(
user_input: str,
short_term: ShortTermBuffer,
user_id: str,
top_k: int = 5,
):
"""단기 버퍼 + 장기 벡터 검색 결과를 합쳐 LLM 입력을 만든다."""
long_term_results = mem0_client.search(
query=user_input,
user_id=user_id,
limit=top_k,
)
long_term_context = "\n".join(
f"- {r['memory']}" for r in long_term_results
)
system_prompt = "당신은 도움이 되는 에이전트입니다."
if long_term_context:
system_prompt += (
f"\n\n[장기 기억에서 검색된 관련 정보]\n{long_term_context}"
)
messages = list(short_term.to_messages())
messages.append({"role": "user", "content": user_input})
return system_prompt, messages, long_term_results루프 실행 및 저장 판단
Anthropic SDK를 그대로 씁니다. 모델 ID는 실제 사용 중인 핀된 버전으로 지정해야 재현 가능한 결과가 나옵니다 (예시로 2026년 시점에 실제 사용하는 ID를 지정).
agent_turn에서 눈여겨볼 부분은 저장 판단입니다. 앞서 다이어그램에서 언급한 "저장 대상 판단"을 코드로 옮기지 않으면, 잡담이나 확인성 응답까지 벡터 스토어에 쌓입니다. 여기서는 간단한 휴리스틱 + LLM 태깅을 붙였습니다.
llm_client = Anthropic()
MODEL_ID = "claude-sonnet-5-YYYYMMDD" # 개념적 예시: 실제 핀된 날짜 접미사로 교체
def should_persist(user_input: str, assistant_reply: str) -> bool:
"""장기 스토어에 기록할 가치가 있는 턴인지 분류."""
# 최소 길이 필터
if len(user_input.strip()) < 8 or len(assistant_reply.strip()) < 8:
return False
# 저장 가치 태깅 — LLM에 짧게 물어봄
tagging = llm_client.messages.create(
model=MODEL_ID,
max_tokens=8,
messages=[{
"role": "user",
"content": (
"다음 대화 턴에 사용자 선호, 사실, 결정, 계획 중 하나가 "
"포함되어 있으면 YES, 아니면 NO만 답하세요.\n\n"
f"USER: {user_input}\nASSISTANT: {assistant_reply}"
),
}],
)
return "YES" in tagging.content[0].text.upper()
def agent_turn(
user_input: str,
short_term: ShortTermBuffer,
user_id: str,
):
system_prompt, messages, retrieved = build_context(
user_input, short_term, user_id
)
response = llm_client.messages.create(
model=MODEL_ID,
max_tokens=1024,
system=system_prompt,
messages=messages,
)
assistant_reply = response.content[0].text
# 단기 버퍼 — 무조건 갱신 (현재 세션 컨텍스트 유지)
short_term.append("user", user_input)
short_term.append("assistant", assistant_reply)
# 장기 스토어 — 저장 가치가 있을 때만 기록
persisted = False
if should_persist(user_input, assistant_reply):
mem0_client.add(
messages=[
{"role": "user", "content": user_input},
{"role": "assistant", "content": assistant_reply},
],
user_id=user_id,
)
persisted = True
return assistant_reply, retrieved, persistedMem0의 add()는 단순 삽입이 아니라 기존 기억과 비교해 ADD/UPDATE/DELETE/NOOP를 자동 판단합니다. "사용자가 Python을 선호한다"는 기억이 이미 있는데 다시 쓰면 중복 저장이 아니라 갱신이 됩니다. 여기에 상위 레벨의 저장 가치 판단을 얹으면, 벡터 스토어에 들어가기 전 필터와 들어간 뒤 정리가 이중으로 걸립니다.
루프 전체 흐름
실무에서 자주 빠지는 함정
할루시네이티드 메모리
잘못 저장된 기억을 에이전트가 사실로 신뢰하는 문제입니다. "사용자가 A를 좋아한다"고 잘못 기록되면, 이후 모든 응답이 그 방향으로 편향됩니다. 메모리 신선도와 충돌 탐지를 평가하는 파이프라인을 별도로 두지 않으면 누적될수록 문제가 커집니다.
망각 설계를 안 했을 때
어떤 정보를 언제 삭제하거나 갱신할지 정책이 없으면 스토어가 오염됩니다. Mem0의 ADD/UPDATE/DELETE/NOOP 시맨틱이 이 문제를 다루는 대표 사례입니다. 직접 벡터 삽입만 하는 방식을 쓴다면 주기적 정리 정책을 명시적으로 설계해야 합니다.
토큰 비용을 잊은 장기 메모리 설계
장기 메모리에서 검색한 내용을 프롬프트에 그대로 붙이면 입력 토큰이 생각보다 많아집니다. arXiv 2603.13017은 개인화 에이전트 메모리를 원문 대신 압축된 사실 단위로 구조화·증류해 저장하는 방식을 제안하고, 개인화 벤치마크 조건에서 프롬프트 토큰이 큰 폭으로 줄어드는 것을 보고합니다. 원문 전체를 벡터 스토어에 넣기 전에, 핵심 사실만 추출해 저장하는 방식을 먼저 고려해보시면 좋습니다.
벡터 검색만으로는 시간 순서를 못 잡습니다
"3번 전 대화에서 뭐라고 했지?"를 벡터 유사도 검색으로 찾기 어렵습니다. 시간 순서나 논리 관계가 중요한 경우 벡터 + 그래프 DB 조합이 2026년 기준 프로덕션 논의에서 자주 등장하는 이유가 여기 있습니다.
관찰성을 처음부터 설계에 넣으세요
메모리 레이어가 에이전트 행동에 어떤 영향을 주는지 추적하기 어렵다는 점이 이 아키텍처의 실질적인 운영 부담입니다. 어떤 기억이 검색됐는지, 그게 응답에 어떻게 반영됐는지를 로깅하지 않으면 디버깅이 매우 고됩니다.
앞서 agent_turn이 retrieved와 persisted를 반환하도록 만든 이유가 여기 있습니다. 로깅 계층에서 검색을 다시 호출하면 API 비용이 이중으로 나가고, 두 호출 사이 인덱스 상태가 바뀌면 로그와 실제 추론 입력이 어긋납니다. 실제로 쓰인 것과 정확히 같은 결과를 그대로 흘려보내야 합니다.
import logging
def agent_turn_with_observability(
user_input: str,
short_term: ShortTermBuffer,
user_id: str,
) -> str:
reply, retrieved, persisted = agent_turn(user_input, short_term, user_id)
logging.info(
"memory_retrieval",
extra={
"user_id": user_id,
"query": user_input,
"retrieved": [r["memory"] for r in retrieved],
"scores": [r.get("score") for r in retrieved],
},
)
logging.info(
"memory_write",
extra={"user_id": user_id, "persisted": persisted},
)
return reply벡터 DB 선택 이야기는 뒤로 미뤄도 됩니다
처음엔 어떤 벡터 DB를 고를지 오래 고민했습니다. 그런데 실제로 해보면 임베딩 모델 품질, 청킹 전략, 재랭킹 유무가 벡터 DB 선택보다 훨씬 큰 영향을 미칩니다. 같은 데이터라도 임베딩 모델이 도메인에 안 맞으면 어떤 DB를 써도 검색 품질이 낮고, 청킹 단위가 너무 크면 검색 결과가 항상 뭉툭합니다.
그다음 순서로 DB를 고를 때 참고할 수 있는 비교입니다.
| DB | 적합한 상황 | 주의점 |
|---|---|---|
| Chroma | 프로토타이핑, 인-프로세스 | 프로덕션 확장성 한계 |
| Qdrant | 자체 호스팅, 중소 규모 | 인프라 운영 필요 |
| Pinecone | 관리형, 대규모 프로덕션 | 비용 |
| Weaviate | 하이브리드 검색 필요 시 | 설정 복잡도 |
| pgvector | 이미 PostgreSQL 사용 중 | 대규모 벡터 성능 한계 |
프로토타이핑은 Chroma로 시작하고, 프로덕션에서 응답 속도가 중요하면 인-프로세스 벡터 인덱스 패턴(arXiv 2607.05690)처럼 외부 네트워크 호출 없이 에이전트 프로세스 내부에서 검색하는 방법도 검토해볼 만합니다. 다만 다시 강조하면, 임베딩·청킹·재랭킹을 먼저 잡고 나서 DB를 고르는 순서가 시행착오를 줄입니다.
내일부터 손댈 수 있는 지점
이 글에서 얘기한 구조를 처음부터 다 만들 필요는 없습니다. 지금 운영 중인 에이전트라면 아래 순서로 하나씩 붙여보시는 걸 권합니다.
- 루프 순서 명시화: 지금 코드에서 "단기 조회 → 장기 검색 → 추론 → 계층별 기록"이 순서대로 보이도록 함수 경계를 정리하세요. 이것만 해도 이후 개선이 훨씬 쉬워집니다.
- 저장 판단 함수 삽입:
should_persist같은 얇은 필터를 벡터 쓰기 직전에 두세요. 잡음이 스토어에 쌓이는 속도가 눈에 띄게 줄어듭니다. - 관찰성 로깅:
retrieved와persisted를 매 턴 로깅에 남기세요. 오답이 났을 때 원인 추적 시간이 줄어드는 효과가 큽니다. - 망각 정책 결정: TTL, 신선도 기반 재평가, 충돌 시 갱신 규칙 중 하나라도 문서화해두세요. 정책이 없으면 스토어는 조용히 오염됩니다.
- 3계층 필요성 재검토: 위 네 가지가 안정된 뒤에, 에피소드·절차적 계층이 정말 필요한 조건이 관측됐는지 다시 보세요.
컨텍스트 윈도우에 모든 걸 우겨넣는 방식은 에이전트가 단순할 때만 통합니다. 두 계층 분리는 구조를 조금 늘리지만, 30턴을 넘긴 뒤부터는 이 구조가 있어야 다음 문제를 풀 여유가 생깁니다.
참고 자료
- Building Memory for AI Agents: Context Windows, Vector Search, PostgreSQL, and Long-Term Recall — Medium
- Memory vs Context Window for LLM and AI Agents — Mem0
- State of AI Agent Memory 2026: Benchmarks & Trends Report — Mem0
- Memory in the Loop: In-Process Retrieval as Extended Working Memory for Language Agents — arXiv 2607.05690
- Experience-Evolving Multi-Turn Tool-Use Agent with Hybrid Episodic-Procedural Memory — arXiv 2512.07287
- Episodic-Semantic Memory Architecture for Long-Horizon Scientific Agents — arXiv 2605.17725
- Structured Distillation for Personalized Agent Memory — arXiv 2603.13017
- Context Engineering - LLM Memory and Retrieval for AI Agents — Weaviate
- Building Long-Term Memory in AI Agents with LangGraph and Mem0 — DigitalOcean
- Long-Term Memory Architectures for AI Agents — Redis
- Agent Memory Architectures: Vector vs Graph vs Episodic — DigitalApplied
- Top 5 Vector Databases 2026 — Deepak Gupta