본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 27. 15:06

【#9】 OpenClaw 해독하기 — 기억 슬롯의 제한, 이중 기억을 방지하기 위한 메커니즘

요약

OpenClaw의 장기 메모리(Long-term Memory) 구현 메커니즘을 분석합니다. 여러 메모리 백엔드가 충돌하지 않도록 하나의 플러그인만 활성화하는 '배타적 슬롯(Exclusive Slot)' 개념과 MemorySearchManager 인터페이스를 통한 Capability 기반 설계를 설명합니다.

핵심 포인트

  • 메모리 데이터 이중화를 방지하기 위해 단 하나의 메모리 플러그인만 활성화하는 배타적 슬롯 구조를 채택함
  • MemorySearchManager 인터페이스를 통해 백엔드 구현(Markdown, LanceDB 등)에 상관없이 일관된 검색 및 읽기 기능 제공
  • Capability 주도 설계 방식을 통해 코어 시스템이 메모리 구현의 세부 사항을 몰라도 동작할 수 있도록 설계됨
  • memory-core와 memory-lancedb는 슬롯 소유권을 가질 수 있으며, wiki와 active-memory는 이를 보완하는 역할을 수행함

본 기사의 코드는 OpenClaw main 브랜치의 cee2aca409 (version 2026.6.10) 시점을 참조합니다. 행 번호는 업데이트에 따라 어긋날 수 있습니다.

연재 「OpenClaw 해독하기」

#08 세션은 '하나의 대화'에 대한 기억이었습니다. 이번에는 대화를 넘어 지속되는 **장기 메모리 (Long-term Memory)**입니다. OpenClaw의 메모리는 여러 플러그인으로 구현되지만, '동시에 활성화할 수 있는 것은 하나뿐'이라는 독특한 배타적 슬롯 (Exclusive Slot)을 가집니다. src/memory/, packages/memory-host-sdk, 그리고 extensions/memory-*를 해독해 보겠습니다.

VISION.md:84가 선언합니다.

Memory is a special plugin slot where only one memory plugin can be active at a time.

여러 메모리 백엔드 (Backend)가 동시에 실행되면 기억의 소재가 이중화되어 파탄에 이릅니다. 그래서 배타적 슬롯을 사용합니다. 설정에서는 plugins.slots.memory (src/config/types.plugins.ts:43)를 통해 '어떤 플러그인이 메모리 슬롯을 소유할지'를 선택합니다 ("none"으로 무효화 가능).

강제 적용은 src/plugins/memory-runtime.ts:11resolveMemoryRuntimePluginIds()입니다.

