-
텔레그램 AI 어시스턴트에 기억을 심다 — 단기·에피소드·장기 메모리 설계기IT 2026. 4. 4. 21:00
Claude Code CLI를 텔레그램 봇으로 연동해서 쓰고 있다. 메시지를 보내면
claude -p로 전달하고 결과를 텔레그램으로 돌려받는 단순한 구조인데, 치명적인 단점이 하나 있었다. 기억이 없다. 매 메시지마다 새로 시작하니 "아까 말한 그거"가 통하지 않았다.그래서 인간의 기억 구조를 참고해 세 가지 메모리 레이어를 설계하고 직접 구현했다. 이 글에서는 각 메모리가 왜 필요한지, 언제 기록되고, 얼마나 유지되고, 어떻게 활용되는지를 정리한다.
문제: 무상태 AI 어시스턴트의 한계
텔레그램 게이트웨이의 원래 구조는 이렇다:
사용자 메시지 → gateway.py → claude -p --no-session-persistence → 응답--no-session-persistence플래그가 핵심이다. 매번 완전히 새로운 세션에서 시작한다. 5분 전에 "RAG 시스템 설계 중이야"라고 말해도, 다음 메시지에서는 아무것도 모른다. 데스크톱 Claude Code에는 자체 메모리 시스템이 있지만, 텔레그램 경유 호출에는 적용되지 않는다.해결: 세 가지 메모리 레이어
인간의 기억을 단순화하면 작업 기억(Working Memory), 에피소드 기억(Episodic Memory), 장기 기억(Long-term Memory)으로 나눌 수 있다. 이 구분을 그대로 AI 어시스턴트 메모리에 적용했다.
메모리 레이어 인간 비유 AI 구현 유지 기간 단기 메모리 지금 대화의 맥락 인메모리 링버퍼 세션 중 (최대 30분) 에피소드 메모리 "지난주에 이런 일이 있었지" JSONL + Qdrant 벡터 DB 90일 (이후 압축) 장기 메모리 "이 사람은 이런 걸 좋아해" 마크다운 파일 영구 (수동 삭제) 1. 단기 메모리 — "방금 뭐라고 했더라"
목적
하나의 대화 세션 안에서 맥락을 유지한다. "아까 말한 그 함수"가 통하게 만드는 것이 목표다.
기록 시점
매 메시지 교환마다 즉시 기록한다. 사용자가 메시지를 보내고 AI가 응답하면, 그 쌍이 바로 메모리에 들어간다.
유지 기간과 삭제
- 세션 내: 최대 20개 메시지를 인메모리 deque에 보관
- 세션 종료: 마지막 메시지로부터 30분이 지나면 세션이 자동 종료됨
- 정리: 종료된 세션은 백그라운드에서 요약 후, 7일 뒤 삭제
활용 방식
Claude를 호출할 때
--append-system-prompt에 최근 대화를 주입한다:[최근 대화] 사용자: RAG 시스템에서 리랭킹 빼면 어떻게 돼? Claude: 리랭킹 없이 bi-encoder만 쓰면 recall은 유지되지만 precision이... 사용자: 그럼 비용 대비 효과는?이렇게 이전 대화가 시스템 프롬프트에 포함되므로, AI는 "그럼"이 리랭킹 이야기의 연장선임을 알 수 있다.
핵심 설계 결정
지연 시간 0ms. 순수 인메모리 Python deque를 사용하므로 GPU도, 디스크 I/O도, API 호출도 없다. 5분마다 디스크에 백업하지만, 그것은 재시작 복구용이지 실시간 경로가 아니다.
2. 에피소드 메모리 — "지난번에 이런 대화 했었잖아"
목적
유의미한 대화 이벤트를 기록하고, 나중에 의미 기반으로 검색할 수 있게 한다. "지난주에 블로그 자동화 이야기했는데"라고 하면 그때의 맥락을 찾아오는 것이 목표다.
기록 시점
모든 메시지가 아니라, 의미 있는 이벤트만 기록한다. 휴리스틱 패턴 매칭으로 실시간 감지한다:
이벤트 유형 감지 패턴 예시 선호도 "~하지 마", "~로 해줘" "코드 블록 대신 설명으로 해줘" 작업/기억 "기억해", "해야 해" "내일까지 PR 올려야 해" 결정 "결정", "그걸로" "Qdrant로 결정했어" 커맨드 /로 시작하는 메시지 /archive, /memory LLM을 호출하지 않고 정규식 패턴으로 감지하므로, 응답 지연에 영향이 없다.
유지 기간과 삭제
- JSONL 파일: 월별로 append-only 저장 (
2026-03.jsonl). 90일 이후 압축 - Qdrant 벡터 DB: 백그라운드에서 30분마다 미인덱싱 이벤트를 임베딩하여 저장. Gaussian decay (half-life 30일)로 오래된 이벤트 가중치 감소
활용 방식
사용자 메시지에 "아까", "전에", "지난번", "그때" 같은 참조어가 포함되면 에피소드 검색이 활성화된다. 최근 이벤트에서 관련 내용을 찾아 컨텍스트에 주입한다:
[관련 과거 대화] - [decision] Qdrant를 벡터 DB로 결정 - [preference] 블로그 글은 HTML로 작성 선호Qdrant 벡터 검색의 역할
기존에 운영 중인 지식금고 RAG 시스템의 Qdrant 인스턴스에
tg_conversations라는 별도 컬렉션을 추가했다. 같은 임베딩 모델(Qwen3-Embedding-8B, 4096차원)을 공유하므로 추가 모델 로딩이 없다. 지식금고 노트 검색과 대화 이력 검색이 하나의 벡터 DB에 공존한다.3. 장기 메모리 — "이 사람은 이런 걸 좋아해"
목적
세션을 넘어 지속되는 사용자 사실, 선호도, 프로젝트 맥락을 기억한다. 매번 "나는 Go 10년차 개발자야"라고 말하지 않아도 되게 만드는 것이 목표다.
기록 시점
백그라운드에서 30분마다 실행되는 추출 작업이 담당한다:
- 종료된 세션을 Claude로 1~2문장 요약
- 요약에서 새로운 사용자 선호도/사실/프로젝트 정보를 추출
- 기존 장기 메모리와 비교하여 중복 제거 후 저장
즉, 사용자가 "기억해"라고 말하지 않아도 자동으로 추출된다.
유지 기간과 삭제
- 영구 저장: 마크다운 파일로 디스크에 보관. 자동 삭제 없음
- 수동 삭제:
/forget <키워드>텔레그램 커맨드로 특정 항목 삭제 가능 - 최대 50개 항목으로 제한하여 무한 증가 방지
파일 형식
Claude Code 데스크톱 버전의 메모리 형식을 그대로 채용했다:
--- name: response-style description: 텔레그램 응답 스타일 선호 type: preference source: telegram created: 2026-03-26 updated: 2026-03-26 --- 코드 블록보다 간결한 텍스트 설명을 선호한다.활용 방식
매 메시지마다 사용자 메시지의 키워드와 장기 메모리 항목을 매칭한다. GPU 호출 없이 순수 Python 문자열 비교로 처리하므로 지연이 없다.
컨텍스트 주입: 2000자 예산 전쟁
세 가지 메모리를 모두 시스템 프롬프트에 넣으면 토큰 비용과 지연이 증가한다. 그래서 총 2000자라는 엄격한 예산을 설정했다:
레이어 예산 활성화 조건 단기 메모리 (대화 히스토리) ~1200자 항상 (세션 내 메시지가 있으면) 장기 메모리 (사용자 정보) ~400자 키워드 매칭 시에만 에피소드 메모리 (과거 대화) ~400자 참조어 감지 시에만 2000자는 약 500토큰이다. 모든 메시지에 추가되는 고정 비용이므로, 이 이상 늘리면 응답 비용이 눈에 띄게 증가한다. 대신 조건부 활성화로 불필요한 주입을 최소화했다.
백그라운드: Hot Path를 지키는 설계
메모리 시스템의 핵심 설계 원칙은 "사용자가 체감하는 응답 지연을 1ms도 늘리지 않는다"이다.
작업 실행 경로 GPU 필요 컨텍스트 조회 동기 (hot path), <5ms X 대화 기록 비동기 fire-and-forget X 세션 요약 백그라운드 30분 주기 X (Claude API) 장기 메모리 추출 백그라운드 30분 주기 X (Claude API) 에피소드 벡터 인덱싱 백그라운드 30분 주기 O (Ollama) 모든 LLM 호출은 백그라운드에서 처리한다. Hot path에서는 인메모리 조회와 키워드 매칭만 수행한다.
부가 기능: 분석과 지식금고 연동
사용 분석
인메모리 카운터가 일별 메시지 수, 응답 시간, 커맨드 사용 빈도, 시간대별 활동량을 추적한다.
/memory커맨드로 오늘 통계를 바로 확인할 수 있다.지식금고 아카이브
중요도가 높은 에피소드 이벤트(importance ≥ 4)는 자동으로 Obsidian 지식금고에 마크다운 노트로 저장된다.
/archive커맨드로 현재 세션 전체를 강제 아카이브할 수도 있다. 아카이브된 노트는 기존 RAG 인덱싱 큐에 등록되어 야간 배치에서 자동 인덱싱된다.텔레그램 대화 → 에피소드 메모리 → 지식금고 노트 → RAG 인덱싱의 흐름으로, 대화에서 발생한 지식이 영구적으로 축적되는 순환 구조다.
메모리 생명주기 한눈에 보기
메시지 도착 ├─ [즉시] 단기 메모리에 기록 (인메모리) ├─ [즉시] 에피소드 이벤트 감지 (휴리스틱) │ └─ 감지 시 → JSONL에 append ├─ [즉시] 분석 카운터 증가 │ ├─ [5분] 단기 메모리 디스크 백업 ├─ [5분] 분석 카운터 디스크 플러시 │ ├─ [30분] 종료 세션 요약 (Claude) ├─ [30분] 장기 메모리 추출 (Claude) ├─ [30분] 에피소드 → Qdrant 벡터 인덱싱 (GPU) ├─ [30분] 지식금고 아카이브 (importance ≥ 4) │ ├─ [7일] 오래된 세션 데이터 삭제 ├─ [90일] 에피소드 이벤트 압축 └─ [수동] /forget으로 장기 메모리 삭제돌아보며
구현하면서 깨달은 것은, 메모리 시스템의 핵심은 "무엇을 기억할 것인가"보다 "무엇을 잊을 것인가"에 있다는 점이다. 모든 대화를 저장하면 노이즈에 파묻히고, 아무것도 저장하지 않으면 맥락을 잃는다.
세 가지 레이어로 나눈 이유도 결국 시간 스케일에 따른 선택적 망각을 구현하기 위해서다. 단기 메모리는 30분이면 사라지고, 에피소드는 90일 뒤 압축되고, 장기 메모리만 남는다. 그리고 그 장기 메모리도 최대 50개로 제한된다.
사람의 기억도 결국 이런 구조가 아닐까. 우리가 10년 전 대화를 단어 단위로 기억하지 못하는 건 버그가 아니라 설계다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
캘린더 싱크의 중복 지옥, event_id로 탈출하기 — Google Calendar → Obsidian 자동화 삽질기 (1) 2026.04.09 Claude Code가 플랜을 짜는 방법 - Plan Mode 내부 동작 원리 (0) 2026.04.08 axios에 악성코드가 심어졌다 - 북한 해커의 npm 공급망 공격 분석 (2) 2026.04.07 Claude Code Hooks로 AI 에이전트의 다단계 파이프라인을 결정적으로 만들기 (0) 2026.04.06 Qwen3.5-122B 양자화 비교: Q4_K_M vs Unsloth UD-Q3_K_XL 실측 (1) 2026.04.05 로컬 VLM으로 가족사진 3만 장 분석하기 — 열흘간의 대장정 (0) 2026.04.03 Context7 분석 (5) 다층 품질 스코어링 (1) 2026.04.02 Context7 분석 (4) 5단계 품질 파이프라인 (0) 2026.04.01 Context7 분석 (3) 서버 사이드 리랭킹 (0) 2026.04.01 Context7 분석 (2) 코드 스니펫 vs 정보 스니펫 (0) 2026.03.31