포스트

컨텍스트 윈도우 이후의 세계: 2026년형 AI Agent 장기 메모리 + 상태 관리 구현 가이드

컨텍스트 윈도우 이후의 세계: 2026년형 AI Agent 장기 메모리 + 상태 관리 구현 가이드

들어가며

에이전트를 “대화형 챗봇”이 아니라 “며칠~몇 달 동안 일하는 소프트웨어 프로세스”로 운영하기 시작하면, 실패 원인의 절반은 모델이 아니라 memory/state에서 터집니다. 흔한 증상은 이렇습니다.

  • 며칠 전 결정(“이 고객은 A 정책 적용”)을 잊고 다시 질문함
  • 오래된 사실(주소/권한/선호)을 최신처럼 사용해 사고를 냄
  • 대화 히스토리를 매번 5k~50k 토큰씩 덤프해서 비용·지연이 폭발
  • 장애/재시작 후 “내가 어디까지 했지?”를 복구하지 못해 반복 실행(중복 결제/중복 티켓 생성)

2026년 4월 기준 트렌드는 명확합니다. (1) short-term은 checkpoint/state로, (2) long-term은 별도 store로, (3) 토큰은 compaction/요약으로 제어하는 쪽으로 수렴하고 있습니다. OpenAI는 Responses API의 native compaction을 전면에 두고 있고, Agents SDK는 Sessions로 “대화 지속성”을 제공하며 compaction wrapper까지 공식화했습니다. (openai.github.io) LangGraph 생태계는 checkpointer(스레드 단기/재개)store(크로스 스레드 장기 메모리)를 분리하는 모델이 확산됐고, MongoDB는 그 “store”를 제품 레벨로 밀고 있습니다. (mongodb.com)

언제 쓰면 좋은가

  • 에이전트가 “업무 연속성”을 가져야 함: 고객지원/세일즈, 개인비서, 장기 리서치, 운영 자동화
  • 재시작/스케일아웃(워커 교체)을 고려해야 함: stateless worker + durable state
  • 비용이 중요한 서비스: 기억을 잘못 설계하면 토큰 비용 = 운영비가 됨(실제로 “memory persistence = cost control”이라는 말이 커뮤니티에서 반복됩니다) (mem0.ai)

언제 쓰면 안 되는가

  • 단발성 툴 호출(“이 PDF 요약해줘”)처럼 세션 지속성이 가치가 없는 경우
  • 법/의료 등 고위험 도메인에서 “기억을 사실로 취급”하면 위험합니다. 장기 메모리는 가이드이고, 항상 최신 소스(시스템 DB/실시간 API)를 우선해야 합니다. (OpenAI Agents SDK의 memory 가이드도 stale 가능성을 전제로 설계합니다) (openai.github.io)

🔧 핵심 개념

1) “Memory”와 “State”를 분리해서 설계하라

실무에서 가장 큰 혼동이 이겁니다.

  • State (workflow state / execution state)
    “지금 이 run에서 무엇을 했고, 다음에 무엇을 해야 하는가?”
    예: step index, tool call 결과, pending approval, idempotency key, job cursor
    → 주로 checkpoint(재개/복구)로 관리

  • Memory (knowledge / experience)
    “다음 run에서도 유효한 지식인가?”
    예: 사용자 선호, 계정 제약, 과거 결정의 근거, 장기 프로젝트 맥락
    → 별도 long-term store에 저장/검색/갱신

LangGraph 진영은 이걸 아예 제품 개념으로 쪼갰습니다: checkpointer는 thread 단위 재개를, store는 cross-thread 장기 메모리를 담당합니다. (docs.langchain.com)

2) 2026년형 장단기 메모리: 3층(또는 4층)로 보는 게 안정적

최근 글/연구에서 반복되는 프레임은 episodic / semantic / procedural 3분류입니다. (zylos.ai)

  • Episodic: 사건 로그(“4/10에 A 결정을 했다”) — 시간/버전이 중요
  • Semantic: 사실/프로필(“배송 주소는 X”, “선호: concise”) — 최신성이 중요(덮어쓰기/버전)
  • Procedural: 규칙/플레이북(“환불은 이 순서로”) — 거의 불변, 강한 우선순위

여기에 실무는 보통 Working(단기 실행 메모리)를 추가해 4층으로 갑니다.

  • Working: run 내부 scratchpad/단기 요약/최근 메시지 (컨텍스트 윈도우 안)

3) “토큰”은 저장 문제가 아니라 retrieval-time 예산 문제

