Skip to content

19 AI记忆系统状态机实现实战(memory_search/memory_get)

这篇讲"记忆系统怎么落地",目标是你能从 0 到 1 做出同类能力。

先看源码入口

  • 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 的记忆系统是"两层管理器 + 一层工具":

  1. 配置解析层 resolveMemoryBackendConfig(...) 把配置解析成运行时结构(backend/citations/qmd)。

  2. 管理器层 getMemorySearchManager(...) 先尝试 QmdMemoryManager,失败时自动切到 MemoryIndexManager

  3. 工具层 createMemorySearchTool(...)createMemoryGetTool(...) 提供给智能体调用。

二、状态机拆解

1) Backend 选择状态机

builtin / qmd 两条路:

  1. 读取 memory.backend
  2. 不是 qmd 就直接 builtin
  3. qmd 则继续解析 command/collections/update/limits
  4. 解析失败不崩,回落 builtin

关键函数:resolveMemoryBackendConfig(...)src/memory/backend-config.ts

2) Manager 获取状态机

getMemorySearchManager(...) 的行为:

  1. backend=qmd

    • 先查 QMD_MANAGER_CACHE(key = buildQmdCacheKey(agentId, qmd))。
    • 未命中则 QmdMemoryManager.create(...)
    • 成功则写入缓存并包一层 FallbackMemoryManager
  2. qmd 初始化失败:

    • 记录 warn。
    • 动态 import MemoryIndexManager.get(...) 兜底。
  3. 兜底也失败:

    • 返回 { manager: null, error },工具层返回 disabled,而不是 throw 崩主流程。

关键函数:getMemorySearchManager(...)src/memory/search-manager.ts

3) Fallback 状态机(最关键,含 cacheEvicted)

FallbackMemoryManager 内部完整核心状态:

ts
class FallbackMemoryManager {
  primaryFailed: boolean = false;
  fallback:      MemorySearchManager | null = null;
  lastError?:    string;
  cacheEvicted:  boolean = false;   // 缓存是否已被主动驱逐
}

状态转换:

  1. 默认 primaryFailed=false,先走 qmd。
  2. qmd search 抛错 → primaryFailed=true → 关闭 primary → 记录 lastError
  3. ensureFallback() 懒加载 builtin manager。
  4. 后续请求都走 fallback。
  5. close() 时回收并调用 evictCacheEntry()

evictCacheEntry() 的关键作用:

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

qmd 失败后主动从缓存删除当前条目, 下次 getMemorySearchManager 时会重新尝试创建 qmd, 而不是永远复用已失效的实例。这是"在线自愈"的核心机制。

三、工具调用链(智能体实际怎么用)

memory_search(源码 description 精确语义)

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

"Mandatory recall step" 是设计意图:强迫模型在回答前先做记忆召回,而不是跳过。

createMemorySearchTool(...) 真实流程:

  1. sessionKey 解析 agentId。
  2. getMemorySearchManager(...) 拿 manager。
  3. scope guard(在 qmd manager 内部):
    ts
    if (!this.isScopeAllowed(opts?.sessionKey)) return [];
    scope 不匹配的 session 搜索结果为空,但不报错。
  4. 等待待处理更新waitForPendingUpdateBeforeSearch() 确保最近写入被索引。
  5. manager.search(query, { maxResults, minScore, sessionKey })
  6. 按 citations 配置做 decorateCitations(...)
  7. qmd 模式下做 clampResultsByInjectedChars(..., limits.maxInjectedChars)

返回值完整结构:

