Skip to content

13 AI记忆框架原理与实现(搜索、读取、回退、注入)

这篇回答"记忆系统怎么做才不会拖垮主对话"。

核心源码入口

  • src/memory/backend-config.ts
  • src/memory/search-manager.ts
  • src/memory/manager.ts
  • src/memory/manager-embedding-ops.ts
  • src/memory/qmd-manager.ts
  • src/agents/tools/memory-tool.ts
  • src/gateway/server-startup-memory.ts

总体结构

OpenClaw 记忆系统 = 配置解析 + 管理器选择 + 工具暴露 + 启动预热 + (可选)Hook注入

模块一 配置解析状态机

resolveMemoryBackendConfig(...) 先决定 backend:

  1. memory.backend != qmd:直接 builtin
  2. memory.backend = qmd:解析 qmd 配置

关键解析项:

  1. searchMode
  2. collections/paths
  3. update(interval、debounce、commandTimeout、updateTimeout)
  4. limits(尤其 maxInjectedChars
  5. citations

qmd limits 完整类型:

ts
type QmdLimits = {
  maxResults:       number;   // 最多返回条数
  maxSnippetChars:  number;   // 单条结果最大字符
  maxInjectedChars: number;   // 注入上下文的总字符上限(默认 4000)
  timeoutMs:        number;   // 搜索超时
};

maxInjectedChars 默认 4000,防记忆挤爆上下文。

模块二 管理器选择与回退

getMemorySearchManager 流程

backend=qmd?
  ├─ 查 QMD_MANAGER_CACHE(key = buildQmdCacheKey(agentId, qmd))
  │    命中 → 直接返回缓存实例
  │    未命中 → QmdMemoryManager.create(...)
  │              成功 → 写缓存 → 包一层 FallbackMemoryManager
  │              失败 → warn → 动态 import MemoryIndexManager(builtin)
  └─ backend!=qmd
       └─ MemoryIndexManager.get(...)

builtin 也失败时:return { manager: null, error }不 throw,主流程继续。

FallbackMemoryManager 完整状态

ts
class FallbackMemoryManager {
  primaryFailed: boolean = false;   // qmd 是否已失效
  fallback:      MemorySearchManager | null = null;  // 懒加载的 builtin
  lastError?:    string;            // 最后一次 primary 失败原因
  cacheEvicted:  boolean = false;   // 缓存是否已被驱逐
}

状态转换:

  1. 默认 primaryFailed=false,所有请求走 qmd。
  2. qmd search 抛错 → primaryFailed=true → 关闭 qmd → 记录 lastError
  3. ensureFallback() 懒加载 builtin manager(不是启动时就加载)。
  4. 后续所有请求走 fallback。
  5. close() 时:关闭 primary + fallback → 调 evictCacheEntry()

evictCacheEntry(缓存主动失效)

ts
// FallbackMemoryManager 内部
private evictCacheEntry() {
  QMD_MANAGER_CACHE.delete(this.cacheKey);  // 移除缓存
  this.cacheEvicted = true;
}

qmd 失败后主动从缓存删除,下次 getMemorySearchManager 调用时会重新尝试创建 qmd, 而不是永远返回已失效的缓存实例。这是"在线自愈"的关键设计。

缓存键:buildQmdCacheKey(agentId, qmd) — 绑定 agentId + qmd 配置内容。

模块三 MemorySearchManager 接口

ts
interface MemorySearchManager {
  search:   (query: string, opts?: SearchOpts) => Promise<MemoryResult[]>;
  readFile: (opts: { relPath: string; from?: number; lines?: number }) => Promise<string>;
  status:   () => MemoryStatus;

  // 可选方法(并非所有 backend 都实现)
  sync?:                      () => Promise<void>;
  probeEmbeddingAvailability: () => Promise<boolean>;
  probeVectorAvailability:    () => Promise<boolean>;
  close?:                     () => Promise<void>;
}

probeVectorAvailability 用于检测向量搜索后端是否可用(启动预热和健康检查用)。

模块四 工具层实现细节

memory_search(源码 description)

"Mandatory recall step: semantically search MEMORY.md + memory/*.md..."

"Mandatory recall step" 是刻意的措辞,强迫模型在回答前必须先做记忆召回。

createMemorySearchTool(...) 流程:

  1. getMemorySearchManager(...)
  2. scope guardQmdMemoryManager.search() 内部先检查 isScopeAllowed(sessionKey), 不在允许范围内的 session 直接返回 []
  3. 等待待处理更新waitForPendingUpdateBeforeSearch() — 确保最新写入的记忆被索引后再搜
  4. manager.search(query, {maxResults, minScore, sessionKey})
  5. shouldIncludeCitations(...) 决定是否加引用
  6. decorateCitations(...) 格式化来源路径和行号
  7. qmd 模式执行 clampResultsByInjectedChars(..., limits.maxInjectedChars)

返回值结构:

ts
{
  results:   MemoryResult[];   // 搜索结果列表
  provider:  string;           // 使用的 provider(qmd 时为 "qmd")
  model:     string;           // 使用的 model
  fallback:  boolean;          // 是否使用了 fallback backend
  citations: boolean;          // 本次是否附带引用
}

memory_get(源码 description)

"Safe snippet read from MEMORY.md or memory/*.md with optional from/lines;
 use after memory_search to pull only the needed lines and keep context small."

createMemoryGetTool(...) 流程:

  1. manager.readFile({ relPath, from, lines })
  2. 只返回指定片段,避免把整文件灌入上下文

工具名称设计暗示了使用顺序:memory_search 先定位,memory_get 再按需读取。

模块五 引用策略(citations)

shouldIncludeCitations(...) 行为:

  1. on:总是显示
  2. off:总是不显示
  3. auto:direct chat 显示,group/channel 默认不显示

这样能在私聊保证可追溯,在群聊避免噪音。

模块六 启动预热

startGatewayMemoryBackend(...) 在网关启动时执行:

  1. 非 qmd 直接跳过
  2. qmd 时提前调用 getMemorySearchManager(...) + probeVectorAvailability()
  3. 成功打 info,失败打 warn

目的:把故障前置到启动阶段,不等用户首问才炸。

模块七 向量索引构建与更新(embedding ops)

manager-embedding-ops.ts 负责"把文本变成可检索向量"的工程细节:

  1. 分块:chunkMarkdown(...) 把文档切成 chunk
  2. 批处理:buildEmbeddingBatches(...) 按 token/字节预算分批
  3. 缓存:embedding_cache 命中则跳过重复 embedding
  4. 回写:chunks/chunks_vec/chunks_fts 三类表同步更新
  5. 重试与超时:远程/本地模型用不同超时阈值,失败走有限次重试

这层决定"索引更新是否快、成本是否可控、失败能否自愈"。

模块八 与智能体链路结合

记忆进入主回答有两条路:

  1. 工具路:模型主动调用 memory_search/memory_get
  2. 注入路:插件在 before_agent_start 生成 prependContext

建议优先做"工具路",更稳定、可控、可调试。

最小复刻骨架

ts
async function resolveMemoryManager(cfg: Config, agentId: string) {
  const resolved = resolveMemoryBackendConfig({ cfg, agentId });
  if (resolved.backend === "qmd") {
    const cacheKey = buildQmdCacheKey(agentId, resolved.qmd);
    const cached = QMD_MANAGER_CACHE.get(cacheKey);
    if (cached) return cached;
    try {
      const primary = await QmdMemoryManager.create(resolved);
      const fallback = new FallbackMemoryManager(primary, cacheKey, () => createBuiltinManager(cfg, agentId));
      QMD_MANAGER_CACHE.set(cacheKey, fallback);
      return fallback;
    } catch {
      return await createBuiltinManager(cfg, agentId);
    }
  }
  return await createBuiltinManager(cfg, agentId);
}

async function memorySearch(query: string, cfg: Config, agentId: string, sessionKey?: string) {
  const manager = await resolveMemoryManager(cfg, agentId);
  // scope guard:由 manager.search 内部处理
  const raw = await manager.search(query, { maxResults: 5, sessionKey });
  const withCitations = decorateCitations(raw, shouldIncludeCitations(cfg));
  return clampResultsByInjectedChars(withCitations, 4000);
}

自检清单

  1. qmd 挂掉后是否能自动切 builtin(primaryFailed=trueensureFallback())。
  2. evictCacheEntry 是否在 primary 失败时调用,下次能重试创建 qmd。
  3. manager 不可用时是否返回 disabled,而不是炸主流程。
  4. maxInjectedChars 是否生效(clampResultsByInjectedChars)。
  5. 引用模式在 direct/group 是否符合预期。
  6. waitForPendingUpdateBeforeSearch 是否在 search 前等待最新写入。
  7. isScopeAllowed scope guard 是否过滤掉不在范围的 session。
  8. 启动阶段是否能提前暴露 qmd 配置错误(probeVectorAvailability)。

用工程视角拆解 AI 智能体框架