function resolveMemoryRuntimePluginIds(config): string[] {
const plugins = normalizePluginsConfig(config.plugins);
const memorySlot = plugins.slots.memory;
...

반환값은 최대 요소가 1개인 배열입니다. createPluginRegistry (#04)의 registerMemoryCapability (src/plugins/types.ts:2882)가 이 배타적 capability를 등록합니다.

메모리 플러그인이 구현해야 할 계약 (Contract)은 MemorySearchManager (packages/memory-host-sdk/src/host/types.ts:144)입니다.

export interface MemorySearchManager {
search(query: string, opts?: {
maxResults?: number; minScore?: number; sessionKey?: string;
...

search (검색), readFile (읽기), status (상태)가 필수입니다. sync (동기화)나 임베딩 (Embedding) 가용성 프로브 (Probe)는 옵션입니다. 코어 (Core)는 이 계약만을 알고 있으며, 배후가 markdown 파일인지 LanceDB인지 묻지 않습니다. #04의 'capability 주도' 방식이 메모리에서도 관철되고 있습니다. capability 자체는 MemoryPluginCapability (src/plugins/memory-state.ts:145)로, promptBuilder / runtime (MemorySearchManager를 감싸는 것) / flushPlanResolver / publicArtifacts를 가집니다.

슬롯을 소유할 수 있는 것은 memory-core 또는 memory-lancedb입니다. wiki와 active-memory는 이를 보완하는 비배타적 플러그인입니다.

플러그인역할특징
memory-core내장 파일 기반 장기 메모리 (Long-term memory)MEMORY.md + memory/*.md, 세션 인덱스, FTS(Full-Text Search) + 벡터 하이브리드 검색 (QMD), "dreaming" (light/REM/deep) 단계
memory-lancedb벡터 특화 배타적 메모리LanceDB 벡터 스토어, OpenAI/Anthropic 임베딩 (Embedding), 매 메시지 자동 recall, 자동 capture, 카테고리별 중요도 점수
memory-wiki영구 지식 Vault (별도 슬롯)Obsidian 호환 Markdown, 결정론적 인덱스 및 백링크 (Backlink), 구조화된 클레임 (Claim) + 에비던스 (Evidence), wiki_search / wiki_get
active-memory응답 전 블로킹 recall (비배타적)응답 전 서브 에이전트 (Sub-agent)를 실행하여 제한적인 메모리 문맥 (Context) 주입

extensions/memory-wiki/README.md는 그 입지를 명확히 밝히고 있습니다. "This plugin is separate from the active memory plugin. The active memory plugin still handles recall, promotion, and dreaming." 즉, wiki는 슬롯을 차지하지 않으며, active 메모리 플러그인을 보완하는 supplement입니다.

메모리는 도구 (Tool)로서 에이전트에게 공개됩니다. 슬롯에 무엇이 담겨 있느냐에 따라 주입되는 도구가 달라집니다.

memory-core: memory_search / memory_get

memory-lancedb: memory_recall / memory_store (배타적)

memory-wiki: wiki_search / wiki_get (비배타적 추가)

active-memory: 블로킹 서브 에이전트가 사용할 도구 설정 (기본값은 memory_search + memory_get, lancedb 사용 시 memory_recall)

memory_search의 description은 운영 철학을 잘 보여줍니다 (extensions/memory-core/src/tools.ts:393).

return createMemoryTool({
name: "memory_search",
description: "Mandatory recall step: semantically search MEMORY.md + memory/*.md ... before answering questions about prior work, decisions, dates, people, preferences, or todos. Optional `corpus=wiki` or `corpus=all` ...",
...

"과거의 작업, 결정, 날짜, 인물, 선호도, TODO를 답하기 전의 필수 recall 단계". corpus 파라미터를 통해 wiki나 세션도 횡단 검색할 수 있습니다. active-memory는 이 도구를 "응답 전 반드시 실행되는 서브 에이전트"로 감싸서 문맥을 미리 읽도록 만드는 장치입니다 (extensions/active-memory/index.ts:61).

루트 AGENTS.md의 메모리 주의사항도 실무적입니다. "Memory wiki prompt digest stays tiny; prefer wiki_search / wiki_get; verify contact data before use; source-class provenance for generated people facts." 즉, wiki의 프롬프트 다이제스트(Prompt digest)는 작게 유지하고, wiki_search / wiki_get 사용을 권장하며, 사용 전 연락처 데이터를 검증하고, 생성된 인물 사실에 대해서는 출처(Provenance)를 명시하라는 것입니다.

벡터 검색에는 임베딩 (Embedding)이 필요합니다. OpenClaw는 두 방식 모두 지원합니다.

로컬 임베딩 (packages/memory-host-sdk/src/host/embeddings.ts:69

): node-llama-cpp + GGUF 모델. 워커 스레드 또는 인프로세스에서 지연 로딩. local.modelPath 등으로 설정합니다. -
원격 임베딩: memory-lancedb가 OpenAI / Anthropic을 사용합니다. getMemoryEmbeddingProvider(providerId, cfg)로 프로바이더를 해결합니다.

가용성은 probeEmbeddingAvailability()로 비동기적으로 확인하며, 사용할 수 없으면 도구는 disabled: true를 반환하고, 프로브 성공 후에 벡터 검색을 재개합니다. LanceDB의 recall은 다음과 같습니다(extensions/memory-lancedb/index.ts:1537)。

let vector: number[];
try {
vector = await embeddings.embed(normalizeRecallQuery(query, currentCfg.recallMaxChars),
...

쿼리를 임베딩 벡터로 변환하고 LanceDB에서 근접 검색을 수행합니다. 임베딩이 실패하더라도 MemoryRecallEmbeddingError로 명시적으로 처리됩니다.

메모리 검색은 외부 의존성(임베딩 프로바이더・벡터 스토어)을 포함하기 때문에 데드라인이 엄격합니다(extensions/memory-core/src/tools.ts:126)。

async function runMemorySearchToolWithDeadline<T>(params): Promise<...> {
const controller = new AbortController();
const timeoutPromise = new Promise((resolve) => {
...

Promise.race로 타임아웃과 작업을 경쟁시키고, 초과하면 AbortController로 중단합니다. 메모리 검색이 느려서 전체 응답이 멈추는 사고를 방지합니다. active-memory는 나아가

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0