콘텐츠로 이동

6. 메모리와 상태

“6. 메모리와 상태” 챕터 컨셉 일러스트

한 줄 정의. 메모리는 컨텍스트 윈도우 안에 늘 떠 있는 것(세션·코어)과 바깥 저장소에 두고 필요할 때만 불러오는 것(롱텀·워크스페이스)으로 나뉘며, 하네스는 이 둘 사이에서 무엇을 적재하고 무엇을 비울지를 관리한다.

메모리 계층(세션/롱텀/워크스페이스) 다이어그램

왜 메모리가 필요한가 — 무상태성과 하네스의 책임

섹션 제목: “왜 메모리가 필요한가 — 무상태성과 하네스의 책임”

LLM 호출은 무상태(stateless)입니다. 한 번의 추론이 끝나면 직전 대화조차 모델 안에 상태로 남지 않습니다.

가중치는 호출 사이에 변하지 않고, 모델이 다음 호출에서 볼 수 있는 것은 오직 그 호출의 입력으로 넘어온 토큰뿐입니다. 따라서 “무엇을 어디에 저장하고, 다음 호출에 어떻게 다시 주입할지”는 모델이 결정하는 일이 아니라 모델을 감싸 돌리는 하네스(러너)가 책임지는 일입니다.

멀티턴 대화가 이어지는 것처럼 보이는 것은 모델이 대화를 “기억”해서가 아니라, 러너가 매 호출마다 과거를 다시 입력에 붙여 넣기 때문입니다.

이 책임을 좀 더 구체적으로 쪼개면, 책임 주체는 두 곳으로 나뉩니다. 하나는 러너의 실행 전후 훅입니다.

실행 직전에 저장소에서 과거를 꺼내(retrieve) 입력 앞에 붙이고(prepend), 실행이 끝나면 그 회전에서 새로 생성된 것을 저장(store)합니다. 다른 하나는 모델 자신이 도구로 호출하는 메모리 편집 함수입니다.

모델은 추론 도중 “이건 오래 기억해야 한다”고 판단하면 메모리에 쓰는 함수를 직접 호출할 수 있습니다. 즉 메모리 관리는 러너의 자동 파이프라인과 모델의 능동적 함수 호출이 분담합니다.

전자는 “흐르는 대화를 빠뜨리지 않는” 역할이고, 후자는 “흐름에서 건져 영구 보관할 것을 고르는” 역할입니다.

문제의 본질은 컨텍스트 윈도우가 유한하다는 데 있습니다. 모든 것을 컨텍스트에 욱여넣으면 00장에서 짚은 성능 저하와 비용 폭증을 부릅니다.

그래서 메모리 설계는 결국 “좁고 빠른 컨텍스트 안”과 “넓고 느린 컨텍스트 밖”을 어떻게 계층으로 나누고, 그 사이의 이동을 어떻게 자동화하느냐의 문제로 귀결됩니다. 이 점에서 메모리는 컨텍스트 엔지니어링과 동전의 양면입니다.

컨텍스트 엔지니어링이 “이번 호출에 무엇을 넣을까”라면, 메모리는 “그 무엇이 호출 사이에 어디에 머무는가”입니다.

세션 메모리 — 대화 이력의 자동 보존

섹션 제목: “세션 메모리 — 대화 이력의 자동 보존”

가장 기본적인 메모리는 한 대화 안에서 주고받은 이력입니다. OpenAI Agents SDK의 Sessions는 이것을 러너 수준에서 자동으로 처리합니다.

러너는 매 run 직전 해당 세션의 대화 이력을 자동으로 retrieve하여 이번 호출의 입력 항목 앞에 prepend하고, run이 끝나면 그 회전에서 생성된 모든 새 항목 — 사용자 입력, 어시스턴트 응답, 도구 호출까지 전부 — 을 자동으로 세션에 store합니다.1 이 덕분에 턴 사이에 수동으로 .to_input_list()를 호출해 이력을 손수 이어 붙일 필요가 사라집니다.

