메모리
OpenClaw 메모리는 에이전트 워크스페이스에 있는 일반 마크다운 파일입니다. 파일이 진실의 원천이며, 모델은 디스크에 기록된 것만 “기억”합니다.
메모리 검색 도구는 활성 메모리 플러그인(기본값: memory-core)이 제공합니다. plugins.slots.memory = "none"으로 메모리 플러그인을 비활성화할 수 있습니다.
메모리 파일 (마크다운)
기본 워크스페이스 구성은 두 가지 메모리 레이어를 사용합니다:
memory/YYYY-MM-DD.md- 일별 로그 (추가 전용).
- 세션 시작 시 오늘 + 어제를 읽습니다.
MEMORY.md(선택)- 큐레이션된 장기 메모리.
- 메인 비공개 세션에서만 로드 (그룹 컨텍스트에서는 사용 금지).
이 파일들은 워크스페이스(agents.defaults.workspace, 기본값 ~/.openclaw/workspace) 아래에 있습니다. 전체 구조는 에이전트 워크스페이스를 참고하세요.
메모리 도구
OpenClaw는 이 마크다운 파일을 위한 두 가지 에이전트용 도구를 제공합니다:
memory_search— 인덱싱된 스니펫에 대한 의미 검색.memory_get— 특정 마크다운 파일/줄 범위의 대상 읽기.
memory_get은 이제 파일이 존재하지 않을 때 우아하게 처리합니다 (예: 첫 번째 기록 전 오늘의 일별 로그). 내장 관리자와 QMD 백엔드 모두 ENOENT를 던지는 대신 { text: "", path }를 반환하므로, 에이전트가 “아직 기록된 것 없음”을 처리하고 try/catch 로직 없이 워크플로를 계속할 수 있습니다.
메모리를 기록해야 할 때
- 결정, 선호사항, 지속적인 사실은
MEMORY.md에 기록합니다. - 일상적인 메모와 진행 중인 컨텍스트는
memory/YYYY-MM-DD.md에 기록합니다. - 누군가 “이거 기억해”라고 말하면 적어두세요 (RAM에 보관하지 마세요).
- 이 영역은 아직 발전 중입니다. 모델에게 메모리를 저장하도록 알려주면 도움이 됩니다. 모델은 무엇을 해야 할지 알고 있습니다.
- 무언가를 확실히 남기고 싶다면 봇에게 메모리에 쓰라고 요청하세요.
자동 메모리 플러시 (압축 전 핑)
세션이 자동 압축에 근접하면 OpenClaw는 컨텍스트가 압축되기 전에 지속적인 메모리를 기록하도록 모델에 알리는 무음 에이전트 턴을 실행합니다. 기본 프롬프트는 모델이 _응답할 수 있다_고 명시적으로 안내하지만, 보통 사용자에게 이 턴이 보이지 않도록 NO_REPLY가 올바른 응답입니다.
agents.defaults.compaction.memoryFlush로 제어합니다:
{
agents: {
defaults: {
compaction: {
reserveTokensFloor: 20000,
memoryFlush: {
enabled: true,
softThresholdTokens: 4000,
systemPrompt: "Session nearing compaction. Store durable memories now.",
prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
},
},
},
},
}
세부사항:
- 소프트 임계값: 세션 토큰 추정치가
contextWindow - reserveTokensFloor - softThresholdTokens를 넘을 때 플러시가 실행됩니다. - 기본적으로 무음: 프롬프트에
NO_REPLY가 포함되어 있어 아무것도 전달되지 않습니다. - 두 개의 프롬프트: 사용자 프롬프트와 시스템 프롬프트가 알림을 추가합니다.
- 압축 사이클당 한 번만 플러시 (
sessions.json에서 추적). - 워크스페이스가 쓰기 가능해야 함: 세션이
workspaceAccess: "ro"또는"none"으로 샌드박스 실행 중이면 플러시가 건너뛰어집니다.
전체 압축 생명주기는 세션 관리 + 압축을 참고하세요.
벡터 메모리 검색
OpenClaw는 MEMORY.md와 memory/*.md에 대해 소규모 벡터 인덱스를 구축하여 표현이 달라도 의미 기반 쿼리로 관련 메모를 찾을 수 있습니다.
기본값:
- 기본적으로 활성화.
- 메모리 파일 변경을 감시 (디바운스 적용).
- 메모리 검색은
agents.defaults.memorySearch아래에서 설정 (최상위memorySearch가 아님). - 기본적으로 원격 임베딩 사용.
memorySearch.provider가 설정되지 않으면 OpenClaw가 자동 선택:memorySearch.local.modelPath가 설정되어 있고 파일이 존재하면local.- OpenAI 키를 확인할 수 있으면
openai. - Gemini 키를 확인할 수 있으면
gemini. - Voyage 키를 확인할 수 있으면
voyage. - Mistral 키를 확인할 수 있으면
mistral. - 그 외에는 설정될 때까지 메모리 검색이 비활성 상태.
- 로컬 모드는 node-llama-cpp를 사용하며
pnpm approve-builds가 필요할 수 있습니다. - sqlite-vec(사용 가능한 경우)를 사용하여 SQLite 내에서 벡터 검색을 가속합니다.
memorySearch.provider = "ollama"도 로컬/자체 호스팅 Ollama 임베딩(/api/embeddings)에 지원되지만 자동 선택되지는 않습니다.
원격 임베딩은 임베딩 프로바이더의 API 키가 필요합니다. OpenClaw는 인증 프로필, models.providers.*.apiKey, 환경 변수에서 키를 확인합니다. Codex OAuth는 chat/completions만 커버하며 메모리 검색의 임베딩을 충족하지 못합니다. Gemini의 경우 GEMINI_API_KEY 또는 models.providers.google.apiKey를 사용하세요. Voyage의 경우 VOYAGE_API_KEY 또는 models.providers.voyage.apiKey를 사용하세요. Mistral의 경우 MISTRAL_API_KEY 또는 models.providers.mistral.apiKey를 사용하세요. Ollama는 일반적으로 실제 API 키가 필요하지 않습니다 (로컬 정책에 필요한 경우 OLLAMA_API_KEY=ollama-local 같은 플레이스홀더면 충분합니다).
커스텀 OpenAI 호환 엔드포인트 사용 시 memorySearch.remote.apiKey (및 선택적 memorySearch.remote.headers)를 설정하세요.
QMD 백엔드 (실험적)
memory.backend = "qmd"로 설정하면 내장 SQLite 인덱서를 QMD로 교체합니다. QMD는 BM25 + 벡터 + 리랭킹을 결합하는 로컬 우선 검색 사이드카입니다. 마크다운이 진실의 원천으로 유지되며, OpenClaw가 검색을 위해 QMD를 셸 호출합니다. 핵심 사항:
전제 조건
- 기본적으로 비활성. 설정별로 선택 (
memory.backend = "qmd"). - QMD CLI를 별도로 설치하고 (
bun install -g https://github.com/tobi/qmd또는 릴리스 다운로드) 게이트웨이의PATH에qmd바이너리가 있는지 확인하세요. - QMD는 확장을 허용하는 SQLite 빌드가 필요합니다 (macOS에서
brew install sqlite). - QMD는 Bun +
node-llama-cpp를 통해 완전히 로컬로 실행되며 첫 사용 시 HuggingFace에서 GGUF 모델을 자동 다운로드합니다 (별도 Ollama 데몬 불필요). - 게이트웨이는
XDG_CONFIG_HOME과XDG_CACHE_HOME을 설정하여~/.openclaw/agents/<agentId>/qmd/아래의 자체 포함 XDG 홈에서 QMD를 실행합니다. - OS 지원: Bun + SQLite가 설치되면 macOS와 Linux에서 바로 동작합니다. Windows는 WSL2를 통해 가장 잘 지원됩니다.
사이드카 실행 방식
- 게이트웨이가
~/.openclaw/agents/<agentId>/qmd/아래에 자체 포함 QMD 홈(설정 + 캐시 + sqlite DB)을 작성합니다. memory.qmd.paths(및 기본 워크스페이스 메모리 파일)에서qmd collection add로 컬렉션을 생성한 후, 부팅 시와 설정 가능한 간격(memory.qmd.update.interval, 기본값 5분)으로qmd update+qmd embed를 실행합니다.- 게이트웨이는 이제 시작 시 QMD 관리자를 초기화하므로, 첫
memory_search호출 전에도 주기적 업데이트 타이머가 준비됩니다. - 부팅 새로고침은 기본적으로 백그라운드에서 실행되므로 채팅 시작이 차단되지 않습니다. 이전의 블로킹 동작을 유지하려면
memory.qmd.update.waitForBootSync = true로 설정하세요. - 검색은
memory.qmd.searchMode를 통해 실행됩니다 (기본값qmd search --json,vsearch와query도 지원). 선택한 모드가 QMD 빌드에서 플래그를 거부하면 OpenClaw가qmd query로 재시도합니다. QMD가 실패하거나 바이너리가 없으면 OpenClaw가 자동으로 내장 SQLite 관리자로 폴백하여 메모리 도구가 계속 작동합니다. - OpenClaw는 현재 QMD 임베딩 배치 크기 조정을 노출하지 않습니다. 배치 동작은 QMD 자체에서 제어됩니다.
- 첫 검색이 느릴 수 있음: QMD가 첫
qmd query실행 시 로컬 GGUF 모델(리랭커/쿼리 확장)을 다운로드할 수 있습니다.-
OpenClaw는 QMD 실행 시
XDG_CONFIG_HOME/XDG_CACHE_HOME을 자동 설정합니다. -
수동으로 모델을 미리 다운로드하고 OpenClaw가 사용하는 동일한 인덱스를 워밍업하려면 에이전트의 XDG 디렉터리로 일회성 쿼리를 실행하세요.
OpenClaw의 QMD 상태는 상태 디렉터리 (기본값
~/.openclaw) 아래에 있습니다. OpenClaw가 사용하는 동일한 XDG 변수를 내보내qmd를 정확히 같은 인덱스에 연결할 수 있습니다:# OpenClaw가 사용하는 동일한 상태 디렉터리 선택 STATE_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}" export XDG_CONFIG_HOME="$STATE_DIR/agents/main/qmd/xdg-config" export XDG_CACHE_HOME="$STATE_DIR/agents/main/qmd/xdg-cache" # (선택) 인덱스 새로고침 + 임베딩 강제 qmd update qmd embed # 워밍업 / 최초 모델 다운로드 트리거 qmd query "test" -c memory-root --json >/dev/null 2>&1
-
설정 범위 (memory.qmd.*)
command(기본값qmd): 실행 파일 경로 오버라이드.searchMode(기본값search):memory_search를 지원하는 QMD 명령어 선택 (search,vsearch,query).includeDefaultMemory(기본값true):MEMORY.md+memory/**/*.md자동 인덱싱.paths[]: 추가 디렉터리/파일 (path, 선택적pattern, 선택적 안정name).sessions: 세션 JSONL 인덱싱 선택 (enabled,retentionDays,exportDir).update: 새로고침 주기와 유지보수 실행 제어: (interval,debounceMs,onBoot,waitForBootSync,embedInterval,commandTimeoutMs,updateTimeoutMs,embedTimeoutMs).limits: 검색 페이로드 제한 (maxResults,maxSnippetChars,maxInjectedChars,timeoutMs).scope:session.sendPolicy와 동일한 스키마. 기본값은 DM 전용 (전체deny, 1:1 채팅allow). 그룹/채널에서 QMD 결과를 표시하려면 완화하세요.match.keyPrefix는 정규화된 세션 키(소문자, 선행agent:<id>:제거)와 매칭합니다. 예:discord:channel:.match.rawKeyPrefix는 원시 세션 키(소문자,agent:<id>:포함)와 매칭합니다. 예:agent:main:discord:.- 레거시:
match.keyPrefix: "agent:..."는 여전히 원시 키 접두사로 처리되지만, 명확성을 위해rawKeyPrefix를 사용하세요.
scope가 검색을 거부하면 OpenClaw가 파생된channel/chatType과 함께 경고를 기록하여 빈 결과를 쉽게 디버깅할 수 있습니다.- 워크스페이스 외부의 스니펫은
memory_search결과에qmd/<collection>/<relative-path>로 표시됩니다.memory_get은 이 접두사를 이해하고 설정된 QMD 컬렉션 루트에서 읽습니다. memory.qmd.sessions.enabled = true이면 OpenClaw가 정제된 세션 트랜스크립트(사용자/어시스턴트 턴)를~/.openclaw/agents/<id>/qmd/sessions/아래의 전용 QMD 컬렉션으로 내보내어 내장 SQLite 인덱스를 건드리지 않고memory_search로 최근 대화를 검색할 수 있습니다.memory_search스니펫은memory.citations가auto/on일 때Source: <path#line>푸터를 포함합니다.memory.citations = "off"로 설정하면 경로 메타데이터를 내부에 유지합니다 (에이전트는memory_get을 위한 경로를 여전히 받지만, 스니펫 텍스트에서 푸터가 생략되고 시스템 프롬프트가 에이전트에게 인용하지 말라고 경고합니다).
예시
memory: {
backend: "qmd",
citations: "auto",
qmd: {
includeDefaultMemory: true,
update: { interval: "5m", debounceMs: 15000 },
limits: { maxResults: 6, timeoutMs: 4000 },
scope: {
default: "deny",
rules: [
{ action: "allow", match: { chatType: "direct" } },
// 정규화된 세션 키 접두사 (`agent:<id>:` 제거).
{ action: "deny", match: { keyPrefix: "discord:channel:" } },
// 원시 세션 키 접두사 (`agent:<id>:` 포함).
{ action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } },
]
},
paths: [
{ name: "docs", path: "~/notes", pattern: "**/*.md" }
]
}
}
인용 & 폴백
memory.citations는 백엔드와 관계없이 적용됩니다 (auto/on/off).qmd가 실행되면 진단에서 어느 엔진이 결과를 제공했는지 확인할 수 있도록status().backend = "qmd"로 태깅합니다. QMD 서브프로세스가 종료되거나 JSON 출력을 파싱할 수 없으면, 검색 관리자가 경고를 기록하고 QMD가 복구될 때까지 내장 프로바이더(기존 마크다운 임베딩)를 반환합니다.
추가 메모리 경로
기본 워크스페이스 구성 외부의 마크다운 파일을 인덱싱하려면 명시적 경로를 추가하세요:
agents: {
defaults: {
memorySearch: {
extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
}
}
}
참고:
- 경로는 절대 경로 또는 워크스페이스 상대 경로 가능.
- 디렉터리는
.md파일을 재귀적으로 스캔. - 기본적으로 마크다운 파일만 인덱싱.
memorySearch.multimodal.enabled = true이면 OpenClaw가extraPaths아래에서만 지원되는 이미지/오디오 파일도 인덱싱합니다. 기본 메모리 루트(MEMORY.md,memory.md,memory/**/*.md)는 마크다운 전용으로 유지.- 심볼릭 링크는 무시됩니다 (파일 또는 디렉터리).
멀티모달 메모리 파일 (Gemini 이미지 + 오디오)
Gemini embedding 2 사용 시 memorySearch.extraPaths에서 이미지와 오디오 파일을 인덱싱할 수 있습니다:
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-2-preview",
extraPaths: ["assets/reference", "voice-notes"],
multimodal: {
enabled: true,
modalities: ["image", "audio"], // 또는 ["all"]
maxFileBytes: 10000000
},
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
참고:
- 멀티모달 메모리는 현재
gemini-embedding-2-preview에서만 지원됩니다. - 멀티모달 인덱싱은
memorySearch.extraPaths를 통해 발견된 파일에만 적용됩니다. - 이 단계에서 지원되는 모달리티: 이미지, 오디오.
- 멀티모달 메모리가 활성화된 동안
memorySearch.fallback은"none"으로 유지해야 합니다. - 매칭되는 이미지/오디오 파일 바이트가 인덱싱 중 설정된 Gemini 임베딩 엔드포인트에 업로드됩니다.
- 지원되는 이미지 확장자:
.jpg,.jpeg,.png,.webp,.gif,.heic,.heif. - 지원되는 오디오 확장자:
.mp3,.wav,.ogg,.opus,.m4a,.aac,.flac. - 검색 쿼리는 텍스트로 유지되지만, Gemini가 텍스트 쿼리를 인덱싱된 이미지/오디오 임베딩과 비교할 수 있습니다.
memory_get은 여전히 마크다운만 읽습니다. 바이너리 파일은 검색 가능하지만 원시 파일 내용으로 반환되지 않습니다.
Gemini 임베딩 (네이티브)
프로바이더를 gemini로 설정하여 Gemini 임베딩 API를 직접 사용합니다:
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-001",
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
참고:
remote.baseUrl은 선택 사항입니다 (기본값은 Gemini API 기본 URL).remote.headers로 필요 시 추가 헤더를 설정할 수 있습니다.- 기본 모델:
gemini-embedding-001. gemini-embedding-2-preview도 지원: 8192 토큰 한계 및 설정 가능한 차원 (768 / 1536 / 3072, 기본값 3072).
Gemini Embedding 2 (프리뷰)
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-2-preview",
outputDimensionality: 3072, // 선택: 768, 1536, 또는 3072 (기본값)
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}
⚠️ 재인덱싱 필요:
gemini-embedding-001(768 차원)에서gemini-embedding-2-preview(3072 차원)로 전환하면 벡터 크기가 변경됩니다. 768, 1536, 3072 간outputDimensionality를 변경할 때도 마찬가지입니다. OpenClaw는 모델 또는 차원 변경을 감지하면 자동으로 재인덱싱합니다.
커스텀 OpenAI 호환 엔드포인트 (OpenRouter, vLLM, 프록시)를 사용하려면
OpenAI 프로바이더로 remote 설정을 사용할 수 있습니다:
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
headers: { "X-Custom-Header": "value" }
}
}
}
}
API 키를 설정하고 싶지 않다면 memorySearch.provider = "local"로 설정하거나
memorySearch.fallback = "none"으로 설정하세요.
폴백:
memorySearch.fallback은openai,gemini,voyage,mistral,ollama,local,none중 선택 가능.- 폴백 프로바이더는 기본 임베딩 프로바이더가 실패할 때만 사용됩니다.
배치 인덱싱 (OpenAI + Gemini + Voyage):
- 기본적으로 비활성. 대규모 코퍼스 인덱싱(OpenAI, Gemini, Voyage)을 위해
agents.defaults.memorySearch.remote.batch.enabled = true로 활성화. - 기본 동작은 배치 완료를 대기합니다. 필요 시
remote.batch.wait,remote.batch.pollIntervalMs,remote.batch.timeoutMinutes를 조정하세요. remote.batch.concurrency로 병렬 제출할 배치 작업 수를 제어합니다 (기본값: 2).- 배치 모드는
memorySearch.provider = "openai"또는"gemini"일 때 적용되며 해당 API 키를 사용합니다. - Gemini 배치 작업은 비동기 임베딩 배치 엔드포인트를 사용하며 Gemini Batch API 가용성이 필요합니다.
OpenAI 배치가 빠르고 저렴한 이유:
- 대규모 백필의 경우, 많은 임베딩 요청을 단일 배치 작업에 제출하고 OpenAI가 비동기적으로 처리하도록 할 수 있어 OpenAI가 일반적으로 가장 빠른 옵션입니다.
- OpenAI는 Batch API 워크로드에 할인 가격을 제공하므로 대규모 인덱싱 실행이 동기 요청보다 저렴한 경우가 많습니다.
- 자세한 내용은 OpenAI Batch API 문서와 가격을 참고하세요:
설정 예시:
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
fallback: "openai",
remote: {
batch: { enabled: true, concurrency: 2 }
},
sync: { watch: true }
}
}
}
도구:
memory_search— 파일 + 줄 범위가 포함된 스니펫 반환.memory_get— 경로로 메모리 파일 내용 읽기.
로컬 모드:
agents.defaults.memorySearch.provider = "local"설정.agents.defaults.memorySearch.local.modelPath(GGUF 또는hf:URI) 제공.- 선택:
agents.defaults.memorySearch.fallback = "none"으로 원격 폴백 방지.
메모리 도구 동작 원리
memory_search는MEMORY.md+memory/**/*.md에서 마크다운 청크(~400 토큰 대상, 80 토큰 오버랩)를 의미 검색합니다. 스니펫 텍스트(최대 ~700자), 파일 경로, 줄 범위, 점수, 프로바이더/모델, 로컬 → 원격 임베딩 폴백 여부를 반환합니다. 전체 파일 페이로드는 반환되지 않습니다.memory_get은 특정 메모리 마크다운 파일(워크스페이스 상대 경로)을 읽으며, 선택적으로 시작 줄과 N줄을 지정합니다.MEMORY.md/memory/외부 경로는 거부됩니다.- 두 도구 모두 에이전트에 대해
memorySearch.enabled가 true로 확인될 때만 활성화됩니다.
인덱싱 대상 (및 시기)
- 파일 유형: 마크다운만 (
MEMORY.md,memory/**/*.md). - 인덱스 저장: 에이전트별 SQLite,
~/.openclaw/memory/<agentId>.sqlite(설정 가능:agents.defaults.memorySearch.store.path,{agentId}토큰 지원). - 최신성:
MEMORY.md+memory/의 워처가 인덱스를 더티로 표시 (디바운스 1.5초). 동기화는 세션 시작 시, 검색 시, 또는 간격에 따라 예약되며 비동기적으로 실행됩니다. 세션 트랜스크립트는 델타 임계값을 사용하여 백그라운드 동기화를 트리거합니다. - 재인덱스 트리거: 인덱스에 임베딩 프로바이더/모델 + 엔드포인트 지문 + 청킹 파라미터가 저장됩니다. 이 중 하나라도 변경되면 OpenClaw가 전체 스토어를 자동으로 초기화하고 재인덱싱합니다.
하이브리드 검색 (BM25 + 벡터)
활성화 시 OpenClaw는 다음을 결합합니다:
- 벡터 유사도 (의미 매칭, 표현이 달라도 가능)
- BM25 키워드 관련도 (ID, 환경 변수, 코드 심볼 같은 정확한 토큰)
플랫폼에서 전문 검색을 사용할 수 없으면 OpenClaw는 벡터 전용 검색으로 폴백합니다.
하이브리드가 필요한 이유
벡터 검색은 “같은 의미”를 찾는 데 뛰어납니다:
- “Mac Studio gateway host” vs “게이트웨이가 실행되는 머신”
- “파일 업데이트 디바운스” vs “매 쓰기마다 인덱싱 방지”
하지만 정확하고 고신호 토큰에는 약합니다:
- ID (
a828e60,b3b9895a…) - 코드 심볼 (
memorySearch.query.hybrid) - 오류 문자열 (“sqlite-vec unavailable”)
BM25(전문 검색)는 그 반대입니다: 정확한 토큰에 강하고, 의역에 약합니다. 하이브리드 검색은 실용적인 중간 지점입니다: 두 가지 검색 신호를 모두 사용하여 “자연어” 쿼리와 “건초 더미에서 바늘 찾기” 쿼리 모두에 좋은 결과를 제공합니다.
결과 병합 방식 (현재 설계)
구현 개요:
- 양쪽에서 후보 풀을 검색:
- 벡터: 코사인 유사도 기준 상위
maxResults * candidateMultiplier. - BM25: FTS5 BM25 순위 기준 상위
maxResults * candidateMultiplier(낮을수록 좋음).
- BM25 순위를 0..1 범위 점수로 변환:
textScore = 1 / (1 + max(0, bm25Rank))
- 청크 ID로 후보를 통합하고 가중 점수 계산:
finalScore = vectorWeight * vectorScore + textWeight * textScore
참고:
vectorWeight+textWeight는 설정 해석 시 1.0으로 정규화되므로 가중치는 백분율처럼 동작합니다.- 임베딩을 사용할 수 없거나 프로바이더가 영벡터를 반환하면 BM25만 실행하고 키워드 매칭 결과를 반환합니다.
- FTS5를 생성할 수 없으면 벡터 전용 검색을 유지합니다 (하드 실패 없음).
이것은 “IR 이론적 완벽함”은 아니지만, 단순하고 빠르며 실제 메모에서 재현율/정밀도를 개선하는 경향이 있습니다. 나중에 더 정교하게 만들고 싶다면 일반적인 다음 단계로 상호 순위 융합(RRF)이나 점수 정규화(최소/최대 또는 z-점수)를 혼합 전에 적용하는 것이 있습니다.
후처리 파이프라인
벡터와 키워드 점수를 병합한 후, 두 가지 선택적 후처리 단계가 에이전트에 전달되기 전에 결과 목록을 정제합니다:
Vector + Keyword → Weighted Merge → Temporal Decay → Sort → MMR → Top-K Results
두 단계 모두 기본적으로 비활성이며 독립적으로 활성화할 수 있습니다.
MMR 리랭킹 (다양성)
하이브리드 검색이 결과를 반환할 때, 여러 청크에 유사하거나 겹치는 내용이 포함될 수 있습니다. 예를 들어 “홈 네트워크 설정”을 검색하면 같은 라우터 설정을 언급하는 거의 동일한 스니펫 다섯 개가 다른 일별 메모에서 반환될 수 있습니다.
**MMR(최대 한계 관련도)**은 관련도와 다양성의 균형을 맞추기 위해 결과를 리랭킹하여, 상위 결과가 같은 정보를 반복하는 대신 쿼리의 다양한 측면을 커버하도록 합니다.
동작 원리:
- 원래 관련도(벡터 + BM25 가중 점수)로 결과를 점수화합니다.
- MMR이 반복적으로 다음을 최대화하는 결과를 선택합니다:
λ × relevance − (1−λ) × max_similarity_to_selected. - 결과 간 유사도는 토큰화된 내용에 대한 Jaccard 텍스트 유사도로 측정됩니다.
lambda 파라미터로 트레이드오프를 제어합니다:
lambda = 1.0→ 순수 관련도 (다양성 페널티 없음)lambda = 0.0→ 최대 다양성 (관련도 무시)- 기본값:
0.7(균형, 약간의 관련도 편향)
예시 — 쿼리: “home network setup”
다음 메모리 파일이 있다고 가정:
memory/2026-02-10.md → "Configured Omada router, set VLAN 10 for IoT devices"
memory/2026-02-08.md → "Configured Omada router, moved IoT to VLAN 10"
memory/2026-02-05.md → "Set up AdGuard DNS on 192.168.10.2"
memory/network.md → "Router: Omada ER605, AdGuard: 192.168.10.2, VLAN 10: IoT"
MMR 없이 — 상위 3개 결과:
1. memory/2026-02-10.md (score: 0.92) ← 라우터 + VLAN
2. memory/2026-02-08.md (score: 0.89) ← 라우터 + VLAN (거의 중복!)
3. memory/network.md (score: 0.85) ← 참조 문서
MMR 적용 시 (λ=0.7) — 상위 3개 결과:
1. memory/2026-02-10.md (score: 0.92) ← 라우터 + VLAN
2. memory/network.md (score: 0.85) ← 참조 문서 (다양!)
3. memory/2026-02-05.md (score: 0.78) ← AdGuard DNS (다양!)
2월 8일의 거의 중복된 결과가 제외되고, 에이전트가 세 가지 서로 다른 정보를 받습니다.
활성화 시기: memory_search가 중복되거나 거의 동일한 스니펫을 반환하는 것을 발견한 경우,
특히 일별 메모가 날짜에 걸쳐 비슷한 정보를 자주 반복하는 경우에 유용합니다.
시간 감쇠 (최신성 부스트)
일별 메모를 쌓아가는 에이전트는 시간이 지남에 따라 수백 개의 날짜별 파일이 축적됩니다. 감쇠 없이는 6개월 전에 잘 작성된 메모가 같은 주제에 대한 어제의 업데이트보다 높은 순위를 차지할 수 있습니다.
시간 감쇠는 각 결과의 연령에 따라 점수에 지수 곱셈기를 적용하여 최근 메모리가 자연스럽게 높은 순위를 차지하고 오래된 것은 사라집니다:
decayedScore = score × e^(-λ × ageInDays)
여기서 λ = ln(2) / halfLifeDays.
기본 반감기 30일 기준:
- 오늘의 메모: 원래 점수의 100%
- 7일 전: ~84%
- 30일 전: 50%
- 90일 전: 12.5%
- 180일 전: ~1.6%
상시 파일은 절대 감쇠되지 않습니다:
MEMORY.md(루트 메모리 파일)memory/내의 날짜가 아닌 파일 (예:memory/projects.md,memory/network.md)- 이들은 항상 정상적으로 순위가 매겨져야 하는 영구 참조 정보를 담고 있습니다.
날짜별 일일 파일 (memory/YYYY-MM-DD.md)은 파일명에서 추출한 날짜를 사용합니다.
다른 소스(예: 세션 트랜스크립트)는 파일 수정 시간(mtime)으로 폴백합니다.
예시 — 쿼리: “what’s Rod’s work schedule?”
다음 메모리 파일이 있다고 가정 (오늘은 2월 10일):
memory/2025-09-15.md → "Rod works Mon-Fri, standup at 10am, pairing at 2pm" (148일 전)
memory/2026-02-10.md → "Rod has standup at 14:15, 1:1 with Zeb at 14:45" (오늘)
memory/2026-02-03.md → "Rod started new team, standup moved to 14:15" (7일 전)
감쇠 없이:
1. memory/2025-09-15.md (score: 0.91) ← 가장 좋은 의미 매칭, 하지만 오래됨!
2. memory/2026-02-10.md (score: 0.82)
3. memory/2026-02-03.md (score: 0.80)
감쇠 적용 시 (halfLife=30):
1. memory/2026-02-10.md (score: 0.82 × 1.00 = 0.82) ← 오늘, 감쇠 없음
2. memory/2026-02-03.md (score: 0.80 × 0.85 = 0.68) ← 7일, 약간의 감쇠
3. memory/2025-09-15.md (score: 0.91 × 0.03 = 0.03) ← 148일, 거의 사라짐
원시 의미 매칭이 가장 좋았음에도 오래된 9월 메모가 맨 아래로 밀립니다.
활성화 시기: 에이전트에 수개월치 일별 메모가 있고 오래되고 부실한 정보가 최근 컨텍스트보다 높은 순위를 차지하는 경우. 일별 메모 중심 워크플로에는 반감기 30일이 적합합니다. 오래된 메모를 자주 참조하는 경우 반감기를 늘리세요 (예: 90일).
설정
두 기능 모두 memorySearch.query.hybrid 아래에서 설정합니다:
agents: {
defaults: {
memorySearch: {
query: {
hybrid: {
enabled: true,
vectorWeight: 0.7,
textWeight: 0.3,
candidateMultiplier: 4,
// 다양성: 중복 결과 줄이기
mmr: {
enabled: true, // 기본값: false
lambda: 0.7 // 0 = 최대 다양성, 1 = 최대 관련도
},
// 최신성: 새로운 메모리 부스트
temporalDecay: {
enabled: true, // 기본값: false
halfLifeDays: 30 // 점수가 30일마다 반감
}
}
}
}
}
}
각 기능을 독립적으로 활성화할 수 있습니다:
- MMR만 — 유사한 메모가 많지만 연령이 중요하지 않을 때 유용.
- 시간 감쇠만 — 최신성이 중요하지만 결과가 이미 충분히 다양할 때 유용.
- 둘 다 — 크고 오랜 일별 메모 히스토리를 가진 에이전트에 권장.
임베딩 캐시
OpenClaw는 청크 임베딩을 SQLite에 캐시하여 재인덱싱과 빈번한 업데이트(특히 세션 트랜스크립트)가 변경되지 않은 텍스트를 다시 임베딩하지 않도록 합니다.
설정:
agents: {
defaults: {
memorySearch: {
cache: {
enabled: true,
maxEntries: 50000
}
}
}
}
세션 메모리 검색 (실험적)
선택적으로 세션 트랜스크립트를 인덱싱하고 memory_search로 표면화할 수 있습니다.
실험적 플래그로 제어됩니다.
agents: {
defaults: {
memorySearch: {
experimental: { sessionMemory: true },
sources: ["memory", "sessions"]
}
}
}
참고:
- 세션 인덱싱은 옵트인 (기본 비활성).
- 세션 업데이트는 디바운스되고 델타 임계값을 넘으면 비동기적으로 인덱싱됩니다 (최선의 노력).
memory_search는 인덱싱을 차단하지 않습니다. 백그라운드 동기화가 완료될 때까지 결과가 약간 오래될 수 있습니다.- 결과에는 여전히 스니펫만 포함됩니다.
memory_get은 메모리 파일로 제한됩니다. - 세션 인덱싱은 에이전트별로 격리됩니다 (해당 에이전트의 세션 로그만 인덱싱).
- 세션 로그는 디스크에 있습니다 (
~/.openclaw/agents/<agentId>/sessions/*.jsonl). 파일시스템 접근 권한이 있는 모든 프로세스/사용자가 읽을 수 있으므로 디스크 접근을 신뢰 경계로 취급하세요. 더 엄격한 격리를 위해 에이전트를 별도 OS 사용자나 호스트에서 실행하세요.
델타 임계값 (기본값 표시):
agents: {
defaults: {
memorySearch: {
sync: {
sessions: {
deltaBytes: 100000, // ~100 KB
deltaMessages: 50 // JSONL 라인
}
}
}
}
}
SQLite 벡터 가속 (sqlite-vec)
sqlite-vec 확장이 사용 가능하면 OpenClaw가 임베딩을 SQLite 가상 테이블(vec0)에 저장하고 데이터베이스 내에서 벡터 거리 쿼리를 수행합니다. 모든 임베딩을 JS에 로드하지 않고도 검색을 빠르게 유지합니다.
설정 (선택):
agents: {
defaults: {
memorySearch: {
store: {
vector: {
enabled: true,
extensionPath: "/path/to/sqlite-vec"
}
}
}
}
}
참고:
enabled기본값은 true. 비활성화하면 저장된 임베딩에 대한 인프로세스 코사인 유사도로 폴백합니다.- sqlite-vec 확장이 없거나 로드에 실패하면 OpenClaw가 오류를 기록하고 JS 폴백(벡터 테이블 없음)으로 계속합니다.
extensionPath는 번들된 sqlite-vec 경로를 오버라이드합니다 (커스텀 빌드나 비표준 설치 위치에 유용).
로컬 임베딩 자동 다운로드
- 기본 로컬 임베딩 모델:
hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf(~0.6 GB). memorySearch.provider = "local"일 때node-llama-cpp가modelPath를 확인합니다. GGUF가 없으면 캐시(또는local.modelCacheDir이 설정된 경우 해당 경로)로 자동 다운로드한 후 로드합니다. 다운로드는 재시도 시 이어받기가 가능합니다.- 네이티브 빌드 요구사항:
pnpm approve-builds를 실행하고node-llama-cpp를 선택한 후pnpm rebuild node-llama-cpp를 실행하세요. - 폴백: 로컬 설정이 실패하고
memorySearch.fallback = "openai"이면 원격 임베딩(openai/text-embedding-3-small, 별도 오버라이드가 없는 경우)으로 자동 전환하고 이유를 기록합니다.
커스텀 OpenAI 호환 엔드포인트 예시
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_REMOTE_API_KEY",
headers: {
"X-Organization": "org-id",
"X-Project": "project-id"
}
}
}
}
}
참고:
remote.*가models.providers.openai.*보다 우선합니다.remote.headers가 OpenAI 헤더와 병합됩니다. 키 충돌 시 remote가 우선. OpenAI 기본값을 사용하려면remote.headers를 생략하세요.