2026년 4월 Mem0는 “token-efficient memory”를 전면에 내세우며, 무식한 풀컨텍스트 방식이 프로덕션에서 깨진다고 지적합니다. (mem0.ai) OpenAI 역시 같은 방향으로, Responses API에서 /compact를 제공해 “모델 친화적인 압축 상태”를 생성하도록 했습니다. (openai.com)

결론: 장기 메모리의 핵심은 무엇을 저장하냐도 중요하지만, 더 실무적으로는

  • “이번 턴에 몇 토큰을 memory로 쓸 건지
  • “충돌/최신성은 언제, 어디서 해소할 건지” 가 시스템 안정성을 좌우합니다.

4) “시간”과 “변경”을 1급 시민으로: append-only + revision

2026년 논문들에서 눈에 띄는 공통점은 “기억을 덮어쓰지 말고 append-only로 변천을 보존하자”입니다.

  • APEX-MEM: 대화를 entity-centric event로 구조화하고 append-only로 시간 변화를 보존, retrieval 시점에 충돌/변경을 해소 (arxiv.org)
  • Graph-native memory(Kumiho): 버전/리비전과 belief revision(AGM) 같은 형식적 의미를 memory architecture에 연결 (arxiv.org)

실무적으로는 “DB row를 덮어쓰기 vs 이벤트를 append”의 차이가 아니라,

  • 최신값이 필요할 때는 materialized view(최신 스냅샷)
  • 감사/추적이 필요할 때는 immutable log 를 같이 가져가는 패턴입니다.

💻 실전 코드

현실적인 시나리오: B2B 고객지원 에이전트

  • 단기 상태: “현재 티켓 처리 단계, 이미 고객 DB 조회했는지, 다음에 어떤 액션을 할지”
  • 장기 메모리: “고객사별 계약 플랜/특이사항, 금지 작업, 커뮤니케이션 톤, 최근 장애 히스토리 요약”
  • 토큰 제어: 대화가 길어지면 자동 compaction + long-term retrieval로만 필요한 것 주입

아래 예제는 OpenAI Agents SDK (Python) Sessions + compaction으로 “대화 지속성/토큰 관리”를 맡기고, long-term memory는 Postgres(+pgvector) 기반의 간단한 semantic+keyword 하이브리드로 구현합니다. (프로덕션에서 가장 많이 밟는 조합입니다)

0) 의존성 / 준비

1
2
pip install openai-agents psycopg[binary] pgvector sqlalchemy
# Postgres에 pgvector 설치는 환경별로 다릅니다. (RDS/Cloud SQL/자체 설치)

Postgres 테이블(최소 스키마): semantic memory는 “최신값이 중요”하므로 upsert를, episodic은 append-only를 씁니다.

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
-- semantic: 고객/계정 프로필(최신값 우선)
create table if not exists agent_semantic_memory (
  tenant_id text not null,
  key text not null,
  value_json jsonb not null,
  updated_at timestamptz not null default now(),
  embedding vector(1536),
  primary key (tenant_id, key)
);

-- episodic: 사건 로그(append-only)
create table if not exists agent_episodic_memory (
  tenant_id text not null,
  id bigserial primary key,
  occurred_at timestamptz not null default now(),
  event_type text not null,
  event_json jsonb not null,
  embedding vector(1536)
);

create index if not exists idx_semantic_embedding
  on agent_semantic_memory using ivfflat (embedding vector_cosine_ops);

create index if not exists idx_episodic_embedding
  on agent_episodic_memory using ivfflat (embedding vector_cosine_ops);

1) 세션(단기 대화/상태) + compaction 붙이기

Agents SDK의 Sessions는 “이전 대화 아이템을 가져와 prepend + 이번 턴 결과를 저장”을 자동화합니다. (openai.github.io)
그리고 OpenAIResponsesCompactionSession은 history가 커지면 responses.compact로 자동 압축합니다. (openai.github.io)

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
import os
import json
from typing import Any, Dict, List

from agents import Agent, Runner
from agents.sessions import SQLiteSession
from agents.memory import OpenAIResponsesCompactionSession

# ---- long-term memory (간단 구현) ----
from sqlalchemy import create_engine, text

DB_URL = os.environ["MEMORY_DB_URL"]
engine = create_engine(DB_URL)

def upsert_semantic(tenant_id: str, key: str, value: Dict[str, Any]):
    with engine.begin() as conn:
        conn.execute(text("""
            insert into agent_semantic_memory (tenant_id, key, value_json)
            values (:tenant_id, :key, :value_json)
            on conflict (tenant_id, key)
            do update set value_json = excluded.value_json, updated_at = now()
        """), {"tenant_id": tenant_id, "key": key, "value_json": json.dumps(value)})

