19 AI记忆系统状态机实现实战(memory_search/memory_get)
这篇讲"记忆系统怎么落地",目标是你能从 0 到 1 做出同类能力。
先看源码入口
src/memory/backend-config.tssrc/memory/search-manager.tssrc/memory/manager.tssrc/memory/manager-embedding-ops.tssrc/memory/qmd-manager.tssrc/agents/tools/memory-tool.tssrc/gateway/server-startup-memory.ts
一、系统结构(真实实现,不是概念图)
OpenClaw 的记忆系统是"两层管理器 + 一层工具":
配置解析层
resolveMemoryBackendConfig(...)把配置解析成运行时结构(backend/citations/qmd)。管理器层
getMemorySearchManager(...)先尝试QmdMemoryManager,失败时自动切到MemoryIndexManager。工具层
createMemorySearchTool(...)和createMemoryGetTool(...)提供给智能体调用。
二、状态机拆解
1) Backend 选择状态机
builtin / qmd 两条路:
- 读取
memory.backend。 - 不是
qmd就直接builtin。 - 是
qmd则继续解析command/collections/update/limits。 - 解析失败不崩,回落
builtin。
关键函数:resolveMemoryBackendConfig(...)(src/memory/backend-config.ts)
2) Manager 获取状态机
getMemorySearchManager(...) 的行为:
若
backend=qmd:- 先查
QMD_MANAGER_CACHE(key =buildQmdCacheKey(agentId, qmd))。 - 未命中则
QmdMemoryManager.create(...)。 - 成功则写入缓存并包一层
FallbackMemoryManager。
- 先查
qmd 初始化失败:
- 记录 warn。
- 动态 import
MemoryIndexManager.get(...)兜底。
兜底也失败:
- 返回
{ manager: null, error },工具层返回 disabled,而不是 throw 崩主流程。
- 返回
关键函数:getMemorySearchManager(...)(src/memory/search-manager.ts)
3) Fallback 状态机(最关键,含 cacheEvicted)
FallbackMemoryManager 内部完整核心状态:
class FallbackMemoryManager {
primaryFailed: boolean = false;
fallback: MemorySearchManager | null = null;
lastError?: string;
cacheEvicted: boolean = false; // 缓存是否已被主动驱逐
}状态转换:
- 默认
primaryFailed=false,先走 qmd。 - qmd
search抛错 →primaryFailed=true→ 关闭 primary → 记录lastError。 ensureFallback()懒加载 builtin manager。- 后续请求都走 fallback。
close()时回收并调用evictCacheEntry()。
evictCacheEntry() 的关键作用:
// 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(...) 真实流程:
- 从
sessionKey解析 agentId。 getMemorySearchManager(...)拿 manager。- scope guard(在 qmd manager 内部):tsscope 不匹配的 session 搜索结果为空,但不报错。
if (!this.isScopeAllowed(opts?.sessionKey)) return []; - 等待待处理更新:
waitForPendingUpdateBeforeSearch()确保最近写入被索引。 manager.search(query, { maxResults, minScore, sessionKey })。- 按 citations 配置做
decorateCitations(...)。 - qmd 模式下做
clampResultsByInjectedChars(..., limits.maxInjectedChars)。
返回值完整结构:
{
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(...) 真实流程:
- 同样先拿 manager。
manager.readFile({ relPath, from, lines })。- 返回精确片段,控制 token 成本。
qmd status() 返回形状
qmd backend 的 status() 固定返回:
{
backend: "qmd",
provider: "qmd",
model: "qmd",
files: totalDocuments, // 文档总数
vector: { enabled: true, available: true },
}四、启动预热机制
网关启动时会执行 startGatewayMemoryBackend(...):
- 只在
backend=qmd时触发。 - 提前
getMemorySearchManager(...)。 - 调用
probeVectorAvailability()探测向量后端是否就绪。 - 成功打 info,失败打 warn。
作用:把故障提前暴露在启动阶段,而不是第一次用户问答时炸。
五、索引构建状态机(embedding ops)
manager-embedding-ops.ts 负责"数据如何进索引":
- 文本分块:
chunkMarkdown(...) - 批量 embedding:
buildEmbeddingBatches(...)按预算切批 - 缓存命中:
loadEmbeddingCache(...)减少重复计算 - 批次失败重试:有限次数 + 退避延时
- 落库:
chunks/chunks_vec/chunks_fts一起更新,保持检索一致性
如果你要从 0 到 1 复刻,不能只做 search,还要把"更新状态机"做完整。
六、可复刻最小实现(含 cacheEvicted 和 scope guard)
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;
}
}
}七、上线验收(必须过)
- qmd 挂掉后,下一次查询可自动切 builtin(
primaryFailed=true)。 evictCacheEntry在 primary 失败后调用,下次能重试创建 qmd。- scope 不匹配的 session 搜索返回
[],不报错。 memory_search出错时返回 disabled/error,不导致整个 agent 失败。- 大量返回结果会被
maxInjectedChars上限裁剪。 - 启动时
probeVectorAvailability能提前暴露向量后端故障。 waitForPendingUpdateBeforeSearch确保刚写入的记忆能被搜到。
八、最常见坑
- 把 memory 出错当致命异常,导致主对话中断。
- 不做注入长度限制,memory 反过来挤爆上下文。
- 无 fallback,依赖单一后端。
- 启动不预热,问题在生产流量下才暴露。
- 缓存失效不清除,qmd 重连后拿到的仍是旧的失效实例(
evictCacheEntry漏掉)。 - scope guard 缺失,跨 session 记忆泄漏。