벡터 데이터베이스 없이 셀프 호스팅 AI 에이전트에 장기 기억을 부여하는 방법
요약
벡터 데이터베이스 없이 SQLite와 numpy를 활용하여 셀프 호스팅 AI 에이전트에 장기 기억 기능을 구현하는 방법을 소개합니다. 데이터 보안을 위해 사용자의 로컬 서버에 모든 임베딩과 기억을 저장하며, 의미론적 및 일화적 기억 계층을 분리하여 설계했습니다.
핵심 포인트
- 벡터 DB 대신 SQLite에 float32 블롭 형태로 임베딩 저장
- numpy를 이용한 브루트 포스 코사인 유사도 검색 활용
- 데이터 보안을 위해 사용자의 로컬 장치 내에 모든 데이터 격리
- 의미론적 기억과 일화적 기억의 두 가지 계층 구조 설계
요약 (TL;DR): 셀프 호스팅 (self-hosted) AI 에이전트에 장기 기억을 추가했습니다. 이 기억은 클라우드나 벡터 데이터베이스 (vector database) 없이 사용자의 자체 서버에 저장됩니다. 임베딩 (Embeddings)은 SQLite에 float32 블롭 (blobs) 형태로 저장되며, 일반적인 numpy 코사인 유사도 (cosine similarity)로 비교됩니다. 쉬운 부분은 기억을 저장하는 것이었고, 어려운 부분은 이를 통합(추가 / 업데이트 / 삭제)하는 것이었으며, 두 가지 LLM 버그가 저에게 가장 큰 교훈을 주었습니다.
모든 것을 결정지은 제약 조건
대부분의 "AI 기억" 튜토리얼은 벡터 데이터베이스를 구축하고 데이터를 누군가의 클라우드로 전송하는 것으로 시작합니다. 저는 둘 다 할 수 없었습니다.
제가 작업 중인 AI 에이전트인 AIDA by Autafy는 셀프 호스팅 (self-hosted) 방식입니다. 즉, 사용자의 자체 서버에서 사용자의 API 키로 실행됩니다. 이 시스템의 핵심 목적은 데이터가 사용자의 손을 절대 떠나지 않게 하는 것입니다. 따라서 기억 기능을 추가할 때도 기억 역시 사용자의 장치 내에, 사용자가 직접 열고, 백업하고, 삭제할 수 있는 파일 형태로 존재해야 했습니다. 이 하나의 제약 조건이 전체 설계를 결정했습니다.
두 가지 종류의 기억: 의미론적 기억과 일화적 기억
대부분의 기억 기능은 "간결한 답변을 선호함", "부동산 분야에서 일함"과 같은 사실만을 저장합니다. 유용하지만 단편적입니다. 저는 두 가지 계층을 원했습니다:
의미론적 기억 (Semantic memory) — 지속적인 사실: 선호도, 프로젝트, 함께 일하는 사람들.
일화적 기억 (Episodic memory) — 기억할 가치가 있는 순간들: "첫 고객을 확보하여 매우 기뻐했음."
일화적 계층은 어시스턴트가 단순히 프로필 카드를 보유하는 것을 넘어, 마치 사용자를 알고 있는 것처럼 느끼게 만드는 요소입니다. 두 가지 모두 SQLite의 행 (rows)일 뿐입니다.
왜 벡터 데이터베이스를 사용하지 않았는가?
사용자당 규모를 고려할 때, 벡터 데이터베이스는 과합니다. 한 사람이 축적하는 기억은 수백 개에서 수천 개 정도이지, 수백만 개가 아닙니다. 따라서 임베딩 (embeddings)은 기억 텍스트 바로 옆에 가공되지 않은 float32 블롭 (float32 blobs) 형태로 저장되며, 검색은 numpy를 이용한 브루트 포스 (brute-force) 코사인 유사도 (cosine similarity)로 수행됩니다:
python
import numpy as np
memories store their embedding as a float32 blob in SQLite
def cosine_search(query_vec, rows, top_k=3):
q = query_vec / np.linalg.norm(query_vec)
scored = []
for mem_id, blob in rows:
v = np.frombuffer(blob, dtype=np.float32)
scored.append((mem_id, float(np.dot(q, v / np.linalg.norm(v)))))
scored.sort(key=lambda x: x[1], reverse=True)
return scored[:top_k]
수천 개의 행(row) 정도의 규모에서는 이 작업이 1밀리초(ms) 미만으로 실행됩니다. 벡터 데이터베이스 (Vector Database)를 건너뜀으로써 얻는 이점은 다음과 같습니다:
동기화할 별도의 저장소가 없음 — 하나의 SQLite 파일이 곧 메모리입니다.
격리 (Isolation) — 메모리 임베딩 (Embedding)은 문서/RAG 저장소와 절대 섞이지 않습니다. 파일이 다르면 코드 경로도 다릅니다.
직관적인 소유권 — 전체 메모리는 사용자가 제어할 수 있는 하나의 휴대 가능한 파일이며, 사용자가 소유하지 않은 데이터베이스의 행(row)들이 아닙니다.
어려운 부분: 저장이 아닌 통합 (Consolidation)
행을 추가하는 것은 누구나 할 수 있습니다. 진짜 문제는 열 번째 대화에서 사용자가 스스로 모순되는 말을 할 때 발생합니다.
"내가 제일 좋아하는 색은 초록색이야." → 저장합니다.
나중에: "사실 초록색은 싫어해, 파란색이 내 색이야." → 이는 두 번째 행을 추가하는 것이 아니라 기존 행을 다시 써야 (rewrite) 합니다.
"나 이제 채식주의자 아니야." → 이는 이상한 부정문을 저장하는 것이 아니라 해당 사실을 삭제해야 합니다.
따라서 야간 작업 (cron sweep — 채팅의 핫 패스(hot path)에서 실행되지 않도록 주의)이 매일의 대화를 검토하고, 후보 사실들을 추출한 뒤, LLM에게 기존 메모리와 대조하여 결정하도록 요청합니다:
새로운 사실(NEW fact)과 최대 3개의 기존 메모리(EXISTING memories)가 주어졌을 때, 다음 중 하나를 선택하십시오:
- ADD (추가) → 진정으로 새로운 정보
- UPDATE (업데이트) → 동일한 주제이나 정보가 변경됨; 해당 메모리를 다시 작성
- DELETE (삭제) → 새로운 사실이 기존 것과 모순되며 대체할 대상이 없음
- NOOP (동작 없음) → 중복이거나 유지할 가치가 없음
모든 결정(작업 유형 및 가장 가까운 기존 메모리와의 거리)은 로그로 기록됩니다. 이를 통해 사전에 추측하는 대신, 실제 데이터를 바탕으로 나중에 임계값 (thresholds)을 조정할 수 있습니다.
나에게 가장 많은 것을 가르쳐준 두 가지 버그
1. 아무것도 반환하지 않고 조용히 종료된 모델. 통합 (Consolidation) 과정이 계속해서 ADD(추가)로 기본 설정되었습니다. 이로 인해 모순된 내용이 삭제되지 않았고, 업데이트가 제대로 이루어지지 않았습니다. 원인은 다음과 같았습니다. 저는 max_tokens가 낮게 제한된 저렴한 추론 모델 (reasoning model)을 사용하고 있었습니다. 추론 모델은 답변하기 전에 생각하는 데 토큰을 소비하므로, 예산이 추론 과정에서 모두 소진되었고, 응답은 아무 내용 없이 잘린 채로 돌아왔습니다. 결과적으로 저의 JSON 파싱 (parse)이 실패했고, 폴백 (fallback) 로직이 조용히 ADD를 선택했습니다. 오류가 발생하지 않았기 때문에 세 번의 스윕 (sweeps)이 그런 방식으로 실행되었습니다. 교훈: 추론 모델에는 충분한 여유를 주고, 디버깅 시에는 전체 API 응답 본문 (response body)을 로그로 남기며, 폴백이 실패를 숨기게 두지 마십시오.
2. 피드백 루프 (feedback loop). 추출기 (extractor)가 어시스턴트 (assistant) 자신의 답변을 새로운 사실로 읽기 시작했습니다. 즉, 기억을 바탕으로 한 답변이 다시 새로운 "사실"로서 기억으로 세탁되고, 심지어 사용자의 질문조차 거짓 사실이 되어버리는 현상이었습니다. 해결책은 추출 프롬프트 (extraction prompt)에 하나의 핵심 규칙을 추가하는 것이었습니다: 오직 사용자의 진술만을 소스로 간주한다. 어시스턴트의 차례는 결코 사실이 될 수 없습니다.
감사 및 삭제가 가능한 기억
이 시스템은 사용자의 자체 서버에서 실행되므로, 핵심 가치는 "데이터를 우리에게 맡기세요"가 아니라 "감사 가능성 (auditability)"에 있습니다. 따라서 모든 제어 기능이 함께 제공됩니다: 기억 기능은 기본적으로 꺼져 있으며, 모든 기억은 확인 및 편집이 가능하고, 각 기억은 출처를 표시하며, 대화별 시크릿 모드 (incognito mode)와 모든 것을 잊기 버튼이 있습니다. 제어할 수 없는 기억은 단순한 감시일 뿐입니다.
핵심 요약 (Takeaways)
- 인간 규모의 사용자별 기억을 구현하기 위해서는, 단순성과 이식성 측면에서 SQLite 블롭 (blobs) + numpy 코사인 유사도 (cosine) 조합이 벡터 데이터베이스 (vector database)보다 뛰어납니다.
- 기억을 저장하는 것은 쉽지만, 이를 통합하는 것 (add / update / delete / noop)이 진짜 핵심 작업입니다.
- 조용한 폴백 (silent fallbacks)을 주의하십시오. 무해해 보이는 동작으로 기본 설정되는 파싱 실패는 며칠 동안 망가진 파이프라인 (pipeline)을 숨길 수 있습니다.
- 추출기가 대화를 읽는다면, 어시스턴트 자신의 말을 그라운드 트루스 (ground truth, 절대적 사실)로 취급하지 못하도록 확실히 하십시오.
이 방식은 autafy.ca의 셀프 호스팅 AI 에이전트인 AIDA에 내장되어 있지만, 메모리를 대여하는 대신 직접 소유하고 싶은 모든 어시스턴트에게 적용 가능한 접근 방식입니다. 저는 데이터 통합 (consolidation) 과정에 대해 진심으로 의견을 나누고 싶습니다. 이 부분은 제가 완벽하게 구현했는지 가장 확신이 서지 않는 부분이기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기