식별은 session_id 단위입니다. 같은 session_id로 다시 실행하면 이전 항목이 자동으로 병합되어 모델에 전달되므로, 개발자가 상태를 직접 들고 다닐 필요가 없습니다.

from agents import Agent, Runner, SQLiteSession
agent = Agent(name="assistant", instructions="사용자를 돕는다.")
# 인메모리(프로세스가 죽으면 휘발)
ephemeral = SQLiteSession("user_123")
# 파일 영속화 — 같은 session_id로 다시 열면 이력이 자동 병합된다
persistent = SQLiteSession("user_123", "conversations.db")

SQLiteSession("user_123")처럼 경로를 주지 않으면 인메모리라 프로세스가 끝나면 휘발하고, SQLiteSession("user_123", "conversations.db")처럼 파일 경로를 주면 디스크에 영속화되어 다시 실행할 때 동일 session_id의 이력이 자동으로 병합됩니다.1

Session 프로토콜 — 커스텀 백엔드의 4메서드

섹션 제목: “Session 프로토콜 — 커스텀 백엔드의 4메서드”

세션의 저장소는 추상화되어 있어 교체할 수 있습니다. SessionABC 프로토콜은 비동기 4메서드만 요구합니다.1

class SessionABC:
async def get_items(self, limit: int | None = None) -> list: ... # 이력 조회(limit=None이면 전체)
async def add_items(self, items) -> None: ... # 항목 추가
async def pop_item(self) -> "item | None": ... # 가장 최근 1개 제거·반환
async def clear_session(self) -> None: ... # 전부 삭제

get_items(limit)는 이력을 가져오되 limit으로 깊이를 제한할 수 있고, add_items는 새 항목을 적재하며, pop_item은 가장 최근 항목 하나를 떼어 반환하고, clear_session은 세션 전체를 비웁니다. 커스텀 백엔드는 이 네 개만 구현하면 러너의 자동 retrieve/store 파이프라인에 그대로 꽂힙니다.1

공식 확장 백엔드도 풍부합니다. AsyncSQLiteSession(aiosqlite 의존), RedisSession.from_url, SQLAlchemySession(.from_url 또는 engine= 주입, create_tables=True로 스키마 생성), MongoDBSession(monotonic seq 카운터로 순서 보장), DaprSession(TTL 지원), AdvancedSQLiteSession(store_run_usage로 사용량 추적, create_branch_from_turn으로 대화 브랜칭), EncryptedSession(임의 세션을 감싸 투명 암호화와 TTL을 덧입힘)이 있습니다.1

MongoDB가 별도 시퀀스 카운터를 두는 이유는 분산 문서 저장소에서 삽입 순서가 곧 시간 순서를 보장하지 않기 때문이고, AdvancedSQLiteSession의 브랜칭은 한 대화에서 특정 턴 이후를 분기시켜 서로 다른 후속을 실험할 수 있게 합니다.

pop_item — 되돌리고 다시 묻기(정정 패턴)

섹션 제목: “pop_item — 되돌리고 다시 묻기(정정 패턴)”

pop_item이 단순한 삭제용이 아니라는 점이 중요합니다. 실제 용도는 정정(correction)입니다. 마지막 어시스턴트 항목과 그 앞의 사용자 항목을 순서대로 pop_item()으로 떼어내 직전 한 턴을 “되돌린” 뒤, 고친 입력으로 다시 실행하는 패턴입니다.1

# 직전 어시스턴트 응답을 떼어내고
await session.pop_item()
# 그것을 유발한 사용자 입력도 떼어낸 뒤
await session.pop_item()
# 정정된 질문으로 다시 실행 — 이력에는 잘못된 왕복이 남지 않는다
result = await Runner.run(agent, "아까 질문 다시: 교토 말고 오사카 3박으로.", session=session)

이렇게 하면 틀린 왕복이 이력에 흔적을 남기지 않으므로, 이후 턴이 잘못된 맥락에 오염되지 않습니다.

이력 제어 — 병합 콜백과 조회 깊이 제한

섹션 제목: “이력 제어 — 병합 콜백과 조회 깊이 제한”