def append_episodic(tenant_id: str, event_type: str, event: Dict[str, Any]):
    with engine.begin() as conn:
        conn.execute(text("""
            insert into agent_episodic_memory (tenant_id, event_type, event_json)
            values (:tenant_id, :event_type, :event_json)
        """), {"tenant_id": tenant_id, "event_type": event_type, "event_json": json.dumps(event)})

def load_semantic_profile(tenant_id: str) -> Dict[str, Any]:
    # 실무에서는 key별로 가져오고, 중요 키만 요약해 주입하세요.
    with engine.begin() as conn:
        rows = conn.execute(text("""
            select key, value_json
            from agent_semantic_memory
            where tenant_id = :tenant_id
            order by updated_at desc
            limit 50
        """), {"tenant_id": tenant_id}).fetchall()
    profile = {}
    for k, v in rows:
        profile[k] = v
    return profile


# ---- Agent ----
SYSTEM_POLICY = """
You are a senior support agent.
Rules:
- Treat long-term memory as guidance; verify critical facts using tools/DB when needed.
- When user provides new stable info (plan, address, constraints), write semantic memory.
- When a decision is made (refund approved/denied, escalation), append episodic memory with rationale.
Return a final answer plus a short 'action_log' JSON for what you stored.
"""

agent = Agent(
    name="B2BSupportAgent",
    instructions=SYSTEM_POLICY,
)

async def run_turn(tenant_id: str, user_text: str):
    # 1) short-term: session + compaction
    underlying = SQLiteSession(session_id=f"support:{tenant_id}")
    session = OpenAIResponsesCompactionSession(
        session_id=f"support:{tenant_id}",
        underlying_session=underlying,
        # 기본 auto compaction 사용(지연이 민감하면 should_trigger_compaction을 커스텀)
    )

    # 2) long-term: semantic profile을 이번 턴의 입력 컨텍스트에 주입(“요약된 형태” 권장)
    profile = load_semantic_profile(tenant_id)
    injected = f"[tenant_profile]\n{json.dumps(profile, ensure_ascii=False)}\n[/tenant_profile]"

    result = await Runner.run(
        agent,
        input=f"{injected}\n\n[user]\n{user_text}\n[/user]",
        session=session,
    )

    # 3) 모델 출력에서 action_log를 파싱해 memory에 반영(툴 호출로 바꿔도 됨)
    # 여기서는 간단히: 결과 텍스트 끝에 JSON이 온다고 가정
    text_out = result.final_output

    action_log = None
    if "action_log" in text_out:
        # 실무에서는 구조화 출력(JSON schema) + robust parser를 쓰세요.
        try:
            tail = text_out.split("action_log", 1)[1]
            action_log = json.loads(tail[tail.find("{"):])
        except Exception:
            action_log = None

    if action_log:
        for item in action_log.get("semantic_updates", []):
            upsert_semantic(tenant_id, item["key"], item["value"])
        for ev in action_log.get("episodic_appends", []):
            append_episodic(tenant_id, ev["event_type"], ev["event"])

    return text_out


"""
예상 출력(요지)
- 사용자 응답 본문
- action_log:
  - semantic_updates: [{"key":"contract.plan","value":{"name":"Enterprise","sla":"4h"}}]
  - episodic_appends: [{"event_type":"refund_decision","event":{"ticket":"123","decision":"deny","reason":"...","time":"..."}}]
"""

2) 확장: “상태(state) 저장”은 memory DB가 아니라 재개 가능한 체크포인트

위 예제는 “대화 지속성”을 session이 해결합니다. 하지만 긴 작업(30분 이상) / human-in-the-loop / 장애 복구는 대화 아이템만 저장해서는 부족합니다. 이때는 LangGraph류의 step-level checkpoint가 강합니다(각 step 후 상태를 저장하고 재시작 시 복원). 커뮤니티/가이드에서도 “read-execute-write 사이클로 매 step 저장 + time travel/branching”이 장점으로 언급됩니다. (fast.io)
(여기서는 코드 분량상 생략하지만, 설계적으로는 “session=대화”, “checkpointer=워크플로우 상태”를 분리하세요.)


⚡ 실전 팁 & 함정

Best Practice 1) “기억의 최신성”을 스키마로 강제하라

  • semantic memory는 (tenant_id, key) 단위 최신값 + updated_at을 유지
  • episodic memory는 append-only로 보존
  • “같은 키인데 값이 바뀌었다”는 흔한 사건입니다(플랜 변경/주소 변경). APEX-MEM처럼 retrieval 시점에 충돌을 해소하려면, 최소한 timestamp/version가 있어야 합니다. (arxiv.org)