ts
{
  results:   MemoryResult[];   // 搜索结果列表
  provider:  string;           // "qmd" | "builtin"
  model:     string;           // qmd 时固定 "qmd"
  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."

"Safe snippet read" + "use after memory_search" — 工具名暗示了使用顺序: 先搜索定位,再精确读取片段,控制 token 成本。

createMemoryGetTool(...) 真实流程:

  1. 同样先拿 manager。
  2. manager.readFile({ relPath, from, lines })
  3. 返回精确片段,控制 token 成本。

qmd status() 返回形状

qmd backend 的 status() 固定返回:

ts
{
  backend:  "qmd",
  provider: "qmd",
  model:    "qmd",
  files:    totalDocuments,          // 文档总数
  vector:   { enabled: true, available: true },
}

四、启动预热机制

网关启动时会执行 startGatewayMemoryBackend(...)

  1. 只在 backend=qmd 时触发。
  2. 提前 getMemorySearchManager(...)
  3. 调用 probeVectorAvailability() 探测向量后端是否就绪。
  4. 成功打 info,失败打 warn。

作用:把故障提前暴露在启动阶段,而不是第一次用户问答时炸。

五、索引构建状态机(embedding ops)

manager-embedding-ops.ts 负责"数据如何进索引":

  1. 文本分块:chunkMarkdown(...)
  2. 批量 embedding:buildEmbeddingBatches(...) 按预算切批
  3. 缓存命中:loadEmbeddingCache(...) 减少重复计算
  4. 批次失败重试:有限次数 + 退避延时
  5. 落库:chunks/chunks_vec/chunks_fts 一起更新,保持检索一致性

如果你要从 0 到 1 复刻,不能只做 search,还要把"更新状态机"做完整。

六、可复刻最小实现(含 cacheEvicted 和 scope guard)

ts
type Manager = {
  search:   (q: string, opts?: { sessionKey?: string }) => Promise<Result[]>;
  readFile: (p: { relPath: string; from?: number; lines?: number }) => Promise<string>;
  status:   () => ManagerStatus;
  probeVectorAvailability: () => Promise<boolean>;
  close?:   () => Promise<void>;
};

class FallbackManager implements Manager {
  private failed   = false;
  private fb:      Manager | null = null;
  private evicted  = false;

  constructor(
    private primary:        Manager,
    private cacheKey:       string,
    private createFallback: () => Promise<Manager | null>,
    private cache:          Map<string, Manager>,
  ) {}

  async search(q: string, opts?: { sessionKey?: string }) {
    if (!this.failed) {
      try {
        return await this.primary.search(q, opts);
      } catch {
        this.failed = true;
        await this.primary.close?.();
        this.evictCacheEntry();
      }
    }
    this.fb ??= await this.createFallback();
    if (!this.fb) throw new Error("memory unavailable");
    return this.fb.search(q, opts);
  }

  async readFile(p: Parameters<Manager["readFile"]>[0]) {
    if (!this.failed) return this.primary.readFile(p);
    this.fb ??= await this.createFallback();
    if (!this.fb) throw new Error("memory unavailable");
    return this.fb.readFile(p);
  }

  status() {
    return this.failed ? (this.fb?.status() ?? { backend: "unavailable" }) : this.primary.status();
  }

  async probeVectorAvailability() {
    return this.primary.probeVectorAvailability();
  }

  async close() {
    await this.primary.close?.();
    await this.fb?.close?.();
    this.evictCacheEntry();
  }

  private evictCacheEntry() {
    if (!this.evicted) {
      this.cache.delete(this.cacheKey);
      this.evicted = true;
    }
  }
}

七、上线验收(必须过)

  1. qmd 挂掉后,下一次查询可自动切 builtin(primaryFailed=true)。
  2. evictCacheEntry 在 primary 失败后调用,下次能重试创建 qmd。
  3. scope 不匹配的 session 搜索返回 [],不报错。
  4. memory_search 出错时返回 disabled/error,不导致整个 agent 失败。
  5. 大量返回结果会被 maxInjectedChars 上限裁剪。
  6. 启动时 probeVectorAvailability 能提前暴露向量后端故障。
  7. waitForPendingUpdateBeforeSearch 确保刚写入的记忆能被搜到。

八、最常见坑

  1. 把 memory 出错当致命异常,导致主对话中断。
  2. 不做注入长度限制,memory 反过来挤爆上下文。
  3. 无 fallback,依赖单一后端。
  4. 启动不预热,问题在生产流量下才暴露。
  5. 缓存失效不清除,qmd 重连后拿到的仍是旧的失效实例(evictCacheEntry 漏掉)。
  6. scope guard 缺失,跨 session 记忆泄漏。

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