이력을 어떻게 합치고 얼마나 깊이 가져올지도 제어할 수 있습니다. RunConfig.session_input_callback(history, new_input)은 병합 방식을 커스터마이즈합니다.

예컨대 history[-10:] + new_input처럼 최근 10개만 남기는 식입니다. 이 콜백은 이력의 복사본을 받으므로, 콜백 안에서 리스트를 변형해도 실제 저장 상태에는 영향이 없습니다.1

한편 RunConfig.session_settings = SessionSettings(limit=N)으로 retrieve 깊이 자체를 제한할 수 있는데, 기본값 limit=None은 전체 이력을 조회합니다.1

수동 상태 관리 — to_input_list와 서버측 연속

섹션 제목: “수동 상태 관리 — to_input_list와 서버측 연속”

세션을 쓰지 않고 수동으로 대화를 이어가려면, 한 실행 결과(RunResult)의 to_input_list()를 다음 Runner.run(..., input=...)에 넘깁니다. 이 메서드는 원본 입력에 더해 그 실행에서 생성된 항목(new_items의 변환분)을 이어 붙여, 다음 턴에 그대로 재생(replay)할 수 있는 입력 리스트를 만들어 줍니다.2

result = await Runner.run(agent, "교토 3박 일정 짜줘.")
# 다음 턴: 직전 결과를 입력으로 재생
next_input = result.to_input_list() + [{"role": "user", "content": "첫날만 오사카로."}]
result2 = await Runner.run(agent, next_input)

to_input_list에는 두 모드가 있습니다. 기본인 mode="preserve_all"new_items의 전체 변환 이력을 유지하고, mode="normalized"는 핸드오프 관련 항목을 필터링한 뒤 정규화된 연속 입력을 선호합니다.2

핸드오프가 얽힌 대화를 이어 붙일 때 원시 항목을 모두 남길지, 깔끔한 연속만 남길지의 선택입니다.

RunResult의 표면(surface)을 알아 두면 무엇을 어떻게 이어 붙이는지 이해하기 쉽습니다.2

  • final_output — 마지막 에이전트의 출력. output_type을 지정했다면 그 타입의 객체이고, 승인 대기(HITL) 중이라면 None입니다.
  • new_items — 이번 실행에서 생성된 RunItem 래퍼들. MessageOutputItem, ToolCallItem, HandoffCallItem, ToolApprovalItem 등입니다.
  • last_agent — 마지막으로 실행된 에이전트. 다음 사용자 턴을 누가 처리할지 결정하는 데 씁니다.
  • raw_responses — 모델 호출별 ModelResponse 원본.
  • input_guardrail_results / output_guardrail_results — 가드레일 검사 결과.

서버측 연속 — conversation_id와 인터럽트 후 재개

섹션 제목: “서버측 연속 — conversation_id와 인터럽트 후 재개”

서버측 저장을 쓰는 길도 있습니다. OpenAIConversationsSession()은 OpenAI Conversations API에 이력을 서버측으로 저장하고, conversation_id="conv_123"로 재개합니다.1 이때는 앞의 주의처럼 전체 이력을 매번 보내는 게 아니라, 새 입력만 보내고 저장된 대화 ID를 재사용합니다.

인터럽트(휴먼 인 더 루프) 후 재개는 결과를 상태로 직렬화한 뒤 승인하고 같은 세션으로 다시 실행하는 흐름입니다. result.to_state()로 실행 상태를 떠낸 다음, 대기 중인 인터럽션을 approve(interruption)로 승인하고, 같은 세션으로 재실행합니다.1

to_state()는 뒤에서 다룰 “실행 상태 체크포인트”의 구체적 사례이기도 합니다.

MemGPT — OS식 가상 컨텍스트 관리

섹션 제목: “MemGPT — OS식 가상 컨텍스트 관리”

세션 이력만으로는 부족합니다. 대화가 길어지면 컨텍스트가 넘치고, 정작 중요한 사실이 오래된 메시지에 묻혀 사라집니다.

MemGPT 논문(2023)은 이 문제를 운영체제의 메모리 계층으로 풉니다. 핵심 발상은 가상 컨텍스트 관리(virtual context management)입니다.