Best Practice 2) compaction은 “요약”이 아니라 토큰 예산 관리 장치로 운영하라

OpenAI는 Responses API에서 native compaction을 제공하며, 모델이 상태를 보존하는 방식으로 압축 아이템을 만든다고 설명합니다. (openai.com)
Agents SDK는 이를 세션 레벨로 래핑했고, 자동 compaction이 스트리밍을 잠깐 막을 수 있다고 명시합니다. (openai.github.io)
실무 팁:

  • turn latency가 중요하면 auto-compaction을 끄고 idle time에 수동 실행
  • compaction 트리거는 “메시지 개수”가 아니라 최근 N턴 토큰 추정치 기반이 더 안전

Best Practice 3) long-term retrieval은 “검색 결과를 그대로 주입”하지 말고 rerank + 목적별 렌더링

LangGraph store는 namespace로 스코프를 나누고 search를 제공합니다. (docs.langchain.com)
실무에서는:

  • (a) 후보 20개 검색 → (b) cheap model로 rerank/필터 → (c) “이번 질문에 필요한 형태”로 렌더링
  • “프로필(semantic)”은 key-value로, “사건(episodic)”은 타임라인 요약으로, “규칙(procedural)”은 우선순위 규칙으로 렌더링

흔한 함정 1) “대화 히스토리 저장”을 long-term memory로 착각

Session/Thread는 “대화 아이템”이고, long-term memory는 “지식”입니다. 대화 전체를 장기 메모리로 그대로 끌고 다니면 결국 토큰 폭탄 + 노이즈로 무너집니다(커뮤니티에서도 ‘context dumping’이 가장 흔한 반패턴으로 언급됩니다). (reddit.com)

흔한 함정 2) 메모리 오염(memory poisoning)과 보안

“지속 메모리”는 공격면이 됩니다. 보안 가이드/워크숍에서는 persistent instruction injection 류 이슈가 반복적으로 다뤄지고 있습니다. (oktsec.com)
대응:

  • procedural/principle 레이어는 사용자 입력으로 직접 쓰지 않기
  • semantic 업데이트는 allowlist 키만 허용(예: user.preference.*, account.plan)
  • episodic에는 “누가/언제/근거”를 남기고, critical action은 원천 시스템에서 재검증

비용/성능/안정성 트레이드오프(현실 체크)

  • Vector DB/pgvector: 검색 품질↑, 운영 복잡도↑
  • Graph(Neo4j 등): 관계/충돌 해소에 강하지만 스키마/운영 난이도↑ (Kumiho/그래프 기반 연구들이 강점을 주장) (arxiv.org)
  • Compaction: 토큰↓, 그러나 compaction 자체 비용/지연↑ (스트리밍 UX에 영향) (openai.github.io)
  • Benchmark 점수: LoCoMo/LongMemEval은 유용하지만, 벤치마크 품질 논쟁도 있습니다. 점수만 보고 채택하면 위험합니다. (reddit.com)

🚀 마무리

핵심은 간단합니다.

1) State는 checkpoint로: 재시작/복구/승인대기 같은 “프로세스 지속성”
2) Memory는 store로: 사용자/테넌트 지식의 “선별 저장 + 목적형 검색”
3) 토큰은 compaction으로: “대화 전체”가 아니라 “필요한 상태”만 남기기

도입 판단 기준:

  • “에이전트가 재시작 후에도 같은 일을 계속해야 하는가?” → Yes면 state/checkpoint가 먼저
  • “사용자/테넌트별 맥락이 누적될수록 가치가 커지는가?” → Yes면 semantic/episodic 분리된 long-term store 필요
  • “월간 토큰 비용이 운영비에서 의미 있는 비중인가?” → Yes면 compaction + retrieval 예산 관리가 필수 (mem0.ai)

다음 학습 추천(순서)

  • OpenAI Agents SDK의 Sessions + compaction 패턴(공식) (openai.github.io)
  • LangGraph의 persistence/store 개념(스레드 단기 vs 크로스 스레드 장기) (docs.langchain.com)
  • 시간/버전/충돌 해소까지 포함한 장기 메모리 연구 흐름(APEX-MEM 같은 append-only + temporal reasoning) (arxiv.org)

원하시면, 위 예제를 (1) 벡터 임베딩 생성까지 포함한 pgvector 하이브리드 검색, (2) memory write 정책(allowlist/검증/TTL), (3) LangGraph 체크포인터와 결합한 “재개 가능한 티켓 처리 워크플로우”까지 확장한 버전으로 이어서 정리해드릴게요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.