콘텐츠로 이동
OmniRoute source

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/UI를 통해 구성합니다. 설정 아래의 관련 설정 데이터베이스 키(src/lib/memory/settings.ts의 normalizeMemorySettings)는 다음과 같습니다.

  • memoryEmbeddingSource: "transformers"(로컬), "remote"(API 기반, 예: OpenAI), "static"(외부 저장소) 또는 "auto"
  • memoryEmbeddingProviderModel: 원격/정적 소스용 모델 식별자(예: "text-embedding-3-small")
  • memoryTransformersEnabled: true | false
  • memoryStaticEnabled: true | false
  • memoryVectorStore: "sqlite-vec", "qdrant" 또는 "auto"

내부적으로 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 # 캐시 디렉터리

캐시는 기본적으로 항상 활성화되며 환경 변수를 통해 구성됩니다.

터미널 창
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 무료

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:typescript preference factual “TypeScript”
decision:postgres_for_this_project decision episodic “이 프로젝트용 Postgres”
pattern:commit_before_pushing pattern factual “푸시하기 전에 커밋”
preference:python preference factual “Python”

과도한 추출을 방지하기 위해 다음 제한이 적용됩니다:

| 최소 콘텐츠 길이 | 3자 | | 최대 콘텐츠 길이 | 500자 |

추출을 비활성화해야 하는 경우

섹션 제목: “추출을 비활성화해야 하는 경우”

메모리가 활성화될 때마다 추출이 자동으로 실행되며, 추출만을 위한 별도의 토글은 없습니다. 추출을 끄려면 PUT /api/settings/memory를 통해 메모리를 완전히 비활성화(enabled: false)하세요. 다음과 같은 경우 비활성화를 고려하세요:

  • 메시지 양이 많고 추출 비용을 무시하기 어려운 경우
  • 대화가 대부분 일시적이며(채팅, 디버깅) 장기적인 가치가 없는 경우
  • 이미 사용자 정의 플러그인을 통해 컨텍스트를 캡처하고 있는 경우

Reciprocal Rank Fusion (RRF) 알고리즘은 FTS5(키워드) 결과와 벡터(의미론적) 결과를 결합합니다. k 매개변수는 순위가 낮은 결과에 부여되는 가중치를 제어합니다.

각 후보 메모리의 RRF 점수는 다음과 같습니다:

RRF(d) = Σ 1 / (k + rank_i(d))

여기서:

  • k는 상수입니다(기본값 60).
  • rank_i(d)는 i번째 검색 시스템(FTS, 벡터)에서 문서 d의 순위입니다.
  • 합산은 모든 검색 시스템에 걸쳐 수행됩니다.
k 값 효과 적합한 경우
k=0 순수 순위 융합(평활화 없음) 이론적 기준선
k=10-30 상위 결과에 높은 가중치를 부여하며, 낮은 순위는 거의 기여하지 않음 상위 3개 결과가 대체로 정확한 경우
k=60 (기본값) 균형 잡힘 — 상위 10개 결과가 모두 유의미하게 기여함 범용 검색
k=100+ 더 평탄함 — 여러 시스템에 나타나면 낮은 순위의 결과도 우세할 수 있음 정밀도보다 재현율이 중요한 경우
터미널 창
# 기본값
MEMORY_RRF_K=60
# 높은 정밀도 우선(작은 메모리, 적은 문서)
MEMORY_RRF_K=20
# 최대 재현율(큰 메모리, 다양한 쿼리)
MEMORY_RRF_K=120

k=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를 낮추기(예: 20) — 최상위 순위의 신뢰도가 더 중요해짐
정답이 상위 5개 안에는 있지만 1위가 아님 k를 높이기(예: 100) — 더 평탄한 점수 체계가 합의를 보상함
재현율은 높지만 정밀도가 낮음 k를 낮추기 — 순위를 더 명확하게 구분
재현율이 낮음(관련 문서가 누락됨) k를 높이기 — 낮은 순위의 문서에도 기회를 부여

Reciprocal Rank Fusion은 의미론적 벡터 순위와 전문 검색 순위에 동일한 가중치를 사용합니다:

RRF(d) = 1/(k + rank_vector) + 1/(k + rank_fts)

개별 가중치를 조정하는 환경 변수는 없습니다(MEMORY_RRF_VECTOR_WEIGHT/MEMORY_RRF_FTS_WEIGHT는 존재하지 않음).


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"

정본: 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) │
└────────────┘ └────────────┘ └──────────────────┘

모든 백엔드는 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&lt;boolean&gt;;
delete(id: string): Promise&lt;boolean&gt;;
list(filter: MemoryFilter): Promise<{ data: Memory[]; total: number; byType: Record<string, number> }>;
// 검색
search(config: SearchConfig): Promise<Memory[]>;
// 상태
health(): Promise<HealthCheckResult>;
// 수명 주기(선택 사항)
initialize?(): Promise&lt;void&gt;;
shutdown?(): Promise&lt;void&gt;;
}

다음을 수행하는 싱글턴 오케스트레이터입니다:

  • register(backend)를 통해 백엔드를 등록합니다. 부팅 시 index.ts에서 호출됩니다.
  • configure(primary, fallbacks)를 통해 기본 백엔드와 폴백을 구성합니다.
  • CRUD/검색 작업을 기본 백엔드로 라우팅하며, 실패 시 폴백 체인을 사용합니다.
  • 모든 백엔드의 상태를 주기적으로 확인합니다.

폴백 동작:

작업 기본 백엔드 폴백
create ✅ 기본 백엔드만 ❌
get ✅ 기본 백엔드 먼저 시도 ✅ null이면 폴백
update ✅ 기본 백엔드만 ✅ 응답을 기다리지 않고 동기화
delete ✅ 기본 백엔드만 ✅ 응답을 기다리지 않고 동기화
list ✅ 기본 백엔드만 ❌
search ✅ 기본 백엔드 먼저 시도 ✅ 오류 발생 시 폴백

모든 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을 가리키는 GenericMemoryBackend
createKnownBackend("notion"); // → api.notion.com/v1을 가리키는 GenericMemoryBackend

기본 주 백엔드입니다. src/lib/memory/store.ts를 사용하여 기존 SQLite 기반 메모리 저장소를 래핑합니다. 부팅 시 자동으로 등록됩니다.

import { sqliteBackend } from "./sqliteBackend";
memoryManager.register(sqliteBackend);

기존 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. 요청 처리 준비 완료
  1. src/lib/memory/&lt;name&gt;Backend.ts에서 MemoryBackend 구현
  2. src/lib/memory/index.ts에서 내보내기
  3. 부팅 시 memoryManager.register(yourBackend)로 등록
  4. 설정을 통해 구성: memoryPrimaryBackend를 백엔드 ID로 설정
  5. src/lib/memory/__tests__/generic-backend.test.ts를 참고하여 테스트
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개.


OmniRoute 소스 코드 (a58000c7685f)

HagiCode

HagiCode는 구조화된 워크플로, 다중 에이전트 실행, Hero Dungeon 뷰를 갖춘 에이전트 코딩 작업 공간입니다.

더 스마트하고 빠르며 즐거운 에이전트 워크플로로 유용한 소프트웨어를 만드세요.

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