RAM에 해당하는 main context(LLM 컨텍스트 윈도우)와 디스크에 해당하는 external context를 두고, 데이터를 둘 사이로 옮겨 마치 “무한 컨텍스트”가 있는 것처럼 흉내 냅니다.3

main context는 세 구역으로 나뉩니다.3

  1. system instructions — 메모리 계층 구조와 함수 호출 방식을 설명하는 고정 지시. 모델이 “내게 어떤 저장소가 있고 어떻게 페이징하는가”를 알게 하는 부분이라, 변하지 않고 항상 떠 있습니다.
  2. working context — 현재 작업과 핵심 사실을 담는 편집 가능한 작업 메모리. Letta의 코어 블록에 대응합니다.
  3. FIFO queue — 대화 메시지의 순차 버퍼. 오래된 것부터 밀려납니다.

external context는 두 종류입니다.3

  • recall storage — 전체 메시지 이력을 담는 read/write DB. 컨텍스트나 큐 밖으로 밀려난 과거 이벤트를 보관하고 검색합니다.
  • archival storage — 에이전트가 임의 정보나 문서를 저장하고 질의하는 범용 read/write DB.

함수 페이징과 인터럽트 기반 제어

섹션 제목: “함수 페이징과 인터럽트 기반 제어”

데이터의 이동은 모델이 직접 함수를 호출해 일으킵니다. 외부 저장소의 데이터를 컨텍스트로 들이거나(page-in) 밀어냅니다(page-out). 이는 OS 커널이 페이지 폴트를 처리하는 방식과 동형이며, 제어 흐름을 조정하기 위해 인터럽트 개념을 차용합니다.3

큐와 축출(eviction)은 queue manager가, 함수 호출 처리와 체이닝은 function executor가 맡습니다.

함수 연쇄도 인터럽트로 설계됩니다. 함수 호출에 request_heartbeat=true를 포함하면, 함수 실행이 끝난 뒤 제어가 에이전트로 되돌아옵니다.

프로세서가 yield하거나 인터럽트를 받는 것과 비슷하게, 한 번의 사용자 입력 없이도 여러 함수를 연쇄로 실행할 수 있습니다.3 예컨대 “검색 → 결과를 working context에 반영 → 후속 검색”을 한 호흡에 잇는 식입니다.

평가 도메인도 이 설계의 목표를 드러냅니다. 하나는 문서 분석으로, LLM 컨텍스트를 초과하는 대형 문서 QA와 중첩 key-value 검색입니다. 다른 하나는 멀티세션 대화로, deep memory retrieval과 페르소나 일관성을 측정합니다.3 둘 다 “컨텍스트 윈도우보다 큰 정보를 다룰 수 있는가”라는 한 가지 질문의 변주입니다.

Letta(MemGPT의 후신)는 이 계층을 실제 제품으로 구현했습니다. 메모리는 블록(block) 단위로 조직되고, 에이전트는 메모리 도구로 이 블록을 직접 편집합니다.

에이전트에 붙은 블록은 인-컨텍스트(시스템 프롬프트에 고정)이고, 떼어낸 블록은 아웃-오브-컨텍스트로 접근은 가능하되 컨텍스트에 주입되지는 않습니다. 결정적으로, 메모리·사용자 메시지·추론(reasoning)·도구 호출이 모두 DB에 영속화되어, 컨텍스트에서 축출되더라도 결코 사라지지 않습니다.4

블록은 다섯 필드로 이뤄집니다.5

  • label — 고유 식별자.
  • value — 실제 내용 문자열.
  • limit — 글자 수 상한.
  • description — 블록의 목적 설명. 에이전트가 “이 블록이 무엇을 담는 곳인가”를 이해하는 데 쓰여, 비어 있으면 에이전트가 블록을 잘못 쓰기 쉽습니다.
  • read_only — 불리언, 기본 false. true면 에이전트가 그 블록을 수정할 수 없습니다(개발자는 여전히 SDK로 수정 가능).

기본 블록 두 종 persona(에이전트가 어떻게 행동하고 응답할지)와 human(대화 상대에 대한 핵심 정보)은 코드 기준 기본 limit이 5000자입니다.5

