Memory System (한국어)
임베딩 제공자 선택하기 (v3.8.16+)
섹션 제목: “임베딩 제공자 선택하기 (v3.8.16+)”OmniRoute의 메모리 엔진은 네 가지 임베딩 소스(src/lib/memory/embedding/)를 지원합니다. 각 소스는 지연 시간, 비용, 모델 품질 및 설정 복잡성 측면에서 서로 다른 장단점이 있습니다.
임베딩 소스
섹션 제목: “임베딩 소스”| 제공자 | 소스 | 지연 시간 | 비용 | 품질 | 설정 |
|---|---|---|---|---|---|
transformers |
로컬 ONNX 모델(Xenova/all-MiniLM-L6-v2) | ~50-150ms (CPU) | 무료 | 우수 | npm install만 필요 |
static |
사전 계산된 벡터(캐시됨) | <1ms | 무료 | 해당 없음(캐시 적중 여부에 따라 다름) | 없음 |
remote |
OpenAI / Cohere / Voyage API | ~100-300ms | $0.02-0.10/1M 토큰 | 매우 우수 | API 키 |
auto |
런타임에 사용 가능한 최적의 소스를 선택 | 선택된 소스와 동일 | 무료 | 선택된 소스와 동일 | 없음 |
| (cache) | 모든 소스 위에 적용되는 인메모리 LRU 계층 | <1ms (적중), 전체 지연 시간(미적중) | 무료 | 기반 소스와 동일 | 항상 활성화됨(선택 가능한 소스가 아님) |
의사 결정 트리
섹션 제목: “의사 결정 트리” 배포 환경은 무엇인가요? │ ┌───────────┼───────────┬──────────────┐ │ │ │ │ 개발/테스트 소규모 운영 대규모 운영 엣지 / 오프라인 │ │ │ │ ▼ ▼ ▼ ▼ transformers transformers remote (Qdrant) transformers (무료, API 불필요) (최고 품질) (인터넷 불필요) │ │ │ │ └────────┬──┴───────────┴──────────────┘ │ ▼ 항상 상단에 `cache` 계층 추가 (LruCache가 모든 제공자를 래핑)데이터베이스 및 API 구성
섹션 제목: “데이터베이스 및 API 구성”메모리 임베딩 옵션은 환경 변수가 아닌 설정 API/UI를 통해 구성합니다. 설정 아래의 관련 설정 데이터베이스 키(src/lib/memory/settings.ts의 normalizeMemorySettings)는 다음과 같습니다.
memoryEmbeddingSource:"transformers"(로컬),"remote"(API 기반, 예: OpenAI),"static"(외부 저장소) 또는"auto"memoryEmbeddingProviderModel: 원격/정적 소스용 모델 식별자(예:"text-embedding-3-small")memoryTransformersEnabled:true|falsememoryStaticEnabled:true|falsememoryVectorStore:"sqlite-vec","qdrant"또는"auto"
로컬 모델 (transformers)
섹션 제목: “로컬 모델 (transformers)”내부적으로 transformers.js를 사용하여 로컬 모델을 실행합니다.
# 코드에서 읽는 환경 변수(src/lib/memory/embedding/index.ts):MEMORY_TRANSFORMERS_MODEL=Xenova/all-MiniLM-L6-v2 # HF 모델 저장소MEMORY_STATIC_MODEL=minishlab/potion-base-8M # HF 정적 potion 모델MEMORY_STATIC_CACHE_DIR=<DATA_DIR>/embeddings # 캐시 디렉터리LRU 임베딩 캐시
섹션 제목: “LRU 임베딩 캐시”캐시는 기본적으로 항상 활성화되며 환경 변수를 통해 구성됩니다.
MEMORY_EMBEDDING_CACHE_MAX=1000 # 캐시되는 최대 항목 수MEMORY_EMBEDDING_CACHE_TTL_MS=300000 # TTL(5분)성능 수치
섹션 제목: “성능 수치”일반적인 4코어 x86 서버에서의 벤치마크(텍스트당 약 100토큰):
| 제공자 | p50 | p95 | p99 | 임베딩 100만 개당 비용 |
|---|---|---|---|---|
transformers (CPU) |
80ms | 180ms | 350ms | 무료 |
remote (OpenAI) |
120ms | 220ms | 400ms | ~$0.02 (ada-002) / $0.13 (3-large) |
static (Qdrant) |
15ms | 30ms | 60ms | Qdrant 호스팅에 따라 다름 |
cache (적중) |
<1ms | <1ms | 2ms | 무료 |
사실 추출 패턴 (v3.8.16+)
섹션 제목: “사실 추출 패턴 (v3.8.16+)”extraction.ts 모듈(src/lib/memory/extraction.ts)은 정규식 패턴 매칭을 사용하여 대화 메시지에서 구조화된 사실을 추출합니다. 이러한 패턴을 이해하면 사용 사례에 맞게 추출 품질을 조정하는 데 도움이 됩니다.
기본 패턴 카테고리
섹션 제목: “기본 패턴 카테고리”| 카테고리 | 패턴 예시 | 캡처하는 내용 |
|---|---|---|
| PREFERENCE_PATTERNS | "나는 <X>를 선호한다", "나는 <X>를 좋아한다", "나는 <X>가 싫다" |
사용자 선호도 |
| DECISION_PATTERNS | "나는 <X>를 사용할 것이다", "나는 <X>하기로 결정했다", "나는 <X>를 선택했다" |
사용자 결정(일화적) |
| PATTERN_PATTERNS | "나는 보통 <X>한다", "나는 항상 <X>한다", "나는 절대 <X>하지 않는다" |
지속적인 행동 패턴 |
패턴 예시(단순화됨)
섹션 제목: “패턴 예시(단순화됨)”// src/lib/memory/extraction.ts에서 가져옴const PREFERENCE_PATTERNS = [ /\bI\s+(?:really\s+)?prefer\s+([^.,\n]+)/gi, /\bI\s+(?:really\s+)?like\s+([^.,\n]+)/gi, /\bI\s+(?:hate|dislike|avoid)\s+([^.,\n]+)/gi,];const DECISION_PATTERNS = [ /\bI'?(?:ll|will)\s+use\s+([^.,\n]+)/gi, /\bI\s+(?:have\s+)?decided\s+(?:to\s+)?([^.,\n]+)/gi,];const PATTERN_PATTERNS = [/\bI\s+usually\s+([^.,\n]+)/gi, /\bI\s+always\s+([^.,\n]+)/gi];추출되는 내용
섹션 제목: “추출되는 내용”사용자가 다음과 같이 말하는 경우:
“나는 TypeScript를 선호한다. 이 프로젝트에는 Postgres를 사용할 것이다. 나는 항상 푸시하기 전에 커밋한다. 나는 Python을 좋아하지 않는다.” 추출 결과로 4개의 메모리가 생성됩니다:
키 카테고리 유형 내용 preference:typescriptpreference factual “TypeScript” decision:postgres_for_this_projectdecision episodic “이 프로젝트용 Postgres” pattern:commit_before_pushingpattern factual “푸시하기 전에 커밋” preference:pythonpreference factual “Python”
추출 제한
섹션 제목: “추출 제한”과도한 추출을 방지하기 위해 다음 제한이 적용됩니다:
| 최소 콘텐츠 길이 | 3자 | | 최대 콘텐츠 길이 | 500자 |
추출을 비활성화해야 하는 경우
섹션 제목: “추출을 비활성화해야 하는 경우”메모리가 활성화될 때마다 추출이 자동으로 실행되며, 추출만을 위한 별도의 토글은 없습니다. 추출을 끄려면 PUT /api/settings/memory를 통해 메모리를 완전히 비활성화(enabled: false)하세요. 다음과 같은 경우 비활성화를 고려하세요:
- 메시지 양이 많고 추출 비용을 무시하기 어려운 경우
- 대화가 대부분 일시적이며(채팅, 디버깅) 장기적인 가치가 없는 경우
- 이미 사용자 정의 플러그인을 통해 컨텍스트를 캡처하고 있는 경우
하이브리드 RRF 조정 (v3.8.16+)
섹션 제목: “하이브리드 RRF 조정 (v3.8.16+)”Reciprocal Rank Fusion (RRF) 알고리즘은 FTS5(키워드) 결과와 벡터(의미론적) 결과를 결합합니다. k 매개변수는 순위가 낮은 결과에 부여되는 가중치를 제어합니다.
각 후보 메모리의 RRF 점수는 다음과 같습니다:
RRF(d) = Σ 1 / (k + rank_i(d))여기서:
k는 상수입니다(기본값 60).rank_i(d)는 i번째 검색 시스템(FTS, 벡터)에서 문서d의 순위입니다.- 합산은 모든 검색 시스템에 걸쳐 수행됩니다.
k가 결과에 미치는 영향
섹션 제목: “k가 결과에 미치는 영향”k 값 |
효과 | 적합한 경우 |
|---|---|---|
k=0 |
순수 순위 융합(평활화 없음) | 이론적 기준선 |
k=10-30 |
상위 결과에 높은 가중치를 부여하며, 낮은 순위는 거의 기여하지 않음 | 상위 3개 결과가 대체로 정확한 경우 |
k=60 (기본값) |
균형 잡힘 — 상위 10개 결과가 모두 유의미하게 기여함 | 범용 검색 |
k=100+ |
더 평탄함 — 여러 시스템에 나타나면 낮은 순위의 결과도 우세할 수 있음 | 정밀도보다 재현율이 중요한 경우 |
실제 환경에서 k 조정하기
섹션 제목: “실제 환경에서 k 조정하기”# 기본값MEMORY_RRF_K=60
# 높은 정밀도 우선(작은 메모리, 적은 문서)MEMORY_RRF_K=20
# 최대 재현율(큰 메모리, 다양한 쿼리)MEMORY_RRF_K=120k=20인 예시:
- FTS 순위 1 → 기여도
1/21 = 0.048 - FTS 순위 10 → 기여도
1/30 = 0.033 - 벡터 순위 1 → 기여도
0.048 - 결합 최댓값:
0.096
k=60인 예시:
- FTS 순위 1 → 기여도
1/61 = 0.016 - FTS 순위 10 → 기여도
1/70 = 0.014 - 벡터 순위 1 → 기여도
0.016 - 결합 최댓값:
0.033
k가 높을수록 상위 1위와 10위 사이의 상대적 차이가 작아지므로, 알고리즘은 최상위 순위의 신뢰도보다 검색 시스템 간의 합의에 더 많이 의존합니다.
k를 변경해야 하는 경우
섹션 제목: “k를 변경해야 하는 경우”| 증상 | 시도할 방법 |
|---|---|
| 최상위 결과가 항상 선택되지만 잘못됨 | k를 낮추기(예: 20) — 최상위 순위의 신뢰도가 더 중요해짐 |
| 정답이 상위 5개 안에는 있지만 1위가 아님 | k를 높이기(예: 100) — 더 평탄한 점수 체계가 합의를 보상함 |
| 재현율은 높지만 정밀도가 낮음 | k를 낮추기 — 순위를 더 명확하게 구분 |
| 재현율이 낮음(관련 문서가 누락됨) | k를 높이기 — 낮은 순위의 문서에도 기회를 부여 |
RRF 가중치
섹션 제목: “RRF 가중치”Reciprocal Rank Fusion은 의미론적 벡터 순위와 전문 검색 순위에 동일한 가중치를 사용합니다:
RRF(d) = 1/(k + rank_vector) + 1/(k + rank_fts)개별 가중치를 조정하는 환경 변수는 없습니다(MEMORY_RRF_VECTOR_WEIGHT/MEMORY_RRF_FTS_WEIGHT는 존재하지 않음).
요약 전략 (v3.8.16+)
섹션 제목: “요약 전략 (v3.8.16+)”summarization.ts 모듈(src/lib/memory/summarization.ts)은 회상 능력을 유지하면서 활성 세트를 작게 유지하기 위해 오래된 메모리를 압축합니다.
요약이 실행되는 시점
섹션 제목: “요약이 실행되는 시점”| 트리거 | 임계값(기본값) |
|---|---|
| API를 통한 수동 트리거 | 해당 없음 |
요약 대상
섹션 제목: “요약 대상”summarization.ts에서는 두 개의 진입점을 내보냅니다:
summarizeMemories(apiKeyId, sessionId?, maxTokens = 4000)— 세션의 메모리를 토큰 예산 내에서 하나의 요약 텍스트로 압축합니다.summarizeMemoriesOlderThan(apiKeyId, days, dryRun)— API에서 사용하는 기간 기반 압축으로,days보다 오래된 모든 메모리를 선택하고 이를 바탕으로 하나의 압축된 요약 메모리를 생성하며,dryRun이false이면 원본을 삭제합니다. 아무것도 수정하지 않고 후보 세트와 총 토큰 수를 미리 확인하려면dryRun: true를 전달합니다.
태그/키 클러스터링 단계나 메모리별 “핵심 또는 요약 가능” 점수 평가는 없습니다. 선택은 전적으로 기간 기준점에 따라 이루어지며, 요약 텍스트는 각 후보를 유형 접두사가 붙은 한 줄로 압축한 형태입니다.
요약 실행하기
섹션 제목: “요약 실행하기”요약은 수동 / 옵트인 방식입니다. autoSummarize 설정은 기본적으로 false이므로
어떤 항목도 자동으로 압축되지 않습니다. API를 통해 실행하세요:
curl -X POST http://localhost:20128/api/memory/summarize \ -H "Authorization: Bearer $OMNIROUTE_KEY"비활성화 상태로 유지하려면 autoSummarize를 기본값(false)으로 두기만 하면 됩니다.
요약 품질 개선 팁
섹션 제목: “요약 품질 개선 팁”- 먼저
dryRun으로 미리 확인하세요 —summarizeMemoriesOlderThan(..., true)는 후보 목록과 총 토큰 수를 반환하므로 원본을 삭제하기 전에 병합될 항목을 확인할 수 있습니다. - 메모리 코퍼스가 큰 경우 트래픽이 적은 시간에 요약을 실행하세요 — LLM 호출이 가장 오래 걸리는 부분입니다
# Cron 방식: 매일 오전 3시에 요약0 3 * * * curl -X POST http://localhost:20128/api/memory/summarize \ -H "Authorization: Bearer $OMNIROUTE_KEY"MemoryBackend 제공자 패턴
섹션 제목: “MemoryBackend 제공자 패턴”정본:
src/lib/memory/backend.ts,src/lib/memory/genericBackend.ts,src/lib/memory/manager.ts테스트:src/lib/memory/__tests__/generic-backend.test.ts
MemoryBackend 제공자 패턴은 기존 메모리 엔진 위에 플러그형 백엔드 추상화 계층을 도입합니다. 단일 저장소 구현에 종속되는 대신, 이제 메모리 시스템은 기본/폴백 라우팅을 구성할 수 있는 여러 백엔드(SQLite, Obsidian, Notion, 사용자 지정 HTTP 백엔드)를 지원합니다.
아키텍처
섹션 제목: “아키텍처”┌──────────────────────────────────────────────────────────┐│ API 라우트 ││ (src/app/api/memory/route.ts) │└──────────────────────┬───────────────────────────────────┘ │┌──────────────────────▼───────────────────────────────────┐│ MemoryManager ││ 싱글턴 오케스트레이터 (manager.ts) ││ ││ 기본 ─────► 백엔드 A (예: SQLite) ││ 폴백 ─────► 백엔드 B (예: Obsidian) ││ 백엔드 C (예: GenericBackend를 통한 Notion) │└──────────────────────┬───────────────────────────────────┘ │ ┌──────────────┼──────────────┐ ▼ ▼ ▼┌────────────┐ ┌────────────┐ ┌──────────────────┐│ SQLite │ │ Obsidian │ │ GenericMemory ││ 백엔드 │ │ 백엔드 │ │ 백엔드 (HTTP) │└────────────┘ └────────────┘ └──────────────────┘핵심 인터페이스(backend.ts)
섹션 제목: “핵심 인터페이스(backend.ts)”모든 백엔드는 MemoryBackend 인터페이스를 구현해야 합니다:
interface MemoryBackend { readonly id: string; readonly displayName: string;
// CRUD create(input: CreateMemoryInput): Promise<Memory>; get(id: string): Promise<Memory | null>; update(id: string, updates: Partial<...>): Promise<boolean>; delete(id: string): Promise<boolean>; list(filter: MemoryFilter): Promise<{ data: Memory[]; total: number; byType: Record<string, number> }>;
// 검색 search(config: SearchConfig): Promise<Memory[]>;
// 상태 health(): Promise<HealthCheckResult>;
// 수명 주기(선택 사항) initialize?(): Promise<void>; shutdown?(): Promise<void>;}MemoryManager (manager.ts)
섹션 제목: “MemoryManager (manager.ts)”다음을 수행하는 싱글턴 오케스트레이터입니다:
register(backend)를 통해 백엔드를 등록합니다. 부팅 시index.ts에서 호출됩니다.configure(primary, fallbacks)를 통해 기본 백엔드와 폴백을 구성합니다.- CRUD/검색 작업을 기본 백엔드로 라우팅하며, 실패 시 폴백 체인을 사용합니다.
- 모든 백엔드의 상태를 주기적으로 확인합니다.
폴백 동작:
| 작업 | 기본 백엔드 | 폴백 |
|---|---|---|
create |
✅ 기본 백엔드만 | ❌ |
get |
✅ 기본 백엔드 먼저 시도 | ✅ null이면 폴백 |
update |
✅ 기본 백엔드만 | ✅ 응답을 기다리지 않고 동기화 |
delete |
✅ 기본 백엔드만 | ✅ 응답을 기다리지 않고 동기화 |
list |
✅ 기본 백엔드만 | ❌ |
search |
✅ 기본 백엔드 먼저 시도 | ✅ 오류 발생 시 폴백 |
GenericMemoryBackend (genericBackend.ts)
섹션 제목: “GenericMemoryBackend (genericBackend.ts)”모든 REST API를 MemoryBackend로 변환하는 범용 HTTP 커넥터입니다. 다음과 같은 용도에 유용합니다:
- Notion — Notion API를 통해 연결
- Obsidian — Obsidian Local REST API를 통해 연결
- 사용자 지정 백엔드 — RESTful 메모리 API를 제공하는 모든 서비스
구성:
interface GenericBackendConfig { baseUrl: string; // 백엔드 API의 기본 URL apiKey?: string; // 인증용 Bearer 토큰 headers?: Record<string, string>; // 사용자 지정 HTTP 헤더 timeout?: number; // 요청 제한 시간(기본값: 30000ms) backendType?: string; // 로깅용
// 엔드포인트 재정의(기본값은 REST 규칙 사용) endpoints?: { search?: string; // 기본값: "/memories/search" create?: string; // 기본값: "/memories" list?: string; // 기본값: "/memories" get?: string; // 기본값: "/memories/{id}" update?: string; // 기본값: "/memories/{id}" delete?: string; // 기본값: "/memories/{id}" health?: string; // 기본값: "/health" };
// 쿼리 매개변수 이름 매핑 queryParams?: { query?/apiKeyId?/limit?/offset?/strategy?/maxTokens?/type?/sessionId?/orderBy?/orderDir?/options? };
// 경로 매개변수 이름 매핑 pathParams?: { id?/memoryId? };}알려진 백엔드는 KNOWN_BACKENDS에 미리 구성되어 있습니다.
createKnownBackend("obsidian"); // → localhost:27123을 가리키는 GenericMemoryBackendcreateKnownBackend("notion"); // → api.notion.com/v1을 가리키는 GenericMemoryBackend기본 제공 백엔드
섹션 제목: “기본 제공 백엔드”SQLiteBackend (sqliteBackend.ts)
섹션 제목: “SQLiteBackend (sqliteBackend.ts)”기본 주 백엔드입니다. src/lib/memory/store.ts를 사용하여 기존 SQLite 기반 메모리 저장소를 래핑합니다. 부팅 시 자동으로 등록됩니다.
import { sqliteBackend } from "./sqliteBackend";memoryManager.register(sqliteBackend);ObsidianBackend (obsidianBackend.ts)
섹션 제목: “ObsidianBackend (obsidianBackend.ts)”기존 Obsidian 통합(src/lib/memory/obsidianBackend.ts)을 래핑합니다. Obsidian Local REST API를 통해 Obsidian 볼트에 연결합니다.
메모리 백엔드 설정은 앱 설정 테이블에 저장되며 src/lib/memory/settings.ts를 통해 관리됩니다.
| 설정 | 환경/구성 키 | 기본값 | 설명 |
|---|---|---|---|
| 주 백엔드 | memoryPrimaryBackend |
"sqlite" |
주 백엔드의 ID |
| 대체 백엔드 | memoryFallbackBackends |
[] |
순서가 지정된 대체 백엔드 ID |
| 백엔드 구성 | memoryBackendConfigs |
{} |
백엔드별 구성 재정의 |
설정은 normalizeMemorySettings()를 통해 정규화되고 getMemorySettings()에서 캐시됩니다.
초기화 흐름
섹션 제목: “초기화 흐름”앱 부트스트랩 → index.ts 가져오기(부수 효과): SQLiteBackend 등록 → 앱 수명 주기에서 initMemoryBackends() 호출: 1. 설정 로드(getMemorySettings) 2. 주 백엔드 및 대체 백엔드 구성 3. 모든 백엔드 초기화(상태 확인) 4. 요청 처리 준비 완료새 백엔드 추가
섹션 제목: “새 백엔드 추가”src/lib/memory/<name>Backend.ts에서MemoryBackend구현src/lib/memory/index.ts에서 내보내기- 부팅 시
memoryManager.register(yourBackend)로 등록 - 설정을 통해 구성:
memoryPrimaryBackend를 백엔드 ID로 설정 src/lib/memory/__tests__/generic-backend.test.ts를 참고하여 테스트
예시: Brain 백엔드
섹션 제목: “예시: Brain 백엔드”import { createGenericMemoryBackend } from "./genericBackend";
const brainBackend = createGenericMemoryBackend("brain", "BK-Brain", { baseUrl: process.env.BRAIN_API_URL || "http://localhost:9099", apiKey: process.env.BRAIN_API_KEY, endpoints: { search: "/api/memory/search", create: "/api/memory", health: "/api/health", },});
memoryManager.register(brainBackend);단위 테스트
섹션 제목: “단위 테스트”npx vitest run src/lib/memory/__tests__/generic-backend.test.ts --reporter=verbose예상 출력: 다음 항목을 다루는 35개 테스트 모두 통과:
- 생성자(2)
- 상태 확인(4) — 성공, 실패 500, 네트워크 오류, 지연 시간
- 초기화(2) — 성공, 실패
- 생성(2) — 기본 엔드포인트, 사용자 지정 엔드포인트
- 조회(4) — 성공, 404 → null, 404 이외의 오류 발생, 사용자 지정 경로 매개변수
- 업데이트(2) — 성공, 404 → false
- 삭제(2) — 성공, 404 → false
- 목록(2) — 쿼리 매개변수, 사용자 지정 매개변수 이름
- 검색(3) — 쿼리 매개변수, 사용자 지정 엔드포인트, 옵션 직렬화
- 인증 헤더(2) — Bearer 토큰, 사용자 지정 헤더
- 팩토리(1)
타입 검사
섹션 제목: “타입 검사”npm run typecheck:core예상 결과: 오류 0개.
HagiCode
HagiCode는 구조화된 워크플로, 다중 에이전트 실행, Hero Dungeon 뷰를 갖춘 에이전트 코딩 작업 공간입니다.
더 스마트하고 빠르며 즐거운 에이전트 워크플로로 유용한 소프트웨어를 만드세요.

- Smart구조화된 워크플로는 의도를 아이디어부터 배포까지 실행 가능한 경로로 바꿉니다.
- Efficient다중 에이전트 워크플로로 조사, 구현, 검토를 병렬로 진행합니다.
- FunHero Dungeon은 긴 코딩 세션을 시각적이고 협업적인 경험으로 만듭니다.