의도적으로 망각하는 AI 메모리 에이전트를 구축하고, 그것이 실제로 작동함을 증명하기 위해 이틀을 보낸 이야기
요약
단순한 벡터 검색 방식의 한계를 넘어, 망각 메커니즘을 적용한 AI 메모리 에이전트 구축 과정을 다룹니다. 수학적 붕괴 모델과 통합, 모순 탐지를 통해 효율적인 정보 관리와 컨텍스트 유지를 구현했습니다.
핵심 포인트
- 단순 벡터 DB 검색과 차별화된 '망각' 메커니즘 구현
- Salience 점수를 활용한 메모리 중요도 및 시간 경과에 따른 감쇠 적용
- Qwen Cloud 모델을 활용한 점수 산정, 추출 및 통합 프로세스 구축
- 구조화된 출력(JSON) 실패를 방지하기 위한 재시도 래퍼 구현
이 프로젝트를 시작하기 전에 살펴본 모든 "AI 메모리 에이전트"는 동일한 트릭을 사용합니다. 모든 메시지를 임베딩(embedding)하고, 이를 벡터 데이터베이스(vector database)에 쏟아부은 다음, 쿼리 시점에 가장 유사한 상위 k개의 청크(chunks)를 검색하는 방식입니다. 그것은 메모리가 아닙니다. 그것은 검색 인덱스(search index)입니다. 그것은 아무것도 잊지 않으며, "오늘 날씨가 좋네요"를 "저는 페니실린 알레르기가 있습니다"와 동일한 가중치로 취급합니다. 또한, 정리되지 않은 유사 중복 데이터가 늘어날수록 검색 결과에 노이즈가 섞여, 실행 시간이 길어질수록 점점 더 느려지고 멍청해집니다.
Qwen Cloud와 함께하는 Global AI Hackathon Series (Track 1: MemoryAgent)의 요건은 세 가지 구체적인 사항을 요구했습니다: 효율적인 저장 및 검색, 오래된 정보의 적시 망각, 그리고 제한된 컨텍스트 윈도우(context window) 내에서 중요한 기억을 회상하는 것입니다. 그중 중간 항목은 거의 아무도 구현하지 않는 부분인데, 왜냐하면 단순한 벡터 저장 방식에는 "오래된"이라는 개념이 없기 때문입니다. 그래서 저는 이를 구축했습니다 — 실제 붕괴 수학(decay math), 실제 통합(consolidation), 실제 모순 탐지(contradiction detection)를 적용하여 — 그리고 단순히 작동한다고 주장하는 대신, 단순한 방식과 벤치마크를 비교하여 실제로 작동함을 증명했습니다.
이것은 Qwen Cloud에서 고장 난 부분들을 포함하여 약 이틀 동안 그 시스템을 구축한 이야기입니다.
실제 메커니즘
Synapse가 저장하는 모든 메모리는 시간이 지남에 따라 변하는 salience(두드러짐) 점수를 가집니다:
salience(t) = importance_score * recall_boost(recall_count) * exp(-lambda * hours_since_last_recall)
recall_boost(n) = 1 + log(1 + n)
lambda = ln(2) / half_life_hours
여기서 중요한 것은 두 가지입니다. 첫째, importance_score는 키워드 휴리스틱(heuristic)이 아니라, 메모리를 명시적인 신호(
그 과정이 즉각적으로 이루어지지는 않았습니다. Qwen Cloud의 OpenAI 호환 엔드포인트(endpoint)는 일반적인 호스트가 아니라 워크스페이스 전용입니다. 즉, 사용자가 직접 콘솔의 API 키 페이지에서 찾아야 하는 ws-<workspace-id>.<region>.maas.aliyuncs.com 형태의 URL을 사용해야 합니다. 이를 해결한 후, qwen-max/qwen3.7-plus 모델이 채팅, 점수 산정(scoring), 추출(extraction) 및 통합(consolidation)을 담당했고, text-embedding-v3가 검색(retrieval)과 클러스터링(clustering)을 위한 모든 임베딩(embedding) 호출을 처리했습니다.
정말로 놀랐던 점 한 가지는, 벤치마크 실행 속도를 높이기 위해 더 빠른 채팅 모델(qwen3.6-flash)로 전환했을 때, 구조화된 출력(structured-output) 호출에서 가끔 잘못된 형식의 JSON이 반환되었다는 것입니다. 실제 점수가 있어야 할 자리에 빈 {}가 반환되는 식이었죠. 저는 이를 단순히 기본값(fallback default)으로 덮어씌워 대충 넘어가기보다, 네트워크 오류뿐만 아니라 출력의 '형태(shape)' 실패 시에 구체적으로 다시 프롬프트를 입력하는 재시도 래퍼(retry wrapper)를 구축했습니다:
def _chat_json_retrying(system_prompt, user_prompt, validate, temperature=0.2, max_attempts=3):
for attempt in range(max_attempts):
result = _chat_json(system_prompt, user_prompt, temperature)
...
사소한 부분일 수 있지만, 이는 쓰레기 같은 수치를 조용히 생성해내는 벤치마크와, 무언가 실제로 잘못되었을 때 명확하게 실패를 알리는 벤치마크 사이의 차이를 만듭니다.
전체 벤치마크를 무효화할 뻔했던 버그
가장 솔직하게 말씀드리고 싶은 부분입니다. 통합(consolidation) 단계는 두 개의 모순되는 메모리 중 어느 것이 더 '최신(newer)'인지 판단하기 위해 타임스탬프(timestamp)를 비교해야 합니다. 저는 40일간의 대화를 시뮬레이션하여 총 110턴의 전체 벤치마크를 실행했는데, 이때 실제 40일을 기다리는 대신 코드에 전달된 조작된 now 값을 사용하여 40일의 시간을 단 한 번의 스크립트 실행으로 압축했습니다.
하지만 단 하나의 함수가 그 조작된 시간을 사용하지 않고 있었습니다. 그 함수는 어떤 메모리가 더 "최신"인지 결정할 때, 조용히 datetime.now() — 즉 실제 벽시계 시간(wall-clock time) — 으로 되돌아가고 있었습니다. 시뮬레이션 내의 모든 메모리는 대화 속 가상의 날짜와 상관없이, 실제로는 거의 동일한 순간에 기록되었습니다. 따라서 모순 확인(contradiction check)에 의존했던 타임스탬프 비교는 실제 시뮬레이션된 연대기가 아닌 노이즈를 비교하고 있었던 것입니다.
벤치마크 실행 결과가 역전된(backwards) 결과를 보여주었을 때, 즉 시스템이 최신 정보 대신 오래된 사실을 자신 있게 답변했을 때 데이터베이스를 직접 쿼리하여 이 문제를 발견했습니다. 이는 막연한 "정확도가 개선될 수 있음"이 아니라, 구체적이고 추적 가능한 버그였습니다. 저는 타임스탬프를 다루는 모든 함수에 시뮬레이션된 now를 명시적으로 전달하여, 어떤 것이라도 실제 시간으로 기본 설정되지 않도록 수정했습니다. 그리고 정확히 동일한 실패를 재현하는 회귀 테스트(regression test)를 작성했습니다:
def test_stale_fact_loses_even_when_inserted_after_correct_one():
# 올바른 사실은 시뮬레이션된 시간상으로는 "더 오래되었지만",
# 오래된 사실 *이후에* DB에 삽입됩니다 — 이는 수정 사항이 삽입 순서에 의존하지 않음을 증명합니다.
...
솔직한 수치들
저는 두 번째 에이전트를 구축했습니다 — 동일한 Qwen 모델, 동일한 임베딩(embeddings)을 사용하되, 유일한 차이점은 감쇠(decay), 통합(consolidation), 가지치기(pruning)가 전혀 없다는 점이었습니다 — 그리고 두 에이전트 모두를 동일한 대화에 투입했습니다. 실제 결과는 다음과 같습니다:
- 메모리 개수 (Memory count): Synapse는 활성 메모리 약 117개 부근에서 정체(plateaus)되는 반면, 단순한 베이스라인(naive baseline)은 220개까지 거의 선형적으로 증가합니다.
- 쿼리당 토큰 비용 (Token cost per query): Synapse는 전체 실행 동안 130~170 범위 내에서 일정하게 유지되지만, 단순한 베이스라인은 인덱스가 커짐에 따라 2000 토큰을 넘어 급증합니다.
- 회상 정확도 (Recall accuracy): Synapse는 단순한 베이스라인의 95%에 비해 71%를 기록했습니다 — 저는 이 수치를 숨기지 않습니다.
마지막 수치는 곱씹어 볼 가치가 있습니다. 프로젝트를 멋지게 보이게 만드는 두 가지 지표만 골라내어(cherry-pick) 거기서 끝낼 수도 있었습니다. 대신 저는 Synapse가 왜 원시 재현율(raw recall)에서 뒤처졌는지 파고들었고, 수정 가능한 두 가지 구체적인 원인을 찾아냈습니다. 하나는 서로 다른 단어로 표현된 실제 모순을 거부해 버리는 유사도 사전 필터 게이트(similarity pre-filter gate)였고(0.75 게이트 기준 유사도를 0.59까지 낮게 측정함), 다른 하나는 자주 회상되는 일반적인 사실이 덜 강화되었지만 더 관련성이 높은 사실보다 우선순위를 가질 수 있는 재순위화(re-ranking) 공식이었습니다. 저는 이 두 가지를 모두 수정했고, 타겟 회귀 테스트(targeted regression tests)와 배포된 앱을 대상으로 한 실시간 엔드 투 엔드(end-to-end) 테스트를 통해 수정 사항을 검증했으며, 마감 전까지 수정된 코드로 몇 시간씩 걸리는 전체 벤치마크를 다시 실행할 시간이 없었다는 점을 포함하여 이 모든 과정을 문서화했습니다.
당신을 치켜세워 주는 차트보다 신뢰할 수 있는 차트가 더 가치 있습니다.
이 경로를 시작하려는 사람에게 해주고 싶은 말
- 메모리 메커니즘이 소규모로만 작동한다면, 진짜 버그를 찾지 못할 것입니다. 저의 버그는 타임스탬프(timestamp)와 현저성(salience)이 실제로 갈라질 여지가 생기는 100회 이상의 턴(turn)이 지나서야 나타났습니다.
- 단순한 베이스라인(naive baseline)을 '먼저' 구축하거나, 최소한 병행해서 구축하세요. 비교할 대상(diff)이 있어야
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기