컨텍스트 윈도우에 렌더되는 모양

섹션 제목: “컨텍스트 윈도우에 렌더되는 모양”

블록은 검색 없이 항상 보입니다. 시스템 프롬프트 앞에 <memory_blocks> XML로 붙기 때문입니다. 각 블록은 <label> 안에 <description>, <metadata>(chars_current, chars_limit), <value>를 포함합니다.5

<memory_blocks>
<human>
<description>대화 상대에 대한 핵심 사실</description>
<metadata>chars_current=84 chars_limit=5000</metadata>
<value>이름: 승형. 교토를 좋아하고 사찰 위주 여행을 선호함.</value>
</human>
</memory_blocks>

chars_current/chars_limit을 메타데이터로 노출하는 이유는, 에이전트가 자기 메모리가 얼마나 찼는지 보고 언제 갱신하거나 정리할지 스스로 판단하게 하기 위함입니다.

자기 편집(self-editing)은 에이전트가 core_memory_appendcore_memory_replace 같은 도구로 자기 블록을 직접 갱신하는 능력입니다. 대화 중 알게 된 사실을 코어 메모리에 적어 두면, 그 사실은 이후 모든 턴에서 별도 검색 없이 컨텍스트에 살아 있습니다.5

붙은 블록은 인-컨텍스트(시스템 프롬프트 고정), 떼어낸 블록은 아웃-오브-컨텍스트(접근 가능하되 주입 안 됨)이며, 어느 쪽이든 DB에 영속화되어 사라지지 않습니다.4

개발자도 API로 블록을 직접 다룰 수 있습니다. client.blocks.create(label/description/value/limit/read_only)로 만들고, client.agents.blocks.attach로 기존 블록을 에이전트에 연결하며, client.blocks.update로 내용을 통째로 교체합니다.

갱신은 last-write-wins라, 동시에 같은 블록을 고치면 마지막 쓰기가 이깁니다.5

Letta 아웃오브컨텍스트 메모리 — archival vs recall

섹션 제목: “Letta 아웃오브컨텍스트 메모리 — archival vs recall”

코어 블록만으로는 담을 수 없는 방대한 지식은 아웃오브컨텍스트로 내려갑니다. 여기에 두 갈래가 있습니다.

Archival memory는 외부 벡터 DB로, 사실상 무제한 저장입니다. 컨텍스트를 차지하지 않습니다.

도구는 archival_memory_insert(선택적 tag로 분류 저장)와 archival_memory_search(임베딩 기반 의미 검색, tag 필터와 페이지네이션 지원)입니다.6 의미 검색이라는 점이 핵심입니다.

키워드가 아니라 개념으로 매칭하므로, artificial memories로 검색해도 implanted memories를 회수합니다.6

archival과 conversation/recall의 차이는 “누가 큐레이션하는가”에 있습니다. archival은 에이전트가 의도적으로 큐레이션한 구조적 지식 저장소이고, conversation_search(recall)는 과거 메시지 이력을 큐레이션 없이 자동으로 검색합니다.6

즉 archival은 “내가 골라 적은 노트”, recall은 “전부 자동 기록된 로그”에 가깝습니다.

운영 규모가 이 설계의 실용성을 보여 줍니다. 개인 지식관리 에이전트가 30,000개 이상, 소셜 에이전트가 32,000개 이상의 archival 메모리를 사용자·주제·상호작용 유형으로 태깅해 운영한 사례가 있습니다.6

공유 블록과 멀티에이전트 메모리 전달

섹션 제목: “공유 블록과 멀티에이전트 메모리 전달”

블록은 한 에이전트 안에만 머물지 않습니다. 하나의 블록을 여러 에이전트에 block_ids로 동시에 attach하면, 한 에이전트가 그 블록을 갱신하는 순간 연결된 모든 에이전트의 컨텍스트에 즉시 전파됩니다.

검색이 필요 없습니다. 반대로 블록을 분리(detach)하면 각자 컨텍스트를 좁게 유지합니다.5

