에이전트 메모리 엔진 (1/10) — LLM 에이전트를 위한 SQLite 메모리 엔진: memcore 설계
SQLite 한 파일로 에이전트 메모리를 구축하는 설계 원리와 구조
핵심 요약
이 글이 전달하는 기술/정보:
- Retain 태그: 대화 시점에 LLM이 구조화된 태그를 생성하도록 강제하고, 사후에 정규식으로 수확하는 추출 기법. LLM 추출 대비 결정적이고 환각 없고 토큰 비용 0.
- 2-Track 실행: 규칙 기반 작업(Track 1)은 스크립트, 자연어가 필요한 작업(Track 2)만 이코노미 LLM. 규칙 기반 가능한 작업에 LLM을 쓰고 있지 않은지 점검하는 판단 기준.
- SQLite 단일 파일 메모리: FTS5(키워드) + sqlite-vec(시맨틱) + 6계층 하이브리드 검색. 외부 의존성 0, 9MB 파일 하나로 전체 에이전트 메모리 운영.
들어가며
LLM 에이전트를 장기간 운영할 때 가장 먼저 부딪히는 엔지니어링 병목은 메모리다. 수십 회 대화 수준에서는 전체 컨텍스트를 매번 붙여도 문제가 없지만, 수백~수천 건의 사실·의견·상태를 누적 관리하면서 지금 필요한 일부만 꺼내 써야 하는 단계로 넘어가면 문제의 성격이 완전히 달라진다.
이 글은 memcore라는 SQLite 기반 메모리 엔진의 설계 원리를 정리한다. 하나의 SQLite 파일에 에이전트 메모리 전체를 담고, 마크다운 파일을 진실의 원천으로 유지하는 구조다. 다루는 내용은 (1) LLM 메모리 설계의 구조적 제약, (2) 이를 해결하기 위한 5가지 설계 원칙, (3) Retain 태그·하이브리드 Recall·의견 감쇠 같은 구체 기법, (4) 다른 에이전트에 이식할 때의 최소 적용 단위다.
1. LLM 메모리가 어려운 4가지 구조적 이유
컨텍스트는 유한하고 비싸다
누적 대화를 매 호출마다 컨텍스트에 포함하면 토큰이 선형 이상으로 증가한다. 모노리식 heartbeat 구조에서는 단일 루프가 하루 450만 토큰 수준까지 소비하는 패턴이 관측됐다. 대화량 증가에 비해 비용이 가속도로 붙는다.
관련성은 규모에 반비례한다
메모리가 50건일 때는 전체 주입이 가능하지만, 500건 규모에서는 지금 필요한 소수만 정확히 선택해야 한다. 단일 벡터 검색만으로는 이 선택이 불안정하다. "reflect에 쓰는 모델은?" 같은 고유명사 질의에서는 시맨틱 유사도보다 키워드 매칭이 더 정확한 신호를 제공한다.
메모리 품질은 시간에 따라 부패한다
의견은 시간이 지나면 무효화되는 경우가 많다. 관리되지 않은 과거 의견은 노이즈로 누적되며, 에이전트는 낡은 판단을 근거로 현재 결정을 내리게 된다.
유지보수에 프론티어 모델을 쓰면 예산이 배관에 소진된다
메모리 정리·충돌 감지·요약 생성에 최고가 모델을 사용하면, 사용자 대화에 할당할 예산이 유지보수에 빨려간다. 배관 비용이 본 업무 비용을 역전하는 현상이 발생한다.
2. 설계 원칙 5가지
원칙 1: Script-first
규칙으로 표현 가능하면 스크립트가 처리한다. 메모리 추출·병합·중복 감지·진단의 대부분은 결정적 규칙으로 환원 가능하다. "이 태그가 있으면 추출", "유사도 80% 이상이면 중복", "30일 경과한 의견은 삭제" 같은 판단에는 LLM이 필요하지 않다.
Track 1 파이프라인(10~30분 주기)은 토큰 소비 0으로 동작한다. LLM은 스크립트로 환원이 불가능한 시맨틱 판단에만 호출된다.
원칙 2: Dual-write
bank/ 마크다운 파일이 진실의 원천(source of truth), memcore.db(SQLite)는 검색/분석 레이어다. 마크다운은 사람이 읽을 수 있고 git으로 추적되며 에디터로 수정 가능하다. DB 손상 시 마크다운에서 재구축 가능하지만, 반대는 비대칭적으로 어렵다. 충돌 시 bank/가 우선한다.
원칙 3: 모듈화 by 스케줄
관심사별로 독립 모듈 + 독립 스케줄을 할당한다. 신규 작업 추가는 모듈 1개 + macOS LaunchAgent 1개로 끝난다. 모듈 간 의존성이 없어 하나의 실패가 전파되지 않는다.
원칙 4: 계층적 LLM 할당
같은 "AI 판단"이라도 모델 선택에 따라 비용이 최대 100배 차이난다. 사용자 대화는 프리미엄, 메모리 유지보수는 무료/로컬, 시맨틱 판단은 이코노미. 이 분리가 전체 토큰 절감의 핵심이다.
원칙 5: 근거 없이 승격 없음
관찰 → 가설 → 검증된 패턴의 3단계를 거쳐야 장기 메모리로 승격된다. 단일 발화를 즉시 규칙으로 굳히지 않는다.
3. 전체 아키텍처
대화 → session-scan (10분, 태그 추출, 0 토큰)
→ micro-cycle (30분, 병합 + 로컬LLM 판단 1건)
→ reflect (매일 03:00, 전체 검증 + 클라우드LLM)
bank/ (md 파일, git 추적) ←→ memcore.db (SQLite, FTS5, 벡터)
↑
recall (6-tier 하이브리드 검색)
전체 메모리가 SQLite 단일 파일에 들어간다. WAL 모드로 동시 읽기를 지원하고, 22개 테이블이 역할을 분담한다. 파일 크기는 약 9MB.
백업은 파일 복사, 마이그레이션은 sqlite3 dump로 처리된다. PostgreSQL·Redis·Pinecone 같은 외부 벡터 DB 의존성이 없다.
SQLite를 선택한 이유는 검색 요구가 순수 시맨틱이 아니기 때문이다. 고유명사 질의는 키워드 매칭이 임베딩보다 정확하다. SQLite + FTS5로 키워드를, sqlite-vec + bge-m3로 시맨틱을 처리한다. 한 파일에 두 검색 체계가 공존한다.
4. Retain 태그 — 추출 계약의 역전
문제: LLM 추출은 신뢰할 수 없다
대화에서 사실을 추출하는 전통적 방법은 사후에 LLM에게 "중요한 사실을 추출해줘"라고 요청하는 방식이다. 구조적 한계는 다음과 같다.
- 비결정적: 매 호출마다 다른 결과가 나온다
- 환각 혼입: 원문에 없는 사실이 생성된다
- 호출 비용: 매 대화마다 LLM 호출이 필요하다
- 디버깅 불가: 추출 근거 추적이 어렵다
해결: 증류 시점에 형식을 강제한다
Retain 태그는 추출 계약을 역전시킨다. 사후 추출이 아니라 대화 시점에 LLM이 직접 구조화된 태그를 생성하도록 프롬프트로 강제한다. 5가지 태그 타입을 사용한다.
- W: Mac Mini M4 32GB RAM 확인 (세계 사실 — World fact)
- B: Reflect v0.5 파이프라인 구축 완료 (활동 기록 — Behavior log)
- O(c=0.90): 역전 구조가 비용 면에서 우위 (의견 + 신뢰도 — Opinion)
- S:openclaw-rebuild: Phase C 완료 (엔티티 상태 — State)
- U-Preference: 검증 없는 기능은 적용 거부 (사용자 패턴 — User pattern)
- W: 객관적 사실. 감쇠하지 않는다.
- B: 활동 로그. "무엇을 했는가"를 기록한다.
- O: 의견. 신뢰도(confidence)가 부착되며 시간에 따라 감쇠한다.
- S: 엔티티 상태. 특정 프로젝트·시스템의 현재 상태를 추적한다.
- U: 사용자 관찰. 선호·습관·패턴을 기록한다.
추출은 순수 정규식
태그 형식이 대화 시점에 강제되므로, 추출은 정규식으로 결정적으로 처리된다.
RETAIN_PATTERNS = [
re.compile(r"^- (W): (.+)", re.MULTILINE),
re.compile(r"^- (B): (.+)", re.MULTILINE),
re.compile(r"^- (O\(c=[\d.]+\)): (.+)", re.MULTILINE),
re.compile(r"^- (S:[^:]+): (.+)", re.MULTILINE),
re.compile(r"^- (U-\w+): (.+)", re.MULTILINE),
]
LLM 호출이 제거된다. 토큰 소비 0, 결정적 출력, 환각 없음, 매칭 실패 시 미추출(거짓 양성 0). 세션 종료를 기다리지 않고 10분 주기로 라이브 JSONL에서 파싱하며, 30분 내에 bank/에 반영된다.
핵심 원리
Retain 태그는 단순한 데이터 포맷이 아니라 강제 함수(forcing function)다. 대화 LLM이 자기 출력을 구조화하도록 만들면 이후 추출 파이프라인이 저렴하고 신뢰할 수 있어진다. 분류의 시점을 "사후 LLM"에서 "동시 LLM"으로 옮기는 설계 결정이다.
5. 6-tier 하이브리드 Recall
메모리는 꺼내지지 않으면 저장되지 않은 것과 같다. memcore는 6계층 하이브리드 검색을 사용한다.
"""Search strategy:
1. Topic keyword match → score × 2.0 (큐레이터 승인, 최고 신호)
2. Vector similarity → score × 1.5 (의미 검색, oMLX/bge-m3)
3. FTS5 full-text → BM25 score (한국어+영어 혼합)
4. LIKE fallback → score × 0.5 (최후 수단)
5. Wiki FTS → 별도 계층 (피처 문서 검색)
6. Wiki LIKE → 별도 계층 (문서 폴백)
"""
1계층 — Topic keyword match (×2.0): 큐레이터 승인 키워드에 대한 정확 매칭. 사람이 붙인 명시적 신호이므로 가중치가 가장 높다.
2계층 — Vector similarity (×1.5): oMLX에서 bge-m3로 생성한 임베딩의 코사인 유사도. "비용 절감 방법" 질의에서 "토큰 효율화"도 회수해야 하는 시맨틱 질의를 담당한다.
3계층 — FTS5 full-text (BM25): SQLite FTS5 확장의 전문 검색. 한국어·영어 혼합 콘텐츠에서 BM25 알고리즘으로 점수를 산출한다.
4계층 — LIKE fallback (×0.5): 상위 3계층 실패 시의 최후 수단. 단순 문자열 포함 검사. 가중치 최저.
5-6계층 — Wiki FTS/LIKE: 피처 문서 검색용 별도 계층. 에이전트가 자기 스펙을 검색 가능한 메모리로 다룰 수 있게 한다.
우아한 성능 저하
oMLX가 꺼져 있으면 2계층(벡터)을 자동 스킵하고 나머지 4개 계층이 커버한다. 벡터 없이도 키워드·전문 검색으로 대부분의 질의에 답할 수 있다. 가용성이 품질보다 우선한다는 의도적 설계다.
6. Micro-Cycle — 30분 파이프라인, LLM은 1건
micro-cycle은 30분 주기 8단계 파이프라인이다.
1. [Script] 새 memory/*.md 확인
2. [Script] retain-extract → staging JSON
3. [Script] retain-merge → bank/ 병합 + 충돌 감지 + fuzzy dedup(80%)
4. [Script] 진단 (bank-lint, memory-optimize)
5. [Script] decision-prepare → 판단 필요 항목 준비
6. [LLM] oMLX/gemma-4로 1건 시맨틱 판단 (로컬, 무료)
7. [Script] decision-apply → bank/ 반영
8. [Script] 로그 기록
8단계 중 7단계가 스크립트다. LLM은 6단계에서 1회만 호출되며, "두 메모리가 충돌하는가?", "이 관찰이 기존 가설을 뒷받침하는가?" 같은 시맨틱 판단 1건만 처리한다.
1건 제한의 근거
micro-cycle은 하루 48회 실행된다. 다건 허용 시 한 cycle에 10건이 쌓이면 480회 호출이 되고, rate limit 소진과 비용 폭주로 이어진다. 1건 제한은 예측 가능한 동작을 보장한다. 메모리 유지보수는 대화와 달리 지연이 허용되는 작업이므로 나머지는 다음 cycle로 이월된다.
로컬 oMLX 선택의 근거
Groq 무료 한도(1,500/일)를 micro-cycle 하나만으로 상당 부분 소진하면 다른 무료 LLM 용도 배분이 축소된다. oMLX + gemma-4는 로컬 실행이므로 오프라인·무제한·전기세 단위 비용이며, 네트워크 장애·API 다운과 독립적으로 동작한다.
7. 의견 감쇠 시스템
O(c=0.90) 태그의 신뢰도는 매일 -0.02씩 감쇠한다. 초기 0.90은 30일 후 0.30이 되며, 이 시점에서 자동 삭제된다.
설계 근거: 의견은 시간이 지나면 무효화될 수 있다. 3개월 전 "이 모델이 가장 효율적"이라는 기록은 신모델 출시로 이미 무의미해졌을 수 있다. 관리되지 않으면 에이전트는 과거 판단으로 현재 결정을 내린다.
감쇠 시스템은 이 문제를 자동화한다. 새 관찰로 재확인되지 않은 믿음은 자연 소멸하며, 여전히 유효한 의견은 후속 대화에서 재확인되면서 신뢰도가 리셋된다.
사실(W 태그)은 감쇠하지 않는다. 객관적 사실은 시간 경과와 무관하게 참이거나 철회된다.
8. U-tag 변증법 — 3단계 승격
사용자 관찰은 근거 없이 확정되지 않고 3단계를 거친다.
Observation (첫 관찰)
→ Hypothesis (2개 이상 관찰이 뒷받침)
→ Verified (3개 이상 관찰, 안정 패턴으로 승격)
단일 발화를 즉시 패턴으로 굳히면 특정 맥락의 발언이 모든 맥락의 규칙이 되는 과적합이 발생한다. "오늘은 간단하게"가 "항상 간단하게"로 굳는 것을 방지하기 위한 장치다.
9. 2-Track 실행 — 규칙 기반과 LLM의 분리
모든 작업을 두 트랙으로 분류한다.
- Track 1 (스크립트): 출력이 규칙으로 결정 가능 → 스크립트. 토큰 0.
- Track 2 (LLM): 출력이 자연어여야 함 → LLM. 단, 프론티어가 아니라 이코노미 모델.
초기 구현에서는 14개 모드가 전부 프론티어 모델을 호출하는 상태였다. 점검 결과 실제 LLM 판단이 필요한 것은 6개였으며, 그중에서도 이코노미 모델로 충분했다. 나머지 8개(파일 존재 확인·태그 파싱·중복 감지·진단 리포트)는 규칙 기반으로 완전 대체 가능했다.
절감 폭의 구체 수치는 기존 호출 패턴에 따라 달라지므로, 핵심은 수치가 아니라 "규칙 기반 가능한 작업에 LLM을 쓰고 있지 않은지 점검한다"는 판단 기준의 도입이다.
10. LLM 5계층 시스템
L0 Premium — gpt-5.4 (구독, 대화 전용)
L1 Standard — claude-sonnet, gemini-pro (종량제, fallback)
L2 Economy — gemini-flash ($0.075/M, reflect/self-review)
L3 Free — groq/llama-70b, github-models ($0, cron 알림)
L4 Local — oMLX/gemma-4 ($0, 메모리 파이프라인)
운영 원칙
- L0는 cron 금지: 구독 모델의 토큰을 자동화에 쓰면 사용자 대화 여유가 축소된다.
- 동일 티어 내 fallback: L2 실패 시 다른 L2로 시도하지, L0로 승급하지 않는다.
- Fallback 모니터링: Groq(1,500req/일)·GitHub Models(150req/일) 등 무료 한도 소진을 실시간 감지한다. Gateway 로그에서 429/timeout/error를 스캔하고 프로바이더별 건강 상태를 보고한다.
11. 피처 문서 ↔ memcore 자동 동기화
에이전트가 자기 자신의 기능을 검색할 수 있어야 한다. "recall 시스템은 어떻게 동작하나?" 질의에 개발자가 작성한 스펙 문서가 회수되어야 한다.
features_sync.py는 docs/features/ 마크다운을 H2 섹션 단위로 분리하고, 콘텐츠 해시로 변경을 감지해 memcore의 wiki 테이블에 동기화한다. 시스템의 자기 스펙이 검색 가능한 메모리로 통합된다.
12. CLI 33개 명령
memcore의 CLI는 3개 카테고리로 구성된다.
Pipeline (10개) — 데이터 흐름
extract, merge, decision-prepare, decision-apply, conflict-apply,
dialectic, skill-stage, session-scan, micro-cycle, migrate
데이터 유입(extract) → 병합(merge) → 판단(decision) → 적용(apply)의 흐름.
Maintain (13개) — 건강 유지
decay, lint, topics-validate, topics-expand, entity-audit, bank-size,
archive, session-cleanup, session-archive, optimize, ontology-sync,
features-sync, backfill-vectors
의견 감쇠(decay), 무결성 검사(lint), 벡터 보충(backfill-vectors) 등 메모리 건강 유지 작업.
Diagnose (10개) — 상태 파악
stats, monitor, warn, wiki-lint, task-scan, upcoming-event,
rollback, fallback-monitor, pricing-show, pricing-report
상태 파악(stats), 문제 감지(warn), 비용 추적(pricing-report) 도구.
13. 핵심 수치 요약
| 항목 | 수치 |
|---|---|
| Track 1 (스크립트) | LLM 호출 없음 |
| Track 2 (LLM) | 이코노미 모델 사용 |
| 큐레이트 데이터 | 295 rows |
| 위키 페이지 | 1,666 pages |
| CLI 명령 | 33개 (3 카테고리) |
| DB 테이블 | 22개 |
| DB 크기 | ~9MB (단일 파일) |
| LLM 계층 | 5 tiers |
| Recall 계층 | 6 tiers |
| Micro-cycle 주기 | 30분 |
| Session-scan 주기 | 10분 |
| 의견 감쇠율 | -0.02/일 |
| 외부 의존성 | 0 (SQLite only) |
14. 다른 에이전트에 이식하는 최소 단위
memcore 전체를 가져갈 필요는 없다. 핵심 개념 5가지만 적용해도 메모리 문제의 대부분을 해결할 수 있다.
1. 추출 계약 정의
3~5개 태그 타입을 정의하고 증류 프롬프트에 형식을 강제한다. 태그의 구체 형태는 에이전트마다 다르지만, "대화 시점에 구조화하고, 사후에 정규식으로 수확한다"는 패턴은 공통이다.
2. SQLite + FTS5
curated 테이블 + FTS5 가상 테이블을 한 파일에 배치한다. 시맨틱 검색이 필요하면 sqlite-vec을 추가한다.
3. bank/ 디렉토리
마크다운 파일로 진실의 원천을 유지한다. git 추적으로 변경 이력이 보존되고, DB 손상 시 재구축 가능하다.
4. micro-cycle 스크립트
30분 스케줄로 메모리를 자동 갱신하되 LLM 없이 동작하도록 설계한다. LLM 호출은 1건으로 제한한다.
5. LLM 계층 분리
대화에는 최고 모델, 유지보수에는 최저 모델. 이 한 가지 원칙만으로도 토큰 대부분을 절약할 수 있다.
마치며
memcore 설계에서 끌어낼 수 있는 일반 원리는 메모리 문제의 대부분은 LLM 문제가 아니라 엔지니어링 문제라는 점이다.
- 추출 → 정규식으로 충분
- 중복 감지 → fuzzy matching으로 충분
- 의견 관리 → 단순 감쇠 함수로 충분
- LLM이 실제 필요한 영역 → "두 사실이 충돌하는가?" 같은 시맨틱 판단
이 구분을 명확히 하면 비용을 축소하면서 메모리 품질을 유지할 수 있다. 스크립트는 LLM과 달리 결정적이고, 환각이 없고, 디버깅 가능하다.
적용 가능 범위와 열린 질문
- 적용 가능한 시나리오: 단일 사용자 또는 소규모 협업 에이전트, 로컬 환경 중심 운영, 하루 수십 MB 이내 메모리 증가 속도.
- 한계가 드러나는 시나리오: 초당 수백 건 이상의 쓰기가 발생하는 멀티테넌트 환경, 분산 읽기/쓰기 일관성이 필요한 환경. 이 경우 SQLite 단일 파일 가정이 먼저 깨진다.
- 열린 질문: (1) Retain 태그 형식을 자동 진화시키는 메커니즘은? (2) 의견 감쇠율을 주제별로 적응적으로 조정하려면 어떤 신호가 필요한가? (3) 피처 문서 동기화를 코드 수준 스펙(함수 시그니처·테스트)까지 확장했을 때의 회수율 변화는?
SQLite 파일 하나, 마크다운 몇 개, 정규식 몇 줄. 이 최소 조합으로 외부 의존성 없는 에이전트 메모리를 구축할 수 있다는 점이 memcore가 전달하려는 핵심이다.
시리즈 전체 안내: 시리즈 목차
댓글
댓글 쓰기