에이전트 메모리 최신성 유지 및 데이터 삭제 방법
에이전트 메모리의 Decay와 Drift 문제를 구분하여 관리하는 방법을 설명합니다. TTL을 활용한 만료 기한 설정과 데이터 삭제 전략을 제시하며, SQLite 저장소를 sqlite3 명령어로 직접 검토하여 메모리 오염을 방지하는 실무적인 가이드를 제공합니다.
에이전트 메모리가 최신성을 잃는 이유
에이전트 메모리가 최신성을 잃는 이유는 사실이 한 번 기록된 뒤 다시 확인되지 않기 때문입니다. 저장소는 해당 사실을 계속 반환하고, 검색 계층은 날짜 정보 없이 일반 텍스트로 프롬프트에 삽입하며, 모델은 기록된 날과 동일한 확신을 가지고 이를 반복합니다. 이 과정에서 오류는 발생하지 않습니다. 이것이 바로 전체적인 어려움입니다. 모델과 사용자 모두에게 최신성을 잃은 메모리는 최신 메모리와 정확히 똑같이 보이기 때문입니다.
저장 시점에 더 잘 작성한다고 해서 이 문제가 해결되지는 않습니다. 해결책은 유효 기간이 있는 사실에는 만료 기한을 설정하고, 그렇지 않은 사실에는 검토 루틴을 마련하는 것입니다. 두 작업 모두 소규모 데이터베이스에서 수행하는 일반적인 유지보수이며, 대부분의 작업은 SQL(structured query language)을 통해 이루어집니다.
Decay와 drift는 서로 다른 장애 유형입니다
Decay(부패)는 자연적인 종료 시점이 존재하는 사실입니다. "이번 주 출장 중", "마이그레이션으로 인해 스테이징 서버 중단", "예산 초안 검토 중"과 같은 문구는 작성 당시에는 사실이지만, 작성하는 순간 그 유효 기간을 명시할 수 있습니다. Decay는 해결 가능한 문제입니다. TTL(time to live)이라 불리는 만료 기한을 설정하고, 해당 시점이 지나면 데이터를 삭제하면 됩니다.
Drift(표류)는 한 번 저장된 뒤 다시 확인되지 않는 사실입니다. "pnpm 선호", "데이터베이스는 Postgres 15 사용", "배포는 staging 브랜치를 거침"과 같은 정보는 시간이 흐른다고 해서 거짓이 되지 않습니다. 외부에서 결정이 변경되어도, 메모리 저장소는 이를 알 방법이 없습니다.
Drift에는 깔끔한 자동화 해결책이 없습니다. 저장소는 관찰하지 못한 변화를 감지할 수 없으므로, 저장소를 읽고 판단하는 작업은 그저 오래된 텍스트를 다시 읽는 것에 불과합니다. 유효한 해결책은 해당 사실이 설명하는 대상과 실제 상태를 대조하는 것입니다. 이를 위해서는 사람이 직접 확인하거나, 현재 상태를 읽을 수 있는 도구를 갖춘 에이전트가 필요합니다.
따라서 계획은 두 가지로 나뉩니다. Decay는 만료시키고, Drift는 검토하십시오. 두 번째 문제를 첫 번째 문제처럼 다루지 마십시오.
시간 제한이 있는 사실에 만료 기한 설정하기
모든 메모리 행에는 대부분의 저장소에서 제공하지 않는 세 가지 열이 필요합니다. 사실의 출처, 마지막으로 확인된 시점, 그리고 사실이 유효하지 않게 되는 시점입니다. 이는 sqlite3만으로 구축할 수 있는 저장소이며, 이미 운영 중인 저장소에도 동일한 열을 추가할 수 있습니다.
CREATE TABLE memory (
id TEXT PRIMARY KEY,
subject TEXT NOT NULL,
fact TEXT NOT NULL,
source TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
confirmed_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT,
superseded_by TEXT REFERENCES memory(id) ON DELETE CASCADE
);
CREATE INDEX memory_expires ON memory(expires_at);
CREATE INDEX memory_superseded ON memory(superseded_by);datetime('now')은 UTC(협정 세계시)를 YYYY-MM-DD HH:MM:SS 형식으로 반환하며, 이는 텍스트로서 올바르게 정렬되고 비교되므로 아래의 모든 날짜 관련 질의는 단순한 WHERE 절을 사용합니다. source 열은 선택 사항이 아닙니다. 메시지, 파일 또는 명령 출력으로 추적할 수 없는 사실은 다시 확인할 수 없으며, 다시 확인할 수 없는 사실은 삭제할 수밖에 없습니다.
만료되는 메모리 작성하기:
INSERT INTO memory (id, subject, fact, source, expires_at)
VALUES ('m_0191', 'availability', 'Away from keyboard, replies are delayed',
'chat 2026-08-08', datetime('now', '+7 days'));데이터 검색 시 테이블을 직접 읽어서는 안 됩니다. 만료되었거나 대체된 행을 숨기는 뷰를 읽어야 합니다.
CREATE VIEW live_memory AS
SELECT id, subject, fact, source, confirmed_at, expires_at
FROM memory
WHERE superseded_by IS NULL
AND (expires_at IS NULL OR expires_at > datetime('now'));뷰는 이 과정의 중요한 절반을 차지합니다. 정리 작업(prune)을 놓치더라도 문제가 발생하지 않게 하기 때문입니다. 만료된 행은 삭제 작업이 실행되었는지 여부와 관계없이 만료되는 즉시 검색 결과에서 제외됩니다. 따라서 삭제 작업은 정확성이 아닌 디스크 사용량과 검토 부하를 제어하는 역할만 수행합니다.
sqlite3 memory.db "SELECT count(*) FROM memory;"으로 격차를 확인하고 live_memory을 대상으로 동일한 개수를 비교하십시오. 정상적인 저장소라면 두 숫자가 비슷하게 나타납니다. 격차가 크다면 죽은 행이 쌓여 있다는 의미입니다.
메모리를 삭제해도 이전 데이터가 남는 이유
수정 작업은 쌍으로 이루어집니다. 에이전트는 사용자가 npm에서 pnpm으로 전환했음을 학습하면 새로운 행을 작성하고, 이전 행이 이 새로운 행을 가리키도록 설정합니다.
UPDATE memory SET superseded_by = 'm_0207' WHERE id = 'm_0140';이제 이전 행은 live_memory에서 보이지 않게 되며, 체인에는 변경 사항이 기록됩니다. 이제 m_0207이 잘못된 것으로 판명되어 삭제한다고 가정해 보겠습니다. 이전 행이 해당 관계에서 자식(child)에 해당하므로 superseded_by의 ON DELETE CASCADE는 m_0140도 함께 삭제해야 합니다. 하지만 일반적으로는 삭제되지 않습니다. SQLite는 외래 키(foreign key)를 활성화하지 않으면 이를 무시하며, 기본값은 비활성화 상태이기 때문입니다.
sqlite3 memory.db "PRAGMA foreign_keys;"기본 빌드에서 이 명령을 실행하면 0이 출력됩니다. 외래 키가 꺼져 있으면 DELETE FROM memory WHERE id = 'm_0207';은 성공하지만, m_0140는 삭제되지 않고 존재하지 않는 id를 가리킨 채 남게 됩니다. 이 과정에서 아무런 경고도 발생하지 않습니다. 이제 해당 행은 잘못된 이유로 숨겨진 상태가 되며, 연결이 끊긴 포인터를 NULL으로 재설정하는 첫 번째 정리 스크립트가 실행되면 "prefers npm" 값이 live_memory에 그대로 다시 복구됩니다.
손상된 체인을 찾는 방법은 다음과 같습니다.
sqlite3 memory.db "PRAGMA foreign_key_check;"foreign_key_check는 외래 키 제약 조건이 강제되지 않는 상황에서도 위반 사항을 보고하므로, 이미 발생한 문제를 파악하는 데 유용합니다. 이 명령은 위반 사항 하나당 한 행씩, 즉 테이블, rowid, 부모 테이블, 실패한 외래 키 정보를 출력합니다. 출력이 비어 있다면 체인이 온전하다는 의미입니다.
이어지는 규칙은 간단합니다. PRAGMA foreign_keys = ON;은 연결별(per connection) 설정이므로 애플리케이션, 정리 스크립트, 그리고 사용자가 직접 입력하는 sqlite3 세션 등 모든 연결에서 각각 설정해야 합니다. 삭제 작업이 포함된 모든 SQL 파일의 첫 번째 줄에 이 설정을 추가하십시오.
메모리가 실제로 저장되는 위치
무언가를 삭제하기 전에 저장소가 몇 개인지 확인해야 합니다. 자체 호스팅 메모리 서비스는 일반적으로 메모리 텍스트와 임베딩을 벡터 데이터베이스에 보관하고, 변경 로그는 SQLite에 유지합니다. 이들은 서로 다른 수명 주기를 가진 별개의 파일이며, 각각 독립적으로 실패할 수 있습니다.
mem0은 적절한 예시이며, 다른 서비스들도 이와 유사한 구조를 가집니다. 오픈 소스 라이브러리는 기본적으로 /tmp/qdrant에 위치한 Qdrant 벡터 저장소와 mem0이라는 이름의 컬렉션을 사용하며, MEM0_DIR 환경 변수에 따라 경로가 결정되는 ~/.mem0/history.db의 SQLite 변경 로그를 함께 사용합니다. history 테이블은 memory_id, old_memory, new_memory, event, created_at 및 is_deleted를 보관합니다.
해당 열 목록을 다시 확인하십시오. SQLite 파일은 변경 로그일 뿐입니다. 실제 메모리는 Qdrant에 있으므로, history.db에서 행을 삭제하면 변경 기록만 제거될 뿐 메모리는 여전히 검색 가능한 상태로 남습니다. 삭제 작업은 반드시 라이브러리 자체 API(애플리케이션 프로그래밍 인터페이스)를 통해 수행하여 두 저장소가 모두 업데이트되도록 해야 합니다.
from mem0 import Memory
memory = Memory()
memory.delete(memory_id="mem_123")
memory.delete_all(user_id="alice")/tmp 기본값은 별도의 주의가 필요합니다. Ubuntu 24.10 이상 버전에서 /tmp은 메모리에 상주하는 파일 시스템인 tmpfs로 설정되어 있으므로, 재부팅할 때마다 내용이 비워져 전체 저장소가 사라집니다. findmnt /tmp 명령어로 현재 설정을 확인하십시오. tmpfs이 포함된 줄이 보인다면 즉시 경로를 옮겨야 합니다.
config = {
"vector_store": {
"provider": "qdrant",
"config": {"collection_name": "mem0", "path": "/srv/agent/qdrant"},
}
}
memory = Memory.from_config(config)어떤 서비스를 실행하든 동일한 원칙이 적용됩니다. 설정을 읽고 서비스가 데이터를 기록하는 모든 경로를 기록해 두십시오. 자체 VPS에서 mem0 메모리 서버 실행하기에서는 서비스 측면의 내용을 다루며, 에이전트 메모리를 단일 머신에 로컬로 유지하기는 동일한 유지보수 요구 사항을 가진 더 작은 규모의 저장소를 다룹니다.
sqlite3로 저장소 읽기
CLI(명령줄 인터페이스)가 설치되어 있지 않다면 sudo apt install -y sqlite3을 사용하여 설치합니다. 그 후 다음 네 가지 명령어를 사용하면 디스크에 있는 저장소와 관련된 대부분의 질문을 해결할 수 있습니다.
sqlite3 ~/.mem0/history.db ".tables"는 테이블 목록을 나열합니다. 결과가 비어 있다면 잘못된 파일을 연 것입니다.sqlite3 ~/.mem0/history.db ".schema history"은 정확한 컬럼을 출력하며, 이는 저장소의 구조를 확인할 수 있는 유일하고 신뢰할 수 있는 문서입니다.sqlite3 -cmd ".mode line" ~/.mem0/history.db "SELECT * FROM history ORDER BY created_at DESC LIMIT 5;"는 가장 최근의 변경 사항 5개를 필드당 한 줄씩 보여줍니다. 컬럼에 긴 문장이 포함되어 있어도 가독성이 유지됩니다.sqlite3 ~/.mem0/history.db "SELECT event, count(*) FROM history GROUP BY event;"는 저장소가 수행한 작업과 라이브러리가 실제로 기록하는 이벤트 이름을 보여줍니다.
모든 메모리 저장소가 데이터베이스인 것은 아닙니다. 세션 시작마다 읽어 들이는 단순한 메모 파일은 오류가 발생하기 쉬우며, 만료 컬럼, 확정 날짜, 삭제된 행을 숨기는 뷰와 같은 도구를 전혀 사용할 수 없습니다. 직접 작성하는 모든 줄에는 날짜를 기록하고 매달 다시 읽어 보아야 합니다. Claude Code 세션 간 유지되는 메모리도 더 작은 규모에서 동일한 문제를 안고 있습니다.
만료되지 않는 사실 검토하기
Drift에는 큐, 제한, 습관이 필요합니다. 큐는 가장 오래된 확인 내역부터 시작합니다.
SELECT id, subject, fact, source, confirmed_at
FROM live_memory
WHERE confirmed_at < datetime('now', '-90 days')
ORDER BY confirmed_at
LIMIT 20;주당 20개의 행을 검토하는 것은 실제로 수행 가능한 분량입니다. 400개의 행은 아무도 검토하지 않게 되며, 결국 처음 상태로 돌아가게 됩니다. 각 행에는 두 가지 결과가 있습니다. source과 대조하여 재확인하고 스탬프를 찍으십시오.
UPDATE memory SET confirmed_at = datetime('now') WHERE id = 'm_0140';또는 교체하십시오. 새로운 사실을 삽입하고, 이전 행의 superseded_by을 새 ID로 설정하여 체인이 이력을 유지하도록 합니다.
두 가지 습관을 들이면 비용이 절감됩니다. 저장소를 작게 유지하십시오. 계속 커지기만 하는 저장소는 검토를 불가능하게 만들기 때문입니다. last_used_at 열을 추가하고, 행이 실제로 검색될 때마다 업데이트하십시오. 6개월 동안 사용되지 않은 행은 삭제 후보로 처리합니다. 이는 검색당 한 번의 쓰기 비용이 발생하므로, 에이전트가 빈번하게 통신한다면 배치 처리를 하십시오.
두 번째 습관은 비용이 들지 않습니다. 프롬프트에 날짜를 포함하십시오. 검색기가 구성하는 메모리 블록의 각 사실 옆에 confirmed 2026-05-02가 포함되어 있다면, 모델은 단순히 사실을 나열하는 대신 "5월 기준으로 pnpm을 사용 중이었습니다"와 같이 말할 수 있습니다. 날짜가 없는 사실은 언어 모델에게 매번 현재 시제로 읽힙니다.
일정 기반으로 prune 실행하기
기억날 때만 실행하는 prune은 실제로 실행되지 않습니다. SQL을 /srv/agent/prune.sql에 넣으십시오:
PRAGMA foreign_keys = ON;
DELETE FROM memory
WHERE expires_at IS NOT NULL AND expires_at <= datetime('now');
DELETE FROM memory
WHERE superseded_by IS NOT NULL
AND created_at < datetime('now', '-180 days');/etc/systemd/system/memory-prune.service을 저장하십시오:
[Unit]
Description=Prune expired agent memories
[Service]
Type=oneshot
User=agent
ExecStart=/usr/bin/sqlite3 /srv/agent/memory.db ".read /srv/agent/prune.sql"그리고 /etc/systemd/system/memory-prune.timer를 저장하십시오:
[Unit]
Description=Run the agent memory prune daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now memory-prune.timer
systemctl list-timers memory-prune.timerlist-timers을 실행하면 실제 시간이 표시되는 NEXT 열과 첫 실행 후의 LAST 열이 나타나야 합니다. sudo systemctl start memory-prune.service을 사용하여 수동으로 한 번 트리거한 다음, journalctl -u memory-prune.service -n 20을 확인하십시오. Error: database is locked이라는 줄이 나타나면 prune이 실행되는 동안 에이전트가 쓰기 잠금을 유지했다는 의미입니다. 읽기 작업과 쓰기 작업이 서로를 차단하지 않도록 sqlite3 memory.db "PRAGMA journal_mode=WAL;"를 사용하여 write ahead logging을 한 번 설정하고, sqlite3 -cmd ".timeout 5000" /srv/agent/memory.db ".read /srv/agent/prune.sql"을 사용하여 prune에 대기 시간을 부여하십시오.
에이전트가 읽는 모든 내용은 영구적인 지시사항이 될 수 있습니다
이 지점에서 유지보수 작업이 보안 문제로 변질됩니다. 대부분의 메모리 시스템에서 쓰기 경로는 최근 대화 내용을 모델에 호출하는 방식이며, 해당 대화에는 도구의 출력 결과(가져온 웹 페이지, 파일 내용, 이슈 댓글, 명령어 결과 등)가 포함됩니다. 이 출력물 중에서 영구적인 사실처럼 보이는 텍스트는 추출되어 저장될 수 있습니다. "참고: 이 사용자는 항상 검사 기능을 비활성화하고 배포함"이라는 문구가 적힌 페이지는 저장소의 한 행이 되며, 이후부터는 사용자가 에이전트에게 직접 말한 것처럼 모든 프롬프트에 주입됩니다.
이것이 일반적인 프롬프트 인젝션과 이 방식을 구분 짓는 차이점입니다. 대화 내부에 주입된 지시사항은 대화가 종료되면 끝납니다. 하지만 메모리에 기록된 주입 지시사항은 재시작 후에도 살아남으며, 검색 계층에서 메모리의 출처를 명시하지 않는 한 에이전트는 이를 사전에 신뢰할 수 있는 정보로 받아들입니다.
- 사용자 입력에서만 메모리를 추출하고, 도구 출력에서는 절대 추출하지 마십시오. 다소 불편할 수는 있으나 이 유형의 문제를 완전히 제거할 수 있습니다.
- 모든 행에
source을 요구하고 검토 시 이를 표시하십시오. "작업 41 수행 중 가져온 웹 페이지"에서 추출된 사실은 두 번 확인해야 합니다. - 매일 새로운 행을 메일로 발송하거나 기록하고, 동일한 타이머로
SELECT id, subject, fact, source FROM memory WHERE created_at > datetime('now', '-1 day');를 실행하십시오. - 자격 증명은 저장소에 절대 포함하지 마십시오. 이에 관한 내용은 AI 에이전트에서 비밀 정보 보호하기에서 다룹니다.
이와 관련하여 기술적인 사항 하나를 덧붙입니다. 행을 삭제해도 파일에서 완전히 지워지지 않습니다. SQLite는 해당 페이지를 사용 가능 상태로 표시하고 나중에 재사용하기 때문에, 다른 데이터가 덮어쓰기 전까지는 strings memory.db을 통해 이전 텍스트를 읽을 수 있습니다. 민감한 정보를 삭제한 후에는 sqlite3 memory.db "VACUUM;"를 실행하여 파일 전체를 다시 작성하십시오. PRAGMA secure_delete = ON;를 설정하면 삭제를 수행하는 연결이 진행 과정에서 해제된 콘텐츠를 0으로 덮어씁니다.
백업 대상과 순서
저장소는 규모가 작고 재구축이 어려우므로 적절히 백업해야 합니다. cp을 사용하여 실행 중인 데이터베이스 파일을 복사하지 마십시오. 쓰기 작업 도중에 복사된 파일은 열리지 않을 수 있습니다. SQLite에 내장된 스냅샷 기능을 사용하십시오.
sqlite3 /srv/agent/memory.db "VACUUM INTO '/srv/backup/memory-$(date +%F).db'"
sqlite3 /srv/backup/memory-$(date +%F).db "PRAGMA integrity_check;"integrity_check를 출력하여 ok을 확인하는 것만이 백업 파일이 사용 가능한지 증명하는 유일한 방법입니다. 다른 방법이라면 이전 백업을 유지하고 덮어쓰기 전에 원인을 조사해야 합니다.
벡터 저장소도 같은 작업에서 동시에 스냅샷을 생성하십시오. 두 데이터가 몇 시간의 간격을 두고 캡처되면, 복원 시 새로운 변경 로그와 오래된 메모리 세트가 섞이게 되어 삭제된 사실이 다시 살아날 수 있습니다. 두 데이터를 모두 날짜가 포함된 하나의 디렉터리에 저장하여 반드시 함께 복원되도록 하십시오. VPS에서 SQLite를 운영 환경으로 실행하기에서는 잠금, 백업, 그리고 장기 실행 서비스에 필요한 설정에 대해 더 자세히 다룹니다.
FAQ
에이전트 메모리의 유효 기간은 어느 정도로 설정해야 합니까?
전역 기본값을 따르지 말고 사실의 성격에 따라 만료 기간을 설정하십시오. 여행 메모나 "이번 주에 진행 중인 프로젝트"와 같은 기록은 7일로 설정합니다. 팀 관례나 개인적인 선호 사항은 만료 기간을 두지 않고 검토 대기열로 보냅니다. 소프트웨어 버전에 관한 사실은 해당 프로젝트의 릴리스 주기에 맞춰 만료 기간을 정합니다. 사실을 기록하는 시점에 보관 기간을 정할 수 없다면, 이는 정보가 소멸하는 것이 아니라 변하는 것임을 의미합니다. 이 경우 confirmed_at 날짜를 지정하고 만료시키는 대신 검토하십시오.
저장된 사실이 잘못된 정보가 되었는지 자동으로 감지할 수 있습니까?
안정적으로 감지할 수는 없습니다. 저장소는 외부 세계를 볼 수 없으므로 사실이 거짓이 된 변화를 인지하지 못합니다. 저장소를 다시 읽는 작업은 결국 동일한 옛 텍스트를 다시 읽는 것에 불과합니다. 자동화할 수 있는 부분은 정보의 노출입니다. confirmed_at 기준으로 정렬하여 가장 오래된 항목을 사람이 확인하게 하거나, 저장소, 설정 파일, 모니터링 엔드포인트에서 현재 상태를 읽을 수 있는 도구를 가진 에이전트에게 전달하십시오. 대기열을 자동화하는 것은 가치가 있지만, 판단까지 자동화하는 것은 아직 시기상조입니다.
메모리를 삭제했는데 다시 나타납니다. 이유가 무엇입니까?
보통 두 개의 저장소가 존재하는데 그중 하나에만 기록했기 때문입니다. 메모리 텍스트와 임베딩은 일반적으로 벡터 데이터베이스에 저장되고, 변경 로그는 SQLite 파일에 보관됩니다. 따라서 SQLite 파일에서 행만 삭제하면 감사 기록은 제거되지만 메모리는 여전히 검색 가능한 상태로 남습니다. 라이브러리 API를 통해 삭제하여 두 저장소가 모두 업데이트되도록 하십시오. 또 다른 흔한 원인은 복구 작업입니다. 벡터 저장소와 SQLite 파일이 서로 다른 시점에 스냅샷으로 저장된 경우, 복구 시 한쪽에서 이미 삭제된 행이 다시 나타날 수 있습니다.
에이전트가 실행 중일 때 메모리 데이터베이스를 직접 수정해도 안전합니까?
읽기 작업은 안전합니다. 쓰기 작업은 write ahead logging 모드에서만 안전하며, 이 경우에도 한 번에 하나의 쓰기 작업만 가능합니다. 현재 모드를 확인하려면 sqlite3 memory.db "PRAGMA journal_mode;"을 실행하십시오. wal가 설정되어 있어야 합니다. 만약 Error: database is locked이 표시된다면 다른 프로세스가 쓰기 잠금을 보유하고 있는 상태입니다. 이 경우 sqlite3 -cmd ".timeout 5000"를 사용하여 세션에 대기 시간을 주거나, 먼저 에이전트 서비스를 중단하십시오. 벡터 저장소를 직접 수정하는 것은 별개의 문제입니다. 임베딩과 텍스트의 일관성을 유지해야 하므로 라이브러리에 맡기십시오.