컨텍스트 윈도우로는 부족하다: 2026년형 AI Agent Long‑Term Memory & 상태 관리 구현 실전 가이드
들어가며
에이전트를 “대화형 API”에서 “장기 실행 시스템”으로 바꾸는 순간, 두 문제가 즉시 터집니다.
1) 기억(메모리) 문제: 세션이 바뀌면 사용자의 선호/업무 맥락/과거 결정이 사라져 매번 재탐색(=비용)합니다.
2) 상태(state) 문제: 워크플로우가 길어질수록 “현재 어디까지 했는지”, “무엇을 확정했는지”, “무엇이 임시인지”가 엉키면서 재시도/재개/병렬 실행이 깨집니다.
2026년 7월 시점의 흐름은 명확합니다. “메모리=벡터DB” 수준을 넘어, (a) 단기/장기 메모리 분리, (b) 구조화된 state + 체크포인트, (c) 시간(temporal)까지 포함한 사실의 진화 관리로 가고 있습니다. OpenAI Agents SDK도 “snapshotting/rehydration(체크포인트 복구)”과 “configurable memory”를 런타임 기능으로 밀어 올렸고, 메모리는 디렉터리/스냅샷 단위로 보존/재개하는 식의 운영 모델을 제시합니다. (openai.com)
언제 쓰면 좋은가
- 동일 사용자/조직과 반복 상호작용(CS, 세일즈 엔지니어링, 리서치 어시스턴트, 사내 DevOps bot)
- 작업이 길고 중간 실패가 잦아 resume이 중요한 에이전트(멀티스텝 티켓 처리, 배포 자동화, 데이터 분석)
- “어제/지난달엔 A였는데 지금은 B” 같은 시간에 따라 바뀌는 사실(정책/요금/선호)을 다뤄야 할 때 (temporal graph류가 강점) (getzep.com)
언제 쓰면 안 되는가
- 단발성 Q&A / 세션 단위 툴 호출: 단순 RAG + 짧은 state면 충분
- 규정/보안상 장기 저장이 위험한 경우: 장기 메모리는 개인정보/내부정보가 “축적”되기 때문에 저장 정책·삭제 정책이 없으면 사고가 납니다(아래 함정 참고)
🔧 핵심 개념
2026년 실무에서 가장 안정적으로 동작하는 모델은 “메모리”를 한 덩어리로 보지 않고 레이어 + 연산자로 보는 것입니다.
1) 메모리 레이어: Short‑term / Episodic / Semantic / Structured
- Short‑term(working context): 지금 턴에서 모델이 바로 쓰는 컨텍스트. “최근 메시지 + 현재 태스크 스펙 + 현재 state 요약”.
- Episodic memory: “무슨 일이 있었나(원문/로그/툴 결과)”를 사건 단위로 저장. 디버깅/감사/재현에 유리.
- Semantic memory: 에피소드에서 추출·압축된 “사실/선호/규칙”. 토큰 절약과 재사용에 유리.
- Structured state: 워크플로우의 진행 상태(예:
phase=awaiting_approval,ticket_id,last_success_step). 이건 retrieval이 아니라 트랜잭션/일관성이 핵심입니다.
요점: 한 DB로 다 때우면 접근 패턴이 충돌합니다. 그래서 2026년에는 “작은 스택” 또는 “converged data layer(단일 엔진이 vector/relational/FTS/graph 제공)” 얘기가 많이 나옵니다. (zylos.ai)
2) 상태(state) 관리: 체크포인트가 “에이전트의 프로세스 메모리”가 된다
긴 워크플로우에서 신뢰성을 만드는 핵심은 “대화 저장”이 아니라 체크포인트 단위의 상태 스냅샷입니다.
- OpenAI Agents SDK는 스냅샷/rehydration으로 실패한 컨테이너에서 상태를 복원해 이어가는 방향을 명시합니다. (openai.com)
- LangGraph 계열도 checkpointer(메모리/포스트그레스 등)를 통해 “thread/graph state”를 직렬화하는 패턴이 보편화되었습니다(다만 운영 이슈/TTL 청소 등 실무 난점도 같이 등장). (labs.cloudsecurityalliance.org)
여기서 중요한 분리:
- 체크포인트 state: “현재 실행을 재개하기 위한 최소 상태” (정확해야 함)
- 장기 메모리: “미래 실행에 도움되는 지식” (부정확/과잉 저장이면 독이 됨)
3) “시간”을 저장하지 않으면 장기 메모리는 결국 망가진다
현업에서 메모리가 무너지는 대표 케이스는 “사용자가 바뀐 사실”입니다.
- 예: “다크모드 좋아요” → 이후 “아, 이제 라이트모드로 바꿨어요”
- 단순 벡터DB는 최신/유효성을 기본으로 다루기 어렵습니다.
- Zep/Graphiti는 temporal validity(사실의 유효 기간)를 모델에 포함시키고, 새로운 증거가 들어오면 기존 사실을 무효화하는 방향을 강조합니다. (getzep.com)
즉 2026년의 “long‑term memory”는 저장소 선택보다 쓰기(write) 정책 + 수정(revision) + 망각(forgetting) 같은 라이프사이클 연산이 더 중요해졌고, 이를 DB 워크로드로 재정의하려는 연구도 나옵니다. (arxiv.org)
💻 실전 코드
아래는 “현실적인” 시나리오로, 고객 지원 에이전트가 (1) 티켓을 처리하면서 (2) 사용자 선호를 장기 메모리로 축적하고 (3) 워크플로우 state를 체크포인트로 관리하며 (4) 다음 세션에 재개(resume)하는 예시입니다.
구성:
- PostgreSQL 하나로
agent_state: 워크플로우 state (ACID)episodic_log: 사건 로그 (감사/디버깅)semantic_memory: 사실/선호 (검색용으로 embedding + pgvector)
- retrieval은 hybrid(키워드 + 벡터)로 만들고(간단 버전), “최신성/신뢰도”로 re-rank 합니다. (프로덕션이면 RRF 같은 결합이 더 낫습니다. (arxiv.org))
0) 의존성/설정
1
2
3
4
python -m venv .venv && source .venv/bin/activate
pip install "psycopg[binary,pool]==3.2.9" pgvector openai pydantic python-dotenv
export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/agentmem"
export OPENAI_API_KEY="..."
Postgres(로컬 예시):
1
docker run --name agentmem-pg -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:16
1) 스키마: state / episodic / semantic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# setup_db.py
import os
import psycopg
DDL = """
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS agent_state (
user_id TEXT PRIMARY KEY,
version BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
state JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS episodic_log (
id BIGSERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
kind TEXT NOT NULL, -- "user_message" | "tool_result" | "decision"
payload JSONB NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_episodic_user_time ON episodic_log(user_id, created_at DESC);
CREATE TABLE IF NOT EXISTS semantic_memory (
id BIGSERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
mem_type TEXT NOT NULL, -- "preference" | "fact" | "policy" ...
content TEXT NOT NULL,
confidence REAL NOT NULL DEFAULT 0.7,
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
valid_to TIMESTAMPTZ NULL,
embedding VECTOR(1536) -- 예시 차원(모델에 맞게 변경)
);
CREATE INDEX IF NOT EXISTS idx_sem_user_time ON semantic_memory(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_sem_embed ON semantic_memory USING ivfflat (embedding vector_cosine_ops);
"""
def main():
db = os.environ["DATABASE_URL"]
with psycopg.connect(db) as conn:
conn.execute(DDL)
conn.commit()
print("DB initialized.")
if __name__ == "__main__":
main()
2) 핵심 로직: (a) state 체크포인트 (b) episodic append (c) semantic upsert(시간 유효성)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# agent_memory.py
import os, json, time
from typing import Any, Dict, List, Optional, Tuple
import psycopg
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
class AgentState(BaseModel):
phase: str = "triage"
ticket_id: Optional[str] = None
last_summary: str = ""
preferences_cached: Dict[str, Any] = {}
def embed(text: str) -> List[float]:
# 실제 사용 모델은 조직 표준에 맞추세요.
# 여기선 예시로 1536 차원 반환 가정
r = client.embeddings.create(model="text-embedding-3-small", input=text)
return r.data[0].embedding
def load_state(conn, user_id: str) -> Tuple[AgentState, int]:
row = conn.execute(
"SELECT state, version FROM agent_state WHERE user_id=%s",
(user_id,)
).fetchone()
if not row:
st = AgentState()
conn.execute(
"INSERT INTO agent_state(user_id, state, version) VALUES (%s, %s, 0)",
(user_id, st.model_dump_json())
)
return st, 0
return AgentState.model_validate_json(row[0]), int(row[1])
def save_state(conn, user_id: str, new_state: AgentState, expected_version: int) -> None:
# 낙관적 락: 병렬 세션/재시도에서 state 꼬임 방지
res = conn.execute(
"""
UPDATE agent_state
SET state=%s, version=version+1, updated_at=now()
WHERE user_id=%s AND version=%s
""",
(new_state.model_dump_json(), user_id, expected_version)
)
if res.rowcount != 1:
raise RuntimeError("State version conflict (concurrent update).")
def log_episode(conn, user_id: str, kind: str, payload: Dict[str, Any]) -> None:
conn.execute(
"INSERT INTO episodic_log(user_id, kind, payload) VALUES (%s, %s, %s)",
(user_id, kind, json.dumps(payload))
)
def invalidate_conflicting_preferences(conn, user_id: str, key: str) -> None:
# 예: "theme=dark"가 새로 들어오면 기존 theme 관련 preference는 valid_to로 닫아버림
conn.execute(
"""
UPDATE semantic_memory
SET valid_to = now()
WHERE user_id=%s
AND mem_type='preference'
AND content ILIKE %s
AND valid_to IS NULL
""",
(user_id, f"%{key}=%")
)
def write_preference(conn, user_id: str, key: str, value: str, confidence: float=0.8) -> None:
invalidate_conflicting_preferences(conn, user_id, key)
content = f"{key}={value}"
conn.execute(
"""
INSERT INTO semantic_memory(user_id, mem_type, content, confidence, embedding)
VALUES (%s, 'preference', %s, %s, %s)
""",
(user_id, content, confidence, embed(content))
)
def hybrid_retrieve_preferences(conn, user_id: str, query: str, k: int=5) -> List[str]:
qemb = embed(query)
rows = conn.execute(
"""
SELECT content, confidence,
1 - (embedding <=> %s::vector) AS sim
FROM semantic_memory
WHERE user_id=%s
AND mem_type='preference'
AND valid_to IS NULL
ORDER BY (0.7*(1 - (embedding <=> %s::vector)) + 0.3*confidence) DESC
LIMIT %s
""",
(qemb, user_id, qemb, k)
).fetchall()
return [r[0] for r in rows]
def handle_message(user_id: str, message: str) -> Dict[str, Any]:
db = os.environ["DATABASE_URL"]
with psycopg.connect(db) as conn:
conn.execute("BEGIN")
state, ver = load_state(conn, user_id)
log_episode(conn, user_id, "user_message", {"text": message, "phase": state.phase})
# 현실적 규칙: 메시지에서 preference를 “명시적으로” 말하면 즉시 반영(LLM 추출은 다음 단계로 확장)
if "다크모드" in message and ("좋아" in message or "써" in message):
write_preference(conn, user_id, "ui.theme", "dark")
if "라이트모드" in message:
write_preference(conn, user_id, "ui.theme", "light")
prefs = hybrid_retrieve_preferences(conn, user_id, "사용자 UI 선호", k=3)
state.preferences_cached = {"preferences": prefs}
# 워크플로우 예시: triage -> drafting
if state.phase == "triage":
state.phase = "drafting"
state.last_summary = f"사용자 요구 접수. prefs={prefs}"
save_state(conn, user_id, state, expected_version=ver)
conn.commit()
return {
"phase": state.phase,
"prefs": prefs,
"summary": state.last_summary
}
3) 실행/예상 출력
1
2
3
python setup_db.py
python -c 'from agent_memory import handle_message; print(handle_message("u123","난 다크모드가 좋아"))'
python -c 'from agent_memory import handle_message; print(handle_message("u123","아 근데 요즘은 라이트모드로 바꿨어"))'
예상 출력(요지):
- 첫 호출:
ui.theme=dark저장, state가drafting으로 이동 - 두 번째 호출: 기존
ui.theme=dark는valid_to로 닫히고ui.theme=light가 유효 preference로 남음
→ “바뀐 사실”을 덮어쓰지 않고 시간 축으로 관리(간이 temporal)하는 셈입니다. (Graphiti 같은 시스템은 이걸 더 정교하게 모델링합니다. (getzep.com))
⚡ 실전 팁 & 함정
Best Practice (실무에서 효과 큰 것 3가지)
1) state는 작게, 메모리는 크게
state는 “재개에 필요한 최소”만. 나머지는 episodic/semantic으로. state가 비대해지면 충돌/마이그레이션/디버깅 비용이 폭증합니다.
2) 메모리 write를 “항상” 하지 말고, policy로 통제
- “명시적 선호”, “반복 등장”, “업무적으로 중요한 사실”만 저장
- OpenAI Agents SDK도 메모리를 디렉터리/스냅샷으로 보존·재개하는 운영을 강조하는데, 결국 비용/보안은 “저장량”과 직결됩니다. (openai.github.io)
3) 시간/유효성(validity)을 최소 단위라도 넣어라
벡터 유사도만으로 “최신 정책”을 뽑는 건 거의 불가능합니다. Graphiti류가 temporal을 전면에 둔 이유가 여기 있습니다. (getzep.com)
흔한 함정/안티패턴
- “대화 전문을 전부 장기 메모리로”: 검색 품질은 떨어지고 비용만 오릅니다. (episodic는 저장하되, semantic은 압축/추출 중심)
- 메모리와 state를 같은 테이블/같은 JSON blob에 몰아넣기: retrieval 요구(유사도/키워드)와 트랜잭션 요구(정합성)가 충돌합니다. (conceptualise.de)
- 삭제/TTL/망각 정책 부재: 장기 메모리는 “데이터 시스템”이라 운영 이슈(청소, 만료, 리인덱싱)가 필연입니다. LangGraph 커뮤니티에서도 TTL/청소 도구가 따로 등장하는 맥락이 그 증거입니다. (reddit.com)
비용/성능/안정성 트레이드오프
- 벡터DB 단독: 구축은 쉬우나 “시간에 따른 사실 변경”, “관계/다중 홉”에 취약
- Graph(temporal KG): 정확도/시간 추론 강점, 대신 운영·추출 비용(LLM extraction)과 복잡성이 증가 (getzep.com)
- 단일 Postgres( pgvector + JSONB ) 스택: 운영 단순화에 강점. 다만 고급 graph traversal/temporal 추론을 직접 구현해야 함(또는 별도 계층 추가).
🚀 마무리
2026년 7월 기준, “AI Agent memory long‑term”을 제대로 하려면 결론은 하나입니다: 메모리는 기능이 아니라 런타임/데이터 레이어입니다. OpenAI Agents SDK가 스냅샷/rehydration과 configurable memory를 전면에 올린 것도 같은 방향이고, temporal graph 계열(Zep/Graphiti)이 “사실의 변화”를 1급 모델로 다루는 것도 같은 이유입니다. (openai.com)
도입 판단 기준(실무 체크리스트):
- “재개(resume) 가능한가?” → 체크포인트 state부터 설계
- “사실이 바뀔 때 어떻게 되는가?” → 최소한
valid_from/valid_to같은 temporal 필드 - “무엇을 저장/삭제할지 정책이 있는가?” → 저장은 설계, 삭제는 운영
다음 학습 추천(실전 감각용):
- OpenAI Agents SDK memory/snapshot 운영 모델 문서 (openai.github.io)
- temporal KG 기반 메모리(특히 Graphiti/Zep의 validity 모델) (getzep.com)
- 메모리를 DB 워크로드로 보는 최신 연구 흐름(ingestion/revision/forgetting/retrieval) (arxiv.org)
원하시면, 위 예제를 (1) LLM 기반 memory extraction(구조화 추출) 단계 추가, (2) RRF 기반 hybrid retrieval, (3) 멀티에이전트에서 shared memory / per-agent memory 분리, (4) OpenAI Agents SDK의 sandbox memory 디렉터리/스냅샷과 Postgres를 함께 쓰는 패턴으로 확장한 버전까지 이어서 작성해드릴게요.