이 공유/분리 구조를 어떻게 엮을지는 오케스트레이션 패턴 선택으로 이어집니다.

서브에이전트와의 관계도 메모리가 결정합니다. 부모가 어떤 컨텍스트를 자식에게 넘기고, 자식의 결과 중 무엇을 부모 메모리로 되돌릴지가 곧 메모리 전달 설계입니다.

공유 블록은 그 상태 공유의 통로가 됩니다. Agents SDK에서는 같은 발상이 세션으로 표현됩니다.

서로 다른 session_id는 격리된 이력을 가지고, 같은 세션을 여러 에이전트가 공유하면 병합된 이력을 함께 봅니다.1

Letta 슬립타임 에이전트 — 비동기 메모리 정리

섹션 제목: “Letta 슬립타임 에이전트 — 비동기 메모리 정리”

메모리 정리를 주 에이전트가 직접 하면 대화 흐름이 끊기고 토큰을 잡아먹습니다. Letta의 슬립타임 아키텍처는 이 부담을 떼어 냅니다.

enable_sleeptime: true면 두 에이전트가 생성됩니다. primary(대화를 처리하며 conversation_search·archival_memory_search를 가짐)와 sleep-time(백그라운드에서 공유 메모리 블록을 비동기로 편집)입니다.

모든 에이전트가 하나 이상의 메모리 블록을 공유하는 멀티에이전트 아키텍처의 한 형태입니다.7

동작은 이렇습니다. sleep-time 에이전트가 원본 컨텍스트(대화 이력·파일)를 처리해 “learned context”를 반복적으로 도출하고, 그중 가장 중요한 정보를 공유 블록에 갱신합니다. 그 결과 메모리 관리 부담이 주 에이전트에서 떨어져 나갑니다.7

세션 컴팩션 — 자동 요약과 그 함정

섹션 제목: “세션 컴팩션 — 자동 요약과 그 함정”

요약은 메모리 계층에서도 일어납니다. OpenAIResponsesCompactionSession은 임의 세션을 감싸 Responses API로 이력을 자동 압축(컴팩트)합니다. should_trigger_compaction 콜백으로 발동 조건을 제어하고, run_compaction({"force": True})로 수동 실행합니다.1

이는 05장의 컨텍스트 압축과 동일한 원리가 메모리 계층에서 구현된 사례입니다. “무엇을 요약해 버릴지”가 곧 손실 정책이라는 점도 같습니다. 요약은 공짜가 아니라, 버린 디테일을 다시 못 살리는 비가역적 선택입니다.

워크스페이스 상태와 체크포인트 — 대화 메모리와의 구분

섹션 제목: “워크스페이스 상태와 체크포인트 — 대화 메모리와의 구분”

지금까지가 “무슨 말을 주고받았나”라면, 또 다른 상태가 있습니다. “지금 파일이 어떤 상태인가”입니다. 대화 메모리와 워크스페이스 상태는 다른 저장소이고 다른 복구 전략을 가집니다. 코딩 에이전트라면 작업 트리(working tree) 자체가 상태입니다.

긴 작업에서는 이 워크스페이스 상태를 중간중간 체크포인트(디스크 스냅숏)로 남기는 일이 중요합니다. 그래야 실행 루프가 중단되거나 실패했을 때 처음이 아니라 마지막 안전 지점에서 재개할 수 있습니다.

Letta식 DB 영속화(대화·메모리)와 워크스페이스 디스크 스냅숏은 보존 대상이 다릅니다. 전자는 “말과 사실”을, 후자는 “파일 트리의 형상”을 지킵니다.

메모리와 실행 루프의 결합 — 행동으로서의 메모리 관리

섹션 제목: “메모리와 실행 루프의 결합 — 행동으로서의 메모리 관리”

메모리는 정적인 창고가 아닙니다. 실행 루프와 매 턴 맞물립니다. 루프가 한 바퀴 돌 때마다 세션에 새 항목이 적재되고, 코어 메모리가 갱신될 수 있으며, 컨텍스트가 차면 함수 호출로 외부 저장소에 page-out 합니다. 즉 메모리 관리 자체가 루프 안의 action(도구 호출) 중 하나입니다.

이 “루프 내 메모리 행동”의 구체 메커니즘이 앞에서 본 것들입니다. MemGPT의 request_heartbeat 체이닝과 메모리 압박 flush가 한 축이고3, Letta의 core_memory_* 도구 호출이 같은 행동 범주의 다른 사례입니다5. 모델이 “기억하라”고 명령받는 게 아니라, 모델이 매 회전에서 기억하는 행위를 스스로 선택한다는 점이 핵심입니다.

계층위치접근 방식대표 예
세션 메모리컨텍스트 안매 run 자동 주입Agents SDK Sessions
코어/블록 메모리컨텍스트 안시스템 프롬프트 고정, 자기 편집Letta 메모리 블록
롱텀 메모리컨텍스트 밖함수 호출로 페이징·의미 검색MemGPT 외부 컨텍스트 · Letta archival
워크스페이스 상태디스크스냅숏·체크포인트작업 트리 재개

여행 비서 에이전트를 같은 session_id로 이틀에 걸쳐 쓴다고 합시다.

from agents import Agent, Runner, SQLiteSession
agent = Agent(name="여행비서", instructions="사용자의 여행을 돕는다.")
session = SQLiteSession("user_123", "trips.db") # 파일로 영속화
# 1일차
await Runner.run(agent, "다음 달 교토 3박 일정 짜줘.", session=session)
# 2일차 — 같은 session_id, 이력이 자동으로 앞에 붙는다
result = await Runner.run(agent, "어제 그 일정에서 첫날만 오사카로 바꿔줘.", session=session)
# "어제 그 일정"을 모델이 이해하는 건, 러너가 get_items()로
# 이전 대화를 불러와 새 입력 앞에 prepend 했기 때문이다.
print(result.final_output)

여기서 “교토를 좋아하고 사찰 위주를 선호한다”처럼 오래 유지할 사실이라면, 매번 대화 이력을 뒤지는 대신 Letta식 코어 메모리 블록(human)에 한 줄 적어 두는 편이 낫습니다. 그러면 컨텍스트가 길어져도 그 선호는 시스템 프롬프트에 고정되어 항상 보이고(검색 불필요), 세부 일정 협의 같은 휘발성 대화는 세션 이력에 맡길 수 있습니다.

만약 사용자가 “아니, 방금 바꾼 거 취소”라고 하면 pop_item()으로 마지막 왕복을 떼어 내 깔끔하게 되돌릴 수 있고, 수천 건의 과거 여행 기록을 검색해야 한다면 그건 archival 메모리의 의미 검색이 맡을 일입니다.

  • 보존할 정보가 “이번 대화 한정”인지 “여러 세션에 걸친 사실”인지 구분하고, 전자는 세션 메모리, 후자는 코어/롱텀 메모리에 배치했는가.
  • 클라이언트측 세션과 서버측 연속(conversation_id/previous_response_id)을 섞지 않고 하나만 선택했는가.
  • 컨텍스트가 넘칠 때 무엇을 외부 저장소로 밀어내고(page-out) 어떻게 다시 불러올지(함수 호출·의미 검색 경로)를 정했는가.
  • 자동 컴팩션·슬립타임의 빈도가 지연·토큰 비용에 미치는 영향을 가늠하고 발동 조건을 보수적으로 잡았는가.
  • 긴 작업의 워크스페이스 상태를 체크포인트로 남겨, 중단 시 마지막 안전 지점에서 재개할 수 있게 했는가.
  1. OpenAI Agents SDK — Sessions 2 3 4 5 6 7 8 9 10 11 12 13 14 15

  2. OpenAI Agents SDK — Results / to_input_list 2 3

  3. Packer et al., MemGPT: Towards LLMs as Operating Systems (arXiv:2310.08560) 2 3 4 5 6 7 8

  4. Letta Docs — Memory 2

  5. Letta Docs — Memory Blocks 2 3 4 5 6 7

  6. Letta Docs — Archival Memory 2 3 4 5

  7. Letta Docs — Sleep-time